kiwi-code 0.0.30__tar.gz → 0.0.31__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.30 → kiwi_code-0.0.31}/PKG-INFO +1 -1
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/pyproject.toml +1 -1
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_cli/cli.py +85 -11
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_cli/client.py +8 -1
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/screens/dashboard.py +251 -37
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/uv.lock +1 -1
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/.gitignore +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/.python-version +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/CLAUDE.md +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/Makefile +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/README.md +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_runtime/main.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_runtime/snake_game/.gitignore +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/main.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/slash_commands.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/test_hello.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/tests/__init__.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/tests/conftest.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.30 → kiwi_code-0.0.31}/tests/test_tui_headless.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Typer CLI for Autobots — thin wrapper over commands.py."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Optional, Callable
|
|
4
4
|
|
|
5
5
|
from loguru import logger
|
|
6
6
|
logger.remove() # Suppress loguru console output in CLI mode
|
|
@@ -55,18 +55,92 @@ def _backend_url() -> str:
|
|
|
55
55
|
return AppConfig().backend_url
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
def _http_status_from_lines(lines: list[str]) -> int | None:
|
|
59
|
+
"""Extract an HTTP status code from commands.py output lines.
|
|
60
|
+
|
|
61
|
+
commands.py returns errors in the form: ["Error: HTTP <code>"]
|
|
62
|
+
"""
|
|
63
|
+
if not lines:
|
|
64
|
+
return None
|
|
65
|
+
if len(lines) != 1:
|
|
66
|
+
return None
|
|
67
|
+
s = lines[0].strip()
|
|
68
|
+
prefix = "Error: HTTP "
|
|
69
|
+
if not s.startswith(prefix):
|
|
70
|
+
return None
|
|
71
|
+
try:
|
|
72
|
+
return int(s[len(prefix):].strip())
|
|
73
|
+
except Exception:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _force_refresh_tokens(tm: TokenManager) -> bool:
|
|
78
|
+
"""Force refresh tokens once and persist them."""
|
|
79
|
+
from .client import AutobotsClientWrapper
|
|
80
|
+
|
|
81
|
+
with tm.file_lock():
|
|
82
|
+
tokens = tm.load_tokens()
|
|
83
|
+
if not tokens:
|
|
84
|
+
return False
|
|
85
|
+
refresh = getattr(tokens, "refresh_token", None)
|
|
86
|
+
if not refresh:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
wrapper = AutobotsClientWrapper(
|
|
90
|
+
base_url=_backend_url(),
|
|
91
|
+
access_token=getattr(tokens, "access_token", None),
|
|
92
|
+
)
|
|
93
|
+
ok, new_tokens, _msg = wrapper.refresh_token(str(refresh))
|
|
94
|
+
if ok and new_tokens:
|
|
95
|
+
tm.save_tokens(new_tokens)
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
58
100
|
def _get_client() -> AuthenticatedClient:
|
|
101
|
+
"""Get an authenticated OpenAPI client.
|
|
102
|
+
|
|
103
|
+
If the access token is expired but a refresh token exists, attempt a refresh
|
|
104
|
+
once. This improves CLI reliability for long-lived sessions.
|
|
105
|
+
"""
|
|
59
106
|
tm = TokenManager()
|
|
60
|
-
|
|
107
|
+
tokens = tm.load_tokens()
|
|
108
|
+
if not tokens:
|
|
109
|
+
typer.echo("Not authenticated. Run `kiwi login` first.", err=True)
|
|
110
|
+
raise typer.Exit(code=1)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
if tokens.is_expired() and getattr(tokens, "refresh_token", None):
|
|
114
|
+
_force_refresh_tokens(tm)
|
|
115
|
+
tokens = tm.load_tokens() or tokens
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
access = getattr(tokens, "access_token", None)
|
|
120
|
+
if not access:
|
|
61
121
|
typer.echo("Not authenticated. Run `kiwi login` first.", err=True)
|
|
62
122
|
raise typer.Exit(code=1)
|
|
123
|
+
|
|
63
124
|
return AuthenticatedClient(
|
|
64
125
|
base_url=_backend_url(),
|
|
65
|
-
token=
|
|
126
|
+
token=str(access),
|
|
66
127
|
raise_on_unexpected_status=False,
|
|
67
128
|
)
|
|
68
129
|
|
|
69
130
|
|
|
131
|
+
def _run_with_refresh_retry(run: Callable[[AuthenticatedClient], list[str]]) -> list[str]:
|
|
132
|
+
"""Run a CLI command with one forced refresh retry on 401/403."""
|
|
133
|
+
tm = TokenManager()
|
|
134
|
+
lines = run(_get_client())
|
|
135
|
+
code = _http_status_from_lines(lines)
|
|
136
|
+
if code in (401, 403):
|
|
137
|
+
try:
|
|
138
|
+
if _force_refresh_tokens(tm):
|
|
139
|
+
lines = run(_get_client())
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
return lines
|
|
143
|
+
|
|
70
144
|
def _print(lines: list[str]) -> None:
|
|
71
145
|
for line in lines:
|
|
72
146
|
typer.echo(line)
|
|
@@ -81,12 +155,12 @@ def actions_list_cmd(
|
|
|
81
155
|
offset: int = typer.Option(0, help="Offset"),
|
|
82
156
|
):
|
|
83
157
|
"""List actions."""
|
|
84
|
-
_print(commands.actions_list(
|
|
158
|
+
_print(_run_with_refresh_retry(lambda c: commands.actions_list(c, name=name, limit=limit, offset=offset)))
|
|
85
159
|
|
|
86
160
|
@actions_app.command("get")
|
|
87
161
|
def actions_get_cmd(id: str = typer.Argument(help="Action ID")):
|
|
88
162
|
"""Get action details."""
|
|
89
|
-
_print(commands.actions_get(
|
|
163
|
+
_print(_run_with_refresh_retry(lambda c: commands.actions_get(c, id=id)))
|
|
90
164
|
|
|
91
165
|
|
|
92
166
|
# -- runs -------------------------------------------------------------------
|
|
@@ -100,12 +174,12 @@ def runs_list_cmd(
|
|
|
100
174
|
offset: int = typer.Option(0, help="Offset"),
|
|
101
175
|
):
|
|
102
176
|
"""List action runs (results)."""
|
|
103
|
-
_print(commands.runs_list(
|
|
177
|
+
_print(_run_with_refresh_retry(lambda c: commands.runs_list(c, action_id=action_id, action_name=action_name, status=status, limit=limit, offset=offset)))
|
|
104
178
|
|
|
105
179
|
@action_runs_app.command("get")
|
|
106
180
|
def runs_get_cmd(id: str = typer.Argument(help="Action run ID")):
|
|
107
181
|
"""Get action run details."""
|
|
108
|
-
_print(commands.runs_get(
|
|
182
|
+
_print(_run_with_refresh_retry(lambda c: commands.runs_get(c, id=id)))
|
|
109
183
|
|
|
110
184
|
|
|
111
185
|
# -- graphs -----------------------------------------------------------------
|
|
@@ -117,12 +191,12 @@ def graphs_list_cmd(
|
|
|
117
191
|
offset: int = typer.Option(0, help="Offset"),
|
|
118
192
|
):
|
|
119
193
|
"""List action graphs."""
|
|
120
|
-
_print(commands.graphs_list(
|
|
194
|
+
_print(_run_with_refresh_retry(lambda c: commands.graphs_list(c, name=name, limit=limit, offset=offset)))
|
|
121
195
|
|
|
122
196
|
@graphs_app.command("get")
|
|
123
197
|
def graphs_get_cmd(id: str = typer.Argument(help="Action graph ID")):
|
|
124
198
|
"""Get action graph details."""
|
|
125
|
-
_print(commands.graphs_get(
|
|
199
|
+
_print(_run_with_refresh_retry(lambda c: commands.graphs_get(c, id=id)))
|
|
126
200
|
|
|
127
201
|
|
|
128
202
|
# -- graph-runs -------------------------------------------------------------
|
|
@@ -136,12 +210,12 @@ def graph_runs_list_cmd(
|
|
|
136
210
|
offset: int = typer.Option(0, help="Offset"),
|
|
137
211
|
):
|
|
138
212
|
"""List action graph runs (results)."""
|
|
139
|
-
_print(commands.graph_runs_list(
|
|
213
|
+
_print(_run_with_refresh_retry(lambda c: commands.graph_runs_list(c, action_graph_id=graph_id, action_graph_name=graph_name, status=status, limit=limit, offset=offset)))
|
|
140
214
|
|
|
141
215
|
@graph_runs_app.command("get")
|
|
142
216
|
def graph_runs_get_cmd(id: str = typer.Argument(help="Graph run ID")):
|
|
143
217
|
"""Get action graph run details."""
|
|
144
|
-
_print(commands.graph_runs_get(
|
|
218
|
+
_print(_run_with_refresh_retry(lambda c: commands.graph_runs_get(c, id=id)))
|
|
145
219
|
|
|
146
220
|
|
|
147
221
|
# -- runtime ----------------------------------------------------------------
|
|
@@ -183,7 +183,14 @@ class AutobotsClientWrapper:
|
|
|
183
183
|
# even if `refresh_token` is provided as a query param.
|
|
184
184
|
url = f"{self.base_url.rstrip('/')}/v1/auth/session/refresh"
|
|
185
185
|
with httpx.Client(timeout=30) as client:
|
|
186
|
-
|
|
186
|
+
# Best-effort: send the (possibly expired) access token for anomaly tracking.
|
|
187
|
+
# The backend refresh endpoint must not *require* it.
|
|
188
|
+
headers: dict[str, str] = {}
|
|
189
|
+
if self.access_token:
|
|
190
|
+
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
191
|
+
resp = client.post(
|
|
192
|
+
url, params={"refresh_token": refresh_token}, headers=headers
|
|
193
|
+
)
|
|
187
194
|
|
|
188
195
|
if resp.status_code != 200:
|
|
189
196
|
detail = (resp.text or "").strip()
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import Callable
|
|
5
6
|
from textual.app import ComposeResult
|
|
6
7
|
from textual.binding import Binding
|
|
7
8
|
from textual.screen import Screen
|
|
@@ -877,11 +878,12 @@ class DashboardScreen(Screen):
|
|
|
877
878
|
show("Usage: /use <action_id>")
|
|
878
879
|
return
|
|
879
880
|
action_id = args[0]
|
|
880
|
-
api_client = self._get_api_client_for_command(command)
|
|
881
|
-
if not api_client:
|
|
882
|
-
return
|
|
883
881
|
from kiwi_cli.commands import actions_get
|
|
884
|
-
lines = await
|
|
882
|
+
lines = await self._run_lines_with_refresh_retry(
|
|
883
|
+
command, lambda c: actions_get(c, id=action_id)
|
|
884
|
+
)
|
|
885
|
+
if lines is None:
|
|
886
|
+
return
|
|
885
887
|
if lines and lines[0].startswith("Error"):
|
|
886
888
|
show(f"Action not found: {action_id}", is_error=True)
|
|
887
889
|
return
|
|
@@ -898,11 +900,12 @@ class DashboardScreen(Screen):
|
|
|
898
900
|
show("Usage: /continue <run_id>")
|
|
899
901
|
return
|
|
900
902
|
run_id = args[0]
|
|
901
|
-
api_client = self._get_api_client_for_command(command)
|
|
902
|
-
if not api_client:
|
|
903
|
-
return
|
|
904
903
|
from kiwi_cli.commands import runs_get
|
|
905
|
-
lines = await
|
|
904
|
+
lines = await self._run_lines_with_refresh_retry(
|
|
905
|
+
command, lambda c: runs_get(c, id=run_id)
|
|
906
|
+
)
|
|
907
|
+
if lines is None:
|
|
908
|
+
return
|
|
906
909
|
if lines and lines[0].startswith("Error"):
|
|
907
910
|
show(f"Run not found: {run_id}", is_error=True)
|
|
908
911
|
return
|
|
@@ -1056,7 +1059,11 @@ class DashboardScreen(Screen):
|
|
|
1056
1059
|
|
|
1057
1060
|
from kiwi_cli.commands import dispatch
|
|
1058
1061
|
try:
|
|
1059
|
-
lines = await
|
|
1062
|
+
lines = await self._run_lines_with_refresh_retry(
|
|
1063
|
+
command, lambda c: dispatch(command, c)
|
|
1064
|
+
)
|
|
1065
|
+
if lines is None:
|
|
1066
|
+
return
|
|
1060
1067
|
show("\n".join(lines))
|
|
1061
1068
|
except Exception as e:
|
|
1062
1069
|
show(f"Command error: {e}", is_error=True)
|
|
@@ -1070,9 +1077,26 @@ class DashboardScreen(Screen):
|
|
|
1070
1077
|
"""Fetch and render conversation history without blocking the UI."""
|
|
1071
1078
|
if not hasattr(self.app, "autobots_client"):
|
|
1072
1079
|
return
|
|
1073
|
-
success, result,
|
|
1080
|
+
success, result, msg = await asyncio.to_thread(
|
|
1074
1081
|
lambda: self.app.autobots_client.get_action_result(run_id)
|
|
1075
1082
|
)
|
|
1083
|
+
if (not success) and msg:
|
|
1084
|
+
code = None
|
|
1085
|
+
try:
|
|
1086
|
+
m = re.search(r"(?:HTTP|status)\s*(\d{3})", str(msg))
|
|
1087
|
+
if m:
|
|
1088
|
+
code = int(m.group(1))
|
|
1089
|
+
except Exception:
|
|
1090
|
+
code = None
|
|
1091
|
+
if code in (401, 403):
|
|
1092
|
+
try:
|
|
1093
|
+
refreshed = bool(self.app._refresh_token_if_needed(force=True))
|
|
1094
|
+
except Exception:
|
|
1095
|
+
refreshed = False
|
|
1096
|
+
if refreshed:
|
|
1097
|
+
success, result, msg = await asyncio.to_thread(
|
|
1098
|
+
lambda: self.app.autobots_client.get_action_result(run_id)
|
|
1099
|
+
)
|
|
1076
1100
|
if not success or not result:
|
|
1077
1101
|
return
|
|
1078
1102
|
self._render_conversation_history_from_action_result(result)
|
|
@@ -1093,6 +1117,22 @@ class DashboardScreen(Screen):
|
|
|
1093
1117
|
offset=offset,
|
|
1094
1118
|
)
|
|
1095
1119
|
)
|
|
1120
|
+
if resp.status_code in (401, 403):
|
|
1121
|
+
# Reactive auth guard: force refresh and retry once.
|
|
1122
|
+
try:
|
|
1123
|
+
refreshed = bool(self.app._refresh_token_if_needed(force=True))
|
|
1124
|
+
except Exception:
|
|
1125
|
+
refreshed = False
|
|
1126
|
+
if refreshed:
|
|
1127
|
+
api_client = getattr(getattr(self.app, "autobots_client", None), "client", api_client)
|
|
1128
|
+
resp = await asyncio.to_thread(
|
|
1129
|
+
lambda: list_actions_v1_actions_get.sync_detailed(
|
|
1130
|
+
client=api_client,
|
|
1131
|
+
name=name if name else UNSET,
|
|
1132
|
+
limit=limit,
|
|
1133
|
+
offset=offset,
|
|
1134
|
+
)
|
|
1135
|
+
)
|
|
1096
1136
|
if resp.status_code != 200 or not resp.parsed:
|
|
1097
1137
|
self._show_command_result(command, f"Error: HTTP {resp.status_code}", is_error=True)
|
|
1098
1138
|
return
|
|
@@ -1147,6 +1187,23 @@ class DashboardScreen(Screen):
|
|
|
1147
1187
|
offset=int(opts.get("offset", "0")),
|
|
1148
1188
|
)
|
|
1149
1189
|
)
|
|
1190
|
+
if resp.status_code in (401, 403):
|
|
1191
|
+
try:
|
|
1192
|
+
refreshed = bool(self.app._refresh_token_if_needed(force=True))
|
|
1193
|
+
except Exception:
|
|
1194
|
+
refreshed = False
|
|
1195
|
+
if refreshed:
|
|
1196
|
+
api_client = getattr(getattr(self.app, "autobots_client", None), "client", api_client)
|
|
1197
|
+
resp = await asyncio.to_thread(
|
|
1198
|
+
lambda: list_action_result_v1_action_results_get.sync_detailed(
|
|
1199
|
+
client=api_client,
|
|
1200
|
+
action_id=opts.get("action_id") or UNSET,
|
|
1201
|
+
action_name=opts.get("action_name") or UNSET,
|
|
1202
|
+
status=status_enum,
|
|
1203
|
+
limit=int(opts.get("limit", "50")),
|
|
1204
|
+
offset=int(opts.get("offset", "0")),
|
|
1205
|
+
)
|
|
1206
|
+
)
|
|
1150
1207
|
if resp.status_code != 200 or not resp.parsed:
|
|
1151
1208
|
self._show_command_result(command, f"Error: HTTP {resp.status_code}", is_error=True)
|
|
1152
1209
|
return
|
|
@@ -1188,6 +1245,21 @@ class DashboardScreen(Screen):
|
|
|
1188
1245
|
offset=offset,
|
|
1189
1246
|
)
|
|
1190
1247
|
)
|
|
1248
|
+
if resp.status_code in (401, 403):
|
|
1249
|
+
try:
|
|
1250
|
+
refreshed = bool(self.app._refresh_token_if_needed(force=True))
|
|
1251
|
+
except Exception:
|
|
1252
|
+
refreshed = False
|
|
1253
|
+
if refreshed:
|
|
1254
|
+
api_client = getattr(getattr(self.app, "autobots_client", None), "client", api_client)
|
|
1255
|
+
resp = await asyncio.to_thread(
|
|
1256
|
+
lambda: list_action_graphs_v1_action_graphs_get.sync_detailed(
|
|
1257
|
+
client=api_client,
|
|
1258
|
+
name=name if name else UNSET,
|
|
1259
|
+
limit=limit,
|
|
1260
|
+
offset=offset,
|
|
1261
|
+
)
|
|
1262
|
+
)
|
|
1191
1263
|
if resp.status_code != 200 or not resp.parsed:
|
|
1192
1264
|
self._show_command_result(command, f"Error: HTTP {resp.status_code}", is_error=True)
|
|
1193
1265
|
return
|
|
@@ -1242,6 +1314,23 @@ class DashboardScreen(Screen):
|
|
|
1242
1314
|
offset=int(opts.get("offset", "0")),
|
|
1243
1315
|
)
|
|
1244
1316
|
)
|
|
1317
|
+
if resp.status_code in (401, 403):
|
|
1318
|
+
try:
|
|
1319
|
+
refreshed = bool(self.app._refresh_token_if_needed(force=True))
|
|
1320
|
+
except Exception:
|
|
1321
|
+
refreshed = False
|
|
1322
|
+
if refreshed:
|
|
1323
|
+
api_client = getattr(getattr(self.app, "autobots_client", None), "client", api_client)
|
|
1324
|
+
resp = await asyncio.to_thread(
|
|
1325
|
+
lambda: list_action_graph_result_v1_action_graphs_results_get.sync_detailed(
|
|
1326
|
+
client=api_client,
|
|
1327
|
+
action_graph_id=opts.get("graph_id") or UNSET,
|
|
1328
|
+
action_graph_name=opts.get("graph_name") or UNSET,
|
|
1329
|
+
status=status_enum,
|
|
1330
|
+
limit=int(opts.get("limit", "50")),
|
|
1331
|
+
offset=int(opts.get("offset", "0")),
|
|
1332
|
+
)
|
|
1333
|
+
)
|
|
1245
1334
|
if resp.status_code != 200 or not resp.parsed:
|
|
1246
1335
|
self._show_command_result(command, f"Error: HTTP {resp.status_code}", is_error=True)
|
|
1247
1336
|
return
|
|
@@ -1302,6 +1391,47 @@ class DashboardScreen(Screen):
|
|
|
1302
1391
|
return api_client
|
|
1303
1392
|
|
|
1304
1393
|
|
|
1394
|
+
|
|
1395
|
+
def _http_status_from_lines(self, lines: list[str]) -> int | None:
|
|
1396
|
+
"""Extract status code from a commands.py-style error list."""
|
|
1397
|
+
if not lines:
|
|
1398
|
+
return None
|
|
1399
|
+
if len(lines) != 1:
|
|
1400
|
+
return None
|
|
1401
|
+
s = str(lines[0]).strip()
|
|
1402
|
+
m = re.match(r"^Error:\s*HTTP\s*(\d{3})\s*$", s)
|
|
1403
|
+
if not m:
|
|
1404
|
+
return None
|
|
1405
|
+
try:
|
|
1406
|
+
return int(m.group(1))
|
|
1407
|
+
except Exception:
|
|
1408
|
+
return None
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
async def _run_lines_with_refresh_retry(
|
|
1412
|
+
self,
|
|
1413
|
+
title: str,
|
|
1414
|
+
fn: Callable[[object], list[str]],
|
|
1415
|
+
) -> list[str] | None:
|
|
1416
|
+
"""Run a slash-command lines function with one refresh+retry on 401/403."""
|
|
1417
|
+
api_client = self._get_api_client_for_command(title)
|
|
1418
|
+
if not api_client:
|
|
1419
|
+
return None
|
|
1420
|
+
lines = await asyncio.to_thread(lambda: fn(api_client))
|
|
1421
|
+
code = self._http_status_from_lines(lines)
|
|
1422
|
+
if code in (401, 403):
|
|
1423
|
+
refreshed = False
|
|
1424
|
+
try:
|
|
1425
|
+
refreshed = bool(self.app._refresh_token_if_needed(force=True))
|
|
1426
|
+
except Exception:
|
|
1427
|
+
refreshed = False
|
|
1428
|
+
if refreshed:
|
|
1429
|
+
api_client = self._get_api_client_for_command(title)
|
|
1430
|
+
if not api_client:
|
|
1431
|
+
return lines
|
|
1432
|
+
lines = await asyncio.to_thread(lambda: fn(api_client))
|
|
1433
|
+
return lines
|
|
1434
|
+
|
|
1305
1435
|
def _parse_command_opts(self, command: str, skip: int = 2) -> dict[str, str]:
|
|
1306
1436
|
"""Parse --key value pairs from a slash command string."""
|
|
1307
1437
|
parts = command.strip().split()
|
|
@@ -1543,11 +1673,12 @@ class DashboardScreen(Screen):
|
|
|
1543
1673
|
|
|
1544
1674
|
async def _on_action_picked_async(self, action_id: str) -> None:
|
|
1545
1675
|
try:
|
|
1546
|
-
api_client = self._get_api_client_for_command("/actions list")
|
|
1547
|
-
if not api_client:
|
|
1548
|
-
return
|
|
1549
1676
|
from kiwi_cli.commands import actions_get
|
|
1550
|
-
lines = await
|
|
1677
|
+
lines = await self._run_lines_with_refresh_retry(
|
|
1678
|
+
"/actions list", lambda c: actions_get(c, id=action_id)
|
|
1679
|
+
)
|
|
1680
|
+
if lines is None:
|
|
1681
|
+
return
|
|
1551
1682
|
if lines and lines[0].startswith("Error"):
|
|
1552
1683
|
self._show_command_result("Action selection", f"Action not found: {action_id}", is_error=True)
|
|
1553
1684
|
return
|
|
@@ -1578,11 +1709,12 @@ class DashboardScreen(Screen):
|
|
|
1578
1709
|
|
|
1579
1710
|
async def _on_run_picked_async(self, run_id: str) -> None:
|
|
1580
1711
|
try:
|
|
1581
|
-
api_client = self._get_api_client_for_command("/runs list")
|
|
1582
|
-
if not api_client:
|
|
1583
|
-
return
|
|
1584
1712
|
from kiwi_cli.commands import runs_get
|
|
1585
|
-
lines = await
|
|
1713
|
+
lines = await self._run_lines_with_refresh_retry(
|
|
1714
|
+
"/runs list", lambda c: runs_get(c, id=run_id)
|
|
1715
|
+
)
|
|
1716
|
+
if lines is None:
|
|
1717
|
+
return
|
|
1586
1718
|
if lines and lines[0].startswith("Error"):
|
|
1587
1719
|
self._show_command_result("Run selection", f"Run not found: {run_id}", is_error=True)
|
|
1588
1720
|
return
|
|
@@ -1612,11 +1744,12 @@ class DashboardScreen(Screen):
|
|
|
1612
1744
|
|
|
1613
1745
|
async def _on_graph_picked_async(self, graph_id: str) -> None:
|
|
1614
1746
|
try:
|
|
1615
|
-
api_client = self._get_api_client_for_command("/graphs list")
|
|
1616
|
-
if not api_client:
|
|
1617
|
-
return
|
|
1618
1747
|
from kiwi_cli.commands import graphs_get
|
|
1619
|
-
lines = await
|
|
1748
|
+
lines = await self._run_lines_with_refresh_retry(
|
|
1749
|
+
"/graphs list", lambda c: graphs_get(c, id=graph_id)
|
|
1750
|
+
)
|
|
1751
|
+
if lines is None:
|
|
1752
|
+
return
|
|
1620
1753
|
if lines and lines[0].startswith("Error"):
|
|
1621
1754
|
self._show_command_result("Graph selection", f"Graph not found: {graph_id}", is_error=True)
|
|
1622
1755
|
return
|
|
@@ -1642,11 +1775,12 @@ class DashboardScreen(Screen):
|
|
|
1642
1775
|
|
|
1643
1776
|
async def _on_graph_run_picked_async(self, graph_run_id: str) -> None:
|
|
1644
1777
|
try:
|
|
1645
|
-
api_client = self._get_api_client_for_command("/graph-runs list")
|
|
1646
|
-
if not api_client:
|
|
1647
|
-
return
|
|
1648
1778
|
from kiwi_cli.commands import graph_runs_get
|
|
1649
|
-
lines = await
|
|
1779
|
+
lines = await self._run_lines_with_refresh_retry(
|
|
1780
|
+
"/graph-runs list", lambda c: graph_runs_get(c, id=graph_run_id)
|
|
1781
|
+
)
|
|
1782
|
+
if lines is None:
|
|
1783
|
+
return
|
|
1650
1784
|
if lines and lines[0].startswith("Error"):
|
|
1651
1785
|
self._show_command_result("Graph run selection", f"Graph run not found: {graph_run_id}", is_error=True)
|
|
1652
1786
|
return
|
|
@@ -2188,17 +2322,48 @@ class DashboardScreen(Screen):
|
|
|
2188
2322
|
return
|
|
2189
2323
|
|
|
2190
2324
|
if not success:
|
|
2191
|
-
|
|
2192
|
-
#
|
|
2325
|
+
# Reactive auth guard: if the request failed with 401/403, force a token
|
|
2326
|
+
# refresh and retry once before surfacing the error.
|
|
2327
|
+
code = None
|
|
2193
2328
|
try:
|
|
2194
|
-
|
|
2195
|
-
if
|
|
2196
|
-
|
|
2329
|
+
m = re.search(r"(?:HTTP|status)\s*(\d{3})", str(message or ""))
|
|
2330
|
+
if m:
|
|
2331
|
+
code = int(m.group(1))
|
|
2197
2332
|
except Exception:
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2333
|
+
code = None
|
|
2334
|
+
|
|
2335
|
+
if code in (401, 403):
|
|
2336
|
+
try:
|
|
2337
|
+
refreshed = bool(self.app._refresh_token_if_needed(force=True))
|
|
2338
|
+
except Exception:
|
|
2339
|
+
refreshed = False
|
|
2340
|
+
if refreshed:
|
|
2341
|
+
try:
|
|
2342
|
+
success, run_id, message = await loop.run_in_executor(
|
|
2343
|
+
None,
|
|
2344
|
+
lambda: client.run_action_async(
|
|
2345
|
+
self.current_action_id,
|
|
2346
|
+
user_input,
|
|
2347
|
+
action_result_id=self.current_run_id,
|
|
2348
|
+
urls=urls if urls else None,
|
|
2349
|
+
metadata=self._metadata if self._metadata else None,
|
|
2350
|
+
),
|
|
2351
|
+
)
|
|
2352
|
+
except Exception:
|
|
2353
|
+
pass
|
|
2354
|
+
|
|
2355
|
+
if not success:
|
|
2356
|
+
self.add_message(f"Error starting action: {message}", "error")
|
|
2357
|
+
# Remove the placeholder row (no run was started).
|
|
2358
|
+
try:
|
|
2359
|
+
w = getattr(self, "_streaming_widget_ref", None)
|
|
2360
|
+
if w:
|
|
2361
|
+
w.remove()
|
|
2362
|
+
except Exception:
|
|
2363
|
+
pass
|
|
2364
|
+
self._streaming_widget_ref = None
|
|
2365
|
+
self._set_streaming(False)
|
|
2366
|
+
return
|
|
2202
2367
|
|
|
2203
2368
|
# Check if this is continuing an existing conversation
|
|
2204
2369
|
if self.current_run_id and run_id == self.current_run_id:
|
|
@@ -2312,17 +2477,51 @@ class DashboardScreen(Screen):
|
|
|
2312
2477
|
if got_final_result:
|
|
2313
2478
|
return True
|
|
2314
2479
|
|
|
2480
|
+
auth_failed_code: int | None = None
|
|
2481
|
+
|
|
2315
2482
|
try:
|
|
2316
2483
|
async with fetch_lock:
|
|
2317
2484
|
if got_final_result:
|
|
2318
2485
|
return True
|
|
2319
|
-
success, final_result,
|
|
2486
|
+
success, final_result, message = await asyncio.to_thread(
|
|
2320
2487
|
client.get_action_result, run_id
|
|
2321
2488
|
)
|
|
2489
|
+
|
|
2490
|
+
if not success:
|
|
2491
|
+
code = None
|
|
2492
|
+
try:
|
|
2493
|
+
m = re.search(r"(?:HTTP|status)\s*(\d{3})", str(message or ""))
|
|
2494
|
+
if m:
|
|
2495
|
+
code = int(m.group(1))
|
|
2496
|
+
except Exception:
|
|
2497
|
+
code = None
|
|
2498
|
+
|
|
2499
|
+
if code in (401, 403):
|
|
2500
|
+
try:
|
|
2501
|
+
refreshed = bool(self.app._refresh_token_if_needed(force=True))
|
|
2502
|
+
except Exception:
|
|
2503
|
+
refreshed = False
|
|
2504
|
+
if refreshed:
|
|
2505
|
+
success, final_result, message = await asyncio.to_thread(
|
|
2506
|
+
client.get_action_result, run_id
|
|
2507
|
+
)
|
|
2508
|
+
if (not success) and (code in (401, 403)):
|
|
2509
|
+
auth_failed_code = code
|
|
2322
2510
|
except Exception as e:
|
|
2323
2511
|
logger.warning(f"Poll error: {e}")
|
|
2324
2512
|
return False
|
|
2325
2513
|
|
|
2514
|
+
|
|
2515
|
+
if auth_failed_code in (401, 403):
|
|
2516
|
+
self.add_message(
|
|
2517
|
+
f"Authentication error while fetching result (HTTP {auth_failed_code}). Please /login again.",
|
|
2518
|
+
"error",
|
|
2519
|
+
)
|
|
2520
|
+
got_final_result = True
|
|
2521
|
+
# Stop spinner/blink immediately when we have a terminal state.
|
|
2522
|
+
self._set_streaming(False)
|
|
2523
|
+
return True
|
|
2524
|
+
|
|
2326
2525
|
if not success or not final_result:
|
|
2327
2526
|
return False
|
|
2328
2527
|
|
|
@@ -2613,7 +2812,22 @@ class DashboardScreen(Screen):
|
|
|
2613
2812
|
"""
|
|
2614
2813
|
if not hasattr(self.app, "autobots_client"):
|
|
2615
2814
|
return
|
|
2616
|
-
success, result,
|
|
2815
|
+
success, result, msg = self.app.autobots_client.get_action_result(run_id)
|
|
2816
|
+
if (not success) and msg:
|
|
2817
|
+
code = None
|
|
2818
|
+
try:
|
|
2819
|
+
m = re.search(r"(?:HTTP|status)\s*(\d{3})", str(msg))
|
|
2820
|
+
if m:
|
|
2821
|
+
code = int(m.group(1))
|
|
2822
|
+
except Exception:
|
|
2823
|
+
code = None
|
|
2824
|
+
if code in (401, 403):
|
|
2825
|
+
try:
|
|
2826
|
+
refreshed = bool(self.app._refresh_token_if_needed(force=True))
|
|
2827
|
+
except Exception:
|
|
2828
|
+
refreshed = False
|
|
2829
|
+
if refreshed:
|
|
2830
|
+
success, result, msg = self.app.autobots_client.get_action_result(run_id)
|
|
2617
2831
|
if not success or not result:
|
|
2618
2832
|
return
|
|
2619
2833
|
self._render_conversation_history_from_action_result(result)
|
|
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
|