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.
Files changed (52) hide show
  1. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/PKG-INFO +1 -1
  2. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/pyproject.toml +1 -1
  3. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/dashboard.py +237 -60
  4. kiwi_code-0.0.28/src/kiwi_tui/screens/id_picker.py +247 -0
  5. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/slash_commands.py +1 -0
  6. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/uv.lock +1 -1
  7. kiwi_code-0.0.27/src/kiwi_tui/screens/id_picker.py +0 -75
  8. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/.github/workflows/publish.yml +0 -0
  9. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/.github/workflows/test.yml +0 -0
  10. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/.gitignore +0 -0
  11. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/.python-version +0 -0
  12. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/CLAUDE.md +0 -0
  13. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/Makefile +0 -0
  14. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/README.md +0 -0
  15. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/__init__.py +0 -0
  16. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/auth.py +0 -0
  17. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/cli.py +0 -0
  18. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/client.py +0 -0
  19. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/commands.py +0 -0
  20. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/logger.py +0 -0
  21. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/models.py +0 -0
  22. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/runtime_manager.py +0 -0
  23. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_cli/server.py +0 -0
  24. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_runtime/__init__.py +0 -0
  25. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_runtime/__main__.py +0 -0
  26. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_runtime/main.py +0 -0
  27. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_runtime/snake_game/.gitignore +0 -0
  28. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
  29. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/__init__.py +0 -0
  30. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/inline_file_picker.py +0 -0
  31. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/main.py +0 -0
  32. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/random_words.py +0 -0
  33. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/runtime_agent.py +0 -0
  34. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/__init__.py +0 -0
  35. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/attach_content.py +0 -0
  36. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/command_result.py +0 -0
  37. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/file_browser.py +0 -0
  38. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/help.py +0 -0
  39. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/login.py +0 -0
  40. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  41. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  42. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/screens/slash_picker.py +0 -0
  43. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/src/kiwi_tui/widgets.py +0 -0
  44. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/test_hello.py +0 -0
  45. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/__init__.py +0 -0
  46. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/conftest.py +0 -0
  47. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/test_cli_help.py +0 -0
  48. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/test_imports.py +0 -0
  49. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/test_reexec_kiwi.py +0 -0
  50. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/test_runtime_log_trimming.py +0 -0
  51. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/test_tokens.py +0 -0
  52. {kiwi_code-0.0.27 → kiwi_code-0.0.28}/tests/test_tui_headless.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwi-code
3
- Version: 0.0.27
3
+ Version: 0.0.28
4
4
  Summary: A textual-based terminal user interface application
5
5
  Project-URL: Homepage, https://meetkiwi.ai
6
6
  Project-URL: Repository, https://github.com/jetoslabs/kiwi-code
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiwi-code"
3
- version = "0.0.27"
3
+ version = "0.0.28"
4
4
  description = "A textual-based terminal user interface application"
5
5
  readme = {file = "README.md", content-type = "text/markdown"}
6
6
  requires-python = ">=3.11,<4.0"
@@ -37,7 +37,7 @@ class UserMessageRow(Horizontal):
37
37
  self._text = text
38
38
 
39
39
  def compose(self) -> ComposeResult:
40
- yield Static("YOU:", classes="user-label", markup=False)
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", "20"))
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
- options: list[tuple[str, str]] = []
1104
+ columns = ["#", "Name", "Type", "Version", "Created"]
1105
+ rows: list[tuple[str, list[str]]] = []
1005
1106
  for idx, doc in enumerate(result.docs, 1):
1006
- label = (
1007
- f"{idx:<4} {_val(doc.name):<30} {_val(doc.type_.value):<25} "
1008
- f"{_val(doc.version):<6} {_fmt_date(doc.created_at)}"
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
- options.append((doc.field_id, label))
1011
- self.app.push_screen(IdPickerScreen(title, options), callback=self._on_action_picked)
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", "20")),
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
- options: list[tuple[str, str]] = []
1158
+ columns = ["#", "Status", "Name", "Created", "Updated"]
1159
+ rows: list[tuple[str, list[str]]] = []
1049
1160
  for idx, doc in enumerate(result.docs, 1):
1050
- label = (
1051
- f"{idx:<4} {doc.status.value:<12} {_val(doc.name):<30} "
1052
- f"{_fmt_date(doc.created_at):<18} {_fmt_date(doc.updated_at)}"
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
- options.append((doc.field_id, label))
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", "20"))
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
- options: list[tuple[str, str]] = []
1199
+ columns = ["#", "Name", "Version", "Published", "Created"]
1200
+ rows: list[tuple[str, list[str]]] = []
1082
1201
  for idx, doc in enumerate(result.docs, 1):
1083
- label = (
1084
- f"{idx:<4} {_val(doc.name):<30} {_val(doc.version):<6} "
1085
- f"{_val(doc.is_published):<10} {_fmt_date(doc.created_at)}"
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
- options.append((doc.field_id, label))
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", "20")),
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
- options: list[tuple[str, str]] = []
1253
+ columns = ["#", "Status", "Name", "Created", "Updated"]
1254
+ rows: list[tuple[str, list[str]]] = []
1128
1255
  for idx, doc in enumerate(result.docs, 1):
1129
- label = (
1130
- f"{idx:<4} {doc.status.value:<12} {_val(doc.name):<30} "
1131
- f"{_fmt_date(doc.created_at):<18} {_fmt_date(doc.updated_at)}"
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
- options.append((doc.field_id, label))
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", "20"))
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
- options: list[tuple[str, str]] = []
1360
+ columns = ["#", "Name", "Type", "Version", "Created"]
1361
+ rows: list[tuple[str, list[str]]] = []
1227
1362
  for idx, doc in enumerate(result.docs, 1):
1228
- label = (
1229
- f"{idx:<4} {_val(doc.name):<30} {_val(doc.type_.value):<25} "
1230
- f"{_val(doc.version):<6} {_fmt_date(doc.created_at)}"
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
- options.append((doc.field_id, label))
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", "20")),
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
- options: list[tuple[str, str]] = []
1414
+ columns = ["#", "Status", "Name", "Created", "Updated"]
1415
+ rows: list[tuple[str, list[str]]] = []
1273
1416
  for idx, doc in enumerate(result.docs, 1):
1274
- label = (
1275
- f"{idx:<4} {doc.status.value:<12} {_val(doc.name):<30} "
1276
- f"{_fmt_date(doc.created_at):<18} {_fmt_date(doc.updated_at)}"
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
- options.append((doc.field_id, label))
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", "20"))
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
- options: list[tuple[str, str]] = []
1454
+ columns = ["#", "Name", "Version", "Published", "Created"]
1455
+ rows: list[tuple[str, list[str]]] = []
1305
1456
  for idx, doc in enumerate(result.docs, 1):
1306
- label = (
1307
- f"{idx:<4} {_val(doc.name):<30} {_val(doc.version):<6} "
1308
- f"{_val(doc.is_published):<10} {_fmt_date(doc.created_at)}"
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
- options.append((doc.field_id, label))
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", "20")),
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
- options: list[tuple[str, str]] = []
1507
+ columns = ["#", "Status", "Name", "Created", "Updated"]
1508
+ rows: list[tuple[str, list[str]]] = []
1350
1509
  for idx, doc in enumerate(result.docs, 1):
1351
- label = (
1352
- f"{idx:<4} {doc.status.value:<12} {_val(doc.name):<30} "
1353
- f"{_fmt_date(doc.created_at):<18} {_fmt_date(doc.updated_at)}"
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
- options.append((doc.field_id, label))
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
@@ -397,7 +397,7 @@ wheels = [
397
397
 
398
398
  [[package]]
399
399
  name = "kiwi-code"
400
- version = "0.0.27"
400
+ version = "0.0.28"
401
401
  source = { editable = "." }
402
402
  dependencies = [
403
403
  { name = "autobots-client" },
@@ -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