kiwi-code 0.0.431__tar.gz → 0.0.433__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.431 → kiwi_code-0.0.433}/PKG-INFO +1 -1
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/pyproject.toml +1 -1
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_cli/auth.py +0 -3
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_cli/cli.py +0 -4
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_cli/client.py +68 -55
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_cli/commands.py +0 -13
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_cli/models.py +0 -2
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_cli/server.py +0 -4
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_cli/terminal_mode.py +0 -2
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_runtime/main.py +4 -31
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/screens/dashboard.py +213 -11
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/slash_commands.py +1 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/status_words.py +22 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/widgets.py +4 -1
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/conftest.py +0 -2
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/test_cli_help.py +0 -2
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/test_imports.py +0 -3
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/test_reexec_kiwi.py +0 -4
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/test_runtime_log_trimming.py +0 -5
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/test_terminal_mode.py +0 -4
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/test_tokens.py +0 -3
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/test_tui_headless.py +0 -3
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/test_tui_interactive_runtime.py +0 -4
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/test_tui_palette.py +0 -1
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/test_worktrees.py +1 -6
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/uv.lock +1 -1
- kiwi_code-0.0.431/src/kiwi_runtime/snake_game/.gitignore +0 -3
- kiwi_code-0.0.431/src/kiwi_runtime/snake_game/requirements.txt +0 -3
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/.gitignore +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/.python-version +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/CLAUDE.md +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/Makefile +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/README.md +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/main.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/src/kiwi_tui/worktrees.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/test_hello.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/__init__.py +0 -0
- {kiwi_code-0.0.431 → kiwi_code-0.0.433}/tests/test_slash_commands.py +0 -0
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
"""Typer CLI for Autobots — thin wrapper over commands.py."""
|
|
2
|
-
|
|
3
2
|
from typing import Optional, Callable
|
|
4
|
-
|
|
5
3
|
from loguru import logger
|
|
6
4
|
logger.remove() # Suppress loguru console output in CLI mode
|
|
7
|
-
|
|
8
5
|
import typer
|
|
9
6
|
from autobots_client import AuthenticatedClient
|
|
10
|
-
|
|
11
7
|
from .auth import TokenManager
|
|
12
8
|
from .models import AppConfig
|
|
13
9
|
from .server import http_url_from_server
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"""Autobots client wrapper for API interactions."""
|
|
2
|
-
|
|
3
2
|
from datetime import datetime, timedelta
|
|
4
3
|
from typing import Optional, Callable
|
|
5
4
|
import time
|
|
@@ -11,7 +10,6 @@ from autobots_client import Client, AuthenticatedClient
|
|
|
11
10
|
from autobots_client.api.hello import hello_v1_hello_get
|
|
12
11
|
from autobots_client.api.auth import (
|
|
13
12
|
return_user_and_session_v1_auth_post,
|
|
14
|
-
refresh_password_email_v1_auth_session_refresh_post,
|
|
15
13
|
)
|
|
16
14
|
from autobots_client.api.actions import async_run_action_v1_actions_id_async_run_post
|
|
17
15
|
from autobots_client.api.action_results import get_action_result_v1_action_results_id_get
|
|
@@ -24,7 +22,6 @@ from autobots_client.models.body_async_run_action_v1_actions_id_async_run_post i
|
|
|
24
22
|
from autobots_client.models.body_async_run_action_v1_actions_id_async_run_post_input import (
|
|
25
23
|
BodyAsyncRunActionV1ActionsIdAsyncRunPostInput,
|
|
26
24
|
)
|
|
27
|
-
|
|
28
25
|
from .models import AuthTokens, LoginCredentials
|
|
29
26
|
|
|
30
27
|
|
|
@@ -525,18 +522,42 @@ class AutobotsClientWrapper:
|
|
|
525
522
|
|
|
526
523
|
return False, result, f"Polling timeout after {max_attempts * interval}s - action may still be running"
|
|
527
524
|
|
|
528
|
-
async def stream_action_result(
|
|
529
|
-
|
|
525
|
+
async def stream_action_result(
|
|
526
|
+
self,
|
|
527
|
+
run_id: str,
|
|
528
|
+
on_message: Callable[[dict], None],
|
|
529
|
+
*,
|
|
530
|
+
topic: str | None = None,
|
|
531
|
+
stop_on_terminal_status: bool = True,
|
|
532
|
+
plain_text_type: str = "status",
|
|
533
|
+
) -> None:
|
|
534
|
+
"""Stream action run updates via Server-Sent Events.
|
|
535
|
+
|
|
536
|
+
The backend publishes plain text frames to Redis topics. Historically Kiwi Code
|
|
537
|
+
subscribed only to the `run_id` topic and wrapped non-JSON frames as
|
|
538
|
+
`{type: 'status', text: ...}`.
|
|
539
|
+
|
|
540
|
+
Some providers (notably OpenAI Responses) publish the assistant text stream to
|
|
541
|
+
a separate topic: `f"{run_id}-text"`. Callers can subscribe to that topic by
|
|
542
|
+
passing `topic=f"{run_id}-text"` and setting `plain_text_type='text'`.
|
|
543
|
+
That topic does not emit JSON status payloads, so callers should typically set
|
|
544
|
+
`stop_on_terminal_status=False` and cancel the stream when polling detects
|
|
545
|
+
completion.
|
|
530
546
|
|
|
531
547
|
Args:
|
|
532
|
-
run_id:
|
|
533
|
-
on_message: Callback
|
|
548
|
+
run_id: Action run id (used for auth and (optionally) terminal detection).
|
|
549
|
+
on_message: Callback for parsed payloads (dict).
|
|
550
|
+
topic: Redis topic to subscribe to. Defaults to `run_id`.
|
|
551
|
+
stop_on_terminal_status: Stop the stream when a JSON payload includes a terminal
|
|
552
|
+
`status` field. Disable for topics that don't send status (e.g. *-text).
|
|
553
|
+
plain_text_type: When a frame is not valid JSON, wrap it as
|
|
554
|
+
`{'type': plain_text_type, 'text': <payload>}`.
|
|
534
555
|
"""
|
|
535
|
-
|
|
556
|
+
topic = topic or run_id
|
|
557
|
+
sse_endpoint = f"{self.base_url}/v1/server_sent_events/stream/{topic}"
|
|
536
558
|
logger.info(f"Connecting to SSE: {sse_endpoint}")
|
|
537
559
|
|
|
538
560
|
try:
|
|
539
|
-
# Create headers
|
|
540
561
|
headers = {
|
|
541
562
|
"Accept": "text/event-stream",
|
|
542
563
|
"Cache-Control": "no-cache",
|
|
@@ -545,7 +566,6 @@ class AutobotsClientWrapper:
|
|
|
545
566
|
if self.access_token:
|
|
546
567
|
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
547
568
|
|
|
548
|
-
# Use httpx to stream SSE with longer timeout
|
|
549
569
|
async with httpx.AsyncClient(timeout=None) as client:
|
|
550
570
|
async with client.stream("GET", sse_endpoint, headers=headers) as response:
|
|
551
571
|
logger.info(f"SSE connected, status: {response.status_code}")
|
|
@@ -554,68 +574,61 @@ class AutobotsClientWrapper:
|
|
|
554
574
|
logger.error(f"SSE connection failed with status {response.status_code}")
|
|
555
575
|
return
|
|
556
576
|
|
|
557
|
-
# Use incremental UTF-8 decoder to handle multi-byte characters across chunk boundaries
|
|
558
577
|
decoder = codecs.getincrementaldecoder("utf-8")()
|
|
559
578
|
buffer = ""
|
|
579
|
+
event_data_lines: list[str] = []
|
|
580
|
+
|
|
581
|
+
def _flush_event() -> bool:
|
|
582
|
+
if not event_data_lines:
|
|
583
|
+
return False
|
|
584
|
+
data_str = "\n".join(event_data_lines).rstrip()
|
|
585
|
+
event_data_lines.clear()
|
|
586
|
+
if not data_str:
|
|
587
|
+
return False
|
|
588
|
+
try:
|
|
589
|
+
data = json.loads(data_str)
|
|
590
|
+
on_message(data)
|
|
591
|
+
if stop_on_terminal_status and isinstance(data, dict):
|
|
592
|
+
status = str(data.get("status", "") or "").lower()
|
|
593
|
+
if status in ["completed", "success", "finished", "failed", "error"]:
|
|
594
|
+
return True
|
|
595
|
+
except json.JSONDecodeError:
|
|
596
|
+
on_message({"type": plain_text_type, "text": data_str})
|
|
597
|
+
except Exception as e:
|
|
598
|
+
logger.error(f"Error processing SSE data: {e}")
|
|
599
|
+
return False
|
|
560
600
|
|
|
561
|
-
# Process SSE stream byte by byte with incremental decoder
|
|
562
601
|
async for chunk in response.aiter_bytes():
|
|
563
|
-
# Decode chunk with incremental decoder (handles partial multi-byte chars)
|
|
564
602
|
text_chunk = decoder.decode(chunk, final=False)
|
|
565
603
|
buffer += text_chunk
|
|
566
604
|
|
|
567
|
-
# Process complete lines from buffer
|
|
568
605
|
while "\n" in buffer:
|
|
569
606
|
line, buffer = buffer.split("\n", 1)
|
|
570
|
-
line = line.
|
|
607
|
+
line = line.rstrip("\r")
|
|
571
608
|
|
|
572
|
-
#
|
|
573
|
-
if
|
|
609
|
+
# SSE event boundary (blank line)
|
|
610
|
+
if line == "":
|
|
611
|
+
if _flush_event():
|
|
612
|
+
return
|
|
574
613
|
continue
|
|
575
614
|
|
|
576
|
-
# Skip keep-alive comments (`:keep-alive`)
|
|
577
615
|
if line.startswith(":"):
|
|
578
|
-
|
|
616
|
+
# keep-alive comments
|
|
579
617
|
continue
|
|
580
618
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
data = json.loads(data_str)
|
|
588
|
-
logger.info(f"SSE JSON received, keys: {data.keys() if isinstance(data, dict) else type(data)}")
|
|
589
|
-
|
|
590
|
-
# Call the callback with parsed data
|
|
591
|
-
on_message(data)
|
|
592
|
-
|
|
593
|
-
# Check if action is complete
|
|
594
|
-
if isinstance(data, dict):
|
|
595
|
-
status = data.get("status", "").lower()
|
|
596
|
-
if status in ["completed", "success", "finished", "failed", "error"]:
|
|
597
|
-
logger.info(f"Action completed with status: {status}")
|
|
598
|
-
return
|
|
599
|
-
|
|
600
|
-
except json.JSONDecodeError:
|
|
601
|
-
# Not JSON, treat as plain text status message
|
|
602
|
-
logger.info(f"SSE text message: {data_str}")
|
|
603
|
-
|
|
604
|
-
# Mark as status so dashboard can distinguish from real output
|
|
605
|
-
text_data = {
|
|
606
|
-
"type": "status",
|
|
607
|
-
"text": data_str,
|
|
608
|
-
}
|
|
609
|
-
on_message(text_data)
|
|
610
|
-
|
|
611
|
-
except Exception as e:
|
|
612
|
-
logger.error(f"Error processing SSE data: {e}")
|
|
619
|
+
if line.startswith("data:"):
|
|
620
|
+
val = line[5:]
|
|
621
|
+
if val.startswith(" "):
|
|
622
|
+
val = val[1:]
|
|
623
|
+
event_data_lines.append(val)
|
|
624
|
+
continue
|
|
613
625
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if remaining:
|
|
617
|
-
buffer += remaining
|
|
626
|
+
if event_data_lines:
|
|
627
|
+
event_data_lines.append(line)
|
|
618
628
|
|
|
629
|
+
# Flush any remaining bytes in decoder + any pending event data.
|
|
630
|
+
decoder.decode(b"", final=True)
|
|
631
|
+
_flush_event()
|
|
619
632
|
except Exception as e:
|
|
620
633
|
logger.error(f"SSE error: {e}")
|
|
621
634
|
raise
|
|
@@ -5,7 +5,6 @@ and returns a list of strings (output lines).
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from typing import Optional
|
|
8
|
-
|
|
9
8
|
from autobots_client import AuthenticatedClient
|
|
10
9
|
from autobots_client.api.actions import (
|
|
11
10
|
list_actions_v1_actions_get,
|
|
@@ -26,9 +25,7 @@ from autobots_client.api.action_graphs_results import (
|
|
|
26
25
|
from autobots_client.types import UNSET
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
# ---------------------------------------------------------------------------
|
|
30
28
|
# Helpers
|
|
31
|
-
# ---------------------------------------------------------------------------
|
|
32
29
|
|
|
33
30
|
def _fmt_date(dt) -> str:
|
|
34
31
|
if dt is None or isinstance(dt, type(UNSET)):
|
|
@@ -45,9 +42,7 @@ def _val(v) -> str:
|
|
|
45
42
|
return str(v)
|
|
46
43
|
|
|
47
44
|
|
|
48
|
-
# ---------------------------------------------------------------------------
|
|
49
45
|
# actions
|
|
50
|
-
# ---------------------------------------------------------------------------
|
|
51
46
|
|
|
52
47
|
def actions_list(client: AuthenticatedClient, *, name: Optional[str] = None, limit: int = 20, offset: int = 0) -> list[str]:
|
|
53
48
|
"""List actions. Returns output lines."""
|
|
@@ -95,9 +90,7 @@ def actions_get(client: AuthenticatedClient, *, id: str) -> list[str]:
|
|
|
95
90
|
]
|
|
96
91
|
|
|
97
92
|
|
|
98
|
-
# ---------------------------------------------------------------------------
|
|
99
93
|
# runs (action results)
|
|
100
|
-
# ---------------------------------------------------------------------------
|
|
101
94
|
|
|
102
95
|
def runs_list(client: AuthenticatedClient, *, action_id: Optional[str] = None, action_name: Optional[str] = None,
|
|
103
96
|
status: Optional[str] = None, limit: int = 20, offset: int = 0) -> list[str]:
|
|
@@ -164,9 +157,7 @@ def runs_get(client: AuthenticatedClient, *, id: str) -> list[str]:
|
|
|
164
157
|
return lines
|
|
165
158
|
|
|
166
159
|
|
|
167
|
-
# ---------------------------------------------------------------------------
|
|
168
160
|
# graphs (action graphs)
|
|
169
|
-
# ---------------------------------------------------------------------------
|
|
170
161
|
|
|
171
162
|
def graphs_list(client: AuthenticatedClient, *, name: Optional[str] = None, limit: int = 20, offset: int = 0) -> list[str]:
|
|
172
163
|
"""List action graphs. Returns output lines."""
|
|
@@ -217,9 +208,7 @@ def graphs_get(client: AuthenticatedClient, *, id: str) -> list[str]:
|
|
|
217
208
|
return [str(doc)]
|
|
218
209
|
|
|
219
210
|
|
|
220
|
-
# ---------------------------------------------------------------------------
|
|
221
211
|
# graph-runs (action graph results)
|
|
222
|
-
# ---------------------------------------------------------------------------
|
|
223
212
|
|
|
224
213
|
def graph_runs_list(client: AuthenticatedClient, *, action_graph_id: Optional[str] = None,
|
|
225
214
|
action_graph_name: Optional[str] = None, status: Optional[str] = None,
|
|
@@ -286,9 +275,7 @@ def graph_runs_get(client: AuthenticatedClient, *, id: str) -> list[str]:
|
|
|
286
275
|
return lines
|
|
287
276
|
|
|
288
277
|
|
|
289
|
-
# ---------------------------------------------------------------------------
|
|
290
278
|
# Command dispatcher — parses "/command args" from TUI input
|
|
291
|
-
# ---------------------------------------------------------------------------
|
|
292
279
|
|
|
293
280
|
HELP_TEXT = """\
|
|
294
281
|
Session:
|
|
@@ -10,10 +10,6 @@ The value can be:
|
|
|
10
10
|
|
|
11
11
|
This module intentionally contains *no persistence*. It is runtime-only.
|
|
12
12
|
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
|
|
17
13
|
SERVER_PRESETS: dict[str, dict[str, str]] = {
|
|
18
14
|
"app": {"ws": "wss://api.meetkiwi.ai", "http": "https://api.meetkiwi.ai"},
|
|
19
15
|
"dev": {"ws": "wss://dev.api.myautobots.com", "http": "https://dev.api.myautobots.com"},
|
|
@@ -12,11 +12,7 @@ Usage:
|
|
|
12
12
|
kiwi connect --server dev --scope full
|
|
13
13
|
kiwi connect --server dev --allow /path/to/extra/dir
|
|
14
14
|
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
15
|
from datetime import datetime
|
|
19
|
-
|
|
20
16
|
import argparse
|
|
21
17
|
import asyncio
|
|
22
18
|
import getpass
|
|
@@ -29,6 +25,10 @@ import subprocess
|
|
|
29
25
|
import sys
|
|
30
26
|
from pathlib import Path
|
|
31
27
|
from typing import Any
|
|
28
|
+
import shutil
|
|
29
|
+
import tempfile
|
|
30
|
+
import uuid
|
|
31
|
+
from dataclasses import dataclass
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
IS_WINDOWS = sys.platform == "win32"
|
|
@@ -44,10 +44,7 @@ import websockets
|
|
|
44
44
|
import threading
|
|
45
45
|
FS_MAX_CONCURRENCY = 8
|
|
46
46
|
|
|
47
|
-
# ---------------------------------------------------------------------------
|
|
48
47
|
# Shared token storage (used by both kiwi-code and standalone kiwi-runtime)
|
|
49
|
-
# ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
48
|
TOKENS_PATH = Path.home() / ".kiwi" / "tokens.json"
|
|
52
49
|
TOKENS_LOCK_PATH = Path.home() / ".kiwi" / "tokens.lock"
|
|
53
50
|
TOKEN_EXPIRY_SKEW_SEC = 60 # refresh a bit early to avoid edge cases
|
|
@@ -311,12 +308,6 @@ _WINDOWS_SENTINEL_RESULT = re.compile(
|
|
|
311
308
|
re.IGNORECASE,
|
|
312
309
|
)
|
|
313
310
|
|
|
314
|
-
import difflib
|
|
315
|
-
import shutil
|
|
316
|
-
import tempfile
|
|
317
|
-
import uuid
|
|
318
|
-
from dataclasses import dataclass
|
|
319
|
-
|
|
320
311
|
|
|
321
312
|
@dataclass
|
|
322
313
|
class ChunkedWriteState:
|
|
@@ -1203,10 +1194,7 @@ SERVER_PRESETS = {
|
|
|
1203
1194
|
"local": {"ws": "ws://localhost:8000", "http": "http://localhost:8000"},
|
|
1204
1195
|
}
|
|
1205
1196
|
|
|
1206
|
-
# ---------------------------------------------------------------------------
|
|
1207
1197
|
# ANSI colors
|
|
1208
|
-
# ---------------------------------------------------------------------------
|
|
1209
|
-
|
|
1210
1198
|
RESET = "\033[0m"
|
|
1211
1199
|
BOLD = "\033[1m"
|
|
1212
1200
|
|
|
@@ -1219,10 +1207,7 @@ YELLOW = "\033[93m"
|
|
|
1219
1207
|
GREEN = "\033[92m"
|
|
1220
1208
|
|
|
1221
1209
|
|
|
1222
|
-
# ---------------------------------------------------------------------------
|
|
1223
1210
|
# Banner — Kiwi logo (from SVG) + KIWI AI text side by side
|
|
1224
|
-
# ---------------------------------------------------------------------------
|
|
1225
|
-
|
|
1226
1211
|
# Logo: 32 wide x 16 tall (half-block, perfectly symmetric from SVG)
|
|
1227
1212
|
LOGO = [
|
|
1228
1213
|
" ▄██▄ ",
|
|
@@ -1257,10 +1242,7 @@ TAGLINE = "Terminal Agent for AI-Powered Automation"
|
|
|
1257
1242
|
|
|
1258
1243
|
|
|
1259
1244
|
|
|
1260
|
-
# ---------------------------------------------------------------------------
|
|
1261
1245
|
# Mini block-text renderer (3-row height, no dependencies)
|
|
1262
|
-
# ---------------------------------------------------------------------------
|
|
1263
|
-
|
|
1264
1246
|
_BLOCK_CHARS = {
|
|
1265
1247
|
"A": ["█▀█", "█▀█", "▀ ▀"], "B": ["█▀▄", "█▀▄", "▀▀ "], "C": ["█▀▀", "█ ", "▀▀▀"],
|
|
1266
1248
|
"D": ["█▀▄", "█ █", "▀▀ "], "E": ["█▀▀", "█▀▀", "▀▀▀"], "F": ["█▀▀", "█▀ ", "▀ "],
|
|
@@ -1408,10 +1390,7 @@ def print_pty_log(session_id: str, message: str, level: str = "info"):
|
|
|
1408
1390
|
_safe_print(f" {icon} {tag} {message}")
|
|
1409
1391
|
|
|
1410
1392
|
|
|
1411
|
-
# ---------------------------------------------------------------------------
|
|
1412
1393
|
# Path validator for restricted mode
|
|
1413
|
-
# ---------------------------------------------------------------------------
|
|
1414
|
-
|
|
1415
1394
|
# Regex to detect Windows absolute paths like C:\ or D:/
|
|
1416
1395
|
_WIN_ABS_PATH = re.compile(r'^[A-Za-z]:[/\\]')
|
|
1417
1396
|
# Regex to detect UNC paths like \\server\share
|
|
@@ -1593,10 +1572,7 @@ def _is_within_allowed(resolved_path: str, allowed_dirs: list[str]) -> bool:
|
|
|
1593
1572
|
return False
|
|
1594
1573
|
|
|
1595
1574
|
|
|
1596
|
-
# ---------------------------------------------------------------------------
|
|
1597
1575
|
# Core logic
|
|
1598
|
-
# ---------------------------------------------------------------------------
|
|
1599
|
-
|
|
1600
1576
|
async def login(http_base_url: str, email: str, password: str) -> str:
|
|
1601
1577
|
"""Authenticate with email/password and return the access token.
|
|
1602
1578
|
|
|
@@ -1916,9 +1892,6 @@ if not IS_WINDOWS:
|
|
|
1916
1892
|
pass
|
|
1917
1893
|
self.slave_fd = None
|
|
1918
1894
|
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
1895
|
class PipeProcess:
|
|
1923
1896
|
"""Manages a persistent shell session using subprocess pipes (Windows-compatible)."""
|
|
1924
1897
|
|
|
@@ -1028,6 +1028,84 @@ class DashboardScreen(Screen):
|
|
|
1028
1028
|
self.process_message(prompt)
|
|
1029
1029
|
return
|
|
1030
1030
|
|
|
1031
|
+
if cmd == "/disconnect-cli":
|
|
1032
|
+
"""Disconnect (kill) the local CLI runtime for the current run (or pending runtime)."""
|
|
1033
|
+
from kiwi_tui.runtime_agent import (
|
|
1034
|
+
get_running_pid_for_pending,
|
|
1035
|
+
get_running_pid_for_run,
|
|
1036
|
+
kill_pid,
|
|
1037
|
+
pid_path_for_pending,
|
|
1038
|
+
pid_path_for_run,
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
killed = False
|
|
1042
|
+
pid: int | None = None
|
|
1043
|
+
target: str | None = None
|
|
1044
|
+
|
|
1045
|
+
try:
|
|
1046
|
+
if self.current_run_id:
|
|
1047
|
+
target = f"run {self.current_run_id}"
|
|
1048
|
+
pid = get_running_pid_for_run(self.current_run_id)
|
|
1049
|
+
if pid:
|
|
1050
|
+
killed = bool(kill_pid(int(pid)))
|
|
1051
|
+
if killed:
|
|
1052
|
+
# Remove pid file so the next /connect-cli spawns a fresh runtime.
|
|
1053
|
+
try:
|
|
1054
|
+
pid_path_for_run(self.current_run_id).unlink(missing_ok=True)
|
|
1055
|
+
except Exception:
|
|
1056
|
+
pass
|
|
1057
|
+
else:
|
|
1058
|
+
pending_id = getattr(self.app, "pending_runtime_id", None)
|
|
1059
|
+
if pending_id:
|
|
1060
|
+
target = f"pending {pending_id}"
|
|
1061
|
+
pid = get_running_pid_for_pending(pending_id)
|
|
1062
|
+
if pid:
|
|
1063
|
+
killed = bool(kill_pid(int(pid)))
|
|
1064
|
+
if killed:
|
|
1065
|
+
try:
|
|
1066
|
+
pid_path_for_pending(pending_id).unlink(missing_ok=True)
|
|
1067
|
+
except Exception:
|
|
1068
|
+
pass
|
|
1069
|
+
# Clear the session-scoped pending runtime id only when we killed it.
|
|
1070
|
+
try:
|
|
1071
|
+
self.app.pending_runtime_id = None
|
|
1072
|
+
except Exception:
|
|
1073
|
+
pass
|
|
1074
|
+
else:
|
|
1075
|
+
# No PID found => stale pending id in memory; clear it.
|
|
1076
|
+
try:
|
|
1077
|
+
self.app.pending_runtime_id = None
|
|
1078
|
+
except Exception:
|
|
1079
|
+
pass
|
|
1080
|
+
except Exception:
|
|
1081
|
+
killed = False
|
|
1082
|
+
|
|
1083
|
+
if pid and killed:
|
|
1084
|
+
toast(
|
|
1085
|
+
f"Disconnected CLI runtime ({target}) by killing PID {pid}.",
|
|
1086
|
+
title="/disconnect-cli",
|
|
1087
|
+
severity="information",
|
|
1088
|
+
)
|
|
1089
|
+
elif pid and not killed:
|
|
1090
|
+
toast(
|
|
1091
|
+
f"Failed to disconnect CLI runtime ({target}). Unable to kill PID {pid}.",
|
|
1092
|
+
title="/disconnect-cli",
|
|
1093
|
+
severity="error",
|
|
1094
|
+
)
|
|
1095
|
+
elif target:
|
|
1096
|
+
toast(
|
|
1097
|
+
f"No running CLI runtime found for {target}.",
|
|
1098
|
+
title="/disconnect-cli",
|
|
1099
|
+
severity="warning",
|
|
1100
|
+
)
|
|
1101
|
+
else:
|
|
1102
|
+
toast(
|
|
1103
|
+
"No CLI runtime is associated with this session yet.",
|
|
1104
|
+
title="/disconnect-cli",
|
|
1105
|
+
severity="warning",
|
|
1106
|
+
)
|
|
1107
|
+
return
|
|
1108
|
+
|
|
1031
1109
|
if cmd == "/show-logs":
|
|
1032
1110
|
self.open_runtime_logs()
|
|
1033
1111
|
return
|
|
@@ -3051,6 +3129,69 @@ class DashboardScreen(Screen):
|
|
|
3051
3129
|
# If we already mounted a placeholder row on submit, reuse it.
|
|
3052
3130
|
status_widget_container = [getattr(self, "_streaming_widget_ref", None)]
|
|
3053
3131
|
|
|
3132
|
+
# Streaming state: main assistant text + latest status/tool output.
|
|
3133
|
+
main_text: str = ""
|
|
3134
|
+
status_text: str = ""
|
|
3135
|
+
tool_text: str = ""
|
|
3136
|
+
openai_text_stream_seen: bool = False
|
|
3137
|
+
text_task: asyncio.Task | None = None
|
|
3138
|
+
|
|
3139
|
+
_last_render: str = ""
|
|
3140
|
+
_last_render_at: float = 0.0
|
|
3141
|
+
|
|
3142
|
+
def _looks_like_tool_text(t: str) -> bool:
|
|
3143
|
+
tt = (t or "").strip()
|
|
3144
|
+
if not tt:
|
|
3145
|
+
return False
|
|
3146
|
+
return (
|
|
3147
|
+
tt.startswith("Calling tool:")
|
|
3148
|
+
or tt.startswith("Calling tool ")
|
|
3149
|
+
or "\nArguments:" in tt
|
|
3150
|
+
or tt.startswith("Arguments:")
|
|
3151
|
+
or tt.startswith("Function call")
|
|
3152
|
+
)
|
|
3153
|
+
|
|
3154
|
+
def _build_stream_markdown() -> str:
|
|
3155
|
+
base = (main_text or "").strip()
|
|
3156
|
+
if not openai_text_stream_seen:
|
|
3157
|
+
# Non-OpenAI providers stream their main output on the run_id topic.
|
|
3158
|
+
# Preserve the previous behavior: show whatever the backend is streaming.
|
|
3159
|
+
return base
|
|
3160
|
+
|
|
3161
|
+
extra_parts: list[str] = []
|
|
3162
|
+
if status_text.strip():
|
|
3163
|
+
extra_parts.append(status_text.strip())
|
|
3164
|
+
if tool_text.strip():
|
|
3165
|
+
extra_parts.append(tool_text.strip())
|
|
3166
|
+
|
|
3167
|
+
if not extra_parts:
|
|
3168
|
+
return base
|
|
3169
|
+
if not base:
|
|
3170
|
+
return "\n\n".join(extra_parts)
|
|
3171
|
+
return base + "\n\n" + "\n\n".join(extra_parts)
|
|
3172
|
+
|
|
3173
|
+
def _maybe_render(*, force: bool = False) -> None:
|
|
3174
|
+
nonlocal _last_render, _last_render_at
|
|
3175
|
+
try:
|
|
3176
|
+
now = time.monotonic()
|
|
3177
|
+
if not force and (now - _last_render_at) < 0.03:
|
|
3178
|
+
return
|
|
3179
|
+
rendered = _build_stream_markdown()
|
|
3180
|
+
if not rendered.strip():
|
|
3181
|
+
return
|
|
3182
|
+
if not force and rendered == _last_render:
|
|
3183
|
+
return
|
|
3184
|
+
status_widget_container[0] = self.update_streaming_message(
|
|
3185
|
+
{"blocks": [{"text": rendered}]},
|
|
3186
|
+
status_widget_container[0],
|
|
3187
|
+
)
|
|
3188
|
+
_last_render = rendered
|
|
3189
|
+
_last_render_at = now
|
|
3190
|
+
except Exception:
|
|
3191
|
+
# Do not break streaming if a render fails mid-refresh.
|
|
3192
|
+
return
|
|
3193
|
+
|
|
3194
|
+
|
|
3054
3195
|
logger.info(f"Starting stream_results for {run_id}")
|
|
3055
3196
|
|
|
3056
3197
|
def _remove_status_widget() -> None:
|
|
@@ -3184,34 +3325,58 @@ class DashboardScreen(Screen):
|
|
|
3184
3325
|
return True
|
|
3185
3326
|
return False
|
|
3186
3327
|
|
|
3328
|
+
def handle_text_message(data: dict) -> None:
|
|
3329
|
+
"""Handle OpenAI *-text topic stream (assistant text)."""
|
|
3330
|
+
nonlocal main_text, openai_text_stream_seen
|
|
3331
|
+
if not isinstance(data, dict) or got_final_result:
|
|
3332
|
+
return
|
|
3333
|
+
if data.get("type") != "text":
|
|
3334
|
+
return
|
|
3335
|
+
text = str(data.get("text", "") or "")
|
|
3336
|
+
if not text.strip():
|
|
3337
|
+
return
|
|
3338
|
+
openai_text_stream_seen = True
|
|
3339
|
+
main_text = text
|
|
3340
|
+
_maybe_render()
|
|
3341
|
+
|
|
3187
3342
|
def handle_status_message(data: dict) -> None:
|
|
3188
|
-
"""Handle SSE messages —
|
|
3343
|
+
"""Handle SSE messages — provider-dependent streaming + status/tool updates."""
|
|
3344
|
+
nonlocal main_text, status_text, tool_text
|
|
3189
3345
|
if not isinstance(data, dict) or got_final_result:
|
|
3190
3346
|
return
|
|
3191
3347
|
|
|
3192
|
-
# Plain-text
|
|
3348
|
+
# Plain-text messages (wrapped as type="status" by the SSE client).
|
|
3193
3349
|
if data.get("type") == "status":
|
|
3194
|
-
text = data.get("text", "")
|
|
3350
|
+
text = str(data.get("text", "") or "")
|
|
3195
3351
|
text_lower = text.lower()
|
|
3196
3352
|
logger.debug(f"SSE status: {text}")
|
|
3197
3353
|
|
|
3198
|
-
|
|
3354
|
+
if not openai_text_stream_seen:
|
|
3355
|
+
# Non-OpenAI providers stream their main output on run_id; preserve old behavior.
|
|
3356
|
+
main_text = text
|
|
3357
|
+
else:
|
|
3358
|
+
# OpenAI: keep the streamed answer from *-text and render status/tool separately.
|
|
3359
|
+
if _looks_like_tool_text(text):
|
|
3360
|
+
tool_text = text
|
|
3361
|
+
else:
|
|
3362
|
+
status_text = text
|
|
3363
|
+
|
|
3199
3364
|
if text.strip():
|
|
3200
|
-
|
|
3201
|
-
{"blocks": [{"text": text}]},
|
|
3202
|
-
status_widget_container[0],
|
|
3203
|
-
)
|
|
3365
|
+
_maybe_render()
|
|
3204
3366
|
|
|
3205
|
-
# Detect completion from text signals (server sends these as plain text)
|
|
3367
|
+
# Detect completion from text signals (server sends these as plain text).
|
|
3206
3368
|
if any(kw in text_lower for kw in ["finishing", "completed", "finished"]):
|
|
3207
3369
|
logger.info(f"Completion signal from SSE text: {text}")
|
|
3208
3370
|
asyncio.create_task(_try_fetch_final_result_async())
|
|
3209
3371
|
return
|
|
3210
3372
|
|
|
3211
|
-
# JSON status messages
|
|
3212
|
-
status = data.get("status", "").lower()
|
|
3373
|
+
# JSON status messages (rare, but some server paths send them).
|
|
3374
|
+
status = str(data.get("status", "") or "").lower()
|
|
3213
3375
|
if status:
|
|
3214
3376
|
logger.info(f"SSE JSON status: {status}")
|
|
3377
|
+
if openai_text_stream_seen:
|
|
3378
|
+
status_text = f"Status: {status}"
|
|
3379
|
+
_maybe_render()
|
|
3215
3380
|
if status in ["completed", "success", "finished", "error", "failed"]:
|
|
3216
3381
|
asyncio.create_task(_try_fetch_final_result_async())
|
|
3217
3382
|
|
|
@@ -3239,11 +3404,30 @@ class DashboardScreen(Screen):
|
|
|
3239
3404
|
client.stream_action_result(run_id, handle_status_message)
|
|
3240
3405
|
)
|
|
3241
3406
|
|
|
3407
|
+
# OpenAI Responses streams assistant text to a separate topic: f"{run_id}-text".
|
|
3408
|
+
# Subscribe here; for non-OpenAI providers this is a no-op (no messages will be published).
|
|
3409
|
+
try:
|
|
3410
|
+
text_task = asyncio.create_task(
|
|
3411
|
+
client.stream_action_result(
|
|
3412
|
+
run_id,
|
|
3413
|
+
handle_text_message,
|
|
3414
|
+
topic=f"{run_id}-text",
|
|
3415
|
+
stop_on_terminal_status=False,
|
|
3416
|
+
plain_text_type="text",
|
|
3417
|
+
)
|
|
3418
|
+
)
|
|
3419
|
+
except Exception:
|
|
3420
|
+
text_task = None
|
|
3421
|
+
|
|
3422
|
+
|
|
3242
3423
|
try:
|
|
3243
3424
|
while not got_final_result:
|
|
3244
3425
|
tasks: list[asyncio.Task] = [poll_task]
|
|
3245
3426
|
if status_task is not None:
|
|
3246
3427
|
tasks.append(status_task)
|
|
3428
|
+
if text_task is not None:
|
|
3429
|
+
tasks.append(text_task)
|
|
3430
|
+
|
|
3247
3431
|
|
|
3248
3432
|
done, _pending = await asyncio.wait(
|
|
3249
3433
|
tasks,
|
|
@@ -3264,6 +3448,15 @@ class DashboardScreen(Screen):
|
|
|
3264
3448
|
logger.warning(f"SSE stream ended for {run_id}: {e}")
|
|
3265
3449
|
status_task = None
|
|
3266
3450
|
|
|
3451
|
+
if text_task is not None and text_task in done:
|
|
3452
|
+
try:
|
|
3453
|
+
text_task.result()
|
|
3454
|
+
except asyncio.CancelledError:
|
|
3455
|
+
pass
|
|
3456
|
+
except Exception as e:
|
|
3457
|
+
logger.warning(f"SSE text stream ended for {run_id}: {e}")
|
|
3458
|
+
text_task = None
|
|
3459
|
+
|
|
3267
3460
|
# Ensure polling completes (it exits when got_final_result becomes True).
|
|
3268
3461
|
if not poll_task.done():
|
|
3269
3462
|
await poll_task
|
|
@@ -3271,6 +3464,8 @@ class DashboardScreen(Screen):
|
|
|
3271
3464
|
logger.info(f"Stream cancelled for {run_id}")
|
|
3272
3465
|
if status_task is not None:
|
|
3273
3466
|
status_task.cancel()
|
|
3467
|
+
if text_task is not None:
|
|
3468
|
+
text_task.cancel()
|
|
3274
3469
|
poll_task.cancel()
|
|
3275
3470
|
_remove_status_widget()
|
|
3276
3471
|
self._set_streaming(False)
|
|
@@ -3295,6 +3490,13 @@ class DashboardScreen(Screen):
|
|
|
3295
3490
|
except (asyncio.CancelledError, Exception):
|
|
3296
3491
|
pass
|
|
3297
3492
|
|
|
3493
|
+
if text_task is not None and not text_task.done():
|
|
3494
|
+
text_task.cancel()
|
|
3495
|
+
try:
|
|
3496
|
+
await text_task
|
|
3497
|
+
except (asyncio.CancelledError, Exception):
|
|
3498
|
+
pass
|
|
3499
|
+
|
|
3298
3500
|
# Clean up status widget if it still exists (we keep it when it becomes the final output).
|
|
3299
3501
|
if status_widget_container[0]:
|
|
3300
3502
|
_remove_status_widget()
|
|
@@ -41,6 +41,7 @@ SLASH_COMMANDS: list[SlashCommand] = [
|
|
|
41
41
|
|
|
42
42
|
# Runtime (local CLI agent)
|
|
43
43
|
SlashCommand("Runtime", "/connect-cli", "Tell the agent to connect to the local CLI", "/connect-cli"),
|
|
44
|
+
SlashCommand("Runtime", "/disconnect-cli", "Disconnect the local CLI runtime for the current run", "/disconnect-cli"),
|
|
44
45
|
SlashCommand("Runtime", "/show-logs", "Show local CLI (runtime) logs", "/show-logs"),
|
|
45
46
|
SlashCommand("Runtime", "/runtime", "Runtime commands (currently disabled)", "/runtime"),
|
|
46
47
|
|
|
@@ -417,6 +417,28 @@ _STATUS_WORDS_RAW: tuple[str, ...] = (
|
|
|
417
417
|
"Alchemizing",
|
|
418
418
|
"Treasure-hunting",
|
|
419
419
|
"Dragon-wrangling",
|
|
420
|
+
"Mapping",
|
|
421
|
+
"Correlating",
|
|
422
|
+
"Quantifying",
|
|
423
|
+
"Cascading",
|
|
424
|
+
"Reconnoitering",
|
|
425
|
+
"Dissecting",
|
|
426
|
+
"Unifying",
|
|
427
|
+
"Segmenting",
|
|
428
|
+
"Partitioning",
|
|
429
|
+
"Deconstructing",
|
|
430
|
+
"Reframing",
|
|
431
|
+
"Bootstrapping",
|
|
432
|
+
"Factoring",
|
|
433
|
+
"Defragmenting",
|
|
434
|
+
"Elucidating",
|
|
435
|
+
"Decoupling",
|
|
436
|
+
"Vectorizing",
|
|
437
|
+
"Rasterizing",
|
|
438
|
+
"Hashing",
|
|
439
|
+
"Checkpointing",
|
|
440
|
+
"Backtracking",
|
|
441
|
+
"Demultiplexing",
|
|
420
442
|
)
|
|
421
443
|
|
|
422
444
|
|
|
@@ -124,6 +124,9 @@ class ChatInput(TextArea):
|
|
|
124
124
|
if len(candidate) >= 2 and candidate[0] == candidate[-1] and candidate[0] in {'"', "'"}:
|
|
125
125
|
candidate = candidate[1:-1]
|
|
126
126
|
|
|
127
|
+
if sys.platform == "win32":
|
|
128
|
+
candidate = candidate.replace("\\ ", " ")
|
|
129
|
+
|
|
127
130
|
if candidate.startswith("file://"):
|
|
128
131
|
parsed = urlparse(candidate)
|
|
129
132
|
if parsed.scheme != "file":
|
|
@@ -144,7 +147,7 @@ class ChatInput(TextArea):
|
|
|
144
147
|
token_sets: list[list[str]] = []
|
|
145
148
|
|
|
146
149
|
try:
|
|
147
|
-
split_tokens = shlex.split(normalized, posix=
|
|
150
|
+
split_tokens = shlex.split(normalized, posix=(sys.platform != "win32"))
|
|
148
151
|
except ValueError:
|
|
149
152
|
split_tokens = []
|
|
150
153
|
if split_tokens:
|
|
@@ -4,8 +4,6 @@ Every test that touches the filesystem through Kiwi's managers goes
|
|
|
4
4
|
through the ``isolated_home`` fixture so that ``~/.kiwi/`` writes land in
|
|
5
5
|
a tmp dir instead of polluting the developer's real home.
|
|
6
6
|
"""
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
7
|
import os
|
|
10
8
|
from pathlib import Path
|
|
11
9
|
|
|
@@ -5,8 +5,6 @@ mirror what ``pipx install kiwi-code`` users experience. They are the
|
|
|
5
5
|
first line of defense against regressions in argument parsing,
|
|
6
6
|
imports, and packaging.
|
|
7
7
|
"""
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
8
|
import subprocess
|
|
11
9
|
import sys
|
|
12
10
|
from pathlib import Path
|
|
@@ -4,11 +4,8 @@ Walks every submodule of the three shipped packages and imports it.
|
|
|
4
4
|
This is the cheapest way to catch syntax errors, circular imports and
|
|
5
5
|
stale references before they reach end users via ``pipx install``.
|
|
6
6
|
"""
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
7
|
import importlib
|
|
10
8
|
import pkgutil
|
|
11
|
-
|
|
12
9
|
import pytest
|
|
13
10
|
|
|
14
11
|
PACKAGES = ["kiwi_cli", "kiwi_tui", "kiwi_runtime"]
|
|
@@ -6,15 +6,11 @@ The goal of the re-exec is to make the kernel-reported process name
|
|
|
6
6
|
We verify that by spawning a child that exec's through the symlink and
|
|
7
7
|
prints its own ``comm``.
|
|
8
8
|
"""
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
9
|
import os
|
|
12
10
|
import subprocess
|
|
13
11
|
import sys
|
|
14
12
|
from pathlib import Path
|
|
15
|
-
|
|
16
13
|
import pytest
|
|
17
|
-
|
|
18
14
|
pytestmark = pytest.mark.skipif(
|
|
19
15
|
sys.platform == "win32",
|
|
20
16
|
reason="Windows uses native kiwi.exe launcher; no re-exec needed",
|
|
@@ -4,12 +4,9 @@ These exercise the real ``AutobotsTUI`` app the way a user would see it
|
|
|
4
4
|
but drive it through Textual's Pilot so they run anywhere (including CI
|
|
5
5
|
without a real terminal).
|
|
6
6
|
"""
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
7
|
import asyncio
|
|
10
8
|
from pathlib import Path
|
|
11
9
|
from types import SimpleNamespace
|
|
12
|
-
|
|
13
10
|
import json
|
|
14
11
|
import pytest
|
|
15
12
|
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import asyncio
|
|
4
2
|
import json
|
|
5
3
|
from pathlib import Path
|
|
6
4
|
from types import SimpleNamespace
|
|
7
|
-
|
|
8
5
|
import pytest
|
|
9
|
-
|
|
10
6
|
from kiwi_runtime.main import _LocalInteractivePTYBridge, _tui_managed_control_dir
|
|
11
7
|
from kiwi_tui.runtime_agent import control_dir_from_meta
|
|
12
8
|
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import os
|
|
4
2
|
import shutil
|
|
5
3
|
import subprocess
|
|
6
|
-
import sys
|
|
7
4
|
from pathlib import Path
|
|
8
|
-
|
|
9
5
|
import pytest
|
|
10
|
-
|
|
11
6
|
from kiwi_tui.worktrees import WorktreeError, ensure_worktree
|
|
12
7
|
|
|
13
8
|
|
|
@@ -61,7 +56,7 @@ def test_ensure_worktree_creates_under_dot_kiwi(tmp_path: Path) -> None:
|
|
|
61
56
|
cwd=str(repo),
|
|
62
57
|
text=True,
|
|
63
58
|
)
|
|
64
|
-
assert
|
|
59
|
+
assert expected.as_posix() in wt_list.replace("\\", "/")
|
|
65
60
|
|
|
66
61
|
|
|
67
62
|
@pytest.mark.skipif(shutil.which("git") is None, reason="git is required for worktree tests")
|
|
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
|