code-puppy 0.0.214__py3-none-any.whl → 0.0.366__py3-none-any.whl

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 (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,74 @@
1
+ """Configuration management API endpoints."""
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+ from pydantic import BaseModel
5
+ from typing import Any, Dict, List
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ class ConfigValue(BaseModel):
11
+ key: str
12
+ value: Any
13
+
14
+
15
+ class ConfigUpdate(BaseModel):
16
+ value: Any
17
+
18
+
19
+ @router.get("/")
20
+ async def list_config() -> Dict[str, Any]:
21
+ """List all configuration keys and their current values."""
22
+ from code_puppy.config import get_config_keys, get_value
23
+
24
+ config = {}
25
+ for key in get_config_keys():
26
+ config[key] = get_value(key)
27
+ return {"config": config}
28
+
29
+
30
+ @router.get("/keys")
31
+ async def get_config_keys_list() -> List[str]:
32
+ """Get list of all valid configuration keys."""
33
+ from code_puppy.config import get_config_keys
34
+
35
+ return get_config_keys()
36
+
37
+
38
+ @router.get("/{key}")
39
+ async def get_config_value(key: str) -> ConfigValue:
40
+ """Get a specific configuration value."""
41
+ from code_puppy.config import get_config_keys, get_value
42
+
43
+ valid_keys = get_config_keys()
44
+ if key not in valid_keys:
45
+ raise HTTPException(
46
+ 404, f"Config key '{key}' not found. Valid keys: {valid_keys}"
47
+ )
48
+
49
+ value = get_value(key)
50
+ return ConfigValue(key=key, value=value)
51
+
52
+
53
+ @router.put("/{key}")
54
+ async def set_config_value(key: str, update: ConfigUpdate) -> ConfigValue:
55
+ """Set a configuration value."""
56
+ from code_puppy.config import get_config_keys, get_value, set_value
57
+
58
+ valid_keys = get_config_keys()
59
+ if key not in valid_keys:
60
+ raise HTTPException(
61
+ 404, f"Config key '{key}' not found. Valid keys: {valid_keys}"
62
+ )
63
+
64
+ set_value(key, str(update.value))
65
+ return ConfigValue(key=key, value=get_value(key))
66
+
67
+
68
+ @router.delete("/{key}")
69
+ async def reset_config_value(key: str) -> Dict[str, str]:
70
+ """Reset a configuration value to default (remove from config file)."""
71
+ from code_puppy.config import reset_value
72
+
73
+ reset_value(key)
74
+ return {"message": f"Config key '{key}' reset to default"}
@@ -0,0 +1,232 @@
1
+ """Sessions API endpoints for retrieving subagent session data."""
2
+
3
+ import asyncio
4
+ import json
5
+ import pickle
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from fastapi import APIRouter, HTTPException
11
+ from pydantic import BaseModel
12
+
13
+ # Thread pool for blocking file I/O
14
+ _executor = ThreadPoolExecutor(max_workers=2)
15
+
16
+ # Timeout for file operations (seconds)
17
+ FILE_IO_TIMEOUT = 10.0
18
+
19
+ router = APIRouter()
20
+
21
+
22
+ class SessionInfo(BaseModel):
23
+ """Session metadata information."""
24
+
25
+ session_id: str
26
+ agent_name: Optional[str] = None
27
+ initial_prompt: Optional[str] = None
28
+ created_at: Optional[str] = None
29
+ last_updated: Optional[str] = None
30
+ message_count: int = 0
31
+
32
+
33
+ class MessageContent(BaseModel):
34
+ """Message content with role and optional timestamp."""
35
+
36
+ role: str
37
+ content: Any
38
+ timestamp: Optional[str] = None
39
+
40
+
41
+ class SessionDetail(SessionInfo):
42
+ """Session info with full message history."""
43
+
44
+ messages: List[Dict[str, Any]] = []
45
+
46
+
47
+ def _get_sessions_dir() -> Path:
48
+ """Get the subagent sessions directory.
49
+
50
+ Returns:
51
+ Path to the subagent sessions directory
52
+ """
53
+ from code_puppy.config import DATA_DIR
54
+
55
+ return Path(DATA_DIR) / "subagent_sessions"
56
+
57
+
58
+ def _serialize_message(msg: Any) -> Dict[str, Any]:
59
+ """Serialize a pydantic-ai message to a JSON-safe dict.
60
+
61
+ Handles various pydantic-ai message types that may be stored
62
+ in the pickle files.
63
+
64
+ Args:
65
+ msg: A pydantic-ai message object
66
+
67
+ Returns:
68
+ JSON-serializable dictionary representation of the message
69
+ """
70
+ # Handle pydantic v2 models with model_dump
71
+ if hasattr(msg, "model_dump"):
72
+ return msg.model_dump(mode="json")
73
+ # Handle objects with __dict__ (convert values to strings for safety)
74
+ elif hasattr(msg, "__dict__"):
75
+ return {k: str(v) for k, v in msg.__dict__.items()}
76
+ # Fallback: wrap in a content dict
77
+ else:
78
+ return {"content": str(msg)}
79
+
80
+
81
+ def _load_json_sync(file_path: Path) -> dict:
82
+ """Synchronous JSON file load (for use in executor)."""
83
+ with open(file_path, "r") as f:
84
+ return json.load(f)
85
+
86
+
87
+ def _load_pickle_sync(file_path: Path) -> Any:
88
+ """Synchronous pickle file load (for use in executor)."""
89
+ with open(file_path, "rb") as f:
90
+ return pickle.load(f)
91
+
92
+
93
+ @router.get("/")
94
+ async def list_sessions() -> List[SessionInfo]:
95
+ """List all available sessions.
96
+
97
+ Returns:
98
+ List of SessionInfo objects for each session found
99
+ """
100
+ sessions_dir = _get_sessions_dir()
101
+ if not sessions_dir.exists():
102
+ return []
103
+
104
+ loop = asyncio.get_running_loop()
105
+ sessions = []
106
+
107
+ for txt_file in sessions_dir.glob("*.txt"):
108
+ session_id = txt_file.stem
109
+ try:
110
+ # Run blocking I/O in thread pool with timeout
111
+ metadata = await asyncio.wait_for(
112
+ loop.run_in_executor(_executor, _load_json_sync, txt_file),
113
+ timeout=FILE_IO_TIMEOUT,
114
+ )
115
+ sessions.append(
116
+ SessionInfo(
117
+ session_id=session_id,
118
+ agent_name=metadata.get("agent_name"),
119
+ initial_prompt=metadata.get("initial_prompt"),
120
+ created_at=metadata.get("created_at"),
121
+ last_updated=metadata.get("last_updated"),
122
+ message_count=metadata.get("message_count", 0),
123
+ )
124
+ )
125
+ except asyncio.TimeoutError:
126
+ # Timed out reading file, include basic info
127
+ sessions.append(SessionInfo(session_id=session_id))
128
+ except Exception:
129
+ # If we can't parse metadata, still include basic session info
130
+ sessions.append(SessionInfo(session_id=session_id))
131
+
132
+ return sessions
133
+
134
+
135
+ @router.get("/{session_id}")
136
+ async def get_session(session_id: str) -> SessionInfo:
137
+ """Get session metadata.
138
+
139
+ Args:
140
+ session_id: The session identifier
141
+
142
+ Returns:
143
+ SessionInfo with metadata for the specified session
144
+
145
+ Raises:
146
+ HTTPException: 404 if session not found, 504 on timeout
147
+ """
148
+ sessions_dir = _get_sessions_dir()
149
+ txt_file = sessions_dir / f"{session_id}.txt"
150
+
151
+ if not txt_file.exists():
152
+ raise HTTPException(404, f"Session '{session_id}' not found")
153
+
154
+ loop = asyncio.get_running_loop()
155
+
156
+ try:
157
+ metadata = await asyncio.wait_for(
158
+ loop.run_in_executor(_executor, _load_json_sync, txt_file),
159
+ timeout=FILE_IO_TIMEOUT,
160
+ )
161
+ except asyncio.TimeoutError:
162
+ raise HTTPException(504, f"Timeout reading session '{session_id}'")
163
+
164
+ return SessionInfo(
165
+ session_id=session_id,
166
+ agent_name=metadata.get("agent_name"),
167
+ initial_prompt=metadata.get("initial_prompt"),
168
+ created_at=metadata.get("created_at"),
169
+ last_updated=metadata.get("last_updated"),
170
+ message_count=metadata.get("message_count", 0),
171
+ )
172
+
173
+
174
+ @router.get("/{session_id}/messages")
175
+ async def get_session_messages(session_id: str) -> List[Dict[str, Any]]:
176
+ """Get the full message history for a session.
177
+
178
+ Args:
179
+ session_id: The session identifier
180
+
181
+ Returns:
182
+ List of serialized message dictionaries
183
+
184
+ Raises:
185
+ HTTPException: 404 if session messages not found, 500 on load error, 504 on timeout
186
+ """
187
+ sessions_dir = _get_sessions_dir()
188
+ pkl_file = sessions_dir / f"{session_id}.pkl"
189
+
190
+ if not pkl_file.exists():
191
+ raise HTTPException(404, f"Session '{session_id}' messages not found")
192
+
193
+ loop = asyncio.get_running_loop()
194
+
195
+ try:
196
+ messages = await asyncio.wait_for(
197
+ loop.run_in_executor(_executor, _load_pickle_sync, pkl_file),
198
+ timeout=FILE_IO_TIMEOUT,
199
+ )
200
+ return [_serialize_message(msg) for msg in messages]
201
+ except asyncio.TimeoutError:
202
+ raise HTTPException(504, f"Timeout loading session '{session_id}' messages")
203
+ except Exception as e:
204
+ raise HTTPException(500, f"Error loading session messages: {e}")
205
+
206
+
207
+ @router.delete("/{session_id}")
208
+ async def delete_session(session_id: str) -> Dict[str, str]:
209
+ """Delete a session and its data.
210
+
211
+ Args:
212
+ session_id: The session identifier
213
+
214
+ Returns:
215
+ Success message dict
216
+
217
+ Raises:
218
+ HTTPException: 404 if session not found
219
+ """
220
+ sessions_dir = _get_sessions_dir()
221
+ txt_file = sessions_dir / f"{session_id}.txt"
222
+ pkl_file = sessions_dir / f"{session_id}.pkl"
223
+
224
+ if not txt_file.exists() and not pkl_file.exists():
225
+ raise HTTPException(404, f"Session '{session_id}' not found")
226
+
227
+ if txt_file.exists():
228
+ txt_file.unlink()
229
+ if pkl_file.exists():
230
+ pkl_file.unlink()
231
+
232
+ return {"message": f"Session '{session_id}' deleted"}
@@ -0,0 +1,361 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>🐶 Code Puppy Terminal</title>
7
+
8
+ <!-- Tailwind CSS -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+
11
+ <!-- xterm.js CSS -->
12
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
13
+
14
+ <!-- xterm.js -->
15
+ <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
16
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
17
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
18
+
19
+ <style>
20
+ /* Custom terminal styling */
21
+ .xterm {
22
+ padding: 8px;
23
+ }
24
+
25
+ .xterm-viewport::-webkit-scrollbar {
26
+ width: 8px;
27
+ }
28
+
29
+ .xterm-viewport::-webkit-scrollbar-track {
30
+ background: #1e1e1e;
31
+ }
32
+
33
+ .xterm-viewport::-webkit-scrollbar-thumb {
34
+ background: #4a4a4a;
35
+ border-radius: 4px;
36
+ }
37
+
38
+ .xterm-viewport::-webkit-scrollbar-thumb:hover {
39
+ background: #5a5a5a;
40
+ }
41
+
42
+ .status-indicator {
43
+ width: 10px;
44
+ height: 10px;
45
+ border-radius: 50%;
46
+ animation: pulse 2s infinite;
47
+ }
48
+
49
+ .status-connected {
50
+ background-color: #22c55e;
51
+ box-shadow: 0 0 8px #22c55e;
52
+ }
53
+
54
+ .status-disconnected {
55
+ background-color: #ef4444;
56
+ box-shadow: 0 0 8px #ef4444;
57
+ animation: none;
58
+ }
59
+
60
+ .status-connecting {
61
+ background-color: #f59e0b;
62
+ box-shadow: 0 0 8px #f59e0b;
63
+ }
64
+
65
+ @keyframes pulse {
66
+ 0%, 100% { opacity: 1; }
67
+ 50% { opacity: 0.5; }
68
+ }
69
+ </style>
70
+ </head>
71
+ <body class="h-full bg-gray-900 text-white overflow-hidden">
72
+ <div class="h-full flex flex-col">
73
+ <!-- Header -->
74
+ <header class="bg-gray-800 border-b border-gray-700 px-4 py-2 flex items-center justify-between">
75
+ <div class="flex items-center space-x-3">
76
+ <span class="text-2xl">🐶</span>
77
+ <h1 class="text-lg font-semibold text-gray-100">Code Puppy Terminal</h1>
78
+ </div>
79
+
80
+ <div class="flex items-center space-x-4">
81
+ <!-- Connection Status -->
82
+ <div class="flex items-center space-x-2">
83
+ <div id="status-indicator" class="status-indicator status-disconnected"></div>
84
+ <span id="status-text" class="text-sm text-gray-400">Disconnected</span>
85
+ </div>
86
+
87
+ <!-- Session ID -->
88
+ <div class="text-sm text-gray-500">
89
+ Session: <span id="session-id" class="font-mono text-gray-400">-</span>
90
+ </div>
91
+
92
+ <!-- Controls -->
93
+ <div class="flex items-center space-x-2">
94
+ <button id="btn-reconnect"
95
+ class="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
96
+ disabled>
97
+ Reconnect
98
+ </button>
99
+ <button id="btn-clear"
100
+ class="px-3 py-1 text-sm bg-gray-600 hover:bg-gray-700 rounded transition-colors">
101
+ Clear
102
+ </button>
103
+ </div>
104
+ </div>
105
+ </header>
106
+
107
+ <!-- Terminal Container -->
108
+ <main class="flex-1 bg-black p-2">
109
+ <div id="terminal" class="h-full w-full"></div>
110
+ </main>
111
+
112
+ <!-- Footer -->
113
+ <footer class="bg-gray-800 border-t border-gray-700 px-4 py-1 flex items-center justify-between text-xs text-gray-500">
114
+ <div>
115
+ <span id="terminal-size">80x24</span>
116
+ </div>
117
+ <div class="flex items-center space-x-4">
118
+ <span>Ctrl+C to interrupt</span>
119
+ <span>•</span>
120
+ <span>Ctrl+D to exit</span>
121
+ </div>
122
+ </footer>
123
+ </div>
124
+
125
+ <script>
126
+ // Terminal configuration
127
+ const CONFIG = {
128
+ wsUrl: `ws://${window.location.host}/ws/terminal`,
129
+ sessionId: new URLSearchParams(window.location.search).get('session') || 'default',
130
+ theme: {
131
+ background: '#1e1e1e',
132
+ foreground: '#d4d4d4',
133
+ cursor: '#aeafad',
134
+ cursorAccent: '#1e1e1e',
135
+ selection: 'rgba(255, 255, 255, 0.3)',
136
+ black: '#000000',
137
+ red: '#cd3131',
138
+ green: '#0dbc79',
139
+ yellow: '#e5e510',
140
+ blue: '#2472c8',
141
+ magenta: '#bc3fbc',
142
+ cyan: '#11a8cd',
143
+ white: '#e5e5e5',
144
+ brightBlack: '#666666',
145
+ brightRed: '#f14c4c',
146
+ brightGreen: '#23d18b',
147
+ brightYellow: '#f5f543',
148
+ brightBlue: '#3b8eea',
149
+ brightMagenta: '#d670d6',
150
+ brightCyan: '#29b8db',
151
+ brightWhite: '#ffffff'
152
+ }
153
+ };
154
+
155
+ // State
156
+ let terminal = null;
157
+ let fitAddon = null;
158
+ let socket = null;
159
+ let reconnectAttempts = 0;
160
+ const MAX_RECONNECT_ATTEMPTS = 5;
161
+
162
+ // DOM Elements
163
+ const statusIndicator = document.getElementById('status-indicator');
164
+ const statusText = document.getElementById('status-text');
165
+ const sessionIdEl = document.getElementById('session-id');
166
+ const terminalSizeEl = document.getElementById('terminal-size');
167
+ const btnReconnect = document.getElementById('btn-reconnect');
168
+ const btnClear = document.getElementById('btn-clear');
169
+
170
+ // Initialize terminal
171
+ function initTerminal() {
172
+ terminal = new Terminal({
173
+ theme: CONFIG.theme,
174
+ fontFamily: '"Cascadia Code", "Fira Code", "JetBrains Mono", Menlo, Monaco, monospace',
175
+ fontSize: 14,
176
+ lineHeight: 1.2,
177
+ cursorBlink: true,
178
+ cursorStyle: 'block',
179
+ scrollback: 10000,
180
+ convertEol: true,
181
+ allowTransparency: true
182
+ });
183
+
184
+ // Load addons
185
+ fitAddon = new FitAddon.FitAddon();
186
+ const webLinksAddon = new WebLinksAddon.WebLinksAddon();
187
+
188
+ terminal.loadAddon(fitAddon);
189
+ terminal.loadAddon(webLinksAddon);
190
+
191
+ // Open terminal in container
192
+ const container = document.getElementById('terminal');
193
+ terminal.open(container);
194
+
195
+ // Fit to container
196
+ fitAddon.fit();
197
+ updateTerminalSize();
198
+
199
+ // Handle user input
200
+ terminal.onData(data => {
201
+ if (socket && socket.readyState === WebSocket.OPEN) {
202
+ socket.send(JSON.stringify({
203
+ type: 'input',
204
+ data: data
205
+ }));
206
+ }
207
+ });
208
+
209
+ // Handle resize
210
+ terminal.onResize(({ cols, rows }) => {
211
+ updateTerminalSize();
212
+ if (socket && socket.readyState === WebSocket.OPEN) {
213
+ socket.send(JSON.stringify({
214
+ type: 'resize',
215
+ cols: cols,
216
+ rows: rows
217
+ }));
218
+ }
219
+ });
220
+
221
+ // Welcome message
222
+ terminal.writeln('\x1b[1;36m🐶 Welcome to Code Puppy Terminal!\x1b[0m');
223
+ terminal.writeln('\x1b[90mConnecting to server...\x1b[0m');
224
+ terminal.writeln('');
225
+
226
+ // Connect to WebSocket
227
+ connect();
228
+
229
+ // Update session ID display
230
+ sessionIdEl.textContent = CONFIG.sessionId;
231
+ }
232
+
233
+ // Update terminal size display
234
+ function updateTerminalSize() {
235
+ if (terminal) {
236
+ terminalSizeEl.textContent = `${terminal.cols}x${terminal.rows}`;
237
+ }
238
+ }
239
+
240
+ // Update connection status
241
+ function setStatus(status) {
242
+ statusIndicator.className = 'status-indicator';
243
+
244
+ switch (status) {
245
+ case 'connected':
246
+ statusIndicator.classList.add('status-connected');
247
+ statusText.textContent = 'Connected';
248
+ btnReconnect.disabled = true;
249
+ break;
250
+ case 'disconnected':
251
+ statusIndicator.classList.add('status-disconnected');
252
+ statusText.textContent = 'Disconnected';
253
+ btnReconnect.disabled = false;
254
+ break;
255
+ case 'connecting':
256
+ statusIndicator.classList.add('status-connecting');
257
+ statusText.textContent = 'Connecting...';
258
+ btnReconnect.disabled = true;
259
+ break;
260
+ }
261
+ }
262
+
263
+ // Connect to WebSocket
264
+ function connect() {
265
+ if (socket && socket.readyState === WebSocket.OPEN) {
266
+ return;
267
+ }
268
+
269
+ setStatus('connecting');
270
+
271
+ const url = CONFIG.wsUrl;
272
+ socket = new WebSocket(url);
273
+
274
+ socket.onopen = () => {
275
+ setStatus('connected');
276
+ reconnectAttempts = 0;
277
+ terminal.writeln('\x1b[1;32m✓ Connected to server\x1b[0m');
278
+ terminal.writeln('');
279
+
280
+ // Send initial resize
281
+ socket.send(JSON.stringify({
282
+ type: 'resize',
283
+ cols: terminal.cols,
284
+ rows: terminal.rows
285
+ }));
286
+ };
287
+
288
+ socket.onmessage = (event) => {
289
+ try {
290
+ const message = JSON.parse(event.data);
291
+
292
+ switch (message.type) {
293
+ case 'output':
294
+ terminal.write(new TextDecoder().decode(Uint8Array.from(atob(message.data), c => c.charCodeAt(0))));
295
+ break;
296
+ case 'error':
297
+ terminal.writeln(`\x1b[1;31mError: ${message.message}\x1b[0m`);
298
+ break;
299
+ case 'session_started':
300
+ terminal.writeln(`\x1b[90mSession started: ${message.session_id}\x1b[0m`);
301
+ break;
302
+ }
303
+ } catch (e) {
304
+ // Plain text output
305
+ terminal.write(event.data);
306
+ }
307
+ };
308
+
309
+ socket.onclose = (event) => {
310
+ setStatus('disconnected');
311
+
312
+ if (!event.wasClean) {
313
+ terminal.writeln('');
314
+ terminal.writeln('\x1b[1;31m✗ Connection lost\x1b[0m');
315
+
316
+ // Auto-reconnect with backoff
317
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
318
+ reconnectAttempts++;
319
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
320
+ terminal.writeln(`\x1b[90mReconnecting in ${delay/1000}s (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...\x1b[0m`);
321
+ setTimeout(connect, delay);
322
+ } else {
323
+ terminal.writeln('\x1b[90mMax reconnection attempts reached. Click "Reconnect" to try again.\x1b[0m');
324
+ }
325
+ }
326
+ };
327
+
328
+ socket.onerror = (error) => {
329
+ console.error('WebSocket error:', error);
330
+ };
331
+ }
332
+
333
+ // Event listeners
334
+ btnReconnect.addEventListener('click', () => {
335
+ reconnectAttempts = 0;
336
+ connect();
337
+ });
338
+
339
+ btnClear.addEventListener('click', () => {
340
+ terminal.clear();
341
+ });
342
+
343
+ // Handle window resize
344
+ window.addEventListener('resize', () => {
345
+ if (fitAddon) {
346
+ fitAddon.fit();
347
+ }
348
+ });
349
+
350
+ // Handle page unload
351
+ window.addEventListener('beforeunload', () => {
352
+ if (socket) {
353
+ socket.close();
354
+ }
355
+ });
356
+
357
+ // Initialize on DOM ready
358
+ document.addEventListener('DOMContentLoaded', initTerminal);
359
+ </script>
360
+ </body>
361
+ </html>