kiwi-code 0.0.5__tar.gz → 0.0.6__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 (33) hide show
  1. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/.gitignore +1 -1
  2. kiwi_code-0.0.6/CLAUDE.md +79 -0
  3. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/PKG-INFO +7 -7
  4. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/README.md +4 -4
  5. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/pyproject.toml +5 -5
  6. kiwi_code-0.0.6/src/kiwi_cli/__init__.py +3 -0
  7. {kiwi_code-0.0.5/src/kiwi_tui → kiwi_code-0.0.6/src/kiwi_cli}/auth.py +2 -2
  8. {kiwi_code-0.0.5/src/kiwi_tui → kiwi_code-0.0.6/src/kiwi_cli}/cli.py +1 -1
  9. {kiwi_code-0.0.5/src/kiwi_tui → kiwi_code-0.0.6/src/kiwi_cli}/client.py +80 -9
  10. {kiwi_code-0.0.5/src/kiwi_tui → kiwi_code-0.0.6/src/kiwi_cli}/commands.py +20 -0
  11. {kiwi_code-0.0.5/src/kiwi_tui → kiwi_code-0.0.6/src/kiwi_cli}/config.py +2 -2
  12. {kiwi_code-0.0.5/src/kiwi_tui → kiwi_code-0.0.6/src/kiwi_cli}/logger.py +1 -1
  13. {kiwi_code-0.0.5/src/kiwi_tui → kiwi_code-0.0.6/src/kiwi_cli}/runtime_manager.py +2 -2
  14. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/src/kiwi_runtime/main.py +1 -1
  15. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/src/kiwi_tui/main.py +35 -5
  16. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/src/kiwi_tui/screens/actions.py +2 -2
  17. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/src/kiwi_tui/screens/autobots.py +2 -2
  18. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/src/kiwi_tui/screens/dashboard.py +531 -11
  19. kiwi_code-0.0.6/src/kiwi_tui/screens/file_browser.py +182 -0
  20. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/src/kiwi_tui/screens/login.py +2 -2
  21. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/uv.lock +1 -1
  22. kiwi_code-0.0.5/poetry.lock +0 -1570
  23. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/.github/workflows/publish.yml +0 -0
  24. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/.python-version +0 -0
  25. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/Makefile +0 -0
  26. {kiwi_code-0.0.5/src/kiwi_tui → kiwi_code-0.0.6/src/kiwi_cli}/models.py +0 -0
  27. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/src/kiwi_runtime/__init__.py +0 -0
  28. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/src/kiwi_runtime/__main__.py +0 -0
  29. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/src/kiwi_tui/__init__.py +0 -0
  30. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/src/kiwi_tui/screens/__init__.py +0 -0
  31. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  32. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/src/kiwi_tui/widgets.py +0 -0
  33. {kiwi_code-0.0.5 → kiwi_code-0.0.6}/test_hello.py +0 -0
@@ -29,4 +29,4 @@ Thumbs.db
29
29
  *.log
30
30
 
31
31
  # Local config & tokens
32
- .autobots-tui/
32
+ .kiwi/
@@ -0,0 +1,79 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ Kiwi Code is a TUI + CLI client for the Kiwi AI platform. It provides a chat interface (Textual) and CLI tools (Typer) for managing AI actions, action graphs, and their runs. A WebSocket-based runtime agent executes shell commands on behalf of the LLM.
8
+
9
+ ## Build & Run Commands
10
+
11
+ ```bash
12
+ # Package manager: uv | Build backend: hatchling | Python: 3.13+
13
+ uv sync # Install/sync dependencies
14
+ uv build # Build package
15
+
16
+ # Run
17
+ make run # Launch TUI (kiwi)
18
+ make dev # TUI with live reload
19
+ make serve # Serve TUI in browser (port 8566)
20
+ make cli # Run CLI (kiwicli)
21
+ make runtime ARGS="..." # Run runtime agent with args
22
+
23
+ # Test
24
+ make test-hello # Test backend API connection
25
+ uv run python test_hello.py # Same, without make
26
+
27
+ # Clean
28
+ make clean # Remove caches and build artifacts
29
+ ```
30
+
31
+ There are three entry points defined in pyproject.toml:
32
+ - `kiwi` → `kiwi_tui.main:main` (TUI)
33
+ - `kiwicli` → `kiwi_cli.cli:cli` (CLI)
34
+ - `kiwi-runtime` → `kiwi_runtime.main:main` (WebSocket agent)
35
+
36
+ ## Architecture
37
+
38
+ ```
39
+ ┌─────────────────────────────────┐
40
+ │ Shared Libraries │
41
+ │ commands.py · client.py │
42
+ │ auth.py · models.py · config.py│
43
+ └──────────┬──────────┬───────────┘
44
+ │ │
45
+ ┌──────▼──────┐ └──────────────┐
46
+ │ kiwi (TUI) │ kiwicli (CLI) │
47
+ │ Textual app │ Typer CLI │
48
+ └──────┬──────┘ └──────────────┘
49
+ │ auto-starts
50
+ ┌──────▼──────────────────┐
51
+ │ kiwi-runtime │
52
+ │ WebSocket agent │
53
+ │ One per working dir │
54
+ │ Survives TUI restarts │
55
+ └─────────────────────────┘
56
+ ```
57
+
58
+ **Key design decisions:**
59
+ - Shared infrastructure lives in `kiwi_cli`; TUI imports from it via absolute imports (e.g., `from kiwi_cli.auth import TokenManager`)
60
+ - One runtime agent per working directory, managed via `runtime_manager.py`
61
+ - Runtime state stored in `~/.kiwi/runtimes/<key>/` (pid, log, cwd, tokens)
62
+ - Auth tokens stored in `~/.kiwi/tokens.json` with 0600 permissions
63
+ - Config stored in `~/.kiwi/config.json`
64
+
65
+ ## Three Source Packages
66
+
67
+ - **`src/kiwi_cli/`** — Typer CLI app and shared infrastructure: auth, client, config, commands, models, runtime_manager, logger. Both `kiwi_tui` and the CLI entry point depend on this package.
68
+ - **`src/kiwi_tui/`** — Textual TUI app, screens, and widgets. Imports shared modules from `kiwi_cli`.
69
+ - **`src/kiwi_runtime/`** — Standalone WebSocket agent (~43KB `main.py`) that connects to Kiwi server and executes shell commands, sandboxed to CWD by default
70
+
71
+ ## Server Presets (runtime)
72
+
73
+ - `app` → `https://api.meetkiwi.ai` (production)
74
+ - `dev` → `https://dev.api.myautobots.com` (default for TUI)
75
+ - `local` → `http://localhost:8000`
76
+
77
+ ## CI/CD
78
+
79
+ GitHub Actions (`publish.yml`): triggered by manual dispatch or `v*.*.*` tags. Runs smoke tests (`kiwicli --help`, `kiwi-runtime --help`, `kiwicli runtime status`), builds, and publishes to PyPI.
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwi-code
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Summary: A textual-based terminal user interface application
5
5
  Project-URL: Homepage, https://meetkiwi.ai
6
6
  Project-URL: Repository, https://github.com/jetoslabs/kiwi-code
7
7
  Author-email: Anurag Jha <anurag@meetkiwi.co>
8
- License: MIT
8
+ License: Proprietary
9
9
  Keywords: cli,terminal,textual,tui
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: License :: Other/Proprietary License
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Python: <4.0,>=3.13
@@ -77,7 +77,7 @@ Kiwi Code has three entry points, but they are **not three services** — they a
77
77
  - **`kiwi` automatically spawns `kiwi-runtime`** when you log in. The runtime runs as an independent process — quitting or restarting the TUI does **not** kill it.
78
78
  - **One runtime per directory.** Each working directory gets its own runtime, scoped to that directory. Opening the TUI in two different project folders runs two independent runtimes.
79
79
  - **`kiwicli` is for headless/scripting use.** It provides the same query commands (actions, runs, graphs) as the TUI, but as standalone CLI commands.
80
- - **Authentication is shared.** Both use `TokenManager` to read/write tokens from `~/.autobots-tui/tokens.json`, so logging in via one works for the other.
80
+ - **Authentication is shared.** Both use `TokenManager` to read/write tokens from `~/.kiwi/tokens.json`, so logging in via one works for the other.
81
81
  - **Runtime lifecycle is independent.** The runtime persists across TUI restarts. Use `kiwicli runtime stop` to explicitly stop it.
82
82
 
83
83
  ## Installation
@@ -207,7 +207,7 @@ In **restricted mode** (default), commands are sandboxed to the current working
207
207
 
208
208
  ## Configuration
209
209
 
210
- Stored at `~/.autobots-tui/config.json`:
210
+ Stored at `~/.kiwi/config.json`:
211
211
 
212
212
  ```json
213
213
  {
@@ -218,7 +218,7 @@ Stored at `~/.autobots-tui/config.json`:
218
218
  }
219
219
  ```
220
220
 
221
- Logs are written to `~/.autobots-tui/autobots_tui.log`.
221
+ Logs are written to `~/.kiwi/kiwi_tui.log`.
222
222
 
223
223
  ## Development
224
224
 
@@ -231,4 +231,4 @@ uv run kiwi
231
231
 
232
232
  ## License
233
233
 
234
- MIT
234
+ Proprietary. All rights reserved.
@@ -51,7 +51,7 @@ Kiwi Code has three entry points, but they are **not three services** — they a
51
51
  - **`kiwi` automatically spawns `kiwi-runtime`** when you log in. The runtime runs as an independent process — quitting or restarting the TUI does **not** kill it.
52
52
  - **One runtime per directory.** Each working directory gets its own runtime, scoped to that directory. Opening the TUI in two different project folders runs two independent runtimes.
53
53
  - **`kiwicli` is for headless/scripting use.** It provides the same query commands (actions, runs, graphs) as the TUI, but as standalone CLI commands.
54
- - **Authentication is shared.** Both use `TokenManager` to read/write tokens from `~/.autobots-tui/tokens.json`, so logging in via one works for the other.
54
+ - **Authentication is shared.** Both use `TokenManager` to read/write tokens from `~/.kiwi/tokens.json`, so logging in via one works for the other.
55
55
  - **Runtime lifecycle is independent.** The runtime persists across TUI restarts. Use `kiwicli runtime stop` to explicitly stop it.
56
56
 
57
57
  ## Installation
@@ -181,7 +181,7 @@ In **restricted mode** (default), commands are sandboxed to the current working
181
181
 
182
182
  ## Configuration
183
183
 
184
- Stored at `~/.autobots-tui/config.json`:
184
+ Stored at `~/.kiwi/config.json`:
185
185
 
186
186
  ```json
187
187
  {
@@ -192,7 +192,7 @@ Stored at `~/.autobots-tui/config.json`:
192
192
  }
193
193
  ```
194
194
 
195
- Logs are written to `~/.autobots-tui/autobots_tui.log`.
195
+ Logs are written to `~/.kiwi/kiwi_tui.log`.
196
196
 
197
197
  ## Development
198
198
 
@@ -205,4 +205,4 @@ uv run kiwi
205
205
 
206
206
  ## License
207
207
 
208
- MIT
208
+ Proprietary. All rights reserved.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiwi-code"
3
- version = "0.0.5"
3
+ version = "0.0.6"
4
4
  description = "A textual-based terminal user interface application"
5
5
  readme = {file = "README.md", content-type = "text/markdown"}
6
6
  requires-python = ">=3.13,<4.0"
@@ -18,12 +18,12 @@ dependencies = [
18
18
  authors = [
19
19
  { name = "Anurag Jha", email = "anurag@meetkiwi.co" }
20
20
  ]
21
- license = { text = "MIT" }
21
+ license = { text = "Proprietary" }
22
22
  keywords = ["tui", "textual", "terminal", "cli"]
23
23
  classifiers = [
24
24
  "Development Status :: 3 - Alpha",
25
25
  "Intended Audience :: Developers",
26
- "License :: OSI Approved :: MIT License",
26
+ "License :: Other/Proprietary License",
27
27
  "Programming Language :: Python :: 3",
28
28
  "Programming Language :: Python :: 3.13",
29
29
  ]
@@ -34,11 +34,11 @@ Repository = "https://github.com/jetoslabs/kiwi-code"
34
34
 
35
35
  [project.scripts]
36
36
  kiwi = "kiwi_tui.main:main"
37
- kiwicli = "kiwi_tui.cli:cli"
37
+ kiwicli = "kiwi_cli.cli:cli"
38
38
  kiwi-runtime = "kiwi_runtime.main:main"
39
39
 
40
40
  [tool.hatch.build.targets.wheel]
41
- packages = ["src/kiwi_tui", "src/kiwi_runtime"]
41
+ packages = ["src/kiwi_cli", "src/kiwi_tui", "src/kiwi_runtime"]
42
42
 
43
43
  [build-system]
44
44
  requires = ["hatchling"]
@@ -0,0 +1,3 @@
1
+ """Kiwi CLI - command-line interface and shared infrastructure modules."""
2
+
3
+ __version__ = "0.1.0"
@@ -15,10 +15,10 @@ class TokenManager:
15
15
  """Initialize token manager.
16
16
 
17
17
  Args:
18
- token_path: Path to token file, defaults to ~/.autobots-tui/tokens.json
18
+ token_path: Path to token file, defaults to ~/.kiwi/tokens.json
19
19
  """
20
20
  if token_path is None:
21
- token_path = Path.home() / ".autobots-tui" / "tokens.json"
21
+ token_path = Path.home() / ".kiwi" / "tokens.json"
22
22
 
23
23
  self.token_path = token_path
24
24
  self._tokens: Optional[AuthTokens] = None
@@ -231,7 +231,7 @@ def whoami():
231
231
  @app.command()
232
232
  def tui():
233
233
  """Launch the interactive TUI."""
234
- from .main import main
234
+ from kiwi_tui.main import main
235
235
  main()
236
236
 
237
237
 
@@ -229,39 +229,110 @@ class AutobotsClientWrapper:
229
229
  logger.error(error_msg)
230
230
  return False, None, error_msg
231
231
 
232
- def run_action_async(self, action_id: str, user_input: str, action_result_id: Optional[str] = None) -> tuple[bool, Optional[str], str]:
232
+ def upload_files(self, file_paths: list[str]) -> tuple[bool, list[str], str]:
233
+ """Upload files via /v1/files/ API and return their URLs.
234
+
235
+ Args:
236
+ file_paths: List of local file paths to upload
237
+
238
+ Returns:
239
+ Tuple of (success, urls, message)
240
+ """
241
+ try:
242
+ if not isinstance(self.client, AuthenticatedClient):
243
+ return False, [], "Authentication required"
244
+
245
+ import mimetypes
246
+ from pathlib import Path
247
+ from autobots_client.api.files import upload_files_v1_files_post
248
+ from autobots_client.models.body_upload_files_v1_files_post import BodyUploadFilesV1FilesPost
249
+
250
+ # Validate all files exist first
251
+ paths = []
252
+ for fp in file_paths:
253
+ p = Path(fp).expanduser().resolve()
254
+ if not p.exists():
255
+ return False, [], f"File not found: {fp}"
256
+ paths.append(p)
257
+
258
+ # The SDK's BodyUploadFilesV1FilesPost.to_multipart() sends files as
259
+ # text/plain strings, which doesn't work for binary uploads.
260
+ # Use the SDK's endpoint URL but build the multipart manually.
261
+ multipart_files = []
262
+ file_handles = []
263
+ for p in paths:
264
+ mime = mimetypes.guess_type(str(p))[0] or "application/octet-stream"
265
+ fh = open(p, "rb")
266
+ file_handles.append(fh)
267
+ multipart_files.append(("files", (p.name, fh, mime)))
268
+
269
+ http_client = self.client.get_httpx_client()
270
+ response = http_client.request(
271
+ method="POST",
272
+ url="/v1/files/public/",
273
+ files=multipart_files,
274
+ )
275
+
276
+ # Close opened file handles
277
+ for fh in file_handles:
278
+ fh.close()
279
+
280
+ if response.status_code == 200:
281
+ result = response.json()
282
+ urls = [item["url"] for item in result if "url" in item]
283
+ names = [item.get("name", "?") for item in result]
284
+ logger.info(f"Uploaded {len(urls)} file(s): {names}")
285
+ return True, urls, f"Uploaded {len(urls)} file(s)"
286
+ else:
287
+ error_msg = f"Upload failed: HTTP {response.status_code}"
288
+ logger.error(error_msg)
289
+ return False, [], error_msg
290
+
291
+ except Exception as e:
292
+ error_msg = f"Upload error: {e}"
293
+ logger.error(error_msg)
294
+ return False, [], error_msg
295
+
296
+ def run_action_async(self, action_id: str, user_input: str, action_result_id: Optional[str] = None, urls: Optional[list[str]] = None, metadata: Optional[dict] = None) -> tuple[bool, Optional[str], str]:
233
297
  """Run an action asynchronously and return the run ID.
234
298
 
235
299
  Args:
236
300
  action_id: ID of the action to run
237
301
  user_input: User input text for the action
238
302
  action_result_id: Optional ID to continue an existing conversation
303
+ urls: Optional list of file URLs to attach
304
+ metadata: Optional metadata dict to merge into the DataBlock
239
305
 
240
306
  Returns:
241
307
  Tuple of (success, run_id, message)
242
308
  """
243
309
  try:
244
310
  if action_result_id:
245
- logger.info(f"Running action {action_id} with input: {user_input}, continuing run_id: {action_result_id}")
311
+ logger.info(f"Running action {action_id} with input: {user_input}, urls: {urls}, continuing run_id: {action_result_id}")
246
312
  else:
247
- logger.info(f"Running action {action_id} with input: {user_input}")
313
+ logger.info(f"Running action {action_id} with input: {user_input}, urls: {urls}")
248
314
 
249
315
  if not isinstance(self.client, AuthenticatedClient):
250
316
  return False, None, "Authentication required"
251
317
 
252
318
  # Create input body - DataBlock structure
319
+ default_metadata = {
320
+ "process_url_in_text": False,
321
+ "process_attachment_url": True,
322
+ "attachment_requested": 0
323
+ }
324
+ if metadata:
325
+ default_metadata.update(metadata)
326
+
253
327
  input_data = BodyAsyncRunActionV1ActionsIdAsyncRunPostInput()
254
328
  input_data.additional_properties = {
255
329
  "text": user_input,
256
- "urls": [],
257
- "metadata": {
258
- "process_url_in_text": False,
259
- "process_attachment_url": True,
260
- "attachment_requested": 0
261
- }
330
+ "urls": urls or [],
331
+ "metadata": default_metadata
262
332
  }
263
333
 
264
334
  body = BodyAsyncRunActionV1ActionsIdAsyncRunPost(input_=input_data)
335
+ logger.info(f"Request body: {body.to_dict()}")
265
336
 
266
337
  # Import UNSET for optional parameter
267
338
  from autobots_client.types import UNSET
@@ -296,6 +296,26 @@ Session:
296
296
  /continue <run_id> Continue an existing run
297
297
  /new Start new conversation
298
298
  /status Show current action & run
299
+ /cancel Cancel active request
300
+
301
+ Files:
302
+ /upload <path> [...] Upload file(s), attach to next message
303
+ /files Show pending file uploads
304
+ /clear-files Clear pending file uploads
305
+
306
+ Metadata (persistent across runs):
307
+ /metadata Show current metadata
308
+ /metadata set <k> <v> Set a metadata field
309
+ /metadata remove <k> Remove a metadata field
310
+ /metadata clear Clear all metadata
311
+
312
+ Runtime:
313
+ /runtime status Show runtime status
314
+ /runtime start Start runtime
315
+ /runtime stop Stop runtime
316
+ /runtime restart Restart runtime
317
+ /runtime list List all runtimes
318
+ /runtime logs Show runtime logs
299
319
 
300
320
  Query:
301
321
  /actions list [--name NAME] [--limit N]
@@ -14,10 +14,10 @@ class ConfigManager:
14
14
  """Initialize config manager.
15
15
 
16
16
  Args:
17
- config_path: Path to config file, defaults to ~/.autobots-tui/config.json
17
+ config_path: Path to config file, defaults to ~/.kiwi/config.json
18
18
  """
19
19
  if config_path is None:
20
- config_path = Path.home() / ".autobots-tui" / "config.json"
20
+ config_path = Path.home() / ".kiwi" / "config.json"
21
21
 
22
22
  self.config_path = config_path
23
23
  self._config: Optional[AppConfig] = None
@@ -16,7 +16,7 @@ def setup_logging(log_level: str = "INFO", log_file: str = "kiwi_tui.log") -> No
16
16
  logger.remove()
17
17
 
18
18
  # Only log to file, not to console (to avoid interference with Textual UI)
19
- log_path = Path.home() / ".autobots-tui" / log_file
19
+ log_path = Path.home() / ".kiwi" / log_file
20
20
  log_path.parent.mkdir(parents=True, exist_ok=True)
21
21
 
22
22
  logger.add(
@@ -1,7 +1,7 @@
1
1
  """Shared helpers for managing per-directory kiwi-runtime processes.
2
2
 
3
3
  Each working directory gets its own runtime, identified by a hash of the
4
- absolute path. State is stored under ~/.autobots-tui/runtimes/<key>/:
4
+ absolute path. State is stored under ~/.kiwi/runtimes/<key>/:
5
5
  pid — PID of the running process
6
6
  log — stdout/stderr of the runtime
7
7
  cwd — the working directory this runtime is scoped to
@@ -13,7 +13,7 @@ import signal
13
13
  import time
14
14
  from pathlib import Path
15
15
 
16
- RUNTIMES_DIR = Path.home() / ".autobots-tui" / "runtimes"
16
+ RUNTIMES_DIR = Path.home() / ".kiwi" / "runtimes"
17
17
 
18
18
 
19
19
  def runtime_key(cwd: str | None = None) -> str:
@@ -978,7 +978,7 @@ def main():
978
978
  connect_parser.add_argument(
979
979
  "--token",
980
980
  default=None,
981
- help="Access token to skip login (used by autobots-tui for shared auth)",
981
+ help="Access token to skip login (used by kiwi for shared auth)",
982
982
  )
983
983
  connect_parser.add_argument(
984
984
  "--scope",
@@ -8,11 +8,11 @@ from textual.app import App
8
8
  from textual.binding import Binding
9
9
  from loguru import logger
10
10
 
11
- from .logger import setup_logging
12
- from .config import ConfigManager
13
- from .client import AutobotsClientWrapper
14
- from .auth import TokenManager
15
- from . import runtime_manager
11
+ from kiwi_cli.logger import setup_logging
12
+ from kiwi_cli.config import ConfigManager
13
+ from kiwi_cli.client import AutobotsClientWrapper
14
+ from kiwi_cli.auth import TokenManager
15
+ from kiwi_cli import runtime_manager
16
16
  from .screens import LoginScreen, DashboardScreen, AutobotsScreen, ActionsScreen, RuntimeLogsScreen
17
17
 
18
18
 
@@ -54,6 +54,7 @@ class AutobotsTUI(App):
54
54
  Binding("ctrl+c", "quit", "Quit", show=True),
55
55
  Binding("ctrl+d", "toggle_dark", "Toggle Dark Mode", show=False),
56
56
  Binding("ctrl+r", "runtime_logs", "Runtime Logs", show=True),
57
+ Binding("ctrl+k", "toggle_runtime", "Stop/Start Runtime", show=True),
57
58
  Binding("ctrl+l", "logout", "Logout", show=False),
58
59
  ]
59
60
 
@@ -267,9 +268,12 @@ class AutobotsTUI(App):
267
268
 
268
269
  def _stop_runtime(self) -> None:
269
270
  """Explicitly stop the kiwi-runtime for the current directory."""
271
+ # Drain any remaining output before closing
272
+ self._drain_kiwi_output()
270
273
  runtime_manager.stop_runtime()
271
274
  self._kiwi_pid = None
272
275
  self._close_log_tail()
276
+ self._kiwi_log_lines.append("--- Runtime disconnected ---")
273
277
 
274
278
  # ------------------------------------------------------------------
275
279
  # Periodic token refresh
@@ -320,6 +324,32 @@ class AutobotsTUI(App):
320
324
 
321
325
  # ------------------------------------------------------------------
322
326
 
327
+ def action_toggle_runtime(self) -> None:
328
+ """Toggle the kiwi-runtime: stop if running, start if stopped."""
329
+ if self._kiwi_pid:
330
+ self._stop_runtime()
331
+ logger.info("Runtime stopped by user")
332
+ self.notify("Runtime stopped (Ctrl+K to start)", severity="information")
333
+ else:
334
+ self._start_kiwi_session()
335
+ if self._kiwi_pid:
336
+ logger.info("Runtime started by user")
337
+ self.notify("Runtime started", severity="information")
338
+ else:
339
+ self.notify("Failed to start runtime", severity="error")
340
+
341
+ def action_restart_runtime(self) -> None:
342
+ """Restart the kiwi-runtime for the current directory."""
343
+ if self._kiwi_pid:
344
+ self._stop_runtime()
345
+ logger.info("Runtime stopped for restart")
346
+ self._start_kiwi_session()
347
+ if self._kiwi_pid:
348
+ logger.info("Runtime restarted by user")
349
+ self.notify("Runtime restarted", severity="information")
350
+ else:
351
+ self.notify("Failed to restart runtime", severity="error")
352
+
323
353
  def action_runtime_logs(self) -> None:
324
354
  """Show the runtime log stream."""
325
355
  self.push_screen("runtime_logs")
@@ -7,8 +7,8 @@ from textual.containers import Container, Horizontal, Vertical, ScrollableContai
7
7
  from textual.widgets import Header, Footer, Static, DataTable, Button, Input, Label
8
8
  from loguru import logger
9
9
 
10
- from ..models import Action, ActionExecution, ActionStatus
11
- from ..widgets import StatusBadge, ActionButton, InfoPanel
10
+ from kiwi_cli.models import Action, ActionExecution, ActionStatus
11
+ from kiwi_tui.widgets import StatusBadge, ActionButton, InfoPanel
12
12
 
13
13
 
14
14
  class ActionsScreen(Screen):
@@ -7,8 +7,8 @@ from textual.containers import Container, Vertical, Horizontal
7
7
  from textual.widgets import Header, Footer, Static, DataTable, Button
8
8
  from loguru import logger
9
9
 
10
- from ..models import Autobot, AutobotType
11
- from ..widgets import StatusBadge, ActionButton
10
+ from kiwi_cli.models import Autobot, AutobotType
11
+ from kiwi_tui.widgets import StatusBadge, ActionButton
12
12
 
13
13
 
14
14
  class AutobotsScreen(Screen):