kiwi-code 0.0.436__tar.gz → 0.0.436.dev1__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 (59) hide show
  1. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/PKG-INFO +1 -1
  2. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/pyproject.toml +1 -1
  3. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/commands.py +0 -1
  4. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/terminal_mode.py +6 -49
  5. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_runtime/main.py +16 -5
  6. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/main.py +0 -6
  7. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/runtime_agent.py +82 -6
  8. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/dashboard.py +20 -111
  9. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/runtime_logs.py +2 -2
  10. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/slash_commands.py +0 -3
  11. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_terminal_mode.py +0 -6
  12. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/uv.lock +1 -1
  13. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/.github/workflows/publish.yml +0 -0
  14. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/.github/workflows/test.yml +0 -0
  15. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/.gitignore +0 -0
  16. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/.python-version +0 -0
  17. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/CLAUDE.md +0 -0
  18. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/Makefile +0 -0
  19. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/README.md +0 -0
  20. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/__init__.py +0 -0
  21. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/auth.py +0 -0
  22. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/checkpoints.py +0 -0
  23. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/cli.py +0 -0
  24. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/client.py +0 -0
  25. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/logger.py +0 -0
  26. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/models.py +0 -0
  27. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/runtime_manager.py +0 -0
  28. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/server.py +0 -0
  29. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_runtime/__init__.py +0 -0
  30. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_runtime/__main__.py +0 -0
  31. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/__init__.py +0 -0
  32. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/inline_file_picker.py +0 -0
  33. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/random_words.py +0 -0
  34. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/__init__.py +0 -0
  35. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/attach_content.py +0 -0
  36. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/command_result.py +0 -0
  37. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/file_browser.py +0 -0
  38. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/help.py +0 -0
  39. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/id_picker.py +0 -0
  40. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/login.py +0 -0
  41. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  42. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/slash_picker.py +0 -0
  43. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/status_words.py +0 -0
  44. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/widgets.py +0 -0
  45. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/worktrees.py +0 -0
  46. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/test_hello.py +0 -0
  47. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/__init__.py +0 -0
  48. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/conftest.py +0 -0
  49. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_checkpoints.py +0 -0
  50. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_cli_help.py +0 -0
  51. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_imports.py +0 -0
  52. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_reexec_kiwi.py +0 -0
  53. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_runtime_log_trimming.py +0 -0
  54. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_slash_commands.py +0 -0
  55. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_tokens.py +0 -0
  56. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_tui_headless.py +0 -0
  57. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_tui_interactive_runtime.py +0 -0
  58. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_tui_palette.py +0 -0
  59. {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_worktrees.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwi-code
3
- Version: 0.0.436
3
+ Version: 0.0.436.dev1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiwi-code"
3
- version = "0.0.436"
3
+ version = "0.0.436.dev1"
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.11,<4.0"
@@ -283,7 +283,6 @@ Session:
283
283
  /continue <run_id> Continue an existing run
284
284
  /new Start new conversation
285
285
  /status Show current action & run
286
- /cancel Cancel active request
287
286
 
288
287
  Files:
289
288
  /upload <path> [...] Upload file(s), attach to next message
@@ -16,14 +16,10 @@ from kiwi_cli.models import AppConfig, LoginCredentials
16
16
  from kiwi_cli.server import http_url_from_server, server_from_backend_url
17
17
  from kiwi_tui.runtime_agent import (
18
18
  RuntimeConnectArgs,
19
- bind_pending_to_run,
20
19
  ensure_runtime_for_run,
21
- start_pending_runtime,
22
20
  )
23
-
24
21
  # Keep this aligned with DashboardScreen.DEFAULT_ACTION_ID.
25
22
  DEFAULT_ACTION_ID = "69c2180355a89324a9926bc6"
26
- CONNECT_CLI_PROMPT = "Connect to the CLI right now before asking or doing anything."
27
23
 
28
24
 
29
25
  class TerminalModeError(RuntimeError):
@@ -36,7 +32,6 @@ class TerminalModeArgs:
36
32
  message: str | None = None
37
33
  action_id: str | None = None
38
34
  run_id: str | None = None
39
- connect_cli: bool = False
40
35
  json_output: bool = False
41
36
  no_stream: bool = False
42
37
  runtime_args: RuntimeConnectArgs = field(default_factory=RuntimeConnectArgs)
@@ -144,7 +139,6 @@ def _build_response_payload(started_run: _StartedRun, result: dict[str, Any]) ->
144
139
  "action_id": started_run.action_id,
145
140
  "status": status,
146
141
  "output": output,
147
- "connect_cli": False,
148
142
  }
149
143
 
150
144
 
@@ -361,41 +355,15 @@ async def _wait_for_terminal_result(
361
355
 
362
356
 
363
357
  async def _run_message_mode(session: _TerminalSession, args: TerminalModeArgs) -> dict[str, Any]:
364
- action_id = args.action_id or DEFAULT_ACTION_ID
365
- started_run = _run_action_with_retry(
366
- session,
367
- action_id=action_id,
368
- user_input=args.message or "",
369
- run_id=args.run_id,
370
- )
371
- result = await _wait_for_terminal_result(
372
- session,
373
- run_id=started_run.run_id,
374
- stream_status=not args.no_stream,
375
- emit_progress=not args.json_output,
376
- )
377
- return _build_response_payload(started_run, result)
378
-
379
-
380
- async def _run_connect_cli_mode(session: _TerminalSession, args: TerminalModeArgs) -> dict[str, Any]:
381
358
  action_id = args.action_id or DEFAULT_ACTION_ID
382
359
  runtime_args = _effective_runtime_args(args.runtime_args, session.backend_url)
383
- pending_id: str | None = None
384
-
385
- if args.run_id:
386
- _ensure_runtime_or_raise(session, runtime_args, args.run_id)
387
- else:
388
- pending_id = _start_pending_runtime_or_raise(session, runtime_args)
389
-
390
360
  started_run = _run_action_with_retry(
391
361
  session,
392
362
  action_id=action_id,
393
- user_input=CONNECT_CLI_PROMPT,
363
+ user_input=args.message or "",
394
364
  run_id=args.run_id,
395
365
  )
396
-
397
- if pending_id:
398
- _bind_pending_runtime_or_raise(pending_id, started_run.run_id)
366
+ _ensure_runtime_or_raise(session, runtime_args, started_run.run_id)
399
367
 
400
368
  result = await _wait_for_terminal_result(
401
369
  session,
@@ -403,9 +371,7 @@ async def _run_connect_cli_mode(session: _TerminalSession, args: TerminalModeArg
403
371
  stream_status=not args.no_stream,
404
372
  emit_progress=not args.json_output,
405
373
  )
406
- payload = _build_response_payload(started_run, result)
407
- payload["connect_cli"] = True
408
- return payload
374
+ return _build_response_payload(started_run, result)
409
375
 
410
376
 
411
377
  def _read_message_from_stdin() -> str | None:
@@ -423,23 +389,17 @@ def validate_terminal_mode_args(args: TerminalModeArgs) -> TerminalModeArgs:
423
389
  if args.action_id and args.run_id:
424
390
  raise TerminalModeError("`--action-id` and `--run-id` cannot be used together.")
425
391
 
426
- if args.connect_cli and args.message:
427
- raise TerminalModeError("`--connect-cli` cannot be combined with a message.")
428
-
429
392
  message = args.message
430
- if not args.connect_cli and not message:
393
+ if not message:
431
394
  message = _read_message_from_stdin()
432
395
  if not message:
433
- raise TerminalModeError(
434
- "Provide a message, pipe one on stdin, or use `--connect-cli`."
435
- )
396
+ raise TerminalModeError("Provide a message or pipe one on stdin.")
436
397
 
437
398
  return TerminalModeArgs(
438
399
  server=args.server,
439
400
  message=message,
440
401
  action_id=args.action_id,
441
402
  run_id=args.run_id,
442
- connect_cli=args.connect_cli,
443
403
  json_output=args.json_output,
444
404
  no_stream=args.no_stream,
445
405
  runtime_args=args.runtime_args,
@@ -451,10 +411,7 @@ def run_terminal_mode(args: TerminalModeArgs) -> int:
451
411
  args = validate_terminal_mode_args(args)
452
412
  session = _build_authenticated_session(args.server)
453
413
 
454
- if args.connect_cli:
455
- payload = asyncio.run(_run_connect_cli_mode(session, args))
456
- else:
457
- payload = asyncio.run(_run_message_mode(session, args))
414
+ payload = asyncio.run(_run_message_mode(session, args))
458
415
 
459
416
  status = str(payload.get("status", "") or "").lower()
460
417
  output = str(payload.get("output", "") or "")
@@ -2784,6 +2784,7 @@ async def connect(
2784
2784
  mode: str = "restricted",
2785
2785
  allowed_dirs: list[str] | None = None,
2786
2786
  agent_id: str | None = None,
2787
+ consumer_id: str | None = None,
2787
2788
  ):
2788
2789
  """Connect to the server WebSocket and process commands."""
2789
2790
  ws_endpoint = f"{ws_url}/v1/terminal/ws/connect"
@@ -2804,17 +2805,18 @@ async def connect(
2804
2805
  # Always prefer a fresh access token from ~/.kiwi/tokens.json when available.
2805
2806
  # This keeps long-running runtimes working even after kiwi-code refreshes tokens.
2806
2807
  token = await _get_valid_access_token(http_base_url, token)
2807
-
2808
- await ws.send(json.dumps({
2808
+ auth_msg = {
2809
2809
  "type": "auth",
2810
2810
  "token": token,
2811
2811
  "agent_id": agent_id,
2812
2812
  "platform": sys.platform,
2813
2813
  "mode": mode,
2814
2814
  "allowed_dirs": allowed_dirs or [],
2815
- }))
2815
+ }
2816
+ if consumer_id:
2817
+ auth_msg["consumer_id"] = str(consumer_id)
2818
+ await ws.send(json.dumps(auth_msg))
2816
2819
  auth_resp = json.loads(await ws.recv())
2817
-
2818
2820
  if auth_resp.get("type") == "error":
2819
2821
  print_status("x", f"Auth failed: {auth_resp.get('message')}", RED)
2820
2822
  return
@@ -3220,6 +3222,15 @@ def main():
3220
3222
  metavar="PATH",
3221
3223
  help="Additional allowed directory (can be specified multiple times).",
3222
3224
  )
3225
+ connect_parser.add_argument(
3226
+ "--runID",
3227
+ "--run-id",
3228
+ dest="run_id",
3229
+ default=None,
3230
+ help="Optional: pin this runtime to a specific agent-run/consumer id.",
3231
+ )
3232
+
3233
+
3223
3234
 
3224
3235
  args = parser.parse_args()
3225
3236
 
@@ -3327,7 +3338,7 @@ def main():
3327
3338
  signal.signal(signal.SIGINT, lambda *_: shutdown())
3328
3339
 
3329
3340
  try:
3330
- loop.run_until_complete(connect(ws_url, token, http_base_url=http_url, mode=mode, allowed_dirs=allowed_dirs , agent_id=agent_id,))
3341
+ loop.run_until_complete(connect(ws_url, token, http_base_url=http_url, mode=mode, allowed_dirs=allowed_dirs, agent_id=agent_id, consumer_id=args.run_id,))
3331
3342
  except asyncio.CancelledError:
3332
3343
  pass
3333
3344
  finally:
@@ -1194,11 +1194,6 @@ def main() -> int:
1194
1194
 
1195
1195
  parser.add_argument("--action-id", default=None, help="Start a fresh conversation for a specific action.")
1196
1196
  parser.add_argument("--run-id", default=None, help="Continue an existing run.")
1197
- parser.add_argument(
1198
- "--connect-cli",
1199
- action="store_true",
1200
- help="Ensure the local CLI runtime is connected for the target run/action.",
1201
- )
1202
1197
  parser.add_argument("--json", dest="json_output", action="store_true", help="Print terminal-mode output as JSON.")
1203
1198
  parser.add_argument(
1204
1199
  "--no-stream",
@@ -1240,7 +1235,6 @@ def main() -> int:
1240
1235
  message=args.message,
1241
1236
  action_id=args.action_id,
1242
1237
  run_id=args.run_id,
1243
- connect_cli=args.connect_cli,
1244
1238
  json_output=args.json_output,
1245
1239
  no_stream=args.no_stream,
1246
1240
  runtime_args=runtime_args,
@@ -259,7 +259,7 @@ def _runtime_valid(pid: int, *, meta: dict[str, Any] | None, log_path: Path) ->
259
259
  expected_ct = None
260
260
  if not _pid_matches_create_time(pid, expected_ct if isinstance(expected_ct, (int, float)) else None):
261
261
  return False
262
- # If logs show a disconnect, restart on next /connect-cli.
262
+ # If logs show a disconnect, treat the runtime as invalid so it can be restarted.
263
263
  if _log_indicates_disconnected(log_path):
264
264
  return False
265
265
  return True
@@ -419,6 +419,7 @@ def _spawn_runtime(
419
419
  pid_path: Path,
420
420
  meta_path: Path,
421
421
  meta_extra: dict[str, Any] | None = None,
422
+ consumer_id: str | None = None,
422
423
  ) -> tuple[bool, int | None, str]:
423
424
  server = args.server
424
425
  if not server:
@@ -445,6 +446,9 @@ def _spawn_runtime(
445
446
  for d in args.allow_dirs:
446
447
  cmd.extend(["--allow", d])
447
448
 
449
+ if consumer_id:
450
+ cmd.extend(["--runID", str(consumer_id)])
451
+
448
452
  env = os.environ.copy()
449
453
  env.setdefault("PYTHONUNBUFFERED", "1")
450
454
 
@@ -490,6 +494,7 @@ def _spawn_runtime(
490
494
  pass
491
495
 
492
496
  meta: dict[str, Any] = {
497
+ "cwd": os.path.realpath(cwd),
493
498
  "server": server,
494
499
  "scope": args.scope,
495
500
  "allow": list(args.allow_dirs),
@@ -510,6 +515,39 @@ def _spawn_runtime(
510
515
  return True, proc.pid, f"Runtime started (pid={proc.pid})"
511
516
 
512
517
 
518
+ def _normalize_allow_dirs(allow_dirs: tuple[str, ...] | list[str] | None) -> list[str]:
519
+ out: list[str] = []
520
+ if not allow_dirs:
521
+ return out
522
+ for d in allow_dirs:
523
+ try:
524
+ rp = os.path.realpath(str(d))
525
+ except Exception:
526
+ rp = str(d)
527
+ if rp not in out:
528
+ out.append(rp)
529
+ return sorted(out)
530
+
531
+
532
+ def _runtime_props_match(*, meta: dict[str, Any] | None, desired: dict[str, Any]) -> bool:
533
+ if not isinstance(meta, dict):
534
+ return False
535
+ try:
536
+ if str(meta.get("server") or "") != str(desired.get("server") or ""):
537
+ return False
538
+ if str(meta.get("scope") or "") != str(desired.get("scope") or ""):
539
+ return False
540
+ if _normalize_allow_dirs(meta.get("allow") or []) != _normalize_allow_dirs(desired.get("allow") or []):
541
+ return False
542
+ # Only meaningful in restricted mode; in full mode the runtime isn't restricted by cwd anyway.
543
+ if str(desired.get("scope") or "") == "restricted":
544
+ if os.path.realpath(str(meta.get("cwd") or "")) != os.path.realpath(str(desired.get("cwd") or "")):
545
+ return False
546
+ except Exception:
547
+ return False
548
+ return True
549
+
550
+
513
551
  def ensure_runtime_for_run(
514
552
  *,
515
553
  run_id: str,
@@ -521,10 +559,47 @@ def ensure_runtime_for_run(
521
559
  """Ensure a runtime exists for run_id; reuse if still alive."""
522
560
  existing = get_running_pid_for_run(run_id)
523
561
  lp = log_path_for_run(run_id)
562
+
563
+ desired_props: dict[str, Any] = {
564
+ "server": args.server,
565
+ "scope": args.scope,
566
+ "allow": list(args.allow_dirs),
567
+ "cwd": os.path.realpath(cwd),
568
+ }
569
+
524
570
  if existing:
525
- _ensure_log_trimmer(log_path=lp, pid=existing)
526
- return True, existing, lp, f"Runtime already running for run {run_id} (pid={existing})"
571
+ # Only reuse runtimes when their effective "properties" match the current session.
572
+ # This avoids inconsistent behavior when users restart the TUI with different
573
+ # flags (server/scope/allow/cwd).
574
+ meta: dict[str, Any] | None = None
575
+ mp = meta_path_for_run(run_id)
576
+ if mp.exists():
577
+ try:
578
+ meta = json.loads(mp.read_text(encoding="utf-8") or "{}")
579
+ except Exception:
580
+ meta = None
527
581
 
582
+ if _runtime_props_match(meta=meta, desired=desired_props):
583
+ _ensure_log_trimmer(log_path=lp, pid=existing)
584
+ return True, existing, lp, f"Runtime already running for run {run_id} (pid={existing})"
585
+
586
+ # Properties changed => discard and spawn a fresh runtime.
587
+ killed = False
588
+ try:
589
+ killed = bool(kill_pid(int(existing)))
590
+ except Exception:
591
+ killed = False
592
+ if not killed:
593
+ return (
594
+ False,
595
+ existing,
596
+ lp,
597
+ f"Runtime properties changed but failed to terminate existing runtime for run {run_id} (pid={existing}).",
598
+ )
599
+ try:
600
+ pid_path_for_run(run_id).unlink(missing_ok=True)
601
+ except Exception:
602
+ pass
528
603
  ok, pid, msg = _spawn_runtime(
529
604
  python_exe=python_exe,
530
605
  cwd=cwd,
@@ -534,6 +609,7 @@ def ensure_runtime_for_run(
534
609
  pid_path=pid_path_for_run(run_id),
535
610
  meta_path=meta_path_for_run(run_id),
536
611
  meta_extra={"run_id": run_id, "state": "by-run"},
612
+ consumer_id=run_id,
537
613
  )
538
614
  return ok, pid, lp if pid else None, msg
539
615
 
@@ -614,8 +690,8 @@ def bind_pending_to_run(
614
690
  pending_meta.update({"bound_run_id": run_id, "bound_mode": "soft"})
615
691
  _write_meta(pending_meta_path, pending_meta)
616
692
 
617
- # Make the by-run PID available so future /connect-cli reuses the same
618
- # runtime instead of spawning duplicates.
693
+ # Make the by-run PID available so future sessions can reuse the same runtime
694
+ # instead of spawning duplicates.
619
695
  if pending_pid:
620
696
  try:
621
697
  (dst / "pid").write_text(str(pending_pid), encoding="utf-8")
@@ -728,7 +804,7 @@ def list_known_runtimes() -> list[dict[str, Any]]:
728
804
  """List runtimes recorded on disk (by-run and pending).
729
805
 
730
806
  Returns items with an `alive` flag computed using `_runtime_valid` (not just pid_exists),
731
- so PID reuse / disconnected runtimes don't wedge `/connect-cli` and don't show up
807
+ so PID reuse / disconnected runtimes don't wedge the runtime lifecycle and don't show up
732
808
  in the exit cleanup prompt.
733
809
  """
734
810
  results: list[dict[str, Any]] = []
@@ -693,7 +693,7 @@ class DashboardScreen(Screen):
693
693
  title = f"CLI logs — pending {pending_id}"
694
694
 
695
695
  if not lp and not control_dir:
696
- self.notify("No CLI runtime logs available (use /connect-cli first).", severity="warning")
696
+ self.notify("No CLI runtime logs available yet.", severity="warning")
697
697
  return
698
698
  self.app.push_screen(RuntimeLogsScreen(lp, title=title, control_dir=control_dir))
699
699
 
@@ -1008,112 +1008,6 @@ class DashboardScreen(Screen):
1008
1008
  )
1009
1009
  return
1010
1010
 
1011
- if cmd == "/cancel":
1012
- workers = self.workers
1013
- for worker in workers:
1014
- if not worker.is_finished:
1015
- worker.cancel()
1016
- self._set_streaming(False)
1017
- show("Cancelled active request.")
1018
- return
1019
-
1020
- if cmd == "/connect-cli":
1021
- # Ensure the correct local CLI agent is running.
1022
- #
1023
- # Runtime identity is per run_id. If we haven't started a run yet
1024
- # (current_run_id is None), we start a *pending* runtime and bind it
1025
- # once the run_id is created.
1026
- try:
1027
- if self.current_run_id:
1028
- self.app.ensure_runtime_for_run_id(self.current_run_id)
1029
- else:
1030
- self.app.ensure_pending_runtime()
1031
- except Exception:
1032
- pass
1033
-
1034
- prompt = "Connect to the CLI right now before asking or doing anything."
1035
- # Show the prompt as a user message for transparency, then run it.
1036
- self.add_message(prompt, "user")
1037
- self.process_message(prompt)
1038
- return
1039
-
1040
- if cmd == "/disconnect-cli":
1041
- """Disconnect (kill) the local CLI runtime for the current run (or pending runtime)."""
1042
- from kiwi_tui.runtime_agent import (
1043
- get_running_pid_for_pending,
1044
- get_running_pid_for_run,
1045
- kill_pid,
1046
- pid_path_for_pending,
1047
- pid_path_for_run,
1048
- )
1049
-
1050
- killed = False
1051
- pid: int | None = None
1052
- target: str | None = None
1053
-
1054
- try:
1055
- if self.current_run_id:
1056
- target = f"run {self.current_run_id}"
1057
- pid = get_running_pid_for_run(self.current_run_id)
1058
- if pid:
1059
- killed = bool(kill_pid(int(pid)))
1060
- if killed:
1061
- # Remove pid file so the next /connect-cli spawns a fresh runtime.
1062
- try:
1063
- pid_path_for_run(self.current_run_id).unlink(missing_ok=True)
1064
- except Exception:
1065
- pass
1066
- else:
1067
- pending_id = getattr(self.app, "pending_runtime_id", None)
1068
- if pending_id:
1069
- target = f"pending {pending_id}"
1070
- pid = get_running_pid_for_pending(pending_id)
1071
- if pid:
1072
- killed = bool(kill_pid(int(pid)))
1073
- if killed:
1074
- try:
1075
- pid_path_for_pending(pending_id).unlink(missing_ok=True)
1076
- except Exception:
1077
- pass
1078
- # Clear the session-scoped pending runtime id only when we killed it.
1079
- try:
1080
- self.app.pending_runtime_id = None
1081
- except Exception:
1082
- pass
1083
- else:
1084
- # No PID found => stale pending id in memory; clear it.
1085
- try:
1086
- self.app.pending_runtime_id = None
1087
- except Exception:
1088
- pass
1089
- except Exception:
1090
- killed = False
1091
-
1092
- if pid and killed:
1093
- toast(
1094
- f"Disconnected CLI runtime ({target}) by killing PID {pid}.",
1095
- title="/disconnect-cli",
1096
- severity="information",
1097
- )
1098
- elif pid and not killed:
1099
- toast(
1100
- f"Failed to disconnect CLI runtime ({target}). Unable to kill PID {pid}.",
1101
- title="/disconnect-cli",
1102
- severity="error",
1103
- )
1104
- elif target:
1105
- toast(
1106
- f"No running CLI runtime found for {target}.",
1107
- title="/disconnect-cli",
1108
- severity="warning",
1109
- )
1110
- else:
1111
- toast(
1112
- "No CLI runtime is associated with this session yet.",
1113
- title="/disconnect-cli",
1114
- severity="warning",
1115
- )
1116
- return
1117
1011
 
1118
1012
  if cmd == "/show-logs":
1119
1013
  self.open_runtime_logs()
@@ -1122,7 +1016,7 @@ class DashboardScreen(Screen):
1122
1016
  if cmd == "/rewind":
1123
1017
  if getattr(self, "_is_streaming", False):
1124
1018
  toast(
1125
- "Can't rewind while a request is running. Wait for it to finish or /cancel first.",
1019
+ "Can't rewind while a request is running. Wait for it to finish first.",
1126
1020
  title="/rewind",
1127
1021
  severity="warning",
1128
1022
  timeout=4,
@@ -3264,6 +3158,15 @@ class DashboardScreen(Screen):
3264
3158
  except Exception:
3265
3159
  pass
3266
3160
  self._checkpoint_clear_active(run_dir=checkpoint_run_dir)
3161
+
3162
+ # Auto-connect local CLI runtime for every prompt.
3163
+ # For existing runs, ensure a pinned runtime is available before running the action.
3164
+ if self.current_run_id:
3165
+ try:
3166
+ self.app.ensure_runtime_for_run_id(self.current_run_id)
3167
+ except Exception:
3168
+ pass
3169
+
3267
3170
  # If this is a continuation (we already have a run_id), start the checkpoint entry
3268
3171
  # *before* sending the request to reduce races with early file edits.
3269
3172
  if self.current_run_id:
@@ -3394,14 +3297,20 @@ class DashboardScreen(Screen):
3394
3297
  else:
3395
3298
  self.current_run_id = run_id
3396
3299
  self.current_run_kind = "action"
3397
- # If a pending runtime was started (e.g. via /connect-cli before the
3398
- # run existed), bind it now that we have a run_id.
3300
+ # If a pending runtime was started before the run existed, bind it now that we have a run_id.
3399
3301
  try:
3400
3302
  self.app.bind_pending_runtime_to_run(run_id)
3401
3303
  except Exception:
3402
3304
  pass
3403
3305
  logger.info(f"Started new conversation with run_id: {run_id}")
3404
3306
 
3307
+ # For new runs, ensure a pinned runtime is started as soon as we have a run_id.
3308
+ try:
3309
+ if self.current_run_id:
3310
+ self.app.ensure_runtime_for_run_id(self.current_run_id)
3311
+ except Exception:
3312
+ pass
3313
+
3405
3314
  self._update_run_status_bar()
3406
3315
 
3407
3316
  # Cache run name (best-effort) so the quit-time runtime cleanup prompt can show it.
@@ -3759,7 +3668,7 @@ class DashboardScreen(Screen):
3759
3668
  # impose a hard timeout on the SSE stream; long-running actions may
3760
3669
  # legitimately take more than a few minutes. If the stream ends early
3761
3670
  # (e.g. network drop), we continue polling until we can fetch a terminal
3762
- # result. Users can always use /cancel to stop waiting.
3671
+ # result.
3763
3672
  status_task: asyncio.Task | None = asyncio.create_task(
3764
3673
  client.stream_action_result(run_id, handle_status_message)
3765
3674
  )
@@ -311,7 +311,7 @@ class RuntimeLogsScreen(Screen):
311
311
  rendered_any = self._attach_interactive_stream(load_existing=True) or rendered_any
312
312
 
313
313
  if not rendered_any:
314
- self._write_placeholder("No runtime log is available yet. Use /connect-cli to start a runtime.")
314
+ self._write_placeholder("No runtime log is available yet. A runtime will start automatically when you send a message.")
315
315
 
316
316
  self._refresh_status_bar()
317
317
  self._refresh_controls()
@@ -379,7 +379,7 @@ class RuntimeLogsScreen(Screen):
379
379
  elif self._log_path and self._log_path.exists():
380
380
  status.update("Runtime log (read-only). Open this screen during an interactive run to send input.")
381
381
  else:
382
- status.update("No runtime output available yet. Use /connect-cli to start a runtime.")
382
+ status.update("No runtime output available yet. A runtime will start automatically when you send a message.")
383
383
 
384
384
  def _refresh_controls(self) -> None:
385
385
  controls = self.query_one("#runtime-console-controls", Horizontal)
@@ -18,7 +18,6 @@ SLASH_COMMANDS: list[SlashCommand] = [
18
18
  SlashCommand("Session", "/help", "Show all slash commands", "/help"),
19
19
  SlashCommand("Session", "/new", "Start new conversation", "/new"),
20
20
  SlashCommand("Session", "/status", "Show current action & run", "/status"),
21
- SlashCommand("Session", "/cancel", "Cancel active request", "/cancel"),
22
21
  SlashCommand("Session", "/use <action_id>", "Switch to a different action", "/use "),
23
22
  SlashCommand("Session", "/autocode-select", "Choose the version of AutoCode you wish to use", "/autocode-select"),
24
23
  SlashCommand("Session", "/name <new name>", "Rename the current run (action run or graph run)", "/name "),
@@ -43,8 +42,6 @@ SLASH_COMMANDS: list[SlashCommand] = [
43
42
  SlashCommand("Metadata", "/metadata clear", "Clear all metadata overrides", "/metadata clear"),
44
43
 
45
44
  # Runtime (local CLI agent)
46
- SlashCommand("Runtime", "/connect-cli", "Tell the agent to connect to the local CLI", "/connect-cli"),
47
- SlashCommand("Runtime", "/disconnect-cli", "Disconnect the local CLI runtime for the current run", "/disconnect-cli"),
48
45
  SlashCommand("Runtime", "/show-logs", "Show local CLI (runtime) logs", "/show-logs"),
49
46
  SlashCommand("Runtime", "/runtime", "Runtime commands (currently disabled)", "/runtime"),
50
47
 
@@ -20,12 +20,6 @@ def test_validate_terminal_mode_rejects_action_and_run() -> None:
20
20
  )
21
21
 
22
22
 
23
- def test_validate_terminal_mode_rejects_connect_cli_with_message() -> None:
24
- with pytest.raises(TerminalModeError):
25
- validate_terminal_mode_args(
26
- TerminalModeArgs(message="hi", connect_cli=True)
27
- )
28
-
29
23
 
30
24
  def test_validate_terminal_mode_reads_message_from_stdin(monkeypatch: pytest.MonkeyPatch) -> None:
31
25
  monkeypatch.setattr("kiwi_cli.terminal_mode.sys.stdin", _FakeStdin("hello from stdin", is_tty=False))
@@ -397,7 +397,7 @@ wheels = [
397
397
 
398
398
  [[package]]
399
399
  name = "kiwi-code"
400
- version = "0.0.436"
400
+ version = "0.0.436.dev1"
401
401
  source = { editable = "." }
402
402
  dependencies = [
403
403
  { name = "autobots-client" },
File without changes
File without changes
File without changes
File without changes