kiwi-code 0.0.23__tar.gz → 0.0.25__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 (49) hide show
  1. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/PKG-INFO +1 -1
  2. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/pyproject.toml +1 -1
  3. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/screens/dashboard.py +83 -19
  4. kiwi_code-0.0.25/src/kiwi_tui/screens/help.py +219 -0
  5. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/screens/slash_picker.py +13 -32
  6. kiwi_code-0.0.25/src/kiwi_tui/slash_commands.py +60 -0
  7. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/widgets.py +4 -28
  8. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/uv.lock +1 -1
  9. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/.github/workflows/publish.yml +0 -0
  10. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/.github/workflows/test.yml +0 -0
  11. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/.gitignore +0 -0
  12. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/.python-version +0 -0
  13. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/CLAUDE.md +0 -0
  14. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/Makefile +0 -0
  15. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/README.md +0 -0
  16. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_cli/__init__.py +0 -0
  17. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_cli/auth.py +0 -0
  18. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_cli/cli.py +0 -0
  19. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_cli/client.py +0 -0
  20. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_cli/commands.py +0 -0
  21. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_cli/logger.py +0 -0
  22. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_cli/models.py +0 -0
  23. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_cli/runtime_manager.py +0 -0
  24. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_cli/server.py +0 -0
  25. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_runtime/__init__.py +0 -0
  26. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_runtime/__main__.py +0 -0
  27. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_runtime/main.py +0 -0
  28. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_runtime/snake_game/.gitignore +0 -0
  29. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
  30. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/__init__.py +0 -0
  31. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/inline_file_picker.py +0 -0
  32. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/main.py +0 -0
  33. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/runtime_agent.py +0 -0
  34. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/screens/__init__.py +0 -0
  35. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/screens/attach_content.py +0 -0
  36. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/screens/command_result.py +0 -0
  37. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/screens/file_browser.py +0 -0
  38. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/screens/id_picker.py +0 -0
  39. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/screens/login.py +0 -0
  40. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  41. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  42. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/test_hello.py +0 -0
  43. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/tests/__init__.py +0 -0
  44. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/tests/conftest.py +0 -0
  45. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/tests/test_cli_help.py +0 -0
  46. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/tests/test_imports.py +0 -0
  47. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/tests/test_reexec_kiwi.py +0 -0
  48. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/tests/test_tokens.py +0 -0
  49. {kiwi_code-0.0.23 → kiwi_code-0.0.25}/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.23
3
+ Version: 0.0.25
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.23"
3
+ version = "0.0.25"
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"
@@ -6,12 +6,13 @@ from textual.app import ComposeResult
6
6
  from textual.binding import Binding
7
7
  from textual.screen import Screen
8
8
  from textual.widgets import Header, Footer, Input, Static, Button, Markdown, LoadingIndicator
9
- from textual.containers import Vertical, VerticalScroll, Horizontal
9
+ from textual.containers import Vertical, VerticalScroll, Horizontal, Center
10
10
  from kiwi_tui.screens.file_browser import FileBrowserScreen
11
11
  from kiwi_tui.screens.attach_content import AttachContentScreen
12
12
  from kiwi_tui.screens.slash_picker import SlashPickerScreen
13
13
  from kiwi_tui.screens.id_picker import IdPickerScreen
14
14
  from kiwi_tui.screens.command_result import CommandResultScreen
15
+ from kiwi_tui.screens.help import HelpScreen
15
16
  from kiwi_tui.widgets import ChatInput
16
17
  from kiwi_tui.inline_file_picker import InlineFilePicker
17
18
  from textual.worker import Worker, WorkerState
@@ -38,6 +39,28 @@ class UserMessageRow(Horizontal):
38
39
  yield Static(self._text, classes="user-body", markup=False)
39
40
 
40
41
 
42
+
43
+ class AssistantMessageRow(Horizontal):
44
+ """A single assistant message rendered with a left dot + markdown body."""
45
+
46
+ def __init__(self, markdown_text: str, *, classes: str = "") -> None:
47
+ super().__init__(classes=classes)
48
+ self._markdown_text = markdown_text
49
+
50
+ def compose(self) -> ComposeResult:
51
+ # Solid dot marker on the left side of the model response.
52
+ yield Static("●", classes="assistant-dot", markup=False)
53
+ yield Markdown(self._markdown_text, classes="assistant-body")
54
+
55
+ def update_markdown(self, markdown_text: str) -> None:
56
+ """Update the markdown content in-place."""
57
+ self._markdown_text = markdown_text
58
+ try:
59
+ self.query_one(".assistant-body", Markdown).update(markdown_text)
60
+ except Exception:
61
+ # Defensive: if the widget tree isn't ready or the child was removed.
62
+ pass
63
+
41
64
  # Footer hint for inserting a newline differs by OS:
42
65
  # - macOS terminals often don't forward modified Enter combos to terminal apps reliably, so we advertise Ctrl+N.
43
66
  # - Windows terminals tend to support Shift+Enter for multi-line input.
@@ -95,7 +118,7 @@ class DashboardScreen(Screen):
95
118
 
96
119
  #run-status-bar {
97
120
  width: 100%;
98
- height: 4;
121
+ height: 5;
99
122
  background: $background;
100
123
  }
101
124
 
@@ -129,12 +152,14 @@ class DashboardScreen(Screen):
129
152
  }
130
153
  #copy-run-id {
131
154
  width: auto;
132
- height: auto;
155
+ /* Explicit height so the full border (top+bottom) is visible. */
156
+ height: 3;
133
157
  padding: 0 1;
134
- margin: 0 1;
158
+ margin: 0;
135
159
  }
136
160
 
137
161
 
162
+
138
163
  #activity-bar {
139
164
  width: 100%;
140
165
  height: 1;
@@ -168,7 +193,8 @@ class DashboardScreen(Screen):
168
193
  width: 100%;
169
194
  height: auto;
170
195
  padding: 0 1;
171
- margin: 0;
196
+ /* Add breathing room between all messages (user/assistant/info/error/etc.). */
197
+ margin: 1 0 0 0;
172
198
  }
173
199
 
174
200
  .user-message {
@@ -190,13 +216,25 @@ class DashboardScreen(Screen):
190
216
 
191
217
  .assistant-message {
192
218
  color: $text;
193
- margin: 0;
194
219
  padding: 0 1;
195
220
  }
196
221
 
222
+ .assistant-dot {
223
+ width: 2;
224
+ padding: 0 1 0 0;
225
+ color: $brand-cyan;
226
+ text-style: bold;
227
+ content-align: center top;
228
+ }
229
+
230
+ .assistant-body {
231
+ width: 1fr;
232
+ }
233
+
197
234
  .assistant-message Markdown {
198
235
  margin: 0;
199
236
  padding: 0;
237
+ width: 1fr;
200
238
  }
201
239
 
202
240
  .error-message {
@@ -372,8 +410,10 @@ class DashboardScreen(Screen):
372
410
  with Vertical(id="status-action-col"):
373
411
  yield Static("", id="status-action", markup=False)
374
412
  with Vertical(id="status-run-col"):
375
- yield Static("", id="status-run", markup=False)
376
- yield Button("Copy", id="copy-run-id", variant="default")
413
+ with Center():
414
+ yield Static("", id="status-run", markup=False)
415
+ with Center():
416
+ yield Button("Copy", id="copy-run-id", variant="default")
377
417
 
378
418
  with Vertical(id="input-bar"):
379
419
  with Horizontal(id="activity-bar"):
@@ -534,6 +574,16 @@ class DashboardScreen(Screen):
534
574
  # If a command is already running, ignore (prevents the "hung" feeling / double execution).
535
575
  if self._cmd_running:
536
576
  return
577
+
578
+ # /help is a UI-first experience (picker + copy). Don't run it through the CLI dispatcher.
579
+ if command.strip().lower() == "/help":
580
+ self.app.push_screen(HelpScreen())
581
+ try:
582
+ self.query_one("#chat-input", ChatInput).focus()
583
+ except Exception:
584
+ pass
585
+ return
586
+
537
587
  self._set_command_running(True, f"Running {command} ...")
538
588
  self.run_worker(
539
589
  self._handle_slash_command_async(command),
@@ -1520,8 +1570,8 @@ class DashboardScreen(Screen):
1520
1570
  messages = self.query_one("#messages", VerticalScroll)
1521
1571
  css_class = f"message {msg_type}-message"
1522
1572
  if msg_type == "assistant":
1523
- widget = Markdown(self._prepare_markdown(text), classes=css_class)
1524
- messages.mount(widget)
1573
+ prepared = self._prepare_markdown(text)
1574
+ messages.mount(AssistantMessageRow(prepared, classes=css_class))
1525
1575
  elif msg_type == "user":
1526
1576
  # Render a styled "YOU" label without enabling markup (keeps input safe).
1527
1577
  messages.mount(UserMessageRow(str(text), classes=css_class))
@@ -1942,7 +1992,11 @@ class DashboardScreen(Screen):
1942
1992
 
1943
1993
  self._set_streaming(False)
1944
1994
 
1945
- def update_streaming_message(self, output: any, widget_ref: any = None) -> Static:
1995
+ def update_streaming_message(
1996
+ self,
1997
+ output: any,
1998
+ widget_ref: Markdown | AssistantMessageRow | None = None,
1999
+ ) -> Markdown | AssistantMessageRow | None:
1946
2000
  """Update or create a streaming message widget with new output.
1947
2001
 
1948
2002
  Returns the widget being updated/created for future updates.
@@ -1953,21 +2007,24 @@ class DashboardScreen(Screen):
1953
2007
 
1954
2008
  messages = self.query_one("#messages", VerticalScroll)
1955
2009
 
1956
- text_content = self._prepare_markdown(text_content)
2010
+ markdown_text = self._prepare_markdown(text_content)
1957
2011
  if widget_ref is None:
1958
- widget_ref = Markdown(text_content, classes="message assistant-message")
2012
+ widget_ref = AssistantMessageRow(markdown_text, classes="message assistant-message")
1959
2013
  messages.mount(widget_ref)
1960
2014
  else:
1961
2015
  try:
1962
- widget_ref.update(text_content)
2016
+ if isinstance(widget_ref, AssistantMessageRow):
2017
+ widget_ref.update_markdown(markdown_text)
2018
+ else:
2019
+ # Backward-compat: if an older ref is a Markdown widget.
2020
+ widget_ref.update(markdown_text)
1963
2021
  except Exception as e:
1964
2022
  logger.warning(f"Failed to update widget: {e}")
1965
- widget_ref = Markdown(text_content, classes="message assistant-message")
2023
+ widget_ref = AssistantMessageRow(markdown_text, classes="message assistant-message")
1966
2024
  messages.mount(widget_ref)
1967
2025
 
1968
2026
  messages.scroll_end(animate=False)
1969
2027
  return widget_ref
1970
-
1971
2028
  def extract_text_from_output(self, output: any) -> str:
1972
2029
  """Extract text content from output blocks structure and clean it for display."""
1973
2030
  if not isinstance(output, dict):
@@ -2157,10 +2214,17 @@ class DashboardScreen(Screen):
2157
2214
  messages = self.query_one("#messages", VerticalScroll)
2158
2215
  assistant_messages = messages.query(".assistant-message")
2159
2216
 
2217
+ prepared = self._prepare_markdown(text)
2218
+
2160
2219
  if assistant_messages:
2161
- # Update the last assistant message
2162
2220
  last_msg = assistant_messages[-1]
2163
- last_msg.update(text)
2221
+ if isinstance(last_msg, AssistantMessageRow):
2222
+ last_msg.update_markdown(prepared)
2223
+ else:
2224
+ # Backward-compat if we have an older Markdown widget in the tree.
2225
+ try:
2226
+ last_msg.update(prepared)
2227
+ except Exception:
2228
+ self.add_message(text, "assistant")
2164
2229
  else:
2165
- # No existing message, create new one
2166
2230
  self.add_message(text, "assistant")
@@ -0,0 +1,219 @@
1
+ import subprocess
2
+ import sys
3
+ import time
4
+
5
+ from rich.text import Text
6
+ from textual.app import ComposeResult
7
+ from textual.binding import Binding
8
+ from textual.containers import Vertical
9
+ from textual.screen import ModalScreen
10
+ from textual.widgets import OptionList, Static
11
+ from textual.widgets.option_list import Option
12
+
13
+ from kiwi_tui.slash_commands import SLASH_COMMANDS
14
+
15
+
16
+ class HelpScreen(ModalScreen[None]):
17
+ """Modal that lists available slash commands and allows copying."""
18
+
19
+ BINDINGS = [
20
+ Binding("escape", "close", "Close", show=True),
21
+ Binding("c", "copy", "Copy", show=True),
22
+ ]
23
+
24
+ CSS = """
25
+ HelpScreen {
26
+ align: center middle;
27
+ }
28
+
29
+ #help-container {
30
+ width: 96;
31
+ height: 80%;
32
+ background: $surface;
33
+ border: solid $accent;
34
+ /* Reduce top padding so the title/hint sit closer to the border. */
35
+ padding: 0 3 1 3;
36
+ }
37
+
38
+ #help-title {
39
+ width: 100%;
40
+ text-align: center;
41
+ text-style: bold;
42
+ color: $primary;
43
+ height: 1;
44
+ /* Tighten vertical spacing at the top of the modal. */
45
+ margin-bottom: 0;
46
+ }
47
+
48
+ #help-hint {
49
+ width: 100%;
50
+ height: 1;
51
+ padding: 0 1;
52
+ color: $text-muted;
53
+ text-style: italic;
54
+ margin-bottom: 0;
55
+ }
56
+
57
+ #help-list {
58
+ height: 1fr;
59
+ width: 100%;
60
+ border: solid $panel;
61
+ background: $surface;
62
+ scrollbar-size: 1 1;
63
+ /* Keep the list away from the container border for consistent margins. */
64
+ margin: 0 1;
65
+ }
66
+
67
+ #help-list > .option-list--option-highlighted {
68
+ background: $primary-background;
69
+ color: $primary;
70
+ text-style: bold;
71
+ }
72
+ """
73
+
74
+ def __init__(self) -> None:
75
+ super().__init__()
76
+ self._last_copy_notify_at: float = 0.0
77
+ self._last_highlight_index: int | None = None
78
+ self._highlight_skip_guard: bool = False
79
+
80
+ def on_mount(self) -> None:
81
+ # Match the UX of the inline @ picker: focus the list and highlight the first command.
82
+ option_list = self.query_one("#help-list", OptionList)
83
+ try:
84
+ for idx, opt in enumerate(option_list.options):
85
+ if not getattr(opt, "disabled", False) and opt.id:
86
+ option_list.highlighted = idx
87
+ break
88
+ except Exception:
89
+ pass
90
+ option_list.focus()
91
+
92
+ def compose(self) -> ComposeResult:
93
+ # Column widths tuned for a typical terminal width; the OptionList will clip if narrower.
94
+ cmd_col_width = 38
95
+ with Vertical(id="help-container"):
96
+ yield Static("Slash Commands", id="help-title")
97
+ yield Static("Enter/click: copy C: copy highlighted Esc: close", id="help-hint")
98
+
99
+ option_list = OptionList(id="help-list")
100
+
101
+ last_category: str | None = None
102
+ for cmd in SLASH_COMMANDS:
103
+ if cmd.category != last_category:
104
+ # Add a blank spacer row between sections for readability.
105
+ if last_category is not None:
106
+ option_list.add_option(Option(Text(""), id=None, disabled=True))
107
+
108
+ last_category = cmd.category
109
+ option_list.add_option(
110
+ Option(Text(f" {cmd.category.upper()}", style="bold cyan"), id=None, disabled=True)
111
+ )
112
+
113
+ # Option id is the command template so we can copy exactly what is shown.
114
+ label = f" {cmd.template:<{cmd_col_width}} {cmd.description}"
115
+ option_list.add_option(Option(Text(label), id=cmd.template))
116
+
117
+ yield option_list
118
+
119
+ def on_option_list_option_highlighted(self, event: OptionList.OptionHighlighted) -> None:
120
+ """Skip spacer/category rows while navigating with arrow keys."""
121
+ if self._highlight_skip_guard:
122
+ return
123
+
124
+ # Track previous position so we can infer direction.
125
+ prev = self._last_highlight_index
126
+
127
+ # Textual versions differ slightly in attribute names.
128
+ idx = getattr(event, "index", None)
129
+ if idx is None:
130
+ idx = getattr(event, "option_index", None)
131
+ if idx is None:
132
+ return
133
+
134
+ self._last_highlight_index = idx
135
+
136
+ option = event.option
137
+ if option.id and not getattr(option, "disabled", False):
138
+ return
139
+
140
+ option_list = event.option_list
141
+ try:
142
+ direction = -1 if (prev is not None and idx < prev) else 1
143
+ n = len(option_list.options)
144
+ self._highlight_skip_guard = True
145
+ cur = idx
146
+ while 0 <= cur < n:
147
+ cur += direction
148
+ if not (0 <= cur < n):
149
+ break
150
+ opt = option_list.options[cur]
151
+ if opt.id and not getattr(opt, "disabled", False):
152
+ option_list.highlighted = cur
153
+ self._last_highlight_index = cur
154
+ break
155
+ finally:
156
+ self._highlight_skip_guard = False
157
+
158
+
159
+
160
+ def _notify_once(self, msg: str, *, severity: str) -> None:
161
+ now = time.monotonic()
162
+ if now - self._last_copy_notify_at < 1.0:
163
+ return
164
+ self._last_copy_notify_at = now
165
+ self.app.notify(msg, severity=severity)
166
+
167
+ def _copy_to_clipboard(self, text: str) -> bool:
168
+ """Copy text to the system clipboard. Returns True if it likely worked."""
169
+ copied = False
170
+
171
+ # Prefer native OS clipboard tools (more reliable in terminal apps).
172
+ try:
173
+ if sys.platform == "darwin":
174
+ subprocess.run(["pbcopy"], input=text.encode("utf-8"), check=True)
175
+ copied = True
176
+ elif sys.platform.startswith("linux"):
177
+ subprocess.run(
178
+ ["xclip", "-selection", "clipboard"],
179
+ input=text.encode("utf-8"),
180
+ check=True,
181
+ )
182
+ copied = True
183
+ elif sys.platform.startswith("win"):
184
+ subprocess.run(["clip"], input=text.encode("utf-8"), check=True)
185
+ copied = True
186
+ except Exception:
187
+ copied = False
188
+
189
+ # Fallback to Textual's clipboard integration.
190
+ if not copied:
191
+ try:
192
+ self.app.copy_to_clipboard(text)
193
+ copied = True
194
+ except Exception:
195
+ copied = False
196
+
197
+ return copied
198
+
199
+ def _copy_selected(self, template: str) -> None:
200
+ if not template.strip():
201
+ return
202
+ if self._copy_to_clipboard(template):
203
+ self._notify_once(f"Copied: {template}", severity="information")
204
+ else:
205
+ self._notify_once("Failed to copy to clipboard", severity="error")
206
+
207
+ def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
208
+ template = event.option.id
209
+ if template:
210
+ self._copy_selected(str(template))
211
+
212
+ def action_copy(self) -> None:
213
+ option_list = self.query_one("#help-list", OptionList)
214
+ opt = option_list.highlighted_option
215
+ if opt and opt.id:
216
+ self._copy_selected(str(opt.id))
217
+
218
+ def action_close(self) -> None:
219
+ self.dismiss(None)
@@ -1,45 +1,26 @@
1
1
  """Slash command picker modal screen."""
2
2
 
3
3
  from textual.app import ComposeResult
4
+ from textual.binding import Binding
5
+ from textual.containers import Vertical
4
6
  from textual.screen import ModalScreen
5
- from textual.widgets import Static, Button, OptionList
7
+ from textual.widgets import OptionList, Static
6
8
  from textual.widgets.option_list import Option
7
- from textual.containers import Vertical
8
- from textual.binding import Binding
9
+
10
+ from kiwi_tui.slash_commands import SLASH_COMMANDS
9
11
 
10
12
 
11
- # Commands and their descriptions
12
- _COMMANDS = [
13
- ("/help", "Show help"),
14
- ("/new", "Start new conversation"),
15
- ("/status", "Show current action & run"),
16
- ("/cancel", "Cancel active request"),
17
- ("/actions list", "List recent actions"),
18
- ("/runs list", "List recent runs"),
19
- ("/graphs list", "List recent graphs"),
20
- ("/graph-runs list", "List recent graph runs"),
21
- ("/use ", "Switch action by ID"),
22
- ("/continue ", "Continue a run by ID"),
23
- ("/upload ", "Upload file(s) by path"),
24
- ("/files", "Show pending files"),
25
- ("/clear-files", "Clear pending files"),
26
- ("/login", "Log in"),
27
- ("/logout", "Log out"),
28
- ("/metadata", "Show metadata"),
29
- ("/metadata set ", "Set metadata key"),
30
- ("/metadata remove ", "Remove metadata key"),
31
- ("/metadata clear", "Clear all metadata"),
32
- ("/connect-cli", "Tell the agent to connect to the local CLI"),
33
- ("/show-logs", "Show local CLI (runtime) logs"),
34
- ]
13
+ # Commands and their descriptions (single source of truth).
14
+ # We show the template in the picker, and return insert_text on selection.
15
+ _COMMANDS = [(c.insert_text, c.template, c.description) for c in SLASH_COMMANDS]
35
16
 
36
17
 
37
18
  class SlashPickerScreen(ModalScreen[str]):
38
19
  """Modal that lets the user pick a slash command.
39
20
 
40
- Returns the command string, or empty string if cancelled.
41
- Commands that don't need arguments (no trailing space) are returned as-is
42
- for immediate execution.
21
+ Returns the command string (insert_text), or empty string if cancelled.
22
+ Commands that don't need arguments are returned as-is for immediate execution.
23
+ Commands that expect args end with a trailing space ("/use ").
43
24
  """
44
25
 
45
26
  BINDINGS = [
@@ -79,8 +60,8 @@ class SlashPickerScreen(ModalScreen[str]):
79
60
  with Vertical(id="slash-container"):
80
61
  yield Static("Select a command", id="slash-title")
81
62
  option_list = OptionList(id="slash-list")
82
- for cmd, desc in _COMMANDS:
83
- option_list.add_option(Option(f"{cmd:<25} {desc}", id=cmd))
63
+ for insert_text, template, desc in _COMMANDS:
64
+ option_list.add_option(Option(f"{template:<32} {desc}", id=insert_text))
84
65
  yield option_list
85
66
 
86
67
  def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
@@ -0,0 +1,60 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass(frozen=True)
4
+ class SlashCommand:
5
+ category: str
6
+ template: str
7
+ description: str
8
+ insert_text: str
9
+
10
+ @property
11
+ def needs_args(self) -> bool:
12
+ return self.insert_text.endswith(" ")
13
+
14
+
15
+ # NOTE: Keep this aligned with DashboardScreen's TUI command handler.
16
+ SLASH_COMMANDS: list[SlashCommand] = [
17
+ # Session
18
+ SlashCommand("Session", "/help", "Show all slash commands", "/help"),
19
+ SlashCommand("Session", "/new", "Start new conversation", "/new"),
20
+ SlashCommand("Session", "/status", "Show current action & run", "/status"),
21
+ SlashCommand("Session", "/cancel", "Cancel active request", "/cancel"),
22
+ SlashCommand("Session", "/use <action_id>", "Switch to a different action", "/use "),
23
+ SlashCommand("Session", "/continue <run_id>", "Continue an existing run", "/continue "),
24
+
25
+ # Files
26
+ SlashCommand("Files", "/upload <path> [path2 ...]", "Upload file(s) and attach to next message", "/upload "),
27
+ SlashCommand("Files", "/files", "Show pending attachments", "/files"),
28
+ SlashCommand("Files", "/clear-files", "Clear pending attachments", "/clear-files"),
29
+
30
+ # Auth
31
+ SlashCommand("Auth", "/login", "Log in", "/login"),
32
+ SlashCommand("Auth", "/logout", "Log out", "/logout"),
33
+
34
+ # Metadata
35
+ SlashCommand("Metadata", "/metadata", "Show effective metadata", "/metadata"),
36
+ SlashCommand("Metadata", "/metadata set <key> <value>", "Set a metadata field", "/metadata set "),
37
+ SlashCommand("Metadata", "/metadata remove <key>", "Remove a metadata field", "/metadata remove "),
38
+ SlashCommand("Metadata", "/metadata clear", "Clear all metadata overrides", "/metadata clear"),
39
+
40
+ # Runtime (local CLI agent)
41
+ SlashCommand("Runtime", "/connect-cli", "Tell the agent to connect to the local CLI", "/connect-cli"),
42
+ SlashCommand("Runtime", "/show-logs", "Show local CLI (runtime) logs", "/show-logs"),
43
+ SlashCommand("Runtime", "/runtime", "Runtime commands (currently disabled)", "/runtime"),
44
+
45
+ # Query (API)
46
+ SlashCommand("Query", "/actions list", "List recent actions", "/actions list"),
47
+ SlashCommand("Query", "/actions get <id>", "Get an action by id", "/actions get "),
48
+ SlashCommand("Query", "/runs list", "List recent runs", "/runs list"),
49
+ SlashCommand("Query", "/runs get <id>", "Get a run by id", "/runs get "),
50
+ SlashCommand("Query", "/graphs list", "List recent graphs", "/graphs list"),
51
+ SlashCommand("Query", "/graphs get <id>", "Get a graph by id", "/graphs get "),
52
+ SlashCommand("Query", "/graph-runs list", "List recent graph runs", "/graph-runs list"),
53
+ SlashCommand("Query", "/graph-runs get <id>", "Get a graph run by id", "/graph-runs get "),
54
+ ]
55
+
56
+
57
+ def autocomplete_commands() -> list[str]:
58
+ """Return strings used for chat input autocomplete."""
59
+ # We keep the insert_text version for consistency with the actual command parser.
60
+ return [c.insert_text for c in SLASH_COMMANDS]
@@ -12,34 +12,10 @@ from textual.widgets._text_area import TextArea
12
12
  from rich.text import Text
13
13
 
14
14
 
15
- # Slash commands for autocomplete
16
- _SLASH_COMMANDS = [
17
- "/help",
18
- "/actions list",
19
- "/actions get ",
20
- "/runs list",
21
- "/runs get ",
22
- "/graphs list",
23
- "/graphs get ",
24
- "/graph-runs list",
25
- "/graph-runs get ",
26
- "/use ",
27
- "/continue ",
28
- "/new",
29
- "/status",
30
- "/cancel",
31
- "/upload ",
32
- "/files",
33
- "/clear-files",
34
- "/login",
35
- "/logout",
36
- "/metadata",
37
- "/metadata set ",
38
- "/metadata remove ",
39
- "/metadata clear",
40
- "/connect-cli",
41
- "/show-logs",
42
- ]
15
+ # Slash commands for autocomplete (single source of truth)
16
+ from kiwi_tui.slash_commands import autocomplete_commands
17
+
18
+ _SLASH_COMMANDS = autocomplete_commands()
43
19
 
44
20
 
45
21
  class ChatInput(TextArea):
@@ -397,7 +397,7 @@ wheels = [
397
397
 
398
398
  [[package]]
399
399
  name = "kiwi-code"
400
- version = "0.0.23"
400
+ version = "0.0.25"
401
401
  source = { editable = "." }
402
402
  dependencies = [
403
403
  { name = "autobots-client" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes