kiwi-code 0.0.40__tar.gz → 0.0.41__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.40 → kiwi_code-0.0.41}/PKG-INFO +1 -1
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/pyproject.toml +1 -1
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_runtime/main.py +226 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/runtime_agent.py +27 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/screens/dashboard.py +12 -3
- kiwi_code-0.0.41/src/kiwi_tui/screens/runtime_logs.py +473 -0
- kiwi_code-0.0.41/tests/test_tui_interactive_runtime.py +387 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/uv.lock +1 -1
- kiwi_code-0.0.40/src/kiwi_tui/screens/runtime_logs.py +0 -186
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/.gitignore +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/.python-version +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/CLAUDE.md +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/Makefile +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/README.md +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_cli/terminal_mode.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_runtime/snake_game/.gitignore +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/main.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/slash_commands.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/test_hello.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/tests/__init__.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/tests/conftest.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/tests/test_slash_commands.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/tests/test_terminal_mode.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/tests/test_tui_headless.py +0 -0
- {kiwi_code-0.0.40 → kiwi_code-0.0.41}/tests/test_tui_palette.py +0 -0
|
@@ -1713,6 +1713,7 @@ async def run_interactive_command(
|
|
|
1713
1713
|
command: str,
|
|
1714
1714
|
mode: str = "restricted",
|
|
1715
1715
|
allowed_dirs: list[str] | None = None,
|
|
1716
|
+
request_id: str | None = None,
|
|
1716
1717
|
) -> dict:
|
|
1717
1718
|
"""Execute a command with the user's terminal attached for interactive I/O."""
|
|
1718
1719
|
if mode == "restricted" and allowed_dirs:
|
|
@@ -1720,6 +1721,16 @@ async def run_interactive_command(
|
|
|
1720
1721
|
if not ok:
|
|
1721
1722
|
return {"stdout": "", "stderr": reason, "exit_code": 1}
|
|
1722
1723
|
|
|
1724
|
+
control_dir = _tui_managed_control_dir()
|
|
1725
|
+
if control_dir is not None:
|
|
1726
|
+
return await run_tui_managed_interactive_command(
|
|
1727
|
+
command,
|
|
1728
|
+
request_id=request_id or str(uuid.uuid4()),
|
|
1729
|
+
control_dir=control_dir,
|
|
1730
|
+
mode=mode,
|
|
1731
|
+
allowed_dirs=allowed_dirs,
|
|
1732
|
+
)
|
|
1733
|
+
|
|
1723
1734
|
try:
|
|
1724
1735
|
cwd = allowed_dirs[0] if allowed_dirs else None
|
|
1725
1736
|
print(f"\n {CB}{'─' * 50}{RESET}")
|
|
@@ -2262,6 +2273,220 @@ def _ensure_windows_persistent_completion(data: str) -> str:
|
|
|
2262
2273
|
|
|
2263
2274
|
return f"{base} & echo {_SENTINEL}!ERRORLEVEL!{_SENTINEL}\r\n"
|
|
2264
2275
|
|
|
2276
|
+
|
|
2277
|
+
def _tui_managed_control_dir() -> Path | None:
|
|
2278
|
+
raw_flag = os.environ.get("KIWI_TUI_MANAGED", "").strip().lower()
|
|
2279
|
+
if raw_flag not in {"1", "true", "yes", "on"}:
|
|
2280
|
+
return None
|
|
2281
|
+
raw_path = os.environ.get("KIWI_RUNTIME_CONTROL_DIR", "").strip()
|
|
2282
|
+
if not raw_path:
|
|
2283
|
+
return None
|
|
2284
|
+
return Path(raw_path)
|
|
2285
|
+
|
|
2286
|
+
|
|
2287
|
+
class _LocalInteractivePTYBridge:
|
|
2288
|
+
def __init__(self, *, control_dir: Path, request_id: str, command: str) -> None:
|
|
2289
|
+
self.control_dir = control_dir
|
|
2290
|
+
self.request_id = request_id
|
|
2291
|
+
self.command = command
|
|
2292
|
+
self.session_dir = control_dir / "interactive"
|
|
2293
|
+
self.status_path = self.session_dir / "status.json"
|
|
2294
|
+
self.input_path = self.session_dir / "input.jsonl"
|
|
2295
|
+
self.output_path = self.session_dir / "output.log"
|
|
2296
|
+
self._input_offset = 0
|
|
2297
|
+
self._input_buffer = ""
|
|
2298
|
+
self.exit_code: int | None = None
|
|
2299
|
+
self.error: str | None = None
|
|
2300
|
+
self._done = asyncio.Event()
|
|
2301
|
+
|
|
2302
|
+
def activate(self) -> None:
|
|
2303
|
+
self.session_dir.mkdir(parents=True, exist_ok=True)
|
|
2304
|
+
self.output_path.write_text("", encoding="utf-8")
|
|
2305
|
+
self.input_path.write_text("", encoding="utf-8")
|
|
2306
|
+
self._input_offset = 0
|
|
2307
|
+
self._input_buffer = ""
|
|
2308
|
+
self._write_status(
|
|
2309
|
+
active=True,
|
|
2310
|
+
request_id=self.request_id,
|
|
2311
|
+
command=self.command,
|
|
2312
|
+
output_path=str(self.output_path),
|
|
2313
|
+
input_path=str(self.input_path),
|
|
2314
|
+
started_at=datetime.now().isoformat(),
|
|
2315
|
+
ended_at=None,
|
|
2316
|
+
exit_code=None,
|
|
2317
|
+
error=None,
|
|
2318
|
+
)
|
|
2319
|
+
|
|
2320
|
+
def _write_status(self, **updates: Any) -> None:
|
|
2321
|
+
status: dict[str, Any] = {}
|
|
2322
|
+
if self.status_path.exists():
|
|
2323
|
+
try:
|
|
2324
|
+
status = json.loads(self.status_path.read_text(encoding="utf-8") or "{}")
|
|
2325
|
+
except Exception:
|
|
2326
|
+
status = {}
|
|
2327
|
+
status.update(updates)
|
|
2328
|
+
self.status_path.write_text(json.dumps(status, indent=2, default=str), encoding="utf-8")
|
|
2329
|
+
|
|
2330
|
+
def append_output(self, data: str) -> None:
|
|
2331
|
+
if not data:
|
|
2332
|
+
return
|
|
2333
|
+
with open(self.output_path, "a", encoding="utf-8", errors="replace") as fp:
|
|
2334
|
+
fp.write(data)
|
|
2335
|
+
fp.flush()
|
|
2336
|
+
|
|
2337
|
+
async def send(self, payload: str) -> None:
|
|
2338
|
+
msg = json.loads(payload)
|
|
2339
|
+
msg_type = msg.get("type")
|
|
2340
|
+
if msg_type == "pty_started":
|
|
2341
|
+
self._write_status(active=True, pty_started=True, session_id=msg.get("session_id"))
|
|
2342
|
+
return
|
|
2343
|
+
if msg_type == "pty_output":
|
|
2344
|
+
self.append_output(str(msg.get("data", "")))
|
|
2345
|
+
return
|
|
2346
|
+
if msg_type == "pty_exited":
|
|
2347
|
+
try:
|
|
2348
|
+
self.exit_code = int(msg.get("exit_code", -1))
|
|
2349
|
+
except Exception:
|
|
2350
|
+
self.exit_code = -1
|
|
2351
|
+
self._write_status(
|
|
2352
|
+
active=False,
|
|
2353
|
+
ended_at=datetime.now().isoformat(),
|
|
2354
|
+
exit_code=self.exit_code,
|
|
2355
|
+
)
|
|
2356
|
+
self._done.set()
|
|
2357
|
+
return
|
|
2358
|
+
if msg_type == "pty_error":
|
|
2359
|
+
self.error = str(msg.get("error", ""))
|
|
2360
|
+
if self.error:
|
|
2361
|
+
self.append_output(f"\r\n[PTY error] {self.error}\r\n")
|
|
2362
|
+
self._write_status(
|
|
2363
|
+
active=False,
|
|
2364
|
+
ended_at=datetime.now().isoformat(),
|
|
2365
|
+
exit_code=-1,
|
|
2366
|
+
error=self.error,
|
|
2367
|
+
)
|
|
2368
|
+
self._done.set()
|
|
2369
|
+
|
|
2370
|
+
def _read_new_events(self) -> list[dict[str, Any]]:
|
|
2371
|
+
if not self.input_path.exists():
|
|
2372
|
+
return []
|
|
2373
|
+
try:
|
|
2374
|
+
with open(self.input_path, "r", encoding="utf-8", errors="replace") as fp:
|
|
2375
|
+
fp.seek(self._input_offset)
|
|
2376
|
+
chunk = fp.read()
|
|
2377
|
+
self._input_offset = fp.tell()
|
|
2378
|
+
except Exception:
|
|
2379
|
+
return []
|
|
2380
|
+
|
|
2381
|
+
if not chunk:
|
|
2382
|
+
return []
|
|
2383
|
+
|
|
2384
|
+
events: list[dict[str, Any]] = []
|
|
2385
|
+
data = self._input_buffer + chunk
|
|
2386
|
+
self._input_buffer = ""
|
|
2387
|
+
for line in data.splitlines(keepends=True):
|
|
2388
|
+
if not line.endswith("\n"):
|
|
2389
|
+
self._input_buffer = line
|
|
2390
|
+
continue
|
|
2391
|
+
raw = line.strip()
|
|
2392
|
+
if not raw:
|
|
2393
|
+
continue
|
|
2394
|
+
try:
|
|
2395
|
+
parsed = json.loads(raw)
|
|
2396
|
+
except Exception:
|
|
2397
|
+
continue
|
|
2398
|
+
if isinstance(parsed, dict):
|
|
2399
|
+
events.append(parsed)
|
|
2400
|
+
return events
|
|
2401
|
+
|
|
2402
|
+
async def pump_inputs(self, pty_proc: Any) -> None:
|
|
2403
|
+
while not self._done.is_set():
|
|
2404
|
+
if getattr(getattr(pty_proc, "proc", None), "returncode", None) is not None:
|
|
2405
|
+
break
|
|
2406
|
+
for event in self._read_new_events():
|
|
2407
|
+
event_type = str(event.get("type", "")).strip().lower()
|
|
2408
|
+
if event_type == "input":
|
|
2409
|
+
data = str(event.get("data", ""))
|
|
2410
|
+
if data:
|
|
2411
|
+
await pty_proc.send_input(data)
|
|
2412
|
+
elif event_type == "interrupt":
|
|
2413
|
+
await pty_proc.interrupt()
|
|
2414
|
+
elif event_type == "close":
|
|
2415
|
+
await pty_proc.close()
|
|
2416
|
+
await asyncio.sleep(0.1)
|
|
2417
|
+
|
|
2418
|
+
async def wait(self) -> int:
|
|
2419
|
+
await self._done.wait()
|
|
2420
|
+
return self.exit_code if self.exit_code is not None else -1
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
async def run_tui_managed_interactive_command(
|
|
2424
|
+
command: str,
|
|
2425
|
+
*,
|
|
2426
|
+
request_id: str,
|
|
2427
|
+
control_dir: Path,
|
|
2428
|
+
mode: str = "restricted",
|
|
2429
|
+
allowed_dirs: list[str] | None = None,
|
|
2430
|
+
) -> dict:
|
|
2431
|
+
if mode == "restricted" and allowed_dirs:
|
|
2432
|
+
ok, reason = validate_command(command, allowed_dirs)
|
|
2433
|
+
if not ok:
|
|
2434
|
+
return {"stdout": "", "stderr": reason, "exit_code": 1}
|
|
2435
|
+
|
|
2436
|
+
cwd = allowed_dirs[0] if allowed_dirs else None
|
|
2437
|
+
bridge = _LocalInteractivePTYBridge(control_dir=control_dir, request_id=request_id, command=command)
|
|
2438
|
+
bridge.activate()
|
|
2439
|
+
bridge.append_output(f"$ {command}\r\n")
|
|
2440
|
+
|
|
2441
|
+
session_id = request_id or str(uuid.uuid4())
|
|
2442
|
+
input_task: asyncio.Task[Any] | None = None
|
|
2443
|
+
pty_proc = None
|
|
2444
|
+
try:
|
|
2445
|
+
if IS_WINDOWS:
|
|
2446
|
+
pty_proc = PipeProcess(
|
|
2447
|
+
session_id,
|
|
2448
|
+
command,
|
|
2449
|
+
120,
|
|
2450
|
+
40,
|
|
2451
|
+
mode=mode,
|
|
2452
|
+
allowed_dirs=allowed_dirs,
|
|
2453
|
+
)
|
|
2454
|
+
else:
|
|
2455
|
+
pty_proc = PTYProcess(session_id, command, 120, 40)
|
|
2456
|
+
await pty_proc.start(bridge, cwd=cwd)
|
|
2457
|
+
input_task = asyncio.create_task(bridge.pump_inputs(pty_proc))
|
|
2458
|
+
exit_code = await bridge.wait()
|
|
2459
|
+
return {
|
|
2460
|
+
"stdout": "(interactive session completed)",
|
|
2461
|
+
"stderr": "",
|
|
2462
|
+
"exit_code": exit_code,
|
|
2463
|
+
}
|
|
2464
|
+
except Exception as e:
|
|
2465
|
+
message = str(e)
|
|
2466
|
+
bridge.append_output(f"\r\n[Interactive error] {message}\r\n")
|
|
2467
|
+
bridge._write_status(
|
|
2468
|
+
active=False,
|
|
2469
|
+
ended_at=datetime.now().isoformat(),
|
|
2470
|
+
exit_code=-1,
|
|
2471
|
+
error=message,
|
|
2472
|
+
)
|
|
2473
|
+
return {"stdout": "", "stderr": message, "exit_code": -1}
|
|
2474
|
+
finally:
|
|
2475
|
+
if input_task and not input_task.done():
|
|
2476
|
+
input_task.cancel()
|
|
2477
|
+
try:
|
|
2478
|
+
await input_task
|
|
2479
|
+
except asyncio.CancelledError:
|
|
2480
|
+
pass
|
|
2481
|
+
except Exception:
|
|
2482
|
+
pass
|
|
2483
|
+
if pty_proc is not None:
|
|
2484
|
+
try:
|
|
2485
|
+
await pty_proc.close()
|
|
2486
|
+
except Exception:
|
|
2487
|
+
pass
|
|
2488
|
+
|
|
2489
|
+
|
|
2265
2490
|
def _zip_directory(resolved, tmp_path, mode, allowed_dirs):
|
|
2266
2491
|
"""Synchronous helper to zip a directory (run via asyncio.to_thread)."""
|
|
2267
2492
|
import zipfile
|
|
@@ -2520,6 +2745,7 @@ async def connect(
|
|
|
2520
2745
|
command,
|
|
2521
2746
|
mode=mode,
|
|
2522
2747
|
allowed_dirs=allowed_dirs,
|
|
2748
|
+
request_id=request_id,
|
|
2523
2749
|
)
|
|
2524
2750
|
exit_code = result["exit_code"]
|
|
2525
2751
|
success = exit_code == 0
|
|
@@ -57,6 +57,7 @@ class RuntimeConnectArgs:
|
|
|
57
57
|
RUNTIMES_DIR = Path.home() / ".kiwi" / "runtimes"
|
|
58
58
|
BY_RUN_DIR = RUNTIMES_DIR / "by-run"
|
|
59
59
|
PENDING_DIR = RUNTIMES_DIR / "pending"
|
|
60
|
+
CONTROL_DIR = RUNTIMES_DIR / "control"
|
|
60
61
|
|
|
61
62
|
|
|
62
63
|
# Runtime log truncation
|
|
@@ -282,6 +283,11 @@ def _runtime_dir_for_pending(pending_id: str) -> Path:
|
|
|
282
283
|
d.mkdir(parents=True, exist_ok=True)
|
|
283
284
|
return d
|
|
284
285
|
|
|
286
|
+
def _control_dir_for_runtime(control_id: str) -> Path:
|
|
287
|
+
d = CONTROL_DIR / control_id
|
|
288
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
289
|
+
return d
|
|
290
|
+
|
|
285
291
|
|
|
286
292
|
def pid_path_for_run(run_id: str) -> Path:
|
|
287
293
|
return _runtime_dir_for_run(run_id) / "pid"
|
|
@@ -336,6 +342,19 @@ def meta_path_for_pending(pending_id: str) -> Path:
|
|
|
336
342
|
return _runtime_dir_for_pending(pending_id) / "meta.json"
|
|
337
343
|
|
|
338
344
|
|
|
345
|
+
def control_dir_from_meta(meta_path: Path) -> Path | None:
|
|
346
|
+
if not meta_path.exists():
|
|
347
|
+
return None
|
|
348
|
+
try:
|
|
349
|
+
meta = json.loads(meta_path.read_text(encoding="utf-8") or "{}")
|
|
350
|
+
except Exception:
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
control_dir = meta.get("control_dir")
|
|
354
|
+
if isinstance(control_dir, str) and control_dir.strip():
|
|
355
|
+
return Path(control_dir)
|
|
356
|
+
return None
|
|
357
|
+
|
|
339
358
|
def get_running_pid_for_run(run_id: str) -> int | None:
|
|
340
359
|
pid = _read_pid(pid_path_for_run(run_id))
|
|
341
360
|
if not pid:
|
|
@@ -434,6 +453,11 @@ def _spawn_runtime(
|
|
|
434
453
|
env.setdefault("PYTHONUTF8", "1")
|
|
435
454
|
env.setdefault("PYTHONIOENCODING", "utf-8:replace")
|
|
436
455
|
|
|
456
|
+
control_id = str(uuid.uuid4())
|
|
457
|
+
control_dir = _control_dir_for_runtime(control_id)
|
|
458
|
+
env["KIWI_TUI_MANAGED"] = "1"
|
|
459
|
+
env["KIWI_RUNTIME_CONTROL_DIR"] = str(control_dir)
|
|
460
|
+
|
|
437
461
|
try:
|
|
438
462
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
439
463
|
log_fp = open(log_path, "a", encoding="utf-8", errors="replace")
|
|
@@ -471,6 +495,9 @@ def _spawn_runtime(
|
|
|
471
495
|
"allow": list(args.allow_dirs),
|
|
472
496
|
"email": args.email,
|
|
473
497
|
"pid": proc.pid,
|
|
498
|
+
"tui_managed": True,
|
|
499
|
+
"control_id": control_id,
|
|
500
|
+
"control_dir": str(control_dir),
|
|
474
501
|
}
|
|
475
502
|
try:
|
|
476
503
|
meta["create_time"] = psutil.Process(proc.pid).create_time()
|
|
@@ -638,23 +638,32 @@ class DashboardScreen(Screen):
|
|
|
638
638
|
def open_runtime_logs(self) -> None:
|
|
639
639
|
"""Open the runtime logs screen for the current run (or pending runtime)."""
|
|
640
640
|
from kiwi_tui.screens.runtime_logs import RuntimeLogsScreen
|
|
641
|
-
from kiwi_tui.runtime_agent import
|
|
641
|
+
from kiwi_tui.runtime_agent import (
|
|
642
|
+
control_dir_from_meta,
|
|
643
|
+
log_path_for_pending,
|
|
644
|
+
log_path_for_run,
|
|
645
|
+
meta_path_for_pending,
|
|
646
|
+
meta_path_for_run,
|
|
647
|
+
)
|
|
642
648
|
|
|
643
649
|
lp = None
|
|
650
|
+
control_dir = None
|
|
644
651
|
title = "CLI logs"
|
|
645
652
|
if self.current_run_id:
|
|
646
653
|
lp = log_path_for_run(self.current_run_id)
|
|
654
|
+
control_dir = control_dir_from_meta(meta_path_for_run(self.current_run_id))
|
|
647
655
|
title = f"CLI logs — run {self.current_run_id}"
|
|
648
656
|
else:
|
|
649
657
|
pending_id = getattr(self.app, "pending_runtime_id", None)
|
|
650
658
|
if pending_id:
|
|
651
659
|
lp = log_path_for_pending(pending_id)
|
|
660
|
+
control_dir = control_dir_from_meta(meta_path_for_pending(pending_id))
|
|
652
661
|
title = f"CLI logs — pending {pending_id}"
|
|
653
662
|
|
|
654
|
-
if not lp:
|
|
663
|
+
if not lp and not control_dir:
|
|
655
664
|
self.notify("No CLI runtime logs available (use /connect-cli first).", severity="warning")
|
|
656
665
|
return
|
|
657
|
-
self.app.push_screen(RuntimeLogsScreen(lp, title=title))
|
|
666
|
+
self.app.push_screen(RuntimeLogsScreen(lp, title=title, control_dir=control_dir))
|
|
658
667
|
|
|
659
668
|
|
|
660
669
|
def on_chat_input_at_triggered(self, event: ChatInput.AtTriggered) -> None:
|