kiwi-code 0.0.512__tar.gz → 0.0.514__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 (143) hide show
  1. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/PKG-INFO +1 -1
  2. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/pyproject.toml +1 -1
  3. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/autocode.py +33 -0
  4. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/terminal_mode.py +3 -2
  5. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/role_mode.py +69 -24
  6. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/term_dashboard.py +21 -11
  7. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_autocode_actions.py +49 -0
  8. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_role_mode_injection.py +76 -18
  9. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_term_dashboard_ui.py +53 -0
  10. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/uv.lock +1 -1
  11. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/.github/workflows/publish.yml +0 -0
  12. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/.github/workflows/test.yml +0 -0
  13. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/.gitignore +0 -0
  14. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/.python-version +0 -0
  15. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/Makefile +0 -0
  16. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/README.md +0 -0
  17. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/docs/runtime_audit.html +0 -0
  18. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/docs/social_login_e2e.md +0 -0
  19. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/scripts/set_edition.py +0 -0
  20. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/scripts/ws_auth_check.py +0 -0
  21. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/common/__init__.py +0 -0
  22. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/common/banner.py +0 -0
  23. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/__init__.py +0 -0
  24. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/_edition.py +0 -0
  25. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/auth.py +0 -0
  26. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/checkpoints.py +0 -0
  27. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/cli.py +0 -0
  28. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/client.py +0 -0
  29. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/commands.py +0 -0
  30. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/edition.py +0 -0
  31. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/human_questions.py +0 -0
  32. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/io_threads.py +0 -0
  33. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/logger.py +0 -0
  34. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/models.py +0 -0
  35. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/oauth.py +0 -0
  36. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/run_activity.py +0 -0
  37. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/runtime_manager.py +0 -0
  38. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/server.py +0 -0
  39. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_cli/subagents.py +0 -0
  40. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/__init__.py +0 -0
  41. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/__main__.py +0 -0
  42. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/auth.py +0 -0
  43. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/checkpoints.py +0 -0
  44. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/cli.py +0 -0
  45. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/commands.py +0 -0
  46. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/constants.py +0 -0
  47. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/fs/__init__.py +0 -0
  48. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/fs/batch.py +0 -0
  49. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/fs/codelens.py +0 -0
  50. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/fs/context.py +0 -0
  51. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/fs/edits.py +0 -0
  52. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/fs/handler.py +0 -0
  53. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/fs/paths.py +0 -0
  54. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/fs/read.py +0 -0
  55. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/fs/search.py +0 -0
  56. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/fs/text.py +0 -0
  57. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/fs/upload.py +0 -0
  58. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/fs/write.py +0 -0
  59. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/handlers.py +0 -0
  60. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/link.py +0 -0
  61. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/main.py +0 -0
  62. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/restricted.py +0 -0
  63. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/sessions.py +0 -0
  64. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/state.py +0 -0
  65. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/tui_bridge.py +0 -0
  66. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/ui.py +0 -0
  67. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_runtime/uploads.py +0 -0
  68. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/__init__.py +0 -0
  69. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/attention.py +0 -0
  70. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/autoupdate.py +0 -0
  71. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/inline_file_picker.py +0 -0
  72. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/latex_unicode.py +0 -0
  73. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/main.py +0 -0
  74. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/random_words.py +0 -0
  75. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/runtime_agent.py +0 -0
  76. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/__init__.py +0 -0
  77. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/attach_content.py +0 -0
  78. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/command_result.py +0 -0
  79. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/detach_files.py +0 -0
  80. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/file_browser.py +0 -0
  81. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/help.py +0 -0
  82. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/id_picker.py +0 -0
  83. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/login.py +0 -0
  84. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  85. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  86. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/slash_picker.py +0 -0
  87. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/term_activity_view.py +0 -0
  88. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/term_dashboard_css.py +0 -0
  89. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/term_dashboard_helpers.py +0 -0
  90. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/term_dashboard_widgets.py +0 -0
  91. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/term_question_widgets.py +0 -0
  92. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/term_status_line.py +0 -0
  93. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/screens/term_subagent_widgets.py +0 -0
  94. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/slash_commands.py +0 -0
  95. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/status_words.py +0 -0
  96. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/term_app.py +0 -0
  97. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/terminal_theme.py +0 -0
  98. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/ui_settings.py +0 -0
  99. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/widgets.py +0 -0
  100. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/src/kiwi_tui/worktrees.py +0 -0
  101. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/test_hello.py +0 -0
  102. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/__init__.py +0 -0
  103. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/conftest.py +0 -0
  104. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_account_server.py +0 -0
  105. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_attention.py +0 -0
  106. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_autoupdate.py +0 -0
  107. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_batch_fs_tool.py +0 -0
  108. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_checkpoints.py +0 -0
  109. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_cli_help.py +0 -0
  110. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_command_tips.py +0 -0
  111. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_copy_path_fs_tool.py +0 -0
  112. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_fs_edit_fidelity.py +0 -0
  113. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_human_questions.py +0 -0
  114. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_imports.py +0 -0
  115. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_io_threads.py +0 -0
  116. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_login_social.py +0 -0
  117. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_math_and_tables.py +0 -0
  118. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_oauth.py +0 -0
  119. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_read_file_streaming.py +0 -0
  120. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_reexec_kiwi.py +0 -0
  121. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_reply_selection.py +0 -0
  122. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_run_activity.py +0 -0
  123. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_run_steering.py +0 -0
  124. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_runtime_cleanup_per_account.py +0 -0
  125. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_runtime_connection.py +0 -0
  126. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_runtime_edit_diffs.py +0 -0
  127. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_runtime_fixes.py +0 -0
  128. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_runtime_imports_on_windows.py +0 -0
  129. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_runtime_log_trimming.py +0 -0
  130. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_runtime_social_login.py +0 -0
  131. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_search_in_files_fs_tool.py +0 -0
  132. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_slash_commands.py +0 -0
  133. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_stats_command.py +0 -0
  134. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_status_line.py +0 -0
  135. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_subagents.py +0 -0
  136. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_terminal_mode.py +0 -0
  137. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_terminal_theme.py +0 -0
  138. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_toasts.py +0 -0
  139. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_tokens.py +0 -0
  140. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_tui_interactive_runtime.py +0 -0
  141. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_tui_palette.py +0 -0
  142. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_unified_diff.py +0 -0
  143. {kiwi_code-0.0.512 → kiwi_code-0.0.514}/tests/test_worktrees.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kiwi-code
3
- Version: 0.0.512
3
+ Version: 0.0.514
4
4
  Summary: A textual-based terminal user interface application
5
5
  Project-URL: Homepage, https://kiwicode.ai
6
6
  Author-email: Anurag <anurag@meetkiwi.co>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiwi-code"
3
- version = "0.0.512"
3
+ version = "0.0.514"
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"
@@ -8,6 +8,11 @@ contains "autocode" in any spelling: "Autocode", "AutoCodeClaude",
8
8
  When two of them have the same name ignoring case, spaces and punctuation
9
9
  ("AutocodeClaude" and "AutoCodeClaude", "Autocode OpenAI Flash" and
10
10
  "AutocodeOpenaiFlash"), only the most recently created one is kept.
11
+
12
+ Which one a new session starts with depends on the account (``default_id_for``):
13
+ Work (Enterprise, api.meetkiwi.ai) starts with "Autocode OpenAI Flash", Personal
14
+ (Subscription, api.runkai.ai) and every other server with "Autocode". The VS Code
15
+ extension makes the same choice (src/defaultAgent.ts).
11
16
  """
12
17
 
13
18
  import re
@@ -17,11 +22,17 @@ from datetime import datetime, timezone
17
22
  from pykiwiai import AuthenticatedClient
18
23
  from pykiwiai.api.actions import list_system_actions_v1_actions_system_actions_get
19
24
 
25
+ from kiwi_cli.server import SERVER_PRESETS
26
+
20
27
  # Fetch every system action in one page: paging with offset is not reliable on
21
28
  # the servers (offset > 0 returns an empty page).
22
29
  _SYSTEM_ACTIONS_LIMIT = 500
23
30
 
24
31
 
32
+ #: The default on the Work (Enterprise) server, compared as _normalize compares names.
33
+ _ENTERPRISE_DEFAULT = "autocodeopenaiflash"
34
+
35
+
25
36
  def _normalize(name: object) -> str:
26
37
  return re.sub(r"[^a-z0-9]", "", str(name or "").lower())
27
38
 
@@ -64,6 +75,28 @@ class AutocodeActions:
64
75
  return action.id
65
76
  return self.actions[0].id if self.actions else None
66
77
 
78
+ def name_of(self, action_id: object) -> str | None:
79
+ """The name of the AutoCode action ``action_id``, or None if it is not one."""
80
+ for action in self.actions:
81
+ if action.id == action_id:
82
+ return action.name
83
+ return None
84
+
85
+ def default_id_for(self, backend_url: object) -> str | None:
86
+ """The action a new session starts with on ``backend_url``: "Autocode OpenAI
87
+ Flash" on the Work (Enterprise) server when it has one, else ``default_id``."""
88
+ if is_enterprise_backend(backend_url):
89
+ for action in self.actions:
90
+ if _normalize(action.name) == _ENTERPRISE_DEFAULT:
91
+ return action.id
92
+ return self.default_id
93
+
94
+
95
+ def is_enterprise_backend(backend_url: object) -> bool:
96
+ """True for the Work (Enterprise) server, api.meetkiwi.ai."""
97
+ url = str(backend_url or "").strip().rstrip("/").lower()
98
+ return url == SERVER_PRESETS["work"]["http"]
99
+
67
100
 
68
101
  def _created(action: AutocodeAction) -> datetime:
69
102
  """When the action was created. Falls back to the timestamp in its ObjectId."""
@@ -227,10 +227,11 @@ def _default_action_id(session: _TerminalSession) -> str:
227
227
  found = fetch_autocode_actions(session.client.client)
228
228
  if found.status_code in (401, 403) and _refresh_tokens(session):
229
229
  found = fetch_autocode_actions(session.client.client)
230
- if found.default_id is None:
230
+ default_id = found.default_id_for(session.backend_url)
231
+ if default_id is None:
231
232
  detail = "no AutoCode actions on this server" if found.ok else f"HTTP {found.status_code}"
232
233
  raise TerminalModeError(f"Could not find the AutoCode action ({detail}). Pass --action-id to choose one.")
233
- return found.default_id
234
+ return default_id
234
235
 
235
236
 
236
237
  def _build_authenticated_session(server: str | None) -> _TerminalSession:
@@ -113,7 +113,20 @@ def mode_skill_line(*, role: str, mode: str) -> str:
113
113
 
114
114
 
115
115
 
116
+ SYSTEM_PROMPT_HEADING = "# System Prompt"
117
+ MODE_SKILL_INTRO = "Plus use this exact mode skill(only)."
118
+
119
+ # Wording sent by older versions; still stripped when rendering stored history.
120
+ _LEGACY_MODE_SKILL_INTRO = "Plus extract this exact mode skill(only)."
121
+
122
+
116
123
  def _general_role_line(role: str) -> str:
124
+ return (
125
+ f"Your Current Role is : {role}. Use autocode.{role}.general skill and act accordingly."
126
+ )
127
+
128
+
129
+ def _legacy_general_role_line(role: str) -> str:
117
130
  return (
118
131
  f"Your Current Role is : {role}. Fetch autocode.{role}.general skill and act accordingly."
119
132
  )
@@ -122,14 +135,35 @@ def _general_role_line(role: str) -> str:
122
135
  _SKILL_LINE_RE = re.compile(r"^autocode\.([a-z\-]+)\.(plan|build|validate) skill$")
123
136
 
124
137
 
125
- def strip_injected_prefix(text: str) -> str:
126
- """Strip the silent injection prefix (if present) from a stored user prompt.
138
+ def _injected_lines_end(lines: list[str], start: int) -> int:
139
+ """Index just past the injected role/mode lines found at `start` (== start if none)."""
140
+ i = start
141
+ current = (lines[i] or "").strip() if i < len(lines) else ""
142
+ for r in CANONICAL_ROLES:
143
+ if current in (_general_role_line(r), _legacy_general_role_line(r)):
144
+ i += 1
145
+ break
146
+
147
+ if i < len(lines) and (lines[i] or "").strip() in (MODE_SKILL_INTRO, _LEGACY_MODE_SKILL_INTRO):
148
+ # Skip the "Plus..." line
149
+ i += 1
150
+ # Skip the skill line only if it matches the expected pattern.
151
+ if i < len(lines) and _SKILL_LINE_RE.match((lines[i] or "").strip()):
152
+ i += 1
153
+ return i
154
+
155
+
156
+ def strip_injected_prompt(text: str) -> str:
157
+ """Strip the silent role/mode injection (if present) from a stored user prompt.
127
158
 
128
159
  The backend persists the full user_input we send, which will include the
129
- injected prefix. When rendering conversation history, we must not show these
160
+ injected lines. When rendering conversation history, we must not show these
130
161
  injected lines to the user.
131
162
 
132
- We only strip when the prefix matches *exactly* our known injection format,
163
+ Current format: the user's text, then a "# System Prompt" block at the end.
164
+ Older formats put the injected lines (with or without the heading) first.
165
+
166
+ We only strip when the lines match *exactly* our known injection format,
133
167
  to avoid altering legitimate user content.
134
168
  """
135
169
  raw = (text or "")
@@ -140,19 +174,29 @@ def strip_injected_prefix(text: str) -> str:
140
174
  if not lines:
141
175
  return raw
142
176
 
177
+ # Current format: a trailing "# System Prompt" block that runs to the end.
178
+ for h in range(len(lines) - 1, -1, -1):
179
+ if (lines[h] or "").strip() != SYSTEM_PROMPT_HEADING:
180
+ continue
181
+ end = _injected_lines_end(lines, h + 1)
182
+ if end > h + 1 and all(not (rest or "").strip() for rest in lines[end:]):
183
+ body = lines[:h]
184
+ while body and body[-1] == "":
185
+ body.pop()
186
+ return "\n".join(body)
187
+ break
188
+
189
+ # Older formats: injected lines at the start.
143
190
  i = 0
144
- first = (lines[0] or "").strip()
145
- for r in CANONICAL_ROLES:
146
- if first == _general_role_line(r):
147
- i = 1
148
- break
149
-
150
- if i < len(lines) and (lines[i] or "").strip() == "Plus extract this exact mode skill(only).":
151
- # Skip the "Plus..." line
152
- i += 1
153
- # Skip the skill line only if it matches the expected pattern.
154
- if i < len(lines) and _SKILL_LINE_RE.match((lines[i] or "").strip()):
155
- i += 1
191
+ has_heading = (lines[0] or "").strip() == SYSTEM_PROMPT_HEADING
192
+ if has_heading:
193
+ i = 1
194
+ body_start = i
195
+ i = _injected_lines_end(lines, i)
196
+
197
+ # A heading with no injected lines after it is the user's own text; keep it.
198
+ if has_heading and i == body_start:
199
+ return raw
156
200
 
157
201
  # Skip blank separator lines
158
202
  while i < len(lines) and lines[i] == "":
@@ -223,7 +267,8 @@ def build_model_user_prompt(
223
267
  ) -> tuple[str, RoleModeState]:
224
268
  """Return (model_user_prompt, new_state).
225
269
 
226
- The returned prompt includes any required silent injection prefix.
270
+ The user's text comes first; any required silent injection follows it as a
271
+ "# System Prompt" block.
227
272
 
228
273
  Injection rules:
229
274
  - If first model prompt OR role changed: inject the role+general line.
@@ -236,16 +281,16 @@ def build_model_user_prompt(
236
281
  inject_general = is_first or state.role_changed
237
282
  inject_mode = is_first or state.role_changed or state.mode_changed
238
283
 
239
- prefix = ""
284
+ injected: list[str] = []
240
285
  if inject_general:
241
- prefix += (
242
- f"Your Current Role is : {state.role}. Fetch autocode.{state.role}.general skill and act accordingly.\n"
243
- )
286
+ injected.append(_general_role_line(state.role))
244
287
  if inject_mode:
245
- prefix += "Plus extract this exact mode skill(only).\n"
246
- prefix += mode_skill_line(role=state.role, mode=state.mode) + "\n"
288
+ injected.append(MODE_SKILL_INTRO)
289
+ injected.append(mode_skill_line(role=state.role, mode=state.mode))
247
290
 
248
- model_prompt = prefix + (raw_user_prompt or "")
291
+ model_prompt = raw_user_prompt or ""
292
+ if injected:
293
+ model_prompt += "\n\n" + "\n".join([SYSTEM_PROMPT_HEADING, *injected])
249
294
 
250
295
  new_state = replace(
251
296
  state,
@@ -28,13 +28,14 @@ from kiwi_tui.random_words import random_verb
28
28
  from kiwi_tui.role_mode import (
29
29
  RoleModeState,
30
30
  ROLE_DISPLAY,
31
+ SYSTEM_PROMPT_HEADING,
31
32
  allowed_modes_for_role,
32
33
  build_model_user_prompt,
33
34
  parse_mode_arg,
34
35
  parse_role_arg,
35
36
  set_mode,
36
37
  set_role,
37
- strip_injected_prefix,
38
+ strip_injected_prompt,
38
39
  )
39
40
  from kiwi_tui.screens.file_browser import FileBrowserScreen
40
41
  from kiwi_tui.screens.help import HelpScreen
@@ -1503,7 +1504,8 @@ class TermDashboardScreen(Screen):
1503
1504
  cwd = str(Path.cwd())
1504
1505
  cwd_short = cwd if len(cwd) <= 44 else f"{cwd[:20]}…{cwd[-20:]}"
1505
1506
 
1506
- action = self.current_action_name or self.current_action_id or ""
1507
+ # While its name loads, an action shows as "…", not as its id.
1508
+ action = self.current_action_name or ("…" if self.current_action_id else "")
1507
1509
  action = str(action)
1508
1510
  action_short = action if len(action) <= 16 else (action[:12] + "…")
1509
1511
 
@@ -1643,7 +1645,7 @@ class TermDashboardScreen(Screen):
1643
1645
  def _refresh_welcome_meta(self) -> None:
1644
1646
  """Update action name + run id displayed in the welcome header."""
1645
1647
  try:
1646
- action = self.current_action_name or self.current_action_id or "—"
1648
+ action = self.current_action_name or ("…" if self.current_action_id else "—")
1647
1649
  run = self.current_run_id or "—"
1648
1650
  self.query_one("#term-welcome-account", Static).update(self._account_text())
1649
1651
  self.query_one("#term-welcome-action", Static).update(f"Action: {action}")
@@ -1764,9 +1766,14 @@ class TermDashboardScreen(Screen):
1764
1766
  except Exception as e:
1765
1767
  logger.debug(f"Loading AutoCode actions failed: {e}")
1766
1768
  if not self.current_action_id and self._autocode is not None:
1767
- self.current_action_id = self._autocode.default_id or ""
1769
+ self.current_action_id = self._autocode.default_id_for(self._autocode_backend) or ""
1768
1770
  requested_id = self.current_action_id
1769
- if not requested_id:
1771
+ # An AutoCode action's name is already in the list just fetched: show it with the
1772
+ # id, rather than the id first and the name a request later.
1773
+ known = self._autocode.name_of(requested_id) if self._autocode is not None and requested_id else None
1774
+ if known and not self.current_action_name:
1775
+ self.current_action_name = known
1776
+ if not requested_id or known:
1770
1777
  self._refresh_top_bar()
1771
1778
  self._refresh_hint_model()
1772
1779
  self._refresh_input_label()
@@ -1779,7 +1786,8 @@ class TermDashboardScreen(Screen):
1779
1786
  if resp.status_code == 200 and resp.parsed:
1780
1787
  self.current_action_name = getattr(resp.parsed, "name", None) or requested_id
1781
1788
  else:
1782
- self.current_action_name = None
1789
+ # The name could not be found: the id is all there is to show.
1790
+ self.current_action_name = requested_id
1783
1791
  self._refresh_top_bar()
1784
1792
  self._refresh_hint_model()
1785
1793
  self._refresh_input_label()
@@ -3806,7 +3814,7 @@ class TermDashboardScreen(Screen):
3806
3814
  show(f"Could not read the queue ({lines[0]}).", is_error=True)
3807
3815
  return
3808
3816
  # AutoCode prompts are stored with their role/mode lines; show what was typed.
3809
- prompts = [strip_injected_prefix(prompt) for prompt in found[0]]
3817
+ prompts = [strip_injected_prompt(prompt) for prompt in found[0]]
3810
3818
  if run_id == self.current_run_id:
3811
3819
  self._waiting_locally()[:] = prompts
3812
3820
  self._set_queued_count(len(prompts))
@@ -3820,7 +3828,7 @@ class TermDashboardScreen(Screen):
3820
3828
  known = self._waiting_locally() if run_id == self.current_run_id else []
3821
3829
  expected = known[number - 1] if number <= len(known) else None
3822
3830
  is_expected = (
3823
- (lambda stored: strip_injected_prefix(stored).strip() == expected.strip())
3831
+ (lambda stored: strip_injected_prompt(stored).strip() == expected.strip())
3824
3832
  if expected is not None else None
3825
3833
  )
3826
3834
  outcome: list[QueueRemoval] = []
@@ -3850,7 +3858,7 @@ class TermDashboardScreen(Screen):
3850
3858
  if number <= len(known):
3851
3859
  del known[number - 1]
3852
3860
  self._set_queued_count(result.waiting)
3853
- text = " ".join(strip_injected_prefix(result.removed).split())
3861
+ text = " ".join(strip_injected_prompt(result.removed).split())
3854
3862
  toast(
3855
3863
  f"Removed: {text if len(text) <= 60 else text[:59] + '…'} ({result.waiting} waiting)",
3856
3864
  title="/queue",
@@ -4530,8 +4538,10 @@ class TermDashboardScreen(Screen):
4530
4538
  )
4531
4539
  user_text = str(extracted_in or "").strip()
4532
4540
  logger.info(f"user text : {user_text}")
4533
- if is_autocode_run or ("Your Current Role is" in user_text and "autocode." in user_text):
4534
- user_text = strip_injected_prefix(user_text)
4541
+ if is_autocode_run or SYSTEM_PROMPT_HEADING in user_text or (
4542
+ "Your Current Role is" in user_text and "autocode." in user_text
4543
+ ):
4544
+ user_text = strip_injected_prompt(user_text)
4535
4545
 
4536
4546
  logger.info(f"User Text after stripping : {user_text}")
4537
4547
  assistant_text = None
@@ -154,3 +154,52 @@ def test_the_default_autocode_is_also_the_latest_one(monkeypatch):
154
154
  )
155
155
  found = fetch_autocode_actions(client=None)
156
156
  assert [a.id for a in found.actions] == ["new"] and found.default_id == "new"
157
+
158
+
159
+ # The default by account: Work (Enterprise, api.meetkiwi.ai) starts with Autocode OpenAI
160
+ # Flash, Personal (Subscription, api.runkai.ai) and every other server with Autocode. The
161
+ # VS Code extension makes the same choice (src/defaultAgent.ts).
162
+
163
+ WORK = "https://api.meetkiwi.ai"
164
+ PERSONAL = "https://api.runkai.ai"
165
+
166
+
167
+ def _flash_and_autocode(monkeypatch):
168
+ _respond(
169
+ monkeypatch,
170
+ 200,
171
+ [
172
+ _doc("a", "Autocode"),
173
+ _doc("f1", "Autocode OpenAI Flash", created_at=datetime(2026, 5, 1)),
174
+ _doc("f2", "AutocodeOpenaiFlash", created_at=datetime(2026, 10, 24)),
175
+ _doc("c", "AutoCodeClaude", "claude"),
176
+ ],
177
+ )
178
+ return fetch_autocode_actions(client=None)
179
+
180
+
181
+ @pytest.mark.parametrize("url", [WORK, WORK + "/", "https://API.meetkiwi.ai"])
182
+ def test_work_starts_with_the_latest_flash(monkeypatch, url):
183
+ assert _flash_and_autocode(monkeypatch).default_id_for(url) == "f2"
184
+
185
+
186
+ @pytest.mark.parametrize("url", [PERSONAL, "https://dev.api.myautobots.com", "http://localhost:8000", "", None])
187
+ def test_personal_and_other_servers_start_with_autocode(monkeypatch, url):
188
+ assert _flash_and_autocode(monkeypatch).default_id_for(url) == "a"
189
+
190
+
191
+ def test_work_without_flash_starts_with_autocode(monkeypatch):
192
+ _respond(monkeypatch, 200, [_doc("a", "Autocode"), _doc("c", "AutoCodeClaude", "claude")])
193
+ assert fetch_autocode_actions(client=None).default_id_for(WORK) == "a"
194
+
195
+
196
+ def test_no_actions_means_no_default_on_work_either():
197
+ assert AutocodeActions(status_code=200).default_id_for(WORK) is None
198
+
199
+
200
+ def test_name_of_gives_an_autocode_actions_name(monkeypatch):
201
+ found = _flash_and_autocode(monkeypatch)
202
+ assert found.name_of("f2") == "AutocodeOpenaiFlash"
203
+ assert found.name_of("a") == "Autocode"
204
+ assert found.name_of("unknown") is None
205
+ assert found.name_of(None) is None
@@ -14,7 +14,7 @@ from kiwi_tui.role_mode import (
14
14
  parse_role_arg,
15
15
  set_mode,
16
16
  set_role,
17
- strip_injected_prefix,
17
+ strip_injected_prompt,
18
18
  )
19
19
 
20
20
 
@@ -58,14 +58,14 @@ def test_first_prompt_injects_general_and_mode_lines() -> None:
58
58
  state = RoleModeState() # default
59
59
  model_prompt, new_state = build_model_user_prompt(state, raw_user_prompt="Hello")
60
60
 
61
- assert "Your Current Role is :" in model_prompt
62
- assert (
63
- f"Your Current Role is : {DEFAULT_ROLE}. Fetch autocode.{DEFAULT_ROLE}.general skill"
64
- in model_prompt
61
+ assert model_prompt == (
62
+ "Hello\n"
63
+ "\n"
64
+ "# System Prompt\n"
65
+ f"Your Current Role is : {DEFAULT_ROLE}. Use autocode.{DEFAULT_ROLE}.general skill and act accordingly.\n"
66
+ "Plus use this exact mode skill(only).\n"
67
+ f"autocode.{DEFAULT_ROLE}.{DEFAULT_MODE} skill"
65
68
  )
66
- assert "Plus extract this exact mode skill(only)." in model_prompt
67
- assert f"autocode.{DEFAULT_ROLE}.{DEFAULT_MODE} skill" in model_prompt
68
- assert model_prompt.endswith("Hello")
69
69
 
70
70
  assert new_state.has_sent_model_prompt is True
71
71
  assert new_state.role_changed is False
@@ -82,10 +82,10 @@ def test_role_change_forces_mode_build_and_triggers_injection_on_next_prompt() -
82
82
 
83
83
  model_prompt, state3 = build_model_user_prompt(state2, raw_user_prompt="Next")
84
84
 
85
+ assert model_prompt.startswith("Next\n\n# System Prompt\n")
85
86
  assert f"Your Current Role is : {ROLE_ZERO_TO_ONE_ENGINEER}." in model_prompt
86
- assert "Plus extract this exact mode skill(only)." in model_prompt
87
- assert "autocode.zero-to-one-engineer.build skill" in model_prompt
88
- assert model_prompt.endswith("Next")
87
+ assert "Plus use this exact mode skill(only)." in model_prompt
88
+ assert model_prompt.endswith("autocode.zero-to-one-engineer.build skill")
89
89
 
90
90
  assert state3.role_changed is False
91
91
  assert state3.mode_changed is False
@@ -98,9 +98,13 @@ def test_mode_change_triggers_only_mode_injection_not_general() -> None:
98
98
 
99
99
  model_prompt, _ = build_model_user_prompt(state2, raw_user_prompt="Ping")
100
100
 
101
- assert "Your Current Role is :" not in model_prompt
102
- assert "Plus extract this exact mode skill(only)." in model_prompt
103
- assert "autocode.senior-software-engineer.validate skill" in model_prompt
101
+ assert model_prompt == (
102
+ "Ping\n"
103
+ "\n"
104
+ "# System Prompt\n"
105
+ "Plus use this exact mode skill(only).\n"
106
+ "autocode.senior-software-engineer.validate skill"
107
+ )
104
108
 
105
109
 
106
110
  def test_x2se_disallows_plan_without_error() -> None:
@@ -112,20 +116,74 @@ def test_x2se_disallows_plan_without_error() -> None:
112
116
  assert state2.mode == MODE_BUILD
113
117
 
114
118
 
115
- def test_strip_injected_prefix_strips_general_and_mode_block() -> None:
119
+ def test_no_change_sends_prompt_without_system_prompt() -> None:
120
+ state = RoleModeState(has_sent_model_prompt=True)
121
+ model_prompt, _ = build_model_user_prompt(state, raw_user_prompt="Plain")
122
+ assert model_prompt == "Plain"
123
+
124
+
125
+ def test_strip_injected_prompt_strips_trailing_general_and_mode_block() -> None:
126
+ injected = (
127
+ "Hello world\n"
128
+ "\n"
129
+ "# System Prompt\n"
130
+ f"Your Current Role is : {DEFAULT_ROLE}. Use autocode.{DEFAULT_ROLE}.general skill and act accordingly.\n"
131
+ "Plus use this exact mode skill(only).\n"
132
+ f"autocode.{DEFAULT_ROLE}.{DEFAULT_MODE} skill"
133
+ )
134
+ assert strip_injected_prompt(injected) == "Hello world"
135
+
136
+
137
+ def test_strip_injected_prompt_strips_trailing_mode_only_block() -> None:
138
+ injected = (
139
+ "Ping\n"
140
+ "\n"
141
+ "# System Prompt\n"
142
+ "Plus use this exact mode skill(only).\n"
143
+ "autocode.senior-software-engineer.validate skill"
144
+ )
145
+ assert strip_injected_prompt(injected) == "Ping"
146
+
147
+
148
+ def test_strip_injected_prompt_round_trips_built_prompt() -> None:
149
+ raw = "first line\n\nsecond paragraph"
150
+ model_prompt, _ = build_model_user_prompt(RoleModeState(), raw_user_prompt=raw)
151
+ assert strip_injected_prompt(model_prompt) == raw
152
+
153
+
154
+ def test_strip_injected_prompt_keeps_users_own_heading() -> None:
155
+ text = "# System Prompt\nthis is my own markdown"
156
+ assert strip_injected_prompt(text) == text
157
+ trailing = "notes\n\n# System Prompt\nmy own section"
158
+ assert strip_injected_prompt(trailing) == trailing
159
+
160
+
161
+ def test_strip_injected_prompt_strips_leading_heading_block() -> None:
162
+ # Format briefly sent before the block moved below the user's text.
163
+ injected = (
164
+ "# System Prompt\n"
165
+ f"Your Current Role is : {DEFAULT_ROLE}. Use autocode.{DEFAULT_ROLE}.general skill and act accordingly.\n"
166
+ "Plus use this exact mode skill(only).\n"
167
+ f"autocode.{DEFAULT_ROLE}.{DEFAULT_MODE} skill\n"
168
+ "hello"
169
+ )
170
+ assert strip_injected_prompt(injected) == "hello"
171
+
172
+
173
+ def test_strip_injected_prompt_strips_legacy_general_and_mode_block() -> None:
116
174
  injected = (
117
175
  f"Your Current Role is : {DEFAULT_ROLE}. Fetch autocode.{DEFAULT_ROLE}.general skill and act accordingly.\n"
118
176
  "Plus extract this exact mode skill(only).\n"
119
177
  f"autocode.{DEFAULT_ROLE}.{DEFAULT_MODE} skill\n"
120
178
  "Hello world"
121
179
  )
122
- assert strip_injected_prefix(injected) == "Hello world"
180
+ assert strip_injected_prompt(injected) == "Hello world"
123
181
 
124
182
 
125
- def test_strip_injected_prefix_strips_mode_only_block() -> None:
183
+ def test_strip_injected_prompt_strips_legacy_mode_only_block() -> None:
126
184
  injected = (
127
185
  "Plus extract this exact mode skill(only).\n"
128
186
  "autocode.senior-software-engineer.validate skill\n"
129
187
  "Ping"
130
188
  )
131
- assert strip_injected_prefix(injected) == "Ping"
189
+ assert strip_injected_prompt(injected) == "Ping"
@@ -979,3 +979,56 @@ async def test_scrolling_up_while_a_reply_streams_stays_up_until_back_at_the_end
979
979
  await pilot.pause(0.3)
980
980
  await stream(5)
981
981
  assert gap() <= 1
982
+
983
+
984
+ @pytest.mark.asyncio
985
+ async def test_an_action_shows_by_name_never_by_its_id_while_the_name_loads() -> None:
986
+ app = _TermDashboardTestApp()
987
+ async with app.run_test(size=(120, 60)) as pilot:
988
+ await pilot.pause(0.05)
989
+ screen = app.screen
990
+ screen.current_action_id = "6a0625b8ab5f80ba8ac5d012"
991
+ screen.current_action_name = None
992
+ screen._refresh_welcome_meta()
993
+ screen._refresh_top_bar()
994
+ await pilot.pause(0)
995
+
996
+ assert str(screen.query_one("#term-welcome-action").content) == "Action: …"
997
+ assert "6a0625b8" not in _top_bar_text(screen)
998
+
999
+
1000
+ @pytest.mark.asyncio
1001
+ async def test_the_default_autocode_is_named_from_its_list_without_a_second_request(monkeypatch) -> None:
1002
+ from kiwi_cli.autocode import AutocodeAction, AutocodeActions
1003
+ from pykiwiai.api.actions import get_action_v1_actions_id_get
1004
+
1005
+ app = _TermDashboardTestApp()
1006
+ async with app.run_test(size=(120, 60)) as pilot:
1007
+ await pilot.pause(0.05)
1008
+ screen = app.screen
1009
+ app.autobots_client = SimpleNamespace(client=object(), base_url="https://api.meetkiwi.ai")
1010
+ app._sync_autobots_client_from_disk = lambda: None
1011
+ listed = AutocodeActions(
1012
+ status_code=200,
1013
+ actions=(
1014
+ AutocodeAction(id="a", name="Autocode", type="openai"),
1015
+ AutocodeAction(id="f", name="Autocode OpenAI Flash", type="openai"),
1016
+ ),
1017
+ )
1018
+
1019
+ async def load(refresh: bool = False):
1020
+ screen._autocode, screen._autocode_backend = listed, "https://api.meetkiwi.ai"
1021
+ return listed
1022
+
1023
+ monkeypatch.setattr(screen, "_load_autocode_actions", load)
1024
+
1025
+ def no_request(**_kwargs):
1026
+ raise AssertionError("the name was already known")
1027
+
1028
+ monkeypatch.setattr(get_action_v1_actions_id_get, "sync_detailed", no_request)
1029
+ screen.current_action_id = ""
1030
+ screen.current_action_name = None
1031
+
1032
+ await screen._fetch_action_name_async()
1033
+ assert screen.current_action_id == "f"
1034
+ assert screen.current_action_name == "Autocode OpenAI Flash"
@@ -412,7 +412,7 @@ wheels = [
412
412
 
413
413
  [[package]]
414
414
  name = "kiwi-code"
415
- version = "0.0.512"
415
+ version = "0.0.514"
416
416
  source = { editable = "." }
417
417
  dependencies = [
418
418
  { name = "codeglass" },
File without changes
File without changes
File without changes
File without changes
File without changes