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.
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/PKG-INFO +1 -1
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/pyproject.toml +1 -1
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/commands.py +0 -1
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/terminal_mode.py +6 -49
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_runtime/main.py +16 -5
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/main.py +0 -6
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/runtime_agent.py +82 -6
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/dashboard.py +20 -111
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/runtime_logs.py +2 -2
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/slash_commands.py +0 -3
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_terminal_mode.py +0 -6
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/uv.lock +1 -1
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/.gitignore +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/.python-version +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/CLAUDE.md +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/Makefile +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/README.md +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/checkpoints.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/src/kiwi_tui/worktrees.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/test_hello.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/__init__.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/conftest.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_checkpoints.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_slash_commands.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_tui_headless.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_tui_interactive_runtime.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_tui_palette.py +0 -0
- {kiwi_code-0.0.436 → kiwi_code-0.0.436.dev1}/tests/test_worktrees.py +0 -0
|
@@ -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=
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
526
|
-
|
|
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
|
|
618
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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))
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|