kiwi-code 0.0.506__tar.gz → 0.0.508__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/PKG-INFO +1 -1
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/pyproject.toml +1 -1
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/edition.py +8 -4
- kiwi_code-0.0.508/src/kiwi_tui/autoupdate.py +216 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/main.py +12 -1
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/term_dashboard.py +66 -13
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/term_dashboard_helpers.py +1 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/slash_commands.py +5 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_attention.py +33 -0
- kiwi_code-0.0.508/tests/test_autoupdate.py +235 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/uv.lock +1 -1
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/.gitignore +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/.python-version +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/Makefile +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/README.md +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/docs/runtime_audit.html +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/docs/social_login_e2e.md +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/scripts/set_edition.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/scripts/ws_auth_check.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/common/__init__.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/common/banner.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/_edition.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/autocode.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/checkpoints.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/human_questions.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/io_threads.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/oauth.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/run_activity.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/subagents.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_cli/terminal_mode.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/auth.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/checkpoints.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/cli.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/commands.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/constants.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/fs/__init__.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/fs/batch.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/fs/codelens.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/fs/context.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/fs/edits.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/fs/handler.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/fs/paths.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/fs/read.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/fs/search.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/fs/text.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/fs/upload.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/fs/write.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/handlers.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/link.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/main.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/restricted.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/sessions.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/state.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/tui_bridge.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/ui.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_runtime/uploads.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/attention.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/latex_unicode.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/role_mode.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/detach_files.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/term_activity_view.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/term_dashboard_css.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/term_dashboard_widgets.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/term_question_widgets.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/term_status_line.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/screens/term_subagent_widgets.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/term_app.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/terminal_theme.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/ui_settings.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/src/kiwi_tui/worktrees.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/test_hello.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/__init__.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/conftest.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_account_server.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_autocode_actions.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_batch_fs_tool.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_checkpoints.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_command_tips.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_copy_path_fs_tool.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_fs_edit_fidelity.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_human_questions.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_io_threads.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_login_social.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_math_and_tables.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_oauth.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_read_file_streaming.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_reply_selection.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_role_mode_injection.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_run_activity.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_run_steering.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_runtime_cleanup_per_account.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_runtime_connection.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_runtime_edit_diffs.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_runtime_fixes.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_runtime_imports_on_windows.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_runtime_social_login.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_search_in_files_fs_tool.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_slash_commands.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_stats_command.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_status_line.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_subagents.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_term_dashboard_ui.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_terminal_mode.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_terminal_theme.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_toasts.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_tui_interactive_runtime.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_tui_palette.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_unified_diff.py +0 -0
- {kiwi_code-0.0.506 → kiwi_code-0.0.508}/tests/test_worktrees.py +0 -0
|
@@ -27,6 +27,10 @@ EDITIONS: dict[str, dict[str, str]] = {
|
|
|
27
27
|
},
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
#: The package this build was published as: kiwi-ai or kiwi-code (see the module's
|
|
31
|
+
#: docstring). Unlike EDITION, KIWI_EDITION does not change it: it names what is
|
|
32
|
+
#: installed, which is what an update must ask PyPI for and install.
|
|
33
|
+
BUILD_EDITION = _BUILD_EDITION
|
|
30
34
|
EDITION = os.environ.get("KIWI_EDITION") or _BUILD_EDITION
|
|
31
35
|
if EDITION not in EDITIONS:
|
|
32
36
|
raise ValueError(f"Unknown KIWI_EDITION {EDITION!r}; expected one of {sorted(EDITIONS)}")
|
|
@@ -38,10 +42,10 @@ DEFAULT_ACCOUNT = EDITIONS[EDITION]["default_account"]
|
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
def _installed_version() -> str:
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
for dist in [*metadata.packages_distributions().get("kiwi_cli", []), *EDITIONS]:
|
|
45
|
+
# The package this build was published as comes first: when kiwi-ai and kiwi-code
|
|
46
|
+
# are both installed in one environment, both ship kiwi_cli. Then whichever
|
|
47
|
+
# distribution ships kiwi_cli, and each edition's name (a local checkout).
|
|
48
|
+
for dist in [BUILD_EDITION, *metadata.packages_distributions().get("kiwi_cli", []), *EDITIONS]:
|
|
45
49
|
try:
|
|
46
50
|
return metadata.version(dist)
|
|
47
51
|
except metadata.PackageNotFoundError:
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Keeping kiwi up to date on its own.
|
|
2
|
+
|
|
3
|
+
While kiwi runs, the dashboard asks PyPI for the latest release (see
|
|
4
|
+
`TermDashboardScreen._check_latest_version`) and remembers it here, in
|
|
5
|
+
`~/.kiwi/update.json`. At the next start, before the TUI opens, a newer release is
|
|
6
|
+
installed with the tool kiwi was installed with, and the new version is started instead.
|
|
7
|
+
Installing at start rather than under a running session matters: kiwi imports parts of
|
|
8
|
+
itself as they are needed, and those must not come from another version.
|
|
9
|
+
|
|
10
|
+
Never updated on its own: an editable or local checkout, kiwi outside a virtual
|
|
11
|
+
environment (a system Python is the user's to manage), Windows (the running `kiwi.exe`
|
|
12
|
+
cannot be replaced), a start that is not interactive, or when turned off with
|
|
13
|
+
`/autoupdate off` or `KIWI_NO_AUTO_UPDATE=1`. Those get the command to run instead.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from importlib import metadata
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
#: How long an update may take before kiwi gives up and starts the version it has.
|
|
27
|
+
INSTALL_TIMEOUT = 180
|
|
28
|
+
#: A release that would not install is tried again after this long.
|
|
29
|
+
RETRY_AFTER = 24 * 3600
|
|
30
|
+
#: Set in the new version's environment, so it can say what it was updated from (and so a
|
|
31
|
+
#: failed update cannot loop).
|
|
32
|
+
UPDATED_FROM_ENV = "KIWI_UPDATED_FROM"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class Installer:
|
|
37
|
+
name: str # uv / pipx / pip
|
|
38
|
+
command: tuple[str, ...] # what updates kiwi
|
|
39
|
+
manual: str # the same, for the user to type
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def package_name() -> str:
|
|
43
|
+
"""The PyPI package kiwi was installed as: kiwi-ai or kiwi-code. Both are built from
|
|
44
|
+
this one codebase; the build bakes in which one it is (scripts/set_edition.py), and
|
|
45
|
+
KIWI_EDITION, which only points kiwi at the other backend, does not change it."""
|
|
46
|
+
from kiwi_cli.edition import BUILD_EDITION
|
|
47
|
+
|
|
48
|
+
return BUILD_EDITION
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _state_path() -> Path:
|
|
52
|
+
from kiwi_cli.checkpoints import kiwi_home_dir
|
|
53
|
+
|
|
54
|
+
return kiwi_home_dir() / "update.json"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _read_all() -> dict:
|
|
58
|
+
try:
|
|
59
|
+
data = json.loads(_state_path().read_text(encoding="utf-8"))
|
|
60
|
+
return data if isinstance(data, dict) else {}
|
|
61
|
+
except (OSError, ValueError):
|
|
62
|
+
return {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _read_state() -> dict:
|
|
66
|
+
"""This package's part: kiwi-ai and kiwi-code share ~/.kiwi, each with its own
|
|
67
|
+
releases and versions."""
|
|
68
|
+
state = _read_all().get(package_name())
|
|
69
|
+
return state if isinstance(state, dict) else {}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _write_state(**patch) -> None:
|
|
73
|
+
try:
|
|
74
|
+
path = _state_path()
|
|
75
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
everything = _read_all()
|
|
77
|
+
everything[package_name()] = {**_read_state(), **patch}
|
|
78
|
+
tmp = path.with_suffix(".tmp")
|
|
79
|
+
tmp.write_text(json.dumps(everything), encoding="utf-8")
|
|
80
|
+
os.replace(tmp, path)
|
|
81
|
+
except OSError:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def record_latest(version: str) -> None:
|
|
86
|
+
"""The latest release PyPI has, for the next start to install."""
|
|
87
|
+
_write_state(latest=str(version), checked_at=time.time())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def newer(latest: str, current: str) -> bool:
|
|
91
|
+
try:
|
|
92
|
+
from packaging.version import Version
|
|
93
|
+
|
|
94
|
+
return Version(latest) > Version(current)
|
|
95
|
+
except Exception:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def pending(current: str, *, now: float | None = None) -> str | None:
|
|
100
|
+
"""A newer release found earlier and not given up on: the version to install."""
|
|
101
|
+
state = _read_state()
|
|
102
|
+
latest = str(state.get("latest") or "")
|
|
103
|
+
if not latest or not newer(latest, current):
|
|
104
|
+
return None
|
|
105
|
+
now = time.time() if now is None else now
|
|
106
|
+
if state.get("failed") == latest and now - float(state.get("failed_at") or 0) < RETRY_AFTER:
|
|
107
|
+
return None
|
|
108
|
+
return latest
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def enabled(env=os.environ) -> bool:
|
|
112
|
+
if env.get("KIWI_NO_AUTO_UPDATE", "").strip().lower() in ("1", "true", "yes"):
|
|
113
|
+
return False
|
|
114
|
+
try:
|
|
115
|
+
from kiwi_tui.ui_settings import load_settings
|
|
116
|
+
|
|
117
|
+
return load_settings().get("auto_update", True) is not False
|
|
118
|
+
except Exception:
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _editable(package: str) -> bool:
|
|
123
|
+
"""A checkout installed in place (`pip install -e`, `uv run`): never updated from PyPI."""
|
|
124
|
+
try:
|
|
125
|
+
raw = metadata.distribution(package).read_text("direct_url.json")
|
|
126
|
+
except Exception:
|
|
127
|
+
return False
|
|
128
|
+
if not raw:
|
|
129
|
+
return False
|
|
130
|
+
try:
|
|
131
|
+
info = json.loads(raw)
|
|
132
|
+
except ValueError:
|
|
133
|
+
return False
|
|
134
|
+
return bool(info.get("dir_info", {}).get("editable")) or str(info.get("url", "")).startswith("file:")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def installer(package: str, *, prefix: str | None = None, base_prefix: str | None = None,
|
|
138
|
+
platform: str | None = None) -> Installer | None:
|
|
139
|
+
"""How kiwi was installed, and so how to update it; None when kiwi must not update
|
|
140
|
+
itself (see the module's docstring)."""
|
|
141
|
+
prefix = sys.prefix if prefix is None else prefix
|
|
142
|
+
base_prefix = sys.base_prefix if base_prefix is None else base_prefix
|
|
143
|
+
platform = platform or sys.platform
|
|
144
|
+
if platform == "win32" or prefix == base_prefix or _editable(package):
|
|
145
|
+
return None
|
|
146
|
+
# Each tool leaves a file of its own in the environments it makes; the path is the
|
|
147
|
+
# fallback (a tool folder can be moved with UV_TOOL_DIR / PIPX_HOME).
|
|
148
|
+
env, parts = Path(prefix), Path(prefix).parts
|
|
149
|
+
if (env / "uv-receipt.toml").exists() or ("uv" in parts and "tools" in parts):
|
|
150
|
+
return Installer("uv", ("uv", "tool", "upgrade", package), f"uv tool upgrade {package}")
|
|
151
|
+
if (env / "pipx_metadata.json").exists() or ("pipx" in parts and "venvs" in parts):
|
|
152
|
+
return Installer("pipx", ("pipx", "upgrade", package), f"pipx upgrade {package}")
|
|
153
|
+
return Installer(
|
|
154
|
+
"pip", (sys.executable, "-m", "pip", "install", "--upgrade", "--quiet", package),
|
|
155
|
+
f"pip install --upgrade {package}",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def manual_command(package: str) -> str:
|
|
160
|
+
"""What the user should type to update, for the notice when kiwi will not do it."""
|
|
161
|
+
found = None
|
|
162
|
+
try:
|
|
163
|
+
found = installer(package, platform="linux") # the right tool even on Windows
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
return found.manual if found else f"pip install --upgrade {package}"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _version_on_disk(package: str) -> str | None:
|
|
170
|
+
"""The installed version as a new process sees it (this one has the old one cached)."""
|
|
171
|
+
try:
|
|
172
|
+
out = subprocess.run(
|
|
173
|
+
[sys.executable, "-c", f"import importlib.metadata as m; print(m.version({package!r}))"],
|
|
174
|
+
capture_output=True, text=True, timeout=30,
|
|
175
|
+
)
|
|
176
|
+
return out.stdout.strip() or None
|
|
177
|
+
except (OSError, subprocess.SubprocessError):
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def update_before_start(current: str, *, interactive: bool) -> bool:
|
|
182
|
+
"""At start, before the TUI: install a newer release found earlier. Whether it was
|
|
183
|
+
installed (the caller then starts the new version)."""
|
|
184
|
+
if not interactive or os.environ.get(UPDATED_FROM_ENV) or not enabled():
|
|
185
|
+
return False
|
|
186
|
+
latest = pending(current)
|
|
187
|
+
if latest is None:
|
|
188
|
+
return False
|
|
189
|
+
package = package_name()
|
|
190
|
+
tool = installer(package)
|
|
191
|
+
if tool is None:
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
print(f"Updating {package} v{current} → v{latest}…", flush=True)
|
|
195
|
+
try:
|
|
196
|
+
done = subprocess.run(
|
|
197
|
+
list(tool.command), stdin=subprocess.DEVNULL, capture_output=True, text=True,
|
|
198
|
+
timeout=INSTALL_TIMEOUT,
|
|
199
|
+
)
|
|
200
|
+
ok = done.returncode == 0
|
|
201
|
+
why = (done.stderr or done.stdout or "").strip().splitlines()[-1:] or [""]
|
|
202
|
+
except (OSError, subprocess.SubprocessError) as e:
|
|
203
|
+
ok, why = False, [str(e)]
|
|
204
|
+
if ok and not newer(_version_on_disk(package) or current, current):
|
|
205
|
+
ok, why = False, ["the new release did not install"]
|
|
206
|
+
if not ok:
|
|
207
|
+
_write_state(failed=latest, failed_at=time.time())
|
|
208
|
+
print(f"Could not update ({why[0]}). Starting v{current}; update with: {tool.manual}", flush=True)
|
|
209
|
+
return False
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def restart_updated(current: str) -> None:
|
|
214
|
+
"""Start the version just installed in place of this process."""
|
|
215
|
+
env = {**os.environ, UPDATED_FROM_ENV: current}
|
|
216
|
+
os.execve(sys.executable, [sys.executable, "-m", "kiwi_tui.main", *sys.argv[1:]], env)
|
|
@@ -1206,7 +1206,10 @@ def main() -> int:
|
|
|
1206
1206
|
argv = sys.argv[1:]
|
|
1207
1207
|
|
|
1208
1208
|
if "--version" in argv:
|
|
1209
|
-
|
|
1209
|
+
# The package as well: kiwi-ai and kiwi-code are both `kiwi`.
|
|
1210
|
+
from kiwi_cli.edition import BUILD_EDITION
|
|
1211
|
+
|
|
1212
|
+
print(f"{BUILD_EDITION} {KIWI_CODE_VERSION}")
|
|
1210
1213
|
return 0
|
|
1211
1214
|
|
|
1212
1215
|
if argv and argv[0] in {"alt", "term"}:
|
|
@@ -1368,6 +1371,14 @@ def main() -> int:
|
|
|
1368
1371
|
)
|
|
1369
1372
|
)
|
|
1370
1373
|
|
|
1374
|
+
# A newer release found by an earlier session is installed now, before the TUI opens,
|
|
1375
|
+
# and started in place of this one.
|
|
1376
|
+
from kiwi_tui import autoupdate
|
|
1377
|
+
|
|
1378
|
+
interactive = sys.stdin.isatty() and sys.stdout.isatty()
|
|
1379
|
+
if autoupdate.update_before_start(KIWI_CODE_VERSION, interactive=interactive):
|
|
1380
|
+
autoupdate.restart_updated(KIWI_CODE_VERSION)
|
|
1381
|
+
|
|
1371
1382
|
_run_term_tui(runtime_args=runtime_args)
|
|
1372
1383
|
return 0
|
|
1373
1384
|
|
|
@@ -40,7 +40,6 @@ from kiwi_tui.screens.file_browser import FileBrowserScreen
|
|
|
40
40
|
from kiwi_tui.screens.help import HelpScreen
|
|
41
41
|
from kiwi_tui.screens.command_result import CommandResultScreen
|
|
42
42
|
from kiwi_cli.autocode import AutocodeActions, fetch_autocode_actions, is_autocode_name
|
|
43
|
-
from kiwi_cli.edition import EDITION
|
|
44
43
|
from kiwi_tui import __version__ as KIWI_CODE_VERSION
|
|
45
44
|
|
|
46
45
|
from common.banner import LOGO, TEXT
|
|
@@ -570,22 +569,36 @@ class TermDashboardScreen(Screen):
|
|
|
570
569
|
|
|
571
570
|
# ── Dynamic tip ──────────────────────────────────────────
|
|
572
571
|
async def _check_latest_version(self) -> None:
|
|
573
|
-
"""Background check: compare installed version against PyPI latest.
|
|
572
|
+
"""Background check: compare installed version against PyPI latest. A newer one is
|
|
573
|
+
remembered, for the next start to install (kiwi_tui.autoupdate)."""
|
|
574
|
+
import os
|
|
575
|
+
|
|
574
576
|
import httpx
|
|
575
|
-
|
|
577
|
+
|
|
578
|
+
from kiwi_tui import autoupdate
|
|
579
|
+
|
|
580
|
+
updated_from = os.environ.get(autoupdate.UPDATED_FROM_ENV)
|
|
581
|
+
if updated_from:
|
|
582
|
+
self._upgrade_tip = f"Updated: v{updated_from} → v{KIWI_CODE_VERSION}."
|
|
583
|
+
self._refresh_tip()
|
|
584
|
+
# The package installed (kiwi-ai or kiwi-code), not the backend kiwi was pointed
|
|
585
|
+
# at: they are released separately, each with its own versions.
|
|
586
|
+
package = autoupdate.package_name()
|
|
576
587
|
try:
|
|
577
588
|
async with httpx.AsyncClient(timeout=5) as client:
|
|
578
|
-
resp = await client.get(f"https://pypi.org/pypi/{
|
|
589
|
+
resp = await client.get(f"https://pypi.org/pypi/{package}/json")
|
|
579
590
|
if resp.status_code != 200:
|
|
580
591
|
return
|
|
581
592
|
latest = resp.json()["info"]["version"]
|
|
582
|
-
if
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
593
|
+
if not autoupdate.newer(latest, KIWI_CODE_VERSION):
|
|
594
|
+
return
|
|
595
|
+
autoupdate.record_latest(latest)
|
|
596
|
+
if autoupdate.enabled() and autoupdate.installer(package) is not None:
|
|
597
|
+
how = "kiwi updates itself at the next start."
|
|
598
|
+
else:
|
|
599
|
+
how = f"Run: {autoupdate.manual_command(package)}"
|
|
600
|
+
self._upgrade_tip = f"A new version is available! v{KIWI_CODE_VERSION} → v{latest}. {how}"
|
|
601
|
+
self._refresh_tip()
|
|
589
602
|
except Exception:
|
|
590
603
|
pass
|
|
591
604
|
|
|
@@ -2750,8 +2763,9 @@ class TermDashboardScreen(Screen):
|
|
|
2750
2763
|
_do()
|
|
2751
2764
|
|
|
2752
2765
|
def _call_for_attention(self, kind: str, *, answer: object = "") -> None:
|
|
2753
|
-
"""The run waits for the user's answer, or
|
|
2754
|
-
away from kiwi (notification, bell, tab title; see kiwi_tui.attention).
|
|
2766
|
+
"""The run waits for the user's answer, is done, or has failed: call them back if
|
|
2767
|
+
they are away from kiwi (notification, bell, tab title; see kiwi_tui.attention).
|
|
2768
|
+
`answer` is the reply, or the error of a failed run."""
|
|
2755
2769
|
if kind == "question":
|
|
2756
2770
|
from kiwi_cli.human_questions import PENDING
|
|
2757
2771
|
|
|
@@ -2762,6 +2776,10 @@ class TermDashboardScreen(Screen):
|
|
|
2762
2776
|
if len(pending) > 1:
|
|
2763
2777
|
body = f"{body} (+{len(pending) - 1} more)"
|
|
2764
2778
|
title, tab = "Kiwi needs your answer", "● kiwi – waiting for you"
|
|
2779
|
+
elif kind == "failed":
|
|
2780
|
+
lines = str(answer or "").strip().splitlines()
|
|
2781
|
+
body = lines[0].strip() if lines else "The run failed."
|
|
2782
|
+
title, tab = "Kiwi run failed", "✗ kiwi – failed"
|
|
2765
2783
|
else:
|
|
2766
2784
|
lines = str(answer or "").strip().splitlines()
|
|
2767
2785
|
body = lines[0].lstrip("#*> ").strip() if lines else "The run has finished."
|
|
@@ -3331,6 +3349,40 @@ class TermDashboardScreen(Screen):
|
|
|
3331
3349
|
self._show_inline_cmd_result(command, token_usage_renderable(usage, cyan=cyan))
|
|
3332
3350
|
return
|
|
3333
3351
|
|
|
3352
|
+
if cmd == "/autoupdate":
|
|
3353
|
+
import os
|
|
3354
|
+
|
|
3355
|
+
from kiwi_tui import autoupdate
|
|
3356
|
+
from kiwi_tui.ui_settings import update_settings
|
|
3357
|
+
|
|
3358
|
+
choice = args[0].lower() if args else ""
|
|
3359
|
+
if choice in ("on", "off"):
|
|
3360
|
+
update_settings({"auto_update": choice == "on"})
|
|
3361
|
+
toast("Auto-update on: a new release is installed when kiwi starts."
|
|
3362
|
+
if choice == "on" else "Auto-update off.", title="/autoupdate")
|
|
3363
|
+
return
|
|
3364
|
+
package = autoupdate.package_name()
|
|
3365
|
+
tool = autoupdate.installer(package)
|
|
3366
|
+
if os.environ.get("KIWI_NO_AUTO_UPDATE"):
|
|
3367
|
+
state = "off (KIWI_NO_AUTO_UPDATE is set)"
|
|
3368
|
+
elif not autoupdate.enabled():
|
|
3369
|
+
state = "off"
|
|
3370
|
+
elif tool is None:
|
|
3371
|
+
state = "not possible for this install (a local checkout, a system Python or Windows)"
|
|
3372
|
+
else:
|
|
3373
|
+
state = f"on (with {tool.name})"
|
|
3374
|
+
show("\n".join([
|
|
3375
|
+
f"Auto-update: {state}",
|
|
3376
|
+
f"Installed: {package} v{KIWI_CODE_VERSION}",
|
|
3377
|
+
"",
|
|
3378
|
+
"A newer release found while kiwi runs is installed at the next start.",
|
|
3379
|
+
"",
|
|
3380
|
+
" /autoupdate on update at start (default)",
|
|
3381
|
+
" /autoupdate off never; kiwi only says a new version is out",
|
|
3382
|
+
f" Update by hand: {autoupdate.manual_command(package)}",
|
|
3383
|
+
]))
|
|
3384
|
+
return
|
|
3385
|
+
|
|
3334
3386
|
if cmd == "/notify":
|
|
3335
3387
|
from kiwi_tui import attention
|
|
3336
3388
|
from kiwi_tui.ui_settings import load_settings, update_settings
|
|
@@ -5262,6 +5314,7 @@ class TermDashboardScreen(Screen):
|
|
|
5262
5314
|
got_final = True
|
|
5263
5315
|
self._set_run_inflight(False)
|
|
5264
5316
|
self._set_streaming(False)
|
|
5317
|
+
self._call_for_attention("failed", answer=err_msg)
|
|
5265
5318
|
return True
|
|
5266
5319
|
# `waiting`: the turn ended asking the user; finished until they answer.
|
|
5267
5320
|
if status not in ("completed", "success", "finished", WAITING_STATUS):
|
|
@@ -56,6 +56,11 @@ SLASH_COMMANDS: list[SlashCommand] = [
|
|
|
56
56
|
SlashCommand("Runtime", "/show-logs", "Show local CLI (runtime) logs", "/show-logs"),
|
|
57
57
|
SlashCommand("Runtime", "/runtime", "Runtime commands (currently disabled)", "/runtime"),
|
|
58
58
|
|
|
59
|
+
# Updates
|
|
60
|
+
SlashCommand("Update", "/autoupdate", "Whether kiwi updates itself, and how", "/autoupdate"),
|
|
61
|
+
SlashCommand("Update", "/autoupdate on", "Install new releases when kiwi starts", "/autoupdate on"),
|
|
62
|
+
SlashCommand("Update", "/autoupdate off", "Never update on its own", "/autoupdate off"),
|
|
63
|
+
|
|
59
64
|
# Notifications
|
|
60
65
|
SlashCommand("Notify", "/notify", "How kiwi calls you back when it needs you", "/notify"),
|
|
61
66
|
SlashCommand("Notify", "/notify on", "Notification and bell when a question waits or a run finishes", "/notify on"),
|
|
@@ -126,10 +126,12 @@ async def test_a_waiting_question_names_itself_and_a_finished_run_its_answer(mon
|
|
|
126
126
|
|
|
127
127
|
screen._call_for_attention("question")
|
|
128
128
|
screen._call_for_attention("finished", answer="## Done\nAll tests pass.")
|
|
129
|
+
screen._call_for_attention("failed", answer="Tool call timed out\nTraceback …")
|
|
129
130
|
assert calls == [
|
|
130
131
|
{"title": "Kiwi needs your answer", "body": "Which database should I use? (+1 more)",
|
|
131
132
|
"tab": "● kiwi – waiting for you"},
|
|
132
133
|
{"title": "Kiwi finished", "body": "Done", "tab": "✓ kiwi – done"},
|
|
134
|
+
{"title": "Kiwi run failed", "body": "Tool call timed out", "tab": "✗ kiwi – failed"},
|
|
133
135
|
]
|
|
134
136
|
|
|
135
137
|
# In kiwi already: a question is pointed out on the screen instead.
|
|
@@ -137,3 +139,34 @@ async def test_a_waiting_question_names_itself_and_a_finished_run_its_answer(mon
|
|
|
137
139
|
screen._call_for_attention("question")
|
|
138
140
|
await pilot.pause(0.05)
|
|
139
141
|
assert any("waiting for your answer" in str(n.message) for n in app._notifications)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class _FailedRunClient:
|
|
145
|
+
def get_action_result(self, run_id):
|
|
146
|
+
return True, {"id": run_id, "status": "failed", "message": "Tool call timed out\nTraceback …"}, ""
|
|
147
|
+
|
|
148
|
+
def get_run_activity(self, run_id):
|
|
149
|
+
return 200, []
|
|
150
|
+
|
|
151
|
+
async def stream_action_result(self, run_id, on_message, **kwargs):
|
|
152
|
+
import asyncio
|
|
153
|
+
|
|
154
|
+
on_message({"type": "turn.started", "src": "w", "seq": 1, "at": 1, "name": "agent"})
|
|
155
|
+
on_message({"type": "turn.finished", "src": "w", "seq": 2, "at": 2, "ok": False, "error": "failed"})
|
|
156
|
+
await asyncio.sleep(3600)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def test_a_run_that_fails_while_followed_calls_the_user_back() -> None:
|
|
160
|
+
import asyncio
|
|
161
|
+
|
|
162
|
+
from tests.test_term_dashboard_ui import _TermDashboardTestApp
|
|
163
|
+
|
|
164
|
+
app = _TermDashboardTestApp()
|
|
165
|
+
async with app.run_test(size=(100, 30)) as pilot:
|
|
166
|
+
await pilot.pause(0.05)
|
|
167
|
+
calls: list[dict] = []
|
|
168
|
+
app.call_for_attention = lambda **kw: calls.append(kw) or True
|
|
169
|
+
app.screen._set_run_inflight(True)
|
|
170
|
+
await asyncio.wait_for(app.screen._consume_sse("run-1", _FailedRunClient()), timeout=10)
|
|
171
|
+
|
|
172
|
+
assert calls == [{"title": "Kiwi run failed", "body": "Tool call timed out", "tab": "✗ kiwi – failed"}]
|