kiwi-code 0.0.27__tar.gz → 0.0.28__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.27 → kiwi_code-0.0.28}/PKG-INFO +1 -1
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/pyproject.toml +1 -1
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/dashboard.py +237 -60
- kiwi_code-0.0.28/src/kiwi_tui/screens/id_picker.py +247 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/slash_commands.py +1 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/uv.lock +1 -1
- kiwi_code-0.0.27/src/kiwi_tui/screens/id_picker.py +0 -75
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/.gitignore +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/.python-version +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/CLAUDE.md +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/Makefile +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/README.md +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_runtime/main.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_runtime/snake_game/.gitignore +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/main.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/test_hello.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/__init__.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/conftest.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/test_tui_headless.py +0 -0
|
@@ -37,7 +37,7 @@ class UserMessageRow(Horizontal):
|
|
|
37
37
|
self._text = text
|
|
38
38
|
|
|
39
39
|
def compose(self) -> ComposeResult:
|
|
40
|
-
yield Static("
|
|
40
|
+
yield Static("", classes="user-label", markup=False)
|
|
41
41
|
yield Static(self._text, classes="user-body", markup=False)
|
|
42
42
|
|
|
43
43
|
|
|
@@ -148,6 +148,7 @@ class DashboardScreen(Screen):
|
|
|
148
148
|
self.current_action_id = self.DEFAULT_ACTION_ID
|
|
149
149
|
self.current_run_id = None # Track the current conversation run_id
|
|
150
150
|
self.active_stream_tasks = [] # Track active SSE stream tasks to cancel them
|
|
151
|
+
self.current_run_kind: str | None = None # 'action' or 'graph' (best-effort)
|
|
151
152
|
self._last_listed_ids: list[str] = [] # Index for #N shortcuts
|
|
152
153
|
self._pending_urls: list[str] = [] # File URLs to attach to next message
|
|
153
154
|
self._metadata: dict = {} # Persistent metadata for all subsequent runs
|
|
@@ -866,6 +867,7 @@ class DashboardScreen(Screen):
|
|
|
866
867
|
self._set_streaming(False)
|
|
867
868
|
self.current_action_id = self.DEFAULT_ACTION_ID
|
|
868
869
|
self.current_run_id = None
|
|
870
|
+
self.current_run_kind = None
|
|
869
871
|
self._update_run_status_bar()
|
|
870
872
|
self._clear_chat_messages()
|
|
871
873
|
show(f"Starting new conversation with default action ({self.DEFAULT_ACTION_ID}).")
|
|
@@ -885,6 +887,7 @@ class DashboardScreen(Screen):
|
|
|
885
887
|
return
|
|
886
888
|
self.current_action_id = action_id
|
|
887
889
|
self.current_run_id = None
|
|
890
|
+
self.current_run_kind = None
|
|
888
891
|
self._update_run_status_bar()
|
|
889
892
|
self._clear_chat_messages()
|
|
890
893
|
show("\n".join(["Switched to action:"] + lines))
|
|
@@ -904,11 +907,108 @@ class DashboardScreen(Screen):
|
|
|
904
907
|
show(f"Run not found: {run_id}", is_error=True)
|
|
905
908
|
return
|
|
906
909
|
self.current_run_id = run_id
|
|
910
|
+
self.current_run_kind = "action"
|
|
907
911
|
self._update_run_status_bar()
|
|
908
912
|
show("\n".join(["Continuing run:"] + lines))
|
|
909
913
|
await self._load_conversation_history_async(run_id)
|
|
910
914
|
return
|
|
911
915
|
|
|
916
|
+
if cmd == "/name":
|
|
917
|
+
new_name = " ".join(args).strip()
|
|
918
|
+
if not new_name:
|
|
919
|
+
show("Usage: /name <new name>")
|
|
920
|
+
return
|
|
921
|
+
if not self.current_run_id:
|
|
922
|
+
show(
|
|
923
|
+
"Error: no run id yet. Start a conversation (send a message), or use /continue <run_id>, or pick one from /runs list.",
|
|
924
|
+
is_error=True,
|
|
925
|
+
)
|
|
926
|
+
return
|
|
927
|
+
|
|
928
|
+
api_client = self._get_api_client_for_command(command)
|
|
929
|
+
if not api_client:
|
|
930
|
+
return
|
|
931
|
+
|
|
932
|
+
run_id = self.current_run_id
|
|
933
|
+
|
|
934
|
+
from autobots_client.models.action_result_update import ActionResultUpdate
|
|
935
|
+
from autobots_client.api.action_results import (
|
|
936
|
+
update_action_result_v1_action_results_put,
|
|
937
|
+
)
|
|
938
|
+
from autobots_client.models.action_graph_result_update import ActionGraphResultUpdate
|
|
939
|
+
from autobots_client.api.action_graphs_results import (
|
|
940
|
+
update_action_graph_result_v1_action_graphs_results_put,
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
def _try_rename_action_run() -> tuple[int, str]:
|
|
944
|
+
try:
|
|
945
|
+
resp = update_action_result_v1_action_results_put.sync_detailed(
|
|
946
|
+
client=api_client,
|
|
947
|
+
id=run_id,
|
|
948
|
+
body=ActionResultUpdate(name=new_name),
|
|
949
|
+
)
|
|
950
|
+
return resp.status_code, ""
|
|
951
|
+
except Exception as e:
|
|
952
|
+
return 0, str(e)
|
|
953
|
+
|
|
954
|
+
def _try_rename_graph_run() -> tuple[int, str]:
|
|
955
|
+
try:
|
|
956
|
+
resp = update_action_graph_result_v1_action_graphs_results_put.sync_detailed(
|
|
957
|
+
client=api_client,
|
|
958
|
+
id=run_id,
|
|
959
|
+
body=ActionGraphResultUpdate(name=new_name),
|
|
960
|
+
)
|
|
961
|
+
return resp.status_code, ""
|
|
962
|
+
except Exception as e:
|
|
963
|
+
return 0, str(e)
|
|
964
|
+
|
|
965
|
+
# Decide which API to target first (action run vs graph run).
|
|
966
|
+
preferred_kind = self.current_run_kind or "action"
|
|
967
|
+
tried: list[str] = []
|
|
968
|
+
status_code: int = 0
|
|
969
|
+
err: str = ""
|
|
970
|
+
|
|
971
|
+
async def _attempt(kind: str) -> tuple[int, str]:
|
|
972
|
+
if kind == "graph":
|
|
973
|
+
return await asyncio.to_thread(_try_rename_graph_run)
|
|
974
|
+
return await asyncio.to_thread(_try_rename_action_run)
|
|
975
|
+
|
|
976
|
+
# Try preferred kind first, then fall back to the other kind for common "wrong endpoint" failures.
|
|
977
|
+
for kind in (preferred_kind, "graph" if preferred_kind != "graph" else "action"):
|
|
978
|
+
tried.append(kind)
|
|
979
|
+
status_code, err = await _attempt(kind)
|
|
980
|
+
if status_code == 200:
|
|
981
|
+
# Lock-in for future /name calls so we don't have to guess again.
|
|
982
|
+
self.current_run_kind = kind
|
|
983
|
+
break
|
|
984
|
+
# Only fall back on "not found" / validation errors (or exceptions -> status_code=0).
|
|
985
|
+
if status_code not in (0, 404, 422):
|
|
986
|
+
break
|
|
987
|
+
|
|
988
|
+
if status_code != 200:
|
|
989
|
+
details = err or ""
|
|
990
|
+
show(
|
|
991
|
+
f"Error: failed to rename run {run_id}. Tried: {', '.join(tried)}. HTTP {status_code}. {details}",
|
|
992
|
+
is_error=True,
|
|
993
|
+
)
|
|
994
|
+
return
|
|
995
|
+
|
|
996
|
+
# Best-effort: update local cached run name (used by the quit-time cleanup prompt).
|
|
997
|
+
try:
|
|
998
|
+
from kiwi_tui.runtime_agent import meta_path_for_run
|
|
999
|
+
|
|
1000
|
+
mp = meta_path_for_run(run_id)
|
|
1001
|
+
meta: dict[str, object] = {}
|
|
1002
|
+
if mp.exists():
|
|
1003
|
+
meta = json.loads(mp.read_text(encoding="utf-8") or "{}")
|
|
1004
|
+
meta["run_name"] = new_name
|
|
1005
|
+
mp.write_text(json.dumps(meta, indent=2, default=str), encoding="utf-8")
|
|
1006
|
+
except Exception:
|
|
1007
|
+
pass
|
|
1008
|
+
|
|
1009
|
+
show(f"Renamed run {run_id} to: {new_name}")
|
|
1010
|
+
return
|
|
1011
|
+
|
|
912
1012
|
if cmd == "/status":
|
|
913
1013
|
info = [
|
|
914
1014
|
f"Action ID: {self.current_action_id}",
|
|
@@ -983,7 +1083,7 @@ class DashboardScreen(Screen):
|
|
|
983
1083
|
from autobots_client.types import UNSET
|
|
984
1084
|
opts = self._parse_command_opts(command)
|
|
985
1085
|
name = opts.get("name")
|
|
986
|
-
limit = int(opts.get("limit", "
|
|
1086
|
+
limit = int(opts.get("limit", "50"))
|
|
987
1087
|
offset = int(opts.get("offset", "0"))
|
|
988
1088
|
resp = await asyncio.to_thread(
|
|
989
1089
|
lambda: list_actions_v1_actions_get.sync_detailed(
|
|
@@ -1001,14 +1101,24 @@ class DashboardScreen(Screen):
|
|
|
1001
1101
|
f"Actions ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}). "
|
|
1002
1102
|
"Select an action"
|
|
1003
1103
|
)
|
|
1004
|
-
|
|
1104
|
+
columns = ["#", "Name", "Type", "Version", "Created"]
|
|
1105
|
+
rows: list[tuple[str, list[str]]] = []
|
|
1005
1106
|
for idx, doc in enumerate(result.docs, 1):
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1107
|
+
rows.append(
|
|
1108
|
+
(
|
|
1109
|
+
doc.field_id,
|
|
1110
|
+
[
|
|
1111
|
+
str(idx),
|
|
1112
|
+
_val(doc.name),
|
|
1113
|
+
_val(doc.type_.value),
|
|
1114
|
+
_val(doc.version),
|
|
1115
|
+
_fmt_date(doc.created_at),
|
|
1116
|
+
],
|
|
1117
|
+
)
|
|
1009
1118
|
)
|
|
1010
|
-
|
|
1011
|
-
|
|
1119
|
+
self.app.push_screen(
|
|
1120
|
+
IdPickerScreen(title, columns=columns, rows=rows), callback=self._on_action_picked
|
|
1121
|
+
)
|
|
1012
1122
|
|
|
1013
1123
|
async def _render_runs_list_async(self, command: str, api_client) -> None:
|
|
1014
1124
|
from kiwi_cli.commands import _val, _fmt_date
|
|
@@ -1033,7 +1143,7 @@ class DashboardScreen(Screen):
|
|
|
1033
1143
|
action_id=opts.get("action_id") or UNSET,
|
|
1034
1144
|
action_name=opts.get("action_name") or UNSET,
|
|
1035
1145
|
status=status_enum,
|
|
1036
|
-
limit=int(opts.get("limit", "
|
|
1146
|
+
limit=int(opts.get("limit", "50")),
|
|
1037
1147
|
offset=int(opts.get("offset", "0")),
|
|
1038
1148
|
)
|
|
1039
1149
|
)
|
|
@@ -1045,14 +1155,22 @@ class DashboardScreen(Screen):
|
|
|
1045
1155
|
f"Action Runs ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}). "
|
|
1046
1156
|
"Select a run"
|
|
1047
1157
|
)
|
|
1048
|
-
|
|
1158
|
+
columns = ["#", "Status", "Name", "Created", "Updated"]
|
|
1159
|
+
rows: list[tuple[str, list[str]]] = []
|
|
1049
1160
|
for idx, doc in enumerate(result.docs, 1):
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1161
|
+
rows.append(
|
|
1162
|
+
(
|
|
1163
|
+
doc.field_id,
|
|
1164
|
+
[
|
|
1165
|
+
str(idx),
|
|
1166
|
+
doc.status.value,
|
|
1167
|
+
_val(doc.name),
|
|
1168
|
+
_fmt_date(doc.created_at),
|
|
1169
|
+
_fmt_date(doc.updated_at),
|
|
1170
|
+
],
|
|
1171
|
+
)
|
|
1053
1172
|
)
|
|
1054
|
-
|
|
1055
|
-
self.app.push_screen(IdPickerScreen(title, options), callback=self._on_run_picked)
|
|
1173
|
+
self.app.push_screen(IdPickerScreen(title, columns=columns, rows=rows), callback=self._on_run_picked)
|
|
1056
1174
|
|
|
1057
1175
|
async def _render_graphs_list_async(self, command: str, api_client) -> None:
|
|
1058
1176
|
from kiwi_cli.commands import _val, _fmt_date
|
|
@@ -1060,7 +1178,7 @@ class DashboardScreen(Screen):
|
|
|
1060
1178
|
from autobots_client.types import UNSET
|
|
1061
1179
|
opts = self._parse_command_opts(command)
|
|
1062
1180
|
name = opts.get("name")
|
|
1063
|
-
limit = int(opts.get("limit", "
|
|
1181
|
+
limit = int(opts.get("limit", "50"))
|
|
1064
1182
|
offset = int(opts.get("offset", "0"))
|
|
1065
1183
|
resp = await asyncio.to_thread(
|
|
1066
1184
|
lambda: list_action_graphs_v1_action_graphs_get.sync_detailed(
|
|
@@ -1078,14 +1196,22 @@ class DashboardScreen(Screen):
|
|
|
1078
1196
|
f"Action Graphs ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}). "
|
|
1079
1197
|
"Select a graph"
|
|
1080
1198
|
)
|
|
1081
|
-
|
|
1199
|
+
columns = ["#", "Name", "Version", "Published", "Created"]
|
|
1200
|
+
rows: list[tuple[str, list[str]]] = []
|
|
1082
1201
|
for idx, doc in enumerate(result.docs, 1):
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1202
|
+
rows.append(
|
|
1203
|
+
(
|
|
1204
|
+
doc.field_id,
|
|
1205
|
+
[
|
|
1206
|
+
str(idx),
|
|
1207
|
+
_val(doc.name),
|
|
1208
|
+
_val(doc.version),
|
|
1209
|
+
_val(doc.is_published),
|
|
1210
|
+
_fmt_date(doc.created_at),
|
|
1211
|
+
],
|
|
1212
|
+
)
|
|
1086
1213
|
)
|
|
1087
|
-
|
|
1088
|
-
self.app.push_screen(IdPickerScreen(title, options), callback=self._on_graph_picked)
|
|
1214
|
+
self.app.push_screen(IdPickerScreen(title, columns=columns, rows=rows), callback=self._on_graph_picked)
|
|
1089
1215
|
|
|
1090
1216
|
async def _render_graph_runs_list_async(self, command: str, api_client) -> None:
|
|
1091
1217
|
from kiwi_cli.commands import _val, _fmt_date
|
|
@@ -1112,7 +1238,7 @@ class DashboardScreen(Screen):
|
|
|
1112
1238
|
action_graph_id=opts.get("graph_id") or UNSET,
|
|
1113
1239
|
action_graph_name=opts.get("graph_name") or UNSET,
|
|
1114
1240
|
status=status_enum,
|
|
1115
|
-
limit=int(opts.get("limit", "
|
|
1241
|
+
limit=int(opts.get("limit", "50")),
|
|
1116
1242
|
offset=int(opts.get("offset", "0")),
|
|
1117
1243
|
)
|
|
1118
1244
|
)
|
|
@@ -1124,14 +1250,22 @@ class DashboardScreen(Screen):
|
|
|
1124
1250
|
f"Action Graph Runs ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}). "
|
|
1125
1251
|
"Select a graph run"
|
|
1126
1252
|
)
|
|
1127
|
-
|
|
1253
|
+
columns = ["#", "Status", "Name", "Created", "Updated"]
|
|
1254
|
+
rows: list[tuple[str, list[str]]] = []
|
|
1128
1255
|
for idx, doc in enumerate(result.docs, 1):
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1256
|
+
rows.append(
|
|
1257
|
+
(
|
|
1258
|
+
doc.field_id,
|
|
1259
|
+
[
|
|
1260
|
+
str(idx),
|
|
1261
|
+
doc.status.value,
|
|
1262
|
+
_val(doc.name),
|
|
1263
|
+
_fmt_date(doc.created_at),
|
|
1264
|
+
_fmt_date(doc.updated_at),
|
|
1265
|
+
],
|
|
1266
|
+
)
|
|
1132
1267
|
)
|
|
1133
|
-
|
|
1134
|
-
self.app.push_screen(IdPickerScreen(title, options), callback=self._on_graph_run_picked)
|
|
1268
|
+
self.app.push_screen(IdPickerScreen(title, columns=columns, rows=rows), callback=self._on_graph_run_picked)
|
|
1135
1269
|
|
|
1136
1270
|
|
|
1137
1271
|
def _get_api_client(self):
|
|
@@ -1207,7 +1341,7 @@ class DashboardScreen(Screen):
|
|
|
1207
1341
|
|
|
1208
1342
|
opts = self._parse_command_opts(command)
|
|
1209
1343
|
name = opts.get("name")
|
|
1210
|
-
limit = int(opts.get("limit", "
|
|
1344
|
+
limit = int(opts.get("limit", "50"))
|
|
1211
1345
|
offset = int(opts.get("offset", "0"))
|
|
1212
1346
|
|
|
1213
1347
|
resp = list_actions_v1_actions_get.sync_detailed(
|
|
@@ -1223,14 +1357,22 @@ class DashboardScreen(Screen):
|
|
|
1223
1357
|
f"Actions ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}). "
|
|
1224
1358
|
"Select an action"
|
|
1225
1359
|
)
|
|
1226
|
-
|
|
1360
|
+
columns = ["#", "Name", "Type", "Version", "Created"]
|
|
1361
|
+
rows: list[tuple[str, list[str]]] = []
|
|
1227
1362
|
for idx, doc in enumerate(result.docs, 1):
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1363
|
+
rows.append(
|
|
1364
|
+
(
|
|
1365
|
+
doc.field_id,
|
|
1366
|
+
[
|
|
1367
|
+
str(idx),
|
|
1368
|
+
_val(doc.name),
|
|
1369
|
+
_val(doc.type_.value),
|
|
1370
|
+
_val(doc.version),
|
|
1371
|
+
_fmt_date(doc.created_at),
|
|
1372
|
+
],
|
|
1373
|
+
)
|
|
1231
1374
|
)
|
|
1232
|
-
|
|
1233
|
-
self.app.push_screen(IdPickerScreen(title, options), callback=self._on_action_picked)
|
|
1375
|
+
self.app.push_screen(IdPickerScreen(title, columns=columns, rows=rows), callback=self._on_action_picked)
|
|
1234
1376
|
|
|
1235
1377
|
def _render_runs_list(self, command: str, api_client) -> None:
|
|
1236
1378
|
"""Render /runs list with clickable rows."""
|
|
@@ -1257,7 +1399,7 @@ class DashboardScreen(Screen):
|
|
|
1257
1399
|
action_id=opts.get("action_id") or UNSET,
|
|
1258
1400
|
action_name=opts.get("action_name") or UNSET,
|
|
1259
1401
|
status=status_enum,
|
|
1260
|
-
limit=int(opts.get("limit", "
|
|
1402
|
+
limit=int(opts.get("limit", "50")),
|
|
1261
1403
|
offset=int(opts.get("offset", "0")),
|
|
1262
1404
|
)
|
|
1263
1405
|
if resp.status_code != 200:
|
|
@@ -1269,14 +1411,22 @@ class DashboardScreen(Screen):
|
|
|
1269
1411
|
f"Action Runs ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}). "
|
|
1270
1412
|
"Select a run"
|
|
1271
1413
|
)
|
|
1272
|
-
|
|
1414
|
+
columns = ["#", "Status", "Name", "Created", "Updated"]
|
|
1415
|
+
rows: list[tuple[str, list[str]]] = []
|
|
1273
1416
|
for idx, doc in enumerate(result.docs, 1):
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1417
|
+
rows.append(
|
|
1418
|
+
(
|
|
1419
|
+
doc.field_id,
|
|
1420
|
+
[
|
|
1421
|
+
str(idx),
|
|
1422
|
+
doc.status.value,
|
|
1423
|
+
_val(doc.name),
|
|
1424
|
+
_fmt_date(doc.created_at),
|
|
1425
|
+
_fmt_date(doc.updated_at),
|
|
1426
|
+
],
|
|
1427
|
+
)
|
|
1277
1428
|
)
|
|
1278
|
-
|
|
1279
|
-
self.app.push_screen(IdPickerScreen(title, options), callback=self._on_run_picked)
|
|
1429
|
+
self.app.push_screen(IdPickerScreen(title, columns=columns, rows=rows), callback=self._on_run_picked)
|
|
1280
1430
|
|
|
1281
1431
|
def _render_graphs_list(self, command: str, api_client) -> None:
|
|
1282
1432
|
"""Render /graphs list with clickable rows."""
|
|
@@ -1286,7 +1436,7 @@ class DashboardScreen(Screen):
|
|
|
1286
1436
|
|
|
1287
1437
|
opts = self._parse_command_opts(command)
|
|
1288
1438
|
name = opts.get("name")
|
|
1289
|
-
limit = int(opts.get("limit", "
|
|
1439
|
+
limit = int(opts.get("limit", "50"))
|
|
1290
1440
|
offset = int(opts.get("offset", "0"))
|
|
1291
1441
|
|
|
1292
1442
|
resp = list_action_graphs_v1_action_graphs_get.sync_detailed(
|
|
@@ -1301,14 +1451,22 @@ class DashboardScreen(Screen):
|
|
|
1301
1451
|
f"Action Graphs ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}). "
|
|
1302
1452
|
"Select a graph"
|
|
1303
1453
|
)
|
|
1304
|
-
|
|
1454
|
+
columns = ["#", "Name", "Version", "Published", "Created"]
|
|
1455
|
+
rows: list[tuple[str, list[str]]] = []
|
|
1305
1456
|
for idx, doc in enumerate(result.docs, 1):
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1457
|
+
rows.append(
|
|
1458
|
+
(
|
|
1459
|
+
doc.field_id,
|
|
1460
|
+
[
|
|
1461
|
+
str(idx),
|
|
1462
|
+
_val(doc.name),
|
|
1463
|
+
_val(doc.version),
|
|
1464
|
+
_val(doc.is_published),
|
|
1465
|
+
_fmt_date(doc.created_at),
|
|
1466
|
+
],
|
|
1467
|
+
)
|
|
1309
1468
|
)
|
|
1310
|
-
|
|
1311
|
-
self.app.push_screen(IdPickerScreen(title, options), callback=self._on_graph_picked)
|
|
1469
|
+
self.app.push_screen(IdPickerScreen(title, columns=columns, rows=rows), callback=self._on_graph_picked)
|
|
1312
1470
|
def _render_graph_runs_list(self, command: str, api_client) -> None:
|
|
1313
1471
|
"""Render /graph-runs list with clickable rows."""
|
|
1314
1472
|
from kiwi_cli.commands import _val, _fmt_date
|
|
@@ -1334,7 +1492,7 @@ class DashboardScreen(Screen):
|
|
|
1334
1492
|
action_graph_id=opts.get("graph_id") or UNSET,
|
|
1335
1493
|
action_graph_name=opts.get("graph_name") or UNSET,
|
|
1336
1494
|
status=status_enum,
|
|
1337
|
-
limit=int(opts.get("limit", "
|
|
1495
|
+
limit=int(opts.get("limit", "50")),
|
|
1338
1496
|
offset=int(opts.get("offset", "0")),
|
|
1339
1497
|
)
|
|
1340
1498
|
if resp.status_code != 200:
|
|
@@ -1346,14 +1504,22 @@ class DashboardScreen(Screen):
|
|
|
1346
1504
|
f"Action Graph Runs ({result.total_count} total, showing {result.offset}-{result.offset + len(result.docs)}). "
|
|
1347
1505
|
"Select a graph run"
|
|
1348
1506
|
)
|
|
1349
|
-
|
|
1507
|
+
columns = ["#", "Status", "Name", "Created", "Updated"]
|
|
1508
|
+
rows: list[tuple[str, list[str]]] = []
|
|
1350
1509
|
for idx, doc in enumerate(result.docs, 1):
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1510
|
+
rows.append(
|
|
1511
|
+
(
|
|
1512
|
+
doc.field_id,
|
|
1513
|
+
[
|
|
1514
|
+
str(idx),
|
|
1515
|
+
doc.status.value,
|
|
1516
|
+
_val(doc.name),
|
|
1517
|
+
_fmt_date(doc.created_at),
|
|
1518
|
+
_fmt_date(doc.updated_at),
|
|
1519
|
+
],
|
|
1520
|
+
)
|
|
1354
1521
|
)
|
|
1355
|
-
|
|
1356
|
-
self.app.push_screen(IdPickerScreen(title, options), callback=self._on_graph_run_picked)
|
|
1522
|
+
self.app.push_screen(IdPickerScreen(title, columns=columns, rows=rows), callback=self._on_graph_run_picked)
|
|
1357
1523
|
|
|
1358
1524
|
|
|
1359
1525
|
# ----------------------------
|
|
@@ -1387,6 +1553,7 @@ class DashboardScreen(Screen):
|
|
|
1387
1553
|
return
|
|
1388
1554
|
self.current_action_id = action_id
|
|
1389
1555
|
self.current_run_id = None
|
|
1556
|
+
self.current_run_kind = None
|
|
1390
1557
|
self._update_run_status_bar()
|
|
1391
1558
|
self._clear_chat_messages()
|
|
1392
1559
|
self.app.notify(f"Switched to action: {action_id}", severity="information")
|
|
@@ -1420,6 +1587,7 @@ class DashboardScreen(Screen):
|
|
|
1420
1587
|
self._show_command_result("Run selection", f"Run not found: {run_id}", is_error=True)
|
|
1421
1588
|
return
|
|
1422
1589
|
self.current_run_id = run_id
|
|
1590
|
+
self.current_run_kind = "action"
|
|
1423
1591
|
self._update_run_status_bar()
|
|
1424
1592
|
self.app.notify(f"Continuing run: {run_id}", severity="information")
|
|
1425
1593
|
await self._load_conversation_history_async(run_id)
|
|
@@ -1483,6 +1651,7 @@ class DashboardScreen(Screen):
|
|
|
1483
1651
|
self._show_command_result("Graph run selection", f"Graph run not found: {graph_run_id}", is_error=True)
|
|
1484
1652
|
return
|
|
1485
1653
|
self.current_run_id = graph_run_id
|
|
1654
|
+
self.current_run_kind = "graph"
|
|
1486
1655
|
self._update_run_status_bar()
|
|
1487
1656
|
self.app.notify(f"Continuing graph run: {graph_run_id}", severity="information")
|
|
1488
1657
|
finally:
|
|
@@ -1610,6 +1779,7 @@ class DashboardScreen(Screen):
|
|
|
1610
1779
|
return
|
|
1611
1780
|
self.current_action_id = action_id
|
|
1612
1781
|
self.current_run_id = None
|
|
1782
|
+
self.current_run_kind = None
|
|
1613
1783
|
self._update_run_status_bar()
|
|
1614
1784
|
self.add_message("\n".join(["Switched to action:"] + lines), "info")
|
|
1615
1785
|
|
|
@@ -1625,28 +1795,34 @@ class DashboardScreen(Screen):
|
|
|
1625
1795
|
|
|
1626
1796
|
elif btn_id.startswith("run-select-"):
|
|
1627
1797
|
# Continue this run
|
|
1628
|
-
run_id = btn_id[len("run-select-"):]
|
|
1798
|
+
run_id = btn_id[len("run-select-") :]
|
|
1629
1799
|
from kiwi_cli.commands import runs_get
|
|
1800
|
+
|
|
1630
1801
|
lines = runs_get(api_client, id=run_id)
|
|
1631
1802
|
if lines and lines[0].startswith("Error"):
|
|
1632
1803
|
self.add_message(f"Run not found: {run_id}", "error")
|
|
1633
1804
|
return
|
|
1805
|
+
|
|
1634
1806
|
self.current_run_id = run_id
|
|
1807
|
+
self.current_run_kind = "action"
|
|
1635
1808
|
self._update_run_status_bar()
|
|
1636
1809
|
self.add_message("\n".join(["Continuing run:"] + lines), "info")
|
|
1637
1810
|
self._load_conversation_history(run_id)
|
|
1638
1811
|
|
|
1639
1812
|
elif btn_id.startswith("graph-run-select-"):
|
|
1640
1813
|
# Continue this graph run
|
|
1641
|
-
graph_run_id = btn_id[len("graph-run-select-"):]
|
|
1814
|
+
graph_run_id = btn_id[len("graph-run-select-") :]
|
|
1642
1815
|
from kiwi_cli.commands import graph_runs_get
|
|
1816
|
+
|
|
1643
1817
|
lines = graph_runs_get(api_client, id=graph_run_id)
|
|
1644
1818
|
if lines and lines[0].startswith("Error"):
|
|
1645
1819
|
self.add_message(f"Graph run not found: {graph_run_id}", "error")
|
|
1646
1820
|
return
|
|
1821
|
+
|
|
1647
1822
|
self.current_run_id = graph_run_id
|
|
1823
|
+
self.current_run_kind = "graph"
|
|
1824
|
+
self._update_run_status_bar()
|
|
1648
1825
|
self.add_message("\n".join(["Continuing graph run:"] + lines), "info")
|
|
1649
|
-
|
|
1650
1826
|
else:
|
|
1651
1827
|
return
|
|
1652
1828
|
|
|
@@ -2014,6 +2190,7 @@ class DashboardScreen(Screen):
|
|
|
2014
2190
|
logger.info(f"Continuing conversation with run_id: {run_id}")
|
|
2015
2191
|
else:
|
|
2016
2192
|
self.current_run_id = run_id
|
|
2193
|
+
self.current_run_kind = "action"
|
|
2017
2194
|
# If a pending runtime was started (e.g. via /connect-cli before the
|
|
2018
2195
|
# run existed), bind it now that we have a run_id.
|
|
2019
2196
|
try:
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Generic modal picker for selecting an item by ID.
|
|
2
|
+
|
|
3
|
+
Used for slash-command list views (e.g. /actions list, /runs list) so the
|
|
4
|
+
list UI is separate from the main chat timeline.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from textual.app import ComposeResult
|
|
10
|
+
from textual.binding import Binding
|
|
11
|
+
from textual.containers import Vertical
|
|
12
|
+
from textual.screen import ModalScreen
|
|
13
|
+
from textual.widgets import DataTable, OptionList, Static
|
|
14
|
+
from textual.widgets.option_list import Option
|
|
15
|
+
|
|
16
|
+
from textual.events import (
|
|
17
|
+
MouseScrollDown,
|
|
18
|
+
MouseScrollLeft,
|
|
19
|
+
MouseScrollRight,
|
|
20
|
+
MouseScrollUp,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _IdPickerDataTable(DataTable):
|
|
25
|
+
"""DataTable that supports horizontal trackpad / shift-scroll gestures.
|
|
26
|
+
|
|
27
|
+
Notes:
|
|
28
|
+
- Some terminals emit horizontal scrolling as MouseScrollLeft/Right events.
|
|
29
|
+
- Some emit MouseScrollDown/Up with delta_x set.
|
|
30
|
+
- Some map horizontal scrolling to Shift+wheel (shift=True with delta_y).
|
|
31
|
+
|
|
32
|
+
We handle all of the above so the IdPicker table can be scrolled horizontally
|
|
33
|
+
with a touchpad when the terminal supports it.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
_H_SCROLL_MULTIPLIER = 4
|
|
37
|
+
|
|
38
|
+
def _scroll_x(self, dx: int) -> None:
|
|
39
|
+
if dx == 0:
|
|
40
|
+
return
|
|
41
|
+
self.scroll_relative(x=dx * self._H_SCROLL_MULTIPLIER, y=0, animate=False)
|
|
42
|
+
|
|
43
|
+
def _try_handle_horizontal_from_delta(
|
|
44
|
+
self, delta_x: int, *, shift: bool, delta_y: int
|
|
45
|
+
) -> bool:
|
|
46
|
+
dx = delta_x
|
|
47
|
+
if dx == 0 and shift and delta_y != 0:
|
|
48
|
+
dx = delta_y
|
|
49
|
+
if dx == 0:
|
|
50
|
+
return False
|
|
51
|
+
self._scroll_x(dx)
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def _on_mouse_scroll_down(self, event: MouseScrollDown) -> None:
|
|
55
|
+
if self._try_handle_horizontal_from_delta(
|
|
56
|
+
event.delta_x, shift=event.shift, delta_y=event.delta_y
|
|
57
|
+
):
|
|
58
|
+
event.stop()
|
|
59
|
+
return
|
|
60
|
+
return super()._on_mouse_scroll_down(event)
|
|
61
|
+
|
|
62
|
+
def _on_mouse_scroll_up(self, event: MouseScrollUp) -> None:
|
|
63
|
+
if self._try_handle_horizontal_from_delta(
|
|
64
|
+
event.delta_x, shift=event.shift, delta_y=event.delta_y
|
|
65
|
+
):
|
|
66
|
+
event.stop()
|
|
67
|
+
return
|
|
68
|
+
return super()._on_mouse_scroll_up(event)
|
|
69
|
+
|
|
70
|
+
def _on_mouse_scroll_left(self, event: MouseScrollLeft) -> None:
|
|
71
|
+
# Ensure negative direction regardless of terminal conventions.
|
|
72
|
+
dx = event.delta_x or -1
|
|
73
|
+
self._scroll_x(-abs(dx))
|
|
74
|
+
event.stop()
|
|
75
|
+
|
|
76
|
+
def _on_mouse_scroll_right(self, event: MouseScrollRight) -> None:
|
|
77
|
+
# Ensure positive direction regardless of terminal conventions.
|
|
78
|
+
dx = event.delta_x or 1
|
|
79
|
+
self._scroll_x(abs(dx))
|
|
80
|
+
event.stop()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class IdPickerScreen(ModalScreen[str]):
|
|
85
|
+
"""Modal that lets the user pick an item (returns its ID).
|
|
86
|
+
|
|
87
|
+
- Returns the selected ID.
|
|
88
|
+
- Returns an empty string if cancelled.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
BINDINGS = [
|
|
92
|
+
Binding("escape", "cancel", "Cancel", show=True),
|
|
93
|
+
Binding("ctrl+left", "hscroll_left", "Scroll left", show=False),
|
|
94
|
+
Binding("ctrl+right", "hscroll_right", "Scroll right", show=False),
|
|
95
|
+
]
|
|
96
|
+
CSS = """
|
|
97
|
+
IdPickerScreen {
|
|
98
|
+
align: center middle;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#idpicker-container {
|
|
102
|
+
width: 90;
|
|
103
|
+
height: 90%;
|
|
104
|
+
background: $surface;
|
|
105
|
+
border: solid $accent;
|
|
106
|
+
padding: 0 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#idpicker-title {
|
|
110
|
+
width: 100%;
|
|
111
|
+
text-align: center;
|
|
112
|
+
text-style: bold;
|
|
113
|
+
color: $primary;
|
|
114
|
+
background: $surface;
|
|
115
|
+
height: 1;
|
|
116
|
+
margin-bottom: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#idpicker-table {
|
|
120
|
+
height: 1fr;
|
|
121
|
+
width: 100%;
|
|
122
|
+
background: $surface;
|
|
123
|
+
/* Slightly dim the row text so it's easier on the eyes. */
|
|
124
|
+
color: $foreground 80%;
|
|
125
|
+
border: none;
|
|
126
|
+
overflow-x: auto;
|
|
127
|
+
overflow-y: auto;
|
|
128
|
+
scrollbar-gutter: auto;
|
|
129
|
+
scrollbar-background: $surface;
|
|
130
|
+
scrollbar-background-hover: $surface;
|
|
131
|
+
scrollbar-background-active: $surface;
|
|
132
|
+
scrollbar-corner-color: $surface;
|
|
133
|
+
scrollbar-color: $panel;
|
|
134
|
+
scrollbar-color-hover: $primary;
|
|
135
|
+
scrollbar-color-active: $primary;
|
|
136
|
+
scrollbar-size-vertical: 1;
|
|
137
|
+
scrollbar-size-horizontal: 1;
|
|
138
|
+
padding: 0 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
/* Softer column-header styling (avoid the high-contrast cyan/white look). */
|
|
143
|
+
#idpicker-table > .datatable--header {
|
|
144
|
+
background: $brand-cyan;
|
|
145
|
+
color: black;
|
|
146
|
+
}
|
|
147
|
+
/* Override Textual's ANSI-mode defaults (it makes headers bright blue by default). */
|
|
148
|
+
#idpicker-table:ansi > .datatable--header {
|
|
149
|
+
background: $brand-cyan;
|
|
150
|
+
color: black;
|
|
151
|
+
}
|
|
152
|
+
#idpicker-table > .datatable--header-cursor {
|
|
153
|
+
background: $brand-cyan;
|
|
154
|
+
color: black;
|
|
155
|
+
}
|
|
156
|
+
#idpicker-table > .datatable--header-hover {
|
|
157
|
+
background: $brand-cyan;
|
|
158
|
+
}
|
|
159
|
+
#idpicker-list {
|
|
160
|
+
height: 1fr;
|
|
161
|
+
width: 100%;
|
|
162
|
+
border: none;
|
|
163
|
+
overflow-x: auto;
|
|
164
|
+
overflow-y: auto;
|
|
165
|
+
scrollbar-gutter: auto;
|
|
166
|
+
scrollbar-background: $surface;
|
|
167
|
+
scrollbar-background-hover: $surface;
|
|
168
|
+
scrollbar-background-active: $surface;
|
|
169
|
+
scrollbar-corner-color: $surface;
|
|
170
|
+
scrollbar-color: $panel;
|
|
171
|
+
scrollbar-color-hover: $primary;
|
|
172
|
+
scrollbar-color-active: $primary;
|
|
173
|
+
scrollbar-size-vertical: 1;
|
|
174
|
+
scrollbar-size-horizontal: 1;
|
|
175
|
+
padding: 0 1;
|
|
176
|
+
}
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
title: str,
|
|
182
|
+
options: list[tuple[str, str]] | None = None,
|
|
183
|
+
*,
|
|
184
|
+
columns: list[str] | None = None,
|
|
185
|
+
rows: list[tuple[str, list[str] | tuple[str, ...]]] | None = None,
|
|
186
|
+
):
|
|
187
|
+
super().__init__()
|
|
188
|
+
self._title = title
|
|
189
|
+
self._options = options or []
|
|
190
|
+
self._columns = columns
|
|
191
|
+
self._rows = rows
|
|
192
|
+
|
|
193
|
+
def compose(self) -> ComposeResult:
|
|
194
|
+
with Vertical(id="idpicker-container"):
|
|
195
|
+
yield Static(self._title, id="idpicker-title")
|
|
196
|
+
if self._columns is not None and self._rows is not None:
|
|
197
|
+
table = _IdPickerDataTable(id="idpicker-table", zebra_stripes=True)
|
|
198
|
+
# Add a bit of breathing room between columns for readability.
|
|
199
|
+
table.cell_padding = 2
|
|
200
|
+
table.cursor_type = "row"
|
|
201
|
+
table.add_columns(*self._columns)
|
|
202
|
+
for item_id, cells in self._rows:
|
|
203
|
+
table.add_row(*list(cells), key=item_id)
|
|
204
|
+
yield table
|
|
205
|
+
else:
|
|
206
|
+
option_list = OptionList(id="idpicker-list")
|
|
207
|
+
for item_id, label in self._options:
|
|
208
|
+
# Option.id is independent of widget IDs; it only needs to be unique in this list.
|
|
209
|
+
option_list.add_option(Option(label, id=item_id))
|
|
210
|
+
yield option_list
|
|
211
|
+
|
|
212
|
+
def on_mount(self) -> None:
|
|
213
|
+
"""Focus the primary list widget."""
|
|
214
|
+
try:
|
|
215
|
+
if self._columns is not None and self._rows is not None:
|
|
216
|
+
self.query_one("#idpicker-table", DataTable).focus()
|
|
217
|
+
else:
|
|
218
|
+
self.query_one("#idpicker-list", OptionList).focus()
|
|
219
|
+
except Exception:
|
|
220
|
+
# Don't crash the modal due to focus issues.
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
224
|
+
self.dismiss(event.option.id or "")
|
|
225
|
+
|
|
226
|
+
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
227
|
+
key = getattr(event.row_key, "value", None)
|
|
228
|
+
self.dismiss(key or "")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def action_hscroll_left(self) -> None:
|
|
232
|
+
"""Scroll the table horizontally to the left (Ctrl+Left)."""
|
|
233
|
+
try:
|
|
234
|
+
table = self.query_one("#idpicker-table", DataTable)
|
|
235
|
+
except Exception:
|
|
236
|
+
return
|
|
237
|
+
table.scroll_relative(x=-10, y=0, animate=False)
|
|
238
|
+
|
|
239
|
+
def action_hscroll_right(self) -> None:
|
|
240
|
+
"""Scroll the table horizontally to the right (Ctrl+Right)."""
|
|
241
|
+
try:
|
|
242
|
+
table = self.query_one("#idpicker-table", DataTable)
|
|
243
|
+
except Exception:
|
|
244
|
+
return
|
|
245
|
+
table.scroll_relative(x=10, y=0, animate=False)
|
|
246
|
+
def action_cancel(self) -> None:
|
|
247
|
+
self.dismiss("")
|
|
@@ -20,6 +20,7 @@ SLASH_COMMANDS: list[SlashCommand] = [
|
|
|
20
20
|
SlashCommand("Session", "/status", "Show current action & run", "/status"),
|
|
21
21
|
SlashCommand("Session", "/cancel", "Cancel active request", "/cancel"),
|
|
22
22
|
SlashCommand("Session", "/use <action_id>", "Switch to a different action", "/use "),
|
|
23
|
+
SlashCommand("Session", "/name <new name>", "Rename the current run (action run or graph run)", "/name "),
|
|
23
24
|
SlashCommand("Session", "/continue <run_id>", "Continue an existing run", "/continue "),
|
|
24
25
|
|
|
25
26
|
# Files
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
"""Generic modal picker for selecting an item by ID.
|
|
2
|
-
|
|
3
|
-
Used for slash-command list views (e.g. /actions list, /runs list) so the
|
|
4
|
-
list UI is separate from the main chat timeline.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from textual.app import ComposeResult
|
|
10
|
-
from textual.binding import Binding
|
|
11
|
-
from textual.containers import Vertical
|
|
12
|
-
from textual.screen import ModalScreen
|
|
13
|
-
from textual.widgets import OptionList, Static
|
|
14
|
-
from textual.widgets.option_list import Option
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class IdPickerScreen(ModalScreen[str]):
|
|
18
|
-
"""Modal that lets the user pick an item (returns its ID).
|
|
19
|
-
|
|
20
|
-
- Returns the selected ID.
|
|
21
|
-
- Returns an empty string if cancelled.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
BINDINGS = [
|
|
25
|
-
Binding("escape", "cancel", "Cancel", show=True),
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
CSS = """
|
|
29
|
-
IdPickerScreen {
|
|
30
|
-
align: center middle;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
#idpicker-container {
|
|
34
|
-
width: 90;
|
|
35
|
-
height: 80%;
|
|
36
|
-
background: $surface;
|
|
37
|
-
border: solid $accent;
|
|
38
|
-
padding: 1 2;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
#idpicker-title {
|
|
42
|
-
width: 100%;
|
|
43
|
-
text-align: center;
|
|
44
|
-
text-style: bold;
|
|
45
|
-
color: $primary;
|
|
46
|
-
height: auto;
|
|
47
|
-
margin-bottom: 1;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
#idpicker-list {
|
|
51
|
-
height: 1fr;
|
|
52
|
-
width: 100%;
|
|
53
|
-
border: solid $panel;
|
|
54
|
-
}
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
def __init__(self, title: str, options: list[tuple[str, str]]):
|
|
58
|
-
super().__init__()
|
|
59
|
-
self._title = title
|
|
60
|
-
self._options = options
|
|
61
|
-
|
|
62
|
-
def compose(self) -> ComposeResult:
|
|
63
|
-
with Vertical(id="idpicker-container"):
|
|
64
|
-
yield Static(self._title, id="idpicker-title")
|
|
65
|
-
option_list = OptionList(id="idpicker-list")
|
|
66
|
-
for item_id, label in self._options:
|
|
67
|
-
# Option.id is independent of widget IDs; it only needs to be unique in this list.
|
|
68
|
-
option_list.add_option(Option(label, id=item_id))
|
|
69
|
-
yield option_list
|
|
70
|
-
|
|
71
|
-
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
72
|
-
self.dismiss(event.option.id or "")
|
|
73
|
-
|
|
74
|
-
def action_cancel(self) -> None:
|
|
75
|
-
self.dismiss("")
|
|
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
|