thwip-cli 1.0.0__tar.gz → 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. thwip_cli-1.0.2/.github/workflows/publish.yml +55 -0
  2. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/PKG-INFO +1 -1
  3. thwip_cli-1.0.2/install.sh +22 -0
  4. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/pyproject.toml +1 -1
  5. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/__init__.py +1 -1
  6. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/agents/base.py +2 -2
  7. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/agents/claude_agent.py +25 -3
  8. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/agents/google_agent.py +20 -7
  9. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/agents/openai_agent.py +28 -6
  10. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/cli.py +92 -15
  11. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/detector.py +21 -0
  12. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/theme.py +85 -11
  13. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/uv.lock +2 -2
  14. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/.gitignore +0 -0
  15. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/README.md +0 -0
  16. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/tests/test_agents.py +0 -0
  17. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/tests/test_detector.py +0 -0
  18. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/tests/test_session.py +0 -0
  19. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/tests/test_tools.py +0 -0
  20. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/__main__.py +0 -0
  21. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/agents/__init__.py +0 -0
  22. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/agents/deepseek_agent.py +0 -0
  23. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/agents/groq_agent.py +0 -0
  24. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/agents/ollama_agent.py +0 -0
  25. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/agents/openrouter_agent.py +0 -0
  26. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/config.py +0 -0
  27. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/limits.py +0 -0
  28. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/session.py +0 -0
  29. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/shortcuts.py +0 -0
  30. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/tools/__init__.py +0 -0
  31. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/tools/code_runner.py +0 -0
  32. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/tools/file_editor.py +0 -0
  33. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/tools/git_ops.py +0 -0
  34. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/tools/terminal.py +0 -0
  35. {thwip_cli-1.0.0 → thwip_cli-1.0.2}/thwip/utils.py +0 -0
@@ -0,0 +1,55 @@
1
+ name: Publish to PyPI and GitHub Releases
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout code
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Install uv
17
+ uses: astral-sh/setup-uv@v5
18
+
19
+ - name: Set up Python
20
+ run: uv python install 3.13
21
+
22
+ - name: Install dependencies and test
23
+ run: |
24
+ uv sync --all-extras
25
+ uv run pytest
26
+
27
+ build-and-publish:
28
+ needs: test
29
+ runs-on: ubuntu-latest
30
+ permissions:
31
+ contents: write
32
+ id-token: write
33
+
34
+ steps:
35
+ - name: Checkout code
36
+ uses: actions/checkout@v4
37
+
38
+ - name: Install uv
39
+ uses: astral-sh/setup-uv@v5
40
+
41
+ - name: Build package
42
+ run: uv build
43
+
44
+ - name: Publish to PyPI
45
+ uses: pypa/gh-action-pypi-publish@release/v1
46
+ with:
47
+ packages-dir: dist/
48
+ continue-on-error: true
49
+
50
+ - name: Create GitHub Release
51
+ uses: softprops/action-gh-release@v2
52
+ if: startsWith(github.ref, 'refs/tags/')
53
+ with:
54
+ files: dist/*
55
+ generate_release_notes: true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thwip-cli
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Universal coding agent multiplexer: detect, switch, and route between AI coding agents seamlessly
5
5
  Project-URL: Homepage, https://github.com/tanmayhutt/thwip-cli
6
6
  Project-URL: Repository, https://github.com/tanmayhutt/thwip-cli
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # Install script for thwip (Universal Coding Agent Multiplexer)
3
+ set -e
4
+
5
+ echo "Installing thwip..."
6
+
7
+ if command -v uv &> /dev/null; then
8
+ echo "Installing via uv tool..."
9
+ uv tool install --force thwip-cli
10
+ elif command -v pipx &> /dev/null; then
11
+ echo "Installing via pipx..."
12
+ pipx install --force thwip-cli
13
+ elif command -v pip3 &> /dev/null; then
14
+ echo "Installing via pip3..."
15
+ pip3 install --upgrade thwip-cli
16
+ else
17
+ echo "Error: Neither uv, pipx, nor pip3 was found on your system."
18
+ echo "Please install Python (>= 3.11) or uv to use thwip."
19
+ exit 1
20
+ fi
21
+
22
+ echo "thwip installed successfully. Run 'thwip' to get started."
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thwip-cli"
7
- version = "1.0.0"
7
+ version = "1.0.2"
8
8
  description = "Universal coding agent multiplexer: detect, switch, and route between AI coding agents seamlessly"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,4 +1,4 @@
1
1
  """thwip: Universal Coding Agent Multiplexer."""
2
2
 
3
- __version__ = "1.0.0"
3
+ __version__ = "1.0.2"
4
4
  __app_name__ = "thwip"
@@ -301,7 +301,7 @@ class BaseAgent(ABC):
301
301
  if not self.is_installed():
302
302
  return ("Not Installed", "dim")
303
303
  if not self.is_configured():
304
- return ("No Key", "status.no_key")
304
+ return ("Installed (No Key)", "status.limited")
305
305
 
306
306
  limit = self.check_limits()
307
307
  if limit == LimitStatus.OK:
@@ -315,7 +315,7 @@ class BaseAgent(ABC):
315
315
  elif limit == LimitStatus.INVALID_KEY:
316
316
  return ("Invalid Key", "error")
317
317
  else:
318
- return ("Unknown", "dim")
318
+ return ("Installed", "status.ready")
319
319
 
320
320
  def to_table_row(self) -> dict:
321
321
  """Convert to a dict suitable for render_agents_table()."""
@@ -149,8 +149,17 @@ class ClaudeAgent(BaseAgent):
149
149
  # --- Detection ---
150
150
 
151
151
  def is_installed(self) -> bool:
152
- """Check if Claude Code CLI is installed or configured."""
153
- return shutil.which("claude") is not None or self.is_configured()
152
+ """Check if Claude Code CLI or Claude desktop app is installed or configured."""
153
+ if self.is_configured():
154
+ return True
155
+ if shutil.which("claude") is not None:
156
+ return True
157
+ app_paths = [
158
+ Path("/Applications/Claude.app"),
159
+ Path.home() / "Applications" / "Claude.app",
160
+ Path.home() / ".claude",
161
+ ]
162
+ return any(p.exists() for p in app_paths)
154
163
 
155
164
  def is_configured(self) -> bool:
156
165
  """Check if we have a valid Anthropic API key."""
@@ -166,7 +175,7 @@ class ClaudeAgent(BaseAgent):
166
175
  path = shutil.which("claude")
167
176
  if path:
168
177
  info["path"] = path
169
- info["method"] = "npm global" # Claude Code is typically installed via npm
178
+ info["method"] = "CLI binary"
170
179
 
171
180
  # Try to get version
172
181
  try:
@@ -179,6 +188,19 @@ class ClaudeAgent(BaseAgent):
179
188
  except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
180
189
  pass
181
190
 
191
+ if not info["path"]:
192
+ app_paths = [
193
+ Path("/Applications/Claude.app"),
194
+ Path.home() / "Applications" / "Claude.app",
195
+ Path.home() / ".claude",
196
+ ]
197
+ for app in app_paths:
198
+ if app.exists():
199
+ info["method"] = "macOS config" if app.name == ".claude" else "macOS app"
200
+ info["path"] = str(app)
201
+ info["version"] = "Installed"
202
+ break
203
+
182
204
  self._install_cache = info
183
205
  return info
184
206
 
@@ -134,11 +134,20 @@ class GoogleAgent(BaseAgent):
134
134
  # --- Detection ---
135
135
 
136
136
  def is_installed(self) -> bool:
137
- """Check if Gemini CLI, Antigravity, or API is configured."""
138
- return self.is_configured() or any(
139
- shutil.which(cmd) is not None
140
- for cmd in ("gemini", "agy", "antigravity")
141
- )
137
+ """Check if Gemini CLI, Antigravity IDE, or Gemini app is installed."""
138
+ if self.is_configured():
139
+ return True
140
+ if any(shutil.which(cmd) is not None for cmd in ("gemini", "agy", "antigravity")):
141
+ return True
142
+ app_paths = [
143
+ Path("/Applications/Antigravity IDE.app"),
144
+ Path("/Applications/Antigravity.app"),
145
+ Path("/Applications/Gemini.app"),
146
+ Path.home() / "Applications" / "Antigravity IDE.app",
147
+ Path.home() / "Applications" / "Antigravity.app",
148
+ Path.home() / ".gemini",
149
+ ]
150
+ return any(p.exists() for p in app_paths)
142
151
 
143
152
  def is_configured(self) -> bool:
144
153
  return bool(self._get_api_key())
@@ -150,7 +159,7 @@ class GoogleAgent(BaseAgent):
150
159
  path = shutil.which(cmd)
151
160
  if path:
152
161
  info["path"] = path
153
- info["method"] = "npm global" if cmd == "gemini" else "binary"
162
+ info["method"] = "CLI binary"
154
163
  try:
155
164
  result = subprocess.run(
156
165
  [cmd, "--version"],
@@ -162,16 +171,20 @@ class GoogleAgent(BaseAgent):
162
171
  pass
163
172
  break
164
173
 
165
- # Also check for Antigravity IDE (macOS app)
174
+ # Also check for Antigravity IDE or Gemini app (macOS app)
166
175
  if not info["path"]:
167
176
  app_paths = [
177
+ Path("/Applications/Antigravity IDE.app"),
168
178
  Path("/Applications/Antigravity.app"),
179
+ Path("/Applications/Gemini.app"),
180
+ Path.home() / "Applications" / "Antigravity IDE.app",
169
181
  Path.home() / "Applications" / "Antigravity.app",
170
182
  ]
171
183
  for app in app_paths:
172
184
  if app.exists():
173
185
  info["method"] = "macOS app"
174
186
  info["path"] = str(app)
187
+ info["version"] = "Installed"
175
188
  break
176
189
 
177
190
  return info
@@ -157,10 +157,18 @@ class OpenAIAgent(BaseAgent):
157
157
  # --- Detection ---
158
158
 
159
159
  def is_installed(self) -> bool:
160
- return self.is_configured() or any(
161
- shutil.which(cmd) is not None
162
- for cmd in ("codex", "openai")
163
- )
160
+ """Check if Codex CLI, OpenAI CLI, or ChatGPT app is installed."""
161
+ if self.is_configured():
162
+ return True
163
+ if any(shutil.which(cmd) is not None for cmd in ("codex", "openai", "chatgpt")):
164
+ return True
165
+ app_paths = [
166
+ Path("/Applications/ChatGPT.app"),
167
+ Path("/Applications/OpenAI.app"),
168
+ Path.home() / "Applications" / "ChatGPT.app",
169
+ Path.home() / ".config" / "openai",
170
+ ]
171
+ return any(p.exists() for p in app_paths)
164
172
 
165
173
  def is_configured(self) -> bool:
166
174
  return bool(self._get_api_key())
@@ -168,11 +176,11 @@ class OpenAIAgent(BaseAgent):
168
176
  def get_install_info(self) -> dict[str, str]:
169
177
  info: dict[str, str] = {"method": "not installed", "path": "", "version": ""}
170
178
 
171
- for cmd in ("codex", "openai"):
179
+ for cmd in ("codex", "openai", "chatgpt"):
172
180
  path = shutil.which(cmd)
173
181
  if path:
174
182
  info["path"] = path
175
- info["method"] = "npm global" if cmd == "codex" else "pip"
183
+ info["method"] = "CLI binary"
176
184
  try:
177
185
  result = subprocess.run(
178
186
  [cmd, "--version"],
@@ -184,6 +192,20 @@ class OpenAIAgent(BaseAgent):
184
192
  pass
185
193
  break
186
194
 
195
+ # Also check for ChatGPT macOS app
196
+ if not info["path"]:
197
+ app_paths = [
198
+ Path("/Applications/ChatGPT.app"),
199
+ Path("/Applications/OpenAI.app"),
200
+ Path.home() / "Applications" / "ChatGPT.app",
201
+ ]
202
+ for app in app_paths:
203
+ if app.exists():
204
+ info["method"] = "macOS app"
205
+ info["path"] = str(app)
206
+ info["version"] = "Installed"
207
+ break
208
+
187
209
  return info
188
210
 
189
211
  def get_subscription_info(self) -> SubscriptionInfo:
@@ -53,6 +53,7 @@ from thwip.theme import (
53
53
  print_info,
54
54
  print_success,
55
55
  print_warning,
56
+ render_about_guide,
56
57
  render_agent_badge,
57
58
  render_agents_table,
58
59
  render_capability_disclaimer,
@@ -182,6 +183,9 @@ class ThwipCLI:
182
183
  if cmd in ("/quit", "/exit", "/q"):
183
184
  return "QUIT"
184
185
 
186
+ elif cmd in ("/about", "/guide", "/info"):
187
+ self.cmd_show_about()
188
+
185
189
  elif cmd in ("/help", "/h"):
186
190
  self.show_help()
187
191
 
@@ -194,6 +198,9 @@ class ThwipCLI:
194
198
  elif cmd in ("/models", "/m"):
195
199
  self.cmd_show_models(arg1)
196
200
 
201
+ elif cmd == "/tools":
202
+ self.cmd_show_tools()
203
+
197
204
  elif cmd == "/status":
198
205
  self.cmd_show_status()
199
206
 
@@ -206,6 +213,10 @@ class ThwipCLI:
206
213
  elif cmd == "/history":
207
214
  self.cmd_show_history()
208
215
 
216
+ elif cmd in ("/clear", "/reset"):
217
+ self.session.messages.clear()
218
+ print_info("Conversation history cleared.")
219
+
209
220
  elif cmd == "/cost":
210
221
  self.cmd_show_cost()
211
222
 
@@ -236,10 +247,47 @@ class ThwipCLI:
236
247
  print_info("Usage: /session [save|load|list|clear] [name]")
237
248
 
238
249
  else:
239
- print_warning(f"Unknown command '{cmd}'. Type /help for available commands.")
250
+ print_warning(f"Unknown command '{cmd}'. Type /help or /about for navigation guide.")
240
251
 
241
252
  return None
242
253
 
254
+ def cmd_show_about(self) -> None:
255
+ """Display the complete About section and navigation guide."""
256
+ detected = len(self.detector.scan_all())
257
+ ready = len(self.registry.get_ready_agents())
258
+ console.print(
259
+ render_about_guide(
260
+ agents_detected=detected,
261
+ agents_ready=ready,
262
+ active_agent=self.current_agent.display_name,
263
+ active_model=self.session.current_model,
264
+ active_company=self.current_agent.company,
265
+ )
266
+ )
267
+
268
+ def cmd_show_tools(self) -> None:
269
+ """Display available universal tools."""
270
+ table = Table(title="Universal Tool Engine", box=box.ROUNDED)
271
+ table.add_column("Tool Name", style="bold white")
272
+ table.add_column("Category", style="cyan")
273
+ table.add_column("Description", style="white")
274
+ table.add_column("Status", style="green")
275
+
276
+ tools_info = [
277
+ ("read_file", "Filesystem", "Read file contents with line offset controls", "Active"),
278
+ ("edit_file", "Filesystem", "Structured string replacement in local files", "Active"),
279
+ ("write_file", "Filesystem", "Create or overwrite full files", "Active"),
280
+ ("list_dir", "Filesystem", "List directory tree and metadata", "Active"),
281
+ ("run_command", "Terminal", "Execute non-interactive shell commands safely", "Active"),
282
+ ("run_code", "Sandbox", "Run inline Python or Node.js code snippets", "Active"),
283
+ ("git_status", "Version Control", "Inspect repository changes and staging", "Active"),
284
+ ("git_diff", "Version Control", "View unified unstaged/staged diffs", "Active"),
285
+ ("git_commit", "Version Control", "Commit staged workspace changes", "Active"),
286
+ ]
287
+ for name, cat, desc, status in tools_info:
288
+ table.add_row(name, cat, desc, f"[bold green]{status}[/bold green]")
289
+ console.print(table)
290
+
243
291
  def show_help(self) -> None:
244
292
  """Show help information."""
245
293
  table = Table(title="thwip Commands & Shortcuts", box=box.ROUNDED)
@@ -247,22 +295,24 @@ class ThwipCLI:
247
295
  table.add_column("Description", style="white")
248
296
 
249
297
  commands = [
298
+ ("/about", "Display full About section, architecture, and navigation guide"),
250
299
  ("/switch [agent] [model]", "Switch active agent/model mid-conversation without losing context"),
251
300
  ("/agents", "Show all detected coding agents, company status & capabilities"),
252
301
  ("/models [agent]", "List available models for current or target agent"),
302
+ ("/tools", "List all universal file, terminal, and git tools"),
253
303
  ("/status", "Display current session, project, and token stats"),
254
304
  ("/limits", "View token usage, quota, and spend metrics"),
255
305
  ("/detect", "Re-scan system for newly installed coding agents"),
256
306
  ("/session save [name]", "Save current chat session"),
257
307
  ("/session load <name>", "Load a previously saved session"),
258
308
  ("/session list", "List all saved sessions"),
259
- ("/session clear", "Clear current conversation memory"),
309
+ ("/clear", "Clear current conversation memory"),
260
310
  ("/history", "View conversation history with model attribution badges"),
261
311
  ("/cost", "Show estimated session and cumulative cost"),
262
312
  ("/project [path]", "View or change project working directory"),
263
- ("Ctrl + S", "Quick switch prompt"),
264
- ("Ctrl + T", "Status view"),
265
- ("Ctrl + H", "Show conversation history"),
313
+ ("Ctrl + S", "Interactive agent/model switcher prompt"),
314
+ ("Ctrl + T", "Status view and token counters"),
315
+ ("Ctrl + C", "Interrupt active response or tool execution"),
266
316
  ("/quit", "Exit thwip"),
267
317
  ]
268
318
  for c, d in commands:
@@ -272,21 +322,32 @@ class ThwipCLI:
272
322
  async def cmd_switch(self, agent_name: str, model_id: str = "") -> None:
273
323
  """Switch to a different agent and/or model."""
274
324
  if not agent_name:
275
- # Interactive selection
276
325
  agents = self.registry.list_agents()
277
- console.print("\n[bold white]Select an agent to switch to:[/bold white]")
278
- for i, a in enumerate(agents, 1):
326
+ console.print("\n[bold white]Available Coding Agents on Your Machine:[/bold white]")
327
+
328
+ # Group: Ready / Configured
329
+ ready_agents = [a for a in agents if a.is_configured()]
330
+ installed_agents = [a for a in agents if a.is_installed() and not a.is_configured()]
331
+ other_agents = [a for a in agents if not a.is_installed()]
332
+
333
+ ordered = ready_agents + installed_agents + other_agents
334
+
335
+ for i, a in enumerate(ordered, 1):
279
336
  brand = get_brand(a.company)
280
- status_str, _ = a.get_status_display()
281
- caps_str = ", ".join(c.value for c in a.capabilities)
337
+ status_str, status_style = a.get_status_display()
338
+ install_info = a.get_install_info()
339
+ loc = f" ({install_info['path']})" if install_info.get("path") else ""
340
+
282
341
  console.print(
283
342
  f" [bold white]{i}.[/bold white] [bold]{a.display_name}[/bold] "
284
- f"({a.company}) - [{status_str}] - [dim]{caps_str}[/dim]"
343
+ f"({a.company}) - [{status_style}][{status_str}][/{status_style}][dim]{loc}[/dim]"
285
344
  )
286
345
 
287
- choice = input(f"\nEnter choice [1-{len(agents)}]: ").strip()
288
- if choice.isdigit() and 1 <= int(choice) <= len(agents):
289
- agent_name = agents[int(choice) - 1].name
346
+ choice = input(f"\nEnter choice [1-{len(ordered)}]: ").strip()
347
+ if choice.isdigit() and 1 <= int(choice) <= len(ordered):
348
+ agent_name = ordered[int(choice) - 1].name
349
+ elif choice.lower() in [a.name for a in agents]:
350
+ agent_name = choice.lower()
290
351
  else:
291
352
  print_warning("Switch cancelled.")
292
353
  return
@@ -313,7 +374,23 @@ class ThwipCLI:
313
374
  unsupported=missing,
314
375
  )
315
376
  )
316
- print_success(f"Now chatting with {new_agent.display_name} ({chosen_model}). Context preserved!")
377
+
378
+ if not new_agent.is_configured():
379
+ key_name = {
380
+ "google": "GEMINI_API_KEY",
381
+ "openai": "OPENAI_API_KEY",
382
+ "claude": "ANTHROPIC_API_KEY",
383
+ "deepseek": "DEEPSEEK_API_KEY",
384
+ "groq": "GROQ_API_KEY",
385
+ "openrouter": "OPENROUTER_API_KEY",
386
+ }.get(new_agent.name, "API_KEY")
387
+ print_warning(
388
+ f"Notice: {new_agent.display_name} was selected, but no API key was found.\n"
389
+ f" Set your key with: export {key_name}=your_key_here\n"
390
+ f" Or add it to ~/.thwip/config.toml"
391
+ )
392
+ else:
393
+ print_success(f"Now chatting with {new_agent.display_name} ({chosen_model}). Context preserved!")
317
394
 
318
395
  def cmd_show_agents(self) -> None:
319
396
  """Show table of all detected agents."""
@@ -96,6 +96,27 @@ KNOWN_CLI_TOOLS: list[dict[str, Any]] = [
96
96
  ]
97
97
 
98
98
  KNOWN_APPS: list[dict[str, Any]] = [
99
+ {
100
+ "name": "Antigravity IDE",
101
+ "company": "Google",
102
+ "mac_path": "/Applications/Antigravity IDE.app",
103
+ "category": "IDE App",
104
+ "capabilities": ["Chat", "File Edit", "Terminal", "Browser", "Search"],
105
+ },
106
+ {
107
+ "name": "ChatGPT Desktop",
108
+ "company": "OpenAI",
109
+ "mac_path": "/Applications/ChatGPT.app",
110
+ "category": "Desktop App",
111
+ "capabilities": ["Chat", "Code Run", "Vision"],
112
+ },
113
+ {
114
+ "name": "Gemini Desktop",
115
+ "company": "Google",
116
+ "mac_path": "/Applications/Gemini.app",
117
+ "category": "Desktop App",
118
+ "capabilities": ["Chat", "Search", "Vision"],
119
+ },
99
120
  {
100
121
  "name": "Cursor",
101
122
  "company": "Cursor Inc",
@@ -130,27 +130,51 @@ def render_startup_banner(
130
130
  agents_detected: int,
131
131
  agents_ready: int,
132
132
  ) -> Panel:
133
- """Render the startup banner with current agent info."""
133
+ """Render the startup banner with ASCII logo and current agent info."""
134
134
  brand = get_brand(company)
135
135
 
136
136
  lines = Text()
137
- lines.append("thwip", style="bold white")
138
- lines.append(" v1.0\n", style="dim")
139
- lines.append("Universal Coding Agent Multiplexer\n\n", style="dim italic")
140
- lines.append(" Agent: ", style="dim")
137
+ # Web sign + Wordmark logo in exact website theme
138
+ lines.append(" \\ | / ", style="bold #8E8B8B")
139
+ lines.append("████████╗██╗ ██╗██╗ ██╗██╗██████╗ \n", style="bold #F1ECEC")
140
+ lines.append(" -- \\ | / -- ", style="bold #8E8B8B")
141
+ lines.append("╚══██╔══╝██║ ██║██║ ██║██║██╔══██╗\n", style="bold #4285F4")
142
+ lines.append(" - - <(", style="bold #8E8B8B")
143
+ lines.append("●", style="bold #FFFFFF")
144
+ lines.append(")> - - ", style="bold #8E8B8B")
145
+ lines.append("██║ ███████║██║ █╗ ██║██║██████╔╝\n", style="bold #D97757")
146
+ lines.append(" -- / | \\ -- ", style="bold #8E8B8B")
147
+ lines.append("██║ ██╔══██║██║███╗██║██║██╔═══╝ \n", style="bold #10A37F")
148
+ lines.append(" / | \\ ", style="bold #8E8B8B")
149
+ lines.append("██║ ██║ ██║╚███╔███╔╝██║██║ \n", style="bold #F1ECEC")
150
+ lines.append(" ", style="bold #8E8B8B")
151
+ lines.append("╚═╝ ╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝╚═╝ \n\n", style="dim")
152
+
153
+ lines.append(" Universal Coding Agent Multiplexer", style="bold white")
154
+ lines.append(" v1.0.2\n\n", style="dim")
155
+ lines.append(" Agent: ", style="dim")
141
156
  lines.append(f"{agent_name}", style=brand.label_style)
142
157
  lines.append(f" ({company})\n", style="dim")
143
- lines.append(" Model: ", style="dim")
158
+ lines.append(" Model: ", style="dim")
144
159
  lines.append(f"{model}\n", style="bold white")
145
- lines.append(" Project: ", style="dim")
160
+ lines.append(" Workspace: ", style="dim")
146
161
  lines.append(f"{project_path}\n", style="white")
147
- lines.append(" Session: ", style="dim")
162
+ lines.append(" Session: ", style="dim")
148
163
  lines.append(f"{session_name}\n", style="white")
149
- lines.append(" Agents: ", style="dim")
164
+ lines.append(" Environment: ", style="dim")
150
165
  lines.append(f"{agents_detected}", style="bold white")
151
- lines.append(" detected, ", style="dim")
166
+ lines.append(" tools detected, ", style="dim")
152
167
  lines.append(f"{agents_ready}", style="success")
153
- lines.append(" ready", style="dim")
168
+ lines.append(" ready\n\n", style="dim")
169
+ lines.append(" Quickstart: Type ", style="dim")
170
+ lines.append("/about", style="bold cyan")
171
+ lines.append(" for guide, ", style="dim")
172
+ lines.append("/switch", style="bold cyan")
173
+ lines.append(" to change agent, ", style="dim")
174
+ lines.append("/agents", style="bold cyan")
175
+ lines.append(" to list tools, ", style="dim")
176
+ lines.append("Ctrl+S", style="bold yellow")
177
+ lines.append(" for switcher.", style="dim")
154
178
 
155
179
  return Panel(
156
180
  lines,
@@ -160,6 +184,56 @@ def render_startup_banner(
160
184
  )
161
185
 
162
186
 
187
+ def render_about_guide(
188
+ agents_detected: int = 0,
189
+ agents_ready: int = 0,
190
+ active_agent: str = "",
191
+ active_model: str = "",
192
+ active_company: str = "",
193
+ ) -> Panel:
194
+ """Render comprehensive about section and navigation guide."""
195
+ brand = get_brand(active_company)
196
+ content = Text()
197
+
198
+ content.append("thwip - Universal Coding Agent Multiplexer\n", style="bold white")
199
+ content.append("Combine, switch, and route across installed AI coding agents seamlessly.\n\n", style="dim")
200
+
201
+ content.append("HOW IT WORKS\n", style="bold cyan")
202
+ content.append(" 1. Auto-Detection: Scans environment variables, local CLI agents, and existing credentials\n", style="dim")
203
+ content.append(" in ~/.claude.json, ~/.gemini/config.json, ~/.config/openai/, or Ollama.\n", style="dim")
204
+ content.append(" 2. Unified State: Maintains conversation history and tool outputs in a portable format.\n", style="dim")
205
+ content.append(" 3. Hot-Swapping: Switch models mid-conversation with zero context loss.\n", style="dim")
206
+ content.append(" 4. Universal Tools: Provides safe file editing, shell commands, execution, and git operations.\n", style="dim")
207
+ content.append(" 5. Quota Failover: Catches HTTP 429 errors and offers immediate one-key fallback switching.\n\n", style="dim")
208
+
209
+ content.append("NAVIGATION & COMMANDS\n", style="bold cyan")
210
+ content.append(" /switch <agent> [model] Hot-swap active model mid-conversation (e.g. /switch google gemini-2.5-pro)\n", style="white")
211
+ content.append(" /agents List all discovered agents, status, and capabilities\n", style="white")
212
+ content.append(" /models [agent] Show available models and context window limits\n", style="white")
213
+ content.append(" /limits View live token counts, cumulative costs, and quotas\n", style="white")
214
+ content.append(" /tools List all universal file, execution, and git tools\n", style="white")
215
+ content.append(" /session save <name> Save current conversation to disk\n", style="white")
216
+ content.append(" /session load <name> Restore and continue a saved session\n", style="white")
217
+ content.append(" /history View turn summary across models\n", style="white")
218
+ content.append(" /about or /guide Display this navigation guide anytime\n", style="white")
219
+ content.append(" /clear Reset conversation memory\n", style="white")
220
+ content.append(" /exit or /quit Exit the REPL\n\n", style="white")
221
+
222
+ content.append("KEYBOARD SHORTCUTS\n", style="bold cyan")
223
+ content.append(" Ctrl+S Interactive model switcher prompt\n", style="white")
224
+ content.append(" Ctrl+T Display current agent status and token counters\n", style="white")
225
+ content.append(" Ctrl+C Interrupt current response or tool execution\n", style="white")
226
+
227
+ return Panel(
228
+ content,
229
+ title="About & Navigation Guide",
230
+ title_align="left",
231
+ border_style=brand.primary,
232
+ box=box.ROUNDED,
233
+ padding=(1, 2),
234
+ )
235
+
236
+
163
237
  def render_agent_badge(agent_name: str, model: str, company: str) -> Text:
164
238
  """Render an inline badge for message attribution."""
165
239
  brand = get_brand(company)
@@ -1021,8 +1021,8 @@ wheels = [
1021
1021
  ]
1022
1022
 
1023
1023
  [[package]]
1024
- name = "thwip"
1025
- version = "1.0.0"
1024
+ name = "thwip-cli"
1025
+ version = "1.0.2"
1026
1026
  source = { editable = "." }
1027
1027
  dependencies = [
1028
1028
  { name = "anthropic" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes