kon-coding-agent 0.3.5__tar.gz → 0.3.6__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.
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/CHANGELOG.md +22 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/PKG-INFO +4 -8
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/README.md +0 -6
- kon_coding_agent-0.3.6/docs/code-health-scan.md +144 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/pyproject.toml +9 -3
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/config.py +9 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/defaults/config.toml +5 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/openai_codex_responses.py +21 -5
- kon_coding_agent-0.3.6/src/kon/notify.py +30 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/tools/find.py +1 -3
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/tools/grep.py +1 -3
- kon_coding_agent-0.3.6/src/kon/tools/web_fetch.py +160 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/turn.py +4 -6
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/app.py +15 -1
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/blocks.py +13 -6
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/chat.py +66 -11
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/commands.py +17 -28
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/formatting.py +3 -0
- kon_coding_agent-0.3.6/src/kon/ui/latex.py +349 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/styles.py +19 -3
- kon_coding_agent-0.3.6/src/kon/ui/welcome.py +85 -0
- kon_coding_agent-0.3.6/tests/test_notifications_config.py +64 -0
- kon_coding_agent-0.3.6/tests/test_notify.py +46 -0
- kon_coding_agent-0.3.6/tests/test_ui_notifications.py +32 -0
- kon_coding_agent-0.3.6/tests/ui/test_latex.py +60 -0
- kon_coding_agent-0.3.6/tests/ui/test_styles.py +16 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/uv.lock +122 -222
- kon_coding_agent-0.3.5/src/kon/tools/web_fetch.py +0 -79
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/.github/workflows/test.yml +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/.gitignore +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/.kon/skills/kon-release-publish/SKILL.md +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/.kon/skills/kon-tmux-test/SKILL.md +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/.kon/skills/kon-tmux-test/run-e2e-tests.sh +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/.kon/skills/kon-tmux-test/setup-test-project.sh +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/.python-version +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/AGENTS.md +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/LICENSE +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/docs/architecture-review.md +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/docs/images/kon-screenshot.png +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/docs/local-models.md +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/scripts/show_themes.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/__init__.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/async_utils.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/builtin_skills/init/SKILL.md +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/context/__init__.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/context/_xml.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/context/agent_mds.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/context/git.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/context/loader.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/context/skills.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/core/__init__.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/core/compaction.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/core/handoff.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/core/types.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/defaults/__init__.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/events.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/__init__.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/base.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/models.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/oauth/__init__.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/oauth/copilot.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/oauth/openai.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/__init__.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/anthropic.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/azure_ai_foundry.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/copilot.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/copilot_anthropic.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/github_copilot_headers.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/mock.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/openai_compat.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/openai_completions.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/openai_responses.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/sanitize.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/loop.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/permissions.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/py.typed +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/session.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/themes.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/tools/__init__.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/tools/_read_image.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/tools/_tool_utils.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/tools/base.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/tools/bash.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/tools/edit.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/tools/read.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/tools/web_search.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/tools/write.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/tools_manager.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/__init__.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/app_protocol.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/autocomplete.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/clipboard.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/export.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/floating_list.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/input.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/path_complete.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/prompt_history.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/selection_mode.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/session_ui.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/ui/widgets.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/update_check.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/conftest.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/context/test_agents.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/context/test_skills.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/llm/__init__.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/llm/test_anthropic_provider.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/llm/test_azure_ai_foundry_provider.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/llm/test_mock_provider.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/llm/test_openai_codex_provider_errors.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/llm/test_openai_oauth.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_agentic_loop.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_cli_auth_flags.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_cli_provider_resolution.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_compaction.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_config_binaries.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_config_error_fallback.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_config_injection.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_config_migration.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_handoff.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_handoff_link_interrupt.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_launch_warnings.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_local_auth_config.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_model_provider_resolution.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_openai_compat.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_permissions.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_session_persistence.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_session_queries.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_session_resume.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_system_prompt.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_system_prompt_git_context.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_tools_manager.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_update_check.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/test_update_notice_behavior.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/tools/test_diff.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/tools/test_edit.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/tools/test_edit_display.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/tools/test_read.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/tools/test_read_image.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/tools/test_read_image_integration.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/tools/test_subprocess_cancellation.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/tools/test_write.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/ui/test_autocomplete.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/ui/test_floating_list.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/ui/test_input_handoff.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/ui/test_input_paste.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/ui/test_prompt_history.py +0 -0
- {kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/tests/ui/test_status_line.py +0 -0
|
@@ -6,6 +6,28 @@ All notable changes to this project will be documented in this file.
|
|
|
6
6
|
|
|
7
7
|
- No changes yet.
|
|
8
8
|
|
|
9
|
+
## 0.3.6 - 2026-04-23
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Configurable terminal bell notifications on response completion.
|
|
14
|
+
- Render LaTeX math as Unicode in markdown output - @toojays.
|
|
15
|
+
- Style approval popup with button-like controls and panel background.
|
|
16
|
+
- Permission popup title card and improved UI formatting.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Improved UI for approval popup.
|
|
21
|
+
- Reduce LaTeX preprocessing overhead.
|
|
22
|
+
- Improved Codex SSE error extraction and retry on transient errors.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Rewrite web_fetch to bypass bot-detection, preserve indentation, and harden against SSRF - @Meltedd (#28, #29).
|
|
27
|
+
- Remove unused turn streaming state.
|
|
28
|
+
- Remove unused tool imports.
|
|
29
|
+
- Remove empty blocks.
|
|
30
|
+
|
|
9
31
|
## 0.3.5 - 2026-04-18
|
|
10
32
|
|
|
11
33
|
### Added
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kon-coding-agent
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: Minimal coding agent
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.12
|
|
7
7
|
Requires-Dist: aiofiles>=25.1.0
|
|
8
8
|
Requires-Dist: aiohttp>=3.13.3
|
|
9
9
|
Requires-Dist: anthropic>=0.79.0
|
|
10
|
+
Requires-Dist: curl-cffi>=0.15.0
|
|
10
11
|
Requires-Dist: ddgs>=9.0.0
|
|
12
|
+
Requires-Dist: html-to-markdown>=3.1.0
|
|
11
13
|
Requires-Dist: openai>=2.21.0
|
|
12
14
|
Requires-Dist: pillow>=12.1.1
|
|
13
15
|
Requires-Dist: pydantic>=2.12.5
|
|
16
|
+
Requires-Dist: readability-lxml>=0.8.4
|
|
14
17
|
Requires-Dist: rich>=14.3.2
|
|
15
18
|
Requires-Dist: textual>=8.0.0
|
|
16
|
-
Requires-Dist: trafilatura>=2.0.0
|
|
17
19
|
Description-Content-Type: text/markdown
|
|
18
20
|
|
|
19
21
|
<h1 align="center">Kon</h1>
|
|
@@ -475,12 +477,6 @@ If `fd` or `rg` are missing, Kon can download them automatically.
|
|
|
475
477
|
|
|
476
478
|
---
|
|
477
479
|
|
|
478
|
-
## Documentation
|
|
479
|
-
|
|
480
|
-
- [Local models](docs/local-models.md) - Running Kon with local OpenAI-compatible models, plus tested setups and example commands.
|
|
481
|
-
|
|
482
|
-
---
|
|
483
|
-
|
|
484
480
|
## Acknowledgements
|
|
485
481
|
|
|
486
482
|
- Kon takes significant inspiration from [pi coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent), especially around philosophy and UI direction.
|
|
@@ -457,12 +457,6 @@ If `fd` or `rg` are missing, Kon can download them automatically.
|
|
|
457
457
|
|
|
458
458
|
---
|
|
459
459
|
|
|
460
|
-
## Documentation
|
|
461
|
-
|
|
462
|
-
- [Local models](docs/local-models.md) - Running Kon with local OpenAI-compatible models, plus tested setups and example commands.
|
|
463
|
-
|
|
464
|
-
---
|
|
465
|
-
|
|
466
460
|
## Acknowledgements
|
|
467
461
|
|
|
468
462
|
- Kon takes significant inspiration from [pi coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent), especially around philosophy and UI direction.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Code Health Scan: Additional Notable Issues
|
|
2
|
+
|
|
3
|
+
This note captures only the additional issues found in a fresh repo scan that are **not** the same as the already-known architectural concerns in `docs/architecture-review.md`.
|
|
4
|
+
|
|
5
|
+
The baseline conclusion from the earlier review still stands:
|
|
6
|
+
|
|
7
|
+
- the core runtime is strong
|
|
8
|
+
- the TUI still owns too much runtime/session orchestration
|
|
9
|
+
- the large controller/runtime-manager refactor is deferred for now
|
|
10
|
+
|
|
11
|
+
This document is intentionally narrower. It records only the standout issue and the smaller cleanup opportunities found in the current repo state after the recent cleanup pass.
|
|
12
|
+
|
|
13
|
+
## Overall verdict
|
|
14
|
+
|
|
15
|
+
Aside from the already-documented UI/runtime orchestration boundary issue, the repo looks to be in decent shape overall.
|
|
16
|
+
|
|
17
|
+
Notable signals:
|
|
18
|
+
|
|
19
|
+
- test suite passed cleanly
|
|
20
|
+
- pyright passed cleanly
|
|
21
|
+
- only a few lint issues remained
|
|
22
|
+
|
|
23
|
+
So this is **not** a case where the repo appears to have multiple additional architectural problems lurking elsewhere. The main new thing worth tracking is a state-consistency problem around provider/model transitions, plus a few smaller cleanup items.
|
|
24
|
+
|
|
25
|
+
## Major issue
|
|
26
|
+
|
|
27
|
+
### Provider/model transition paths are not transactional
|
|
28
|
+
|
|
29
|
+
The main additional issue is that some provider/model/session transition flows update app state before it is certain that the replacement provider/runtime state can be constructed successfully.
|
|
30
|
+
|
|
31
|
+
Relevant files:
|
|
32
|
+
|
|
33
|
+
- `src/kon/ui/commands.py`
|
|
34
|
+
- `src/kon/ui/session_ui.py`
|
|
35
|
+
|
|
36
|
+
### Where it shows up
|
|
37
|
+
|
|
38
|
+
#### `src/kon/ui/commands.py::_select_model`
|
|
39
|
+
|
|
40
|
+
This path updates:
|
|
41
|
+
|
|
42
|
+
- `self._model`
|
|
43
|
+
- `self._model_provider`
|
|
44
|
+
|
|
45
|
+
before provider recreation is guaranteed to succeed.
|
|
46
|
+
|
|
47
|
+
If `_create_provider(...)` fails, the UI reports the error, but some selected-model state may already have been changed while the active provider/agent runtime is still using the old provider instance.
|
|
48
|
+
|
|
49
|
+
#### `src/kon/ui/session_ui.py::_load_session`
|
|
50
|
+
|
|
51
|
+
This path loads session metadata and updates model/provider-related state while also trying to reconcile the active provider.
|
|
52
|
+
|
|
53
|
+
If provider recreation fails during session load, the code reports the error, but can still leave partially updated state behind:
|
|
54
|
+
|
|
55
|
+
- loaded session state may now be active
|
|
56
|
+
- selected model/provider values may reflect the resumed session
|
|
57
|
+
- the provider instance may still be the old one or only partially reconfigured
|
|
58
|
+
|
|
59
|
+
### Why this matters
|
|
60
|
+
|
|
61
|
+
This is not a large-architecture problem, but it is a real correctness risk.
|
|
62
|
+
|
|
63
|
+
It can produce subtle mismatches between:
|
|
64
|
+
|
|
65
|
+
- selected model/provider UI state
|
|
66
|
+
- active provider config
|
|
67
|
+
- agent runtime state
|
|
68
|
+
- resumed session metadata
|
|
69
|
+
|
|
70
|
+
Those failures are harder to reason about than a simple hard error because the app can continue running in a partially updated state.
|
|
71
|
+
|
|
72
|
+
### Suggested direction
|
|
73
|
+
|
|
74
|
+
Without doing the large architecture refactor, the safer short-term fix would be to make these transitions more atomic:
|
|
75
|
+
|
|
76
|
+
1. compute the target model/provider/session state first
|
|
77
|
+
2. build or validate the replacement provider first
|
|
78
|
+
3. only commit the new state to `self._model`, `self._model_provider`, `self._provider`, `self._session`, and `self._agent` after success
|
|
79
|
+
4. otherwise leave the old runtime state untouched
|
|
80
|
+
|
|
81
|
+
Even a small helper that stages the new state before assignment would reduce the sync-risk significantly.
|
|
82
|
+
|
|
83
|
+
## Small cleanup opportunities
|
|
84
|
+
|
|
85
|
+
### 1. `CompactionEntry.first_kept_entry_id` is currently misleading
|
|
86
|
+
|
|
87
|
+
Relevant file:
|
|
88
|
+
|
|
89
|
+
- `src/kon/session.py`
|
|
90
|
+
|
|
91
|
+
`CompactionEntry` stores `first_kept_entry_id`, and comments imply that the compacted view depends on it.
|
|
92
|
+
|
|
93
|
+
However, `Session.messages` currently reconstructs the compacted view by:
|
|
94
|
+
|
|
95
|
+
- finding the last compaction entry
|
|
96
|
+
- inserting the synthetic summary pair
|
|
97
|
+
- including message entries after the compaction entry itself
|
|
98
|
+
|
|
99
|
+
It does **not** currently use `first_kept_entry_id` to decide what to retain.
|
|
100
|
+
|
|
101
|
+
This is not a functional bug in current usage because existing callers/tests append compaction entries in a way that still makes the behavior correct. But the data model and implementation intent are slightly out of sync, which makes the field more confusing than helpful right now.
|
|
102
|
+
|
|
103
|
+
Small fix options:
|
|
104
|
+
|
|
105
|
+
- actually use `first_kept_entry_id` in `Session.messages`, or
|
|
106
|
+
- simplify the field/comment contract if the compaction-entry position is the real source of truth
|
|
107
|
+
|
|
108
|
+
### 2. System-prompt fallback logic is still duplicated across UI mixins
|
|
109
|
+
|
|
110
|
+
Relevant files:
|
|
111
|
+
|
|
112
|
+
- `src/kon/ui/commands.py`
|
|
113
|
+
- `src/kon/ui/session_ui.py`
|
|
114
|
+
|
|
115
|
+
Both files define the same `_resolve_system_prompt(...)` helper:
|
|
116
|
+
|
|
117
|
+
- use persisted `session.system_prompt` when available
|
|
118
|
+
- otherwise rebuild with `build_system_prompt(...)`
|
|
119
|
+
|
|
120
|
+
This is a small issue, not a major design problem, but it means the recent centralization is still only partial. If the fallback behavior changes again, both copies will need to stay in sync.
|
|
121
|
+
|
|
122
|
+
Small fix option:
|
|
123
|
+
|
|
124
|
+
- move the helper to one shared non-duplicated location used by both mixins/app paths
|
|
125
|
+
|
|
126
|
+
### 3. Lint cleanup remains in a few tool modules
|
|
127
|
+
|
|
128
|
+
Relevant files:
|
|
129
|
+
|
|
130
|
+
- `src/kon/tools/find.py`
|
|
131
|
+
- `src/kon/tools/grep.py`
|
|
132
|
+
- `src/kon/tools/web_fetch.py`
|
|
133
|
+
|
|
134
|
+
Current lint output shows unused imports of `truncate_text` in these files.
|
|
135
|
+
|
|
136
|
+
This is only a hygiene issue, but it is a useful signal that a full lint pass likely did not happen after some recent edits.
|
|
137
|
+
|
|
138
|
+
## Final assessment
|
|
139
|
+
|
|
140
|
+
The repo looks fine overall.
|
|
141
|
+
|
|
142
|
+
Aside from the already-known concerns in `docs/architecture-review.md`, the only additional issue that really stands out is the non-transactional provider/model/session transition behavior.
|
|
143
|
+
|
|
144
|
+
The other findings are small cleanup opportunities rather than signs of deeper architectural trouble.
|
|
@@ -14,7 +14,7 @@ default = true
|
|
|
14
14
|
|
|
15
15
|
[project]
|
|
16
16
|
name = "kon-coding-agent"
|
|
17
|
-
version = "0.3.
|
|
17
|
+
version = "0.3.6"
|
|
18
18
|
description = "Minimal coding agent"
|
|
19
19
|
readme = "README.md"
|
|
20
20
|
requires-python = ">=3.12"
|
|
@@ -22,13 +22,15 @@ dependencies = [
|
|
|
22
22
|
"aiofiles>=25.1.0",
|
|
23
23
|
"aiohttp>=3.13.3",
|
|
24
24
|
"anthropic>=0.79.0",
|
|
25
|
+
"curl-cffi>=0.15.0",
|
|
26
|
+
"ddgs>=9.0.0",
|
|
27
|
+
"html-to-markdown>=3.1.0",
|
|
25
28
|
"openai>=2.21.0",
|
|
26
29
|
"pillow>=12.1.1",
|
|
27
30
|
"pydantic>=2.12.5",
|
|
31
|
+
"readability-lxml>=0.8.4",
|
|
28
32
|
"rich>=14.3.2",
|
|
29
33
|
"textual>=8.0.0",
|
|
30
|
-
"ddgs>=9.0.0",
|
|
31
|
-
"trafilatura>=2.0.0",
|
|
32
34
|
]
|
|
33
35
|
|
|
34
36
|
[dependency-groups]
|
|
@@ -60,6 +62,10 @@ select = [
|
|
|
60
62
|
"RUF", # ruff-specific rules
|
|
61
63
|
]
|
|
62
64
|
|
|
65
|
+
[tool.ruff.lint.per-file-ignores]
|
|
66
|
+
"src/kon/ui/latex.py" = ["RUF001"]
|
|
67
|
+
"tests/ui/test_latex.py" = ["RUF001"]
|
|
68
|
+
|
|
63
69
|
[tool.ruff.lint.isort]
|
|
64
70
|
split-on-trailing-comma = false
|
|
65
71
|
|
|
@@ -98,6 +98,10 @@ class ToolsConfig(BaseModel):
|
|
|
98
98
|
extra: list[str] = []
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
class NotificationsConfig(BaseModel):
|
|
102
|
+
enabled: bool = False
|
|
103
|
+
|
|
104
|
+
|
|
101
105
|
class ConfigSchema(BaseModel):
|
|
102
106
|
meta: MetaConfig
|
|
103
107
|
llm: LLMConfig
|
|
@@ -106,6 +110,7 @@ class ConfigSchema(BaseModel):
|
|
|
106
110
|
agent: AgentConfig
|
|
107
111
|
tools: ToolsConfig = ToolsConfig()
|
|
108
112
|
permissions: PermissionsConfig
|
|
113
|
+
notifications: NotificationsConfig = NotificationsConfig()
|
|
109
114
|
|
|
110
115
|
|
|
111
116
|
# =================================================================================================
|
|
@@ -194,6 +199,10 @@ class Config:
|
|
|
194
199
|
def tools(self) -> ToolsConfig:
|
|
195
200
|
return self._parsed.tools
|
|
196
201
|
|
|
202
|
+
@property
|
|
203
|
+
def notifications(self) -> NotificationsConfig:
|
|
204
|
+
return self._parsed.notifications
|
|
205
|
+
|
|
197
206
|
@property
|
|
198
207
|
def binaries(self) -> _BinariesConfig:
|
|
199
208
|
return _BinariesConfig(AVAILABLE_BINARIES)
|
|
@@ -73,3 +73,8 @@ collapse_thinking = true
|
|
|
73
73
|
# "prompt" asks before edits/writes and other mutating actions.
|
|
74
74
|
# "auto" allows tool calls without approval prompts.
|
|
75
75
|
mode = "prompt" # "prompt" or "auto"
|
|
76
|
+
|
|
77
|
+
[notifications]
|
|
78
|
+
# Ring the terminal bell when kon finishes a task or waits for tool approval.
|
|
79
|
+
# Disabled by default.
|
|
80
|
+
enabled = false
|
{kon_coding_agent-0.3.5 → kon_coding_agent-0.3.6}/src/kon/llm/providers/openai_codex_responses.py
RENAMED
|
@@ -279,11 +279,26 @@ class OpenAICodexResponsesProvider(BaseProvider):
|
|
|
279
279
|
yield StreamDone(stop_reason=stop_reason)
|
|
280
280
|
return
|
|
281
281
|
|
|
282
|
-
elif event_type
|
|
282
|
+
elif event_type == "error":
|
|
283
|
+
code = event.get("code")
|
|
283
284
|
message = event.get("message")
|
|
284
|
-
if
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
if isinstance(message, str) and message:
|
|
286
|
+
yield StreamError(error=f"Codex error: {message}")
|
|
287
|
+
elif isinstance(code, str) and code:
|
|
288
|
+
yield StreamError(error=f"Codex error: {code}")
|
|
289
|
+
else:
|
|
290
|
+
yield StreamError(error=f"Codex error: {json.dumps(event)}")
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
elif event_type == "response.failed":
|
|
294
|
+
response_obj = event.get("response")
|
|
295
|
+
msg = None
|
|
296
|
+
if isinstance(response_obj, dict):
|
|
297
|
+
err = response_obj.get("error")
|
|
298
|
+
if isinstance(err, dict):
|
|
299
|
+
msg = err.get("message")
|
|
300
|
+
err_msg = msg if isinstance(msg, str) and msg else "Codex response failed"
|
|
301
|
+
yield StreamError(error=err_msg)
|
|
287
302
|
return
|
|
288
303
|
finally:
|
|
289
304
|
await session.close()
|
|
@@ -321,7 +336,8 @@ class OpenAICodexResponsesProvider(BaseProvider):
|
|
|
321
336
|
return StopReason.STOP
|
|
322
337
|
|
|
323
338
|
def should_retry_for_error(self, error: Exception) -> bool:
|
|
324
|
-
|
|
339
|
+
msg = str(error).lower()
|
|
340
|
+
return any(kw in msg for kw in ("429", "rate_limit", "server_error", "502", "503", "504"))
|
|
325
341
|
|
|
326
342
|
|
|
327
343
|
def is_openai_logged_in() -> bool:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
_BELL_DEBOUNCE_S = 0.5
|
|
5
|
+
_last_bell_time: float = 0.0
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _raw_write(data: bytes) -> None:
|
|
9
|
+
try:
|
|
10
|
+
fd = os.open("/dev/tty", os.O_WRONLY | os.O_NOCTTY)
|
|
11
|
+
try:
|
|
12
|
+
os.write(fd, data)
|
|
13
|
+
finally:
|
|
14
|
+
os.close(fd)
|
|
15
|
+
except OSError:
|
|
16
|
+
os.write(2, data)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _bell() -> None:
|
|
20
|
+
global _last_bell_time
|
|
21
|
+
now = time.monotonic()
|
|
22
|
+
if now - _last_bell_time < _BELL_DEBOUNCE_S:
|
|
23
|
+
return
|
|
24
|
+
_last_bell_time = now
|
|
25
|
+
_raw_write(b"\a")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def notify(title: str, message: str) -> None:
|
|
29
|
+
del title, message
|
|
30
|
+
_bell()
|
|
@@ -10,7 +10,6 @@ from ._tool_utils import (
|
|
|
10
10
|
communicate_or_cancel,
|
|
11
11
|
shorten_path,
|
|
12
12
|
truncate_lines_by_bytes,
|
|
13
|
-
truncate_text,
|
|
14
13
|
)
|
|
15
14
|
from .base import BaseTool
|
|
16
15
|
|
|
@@ -44,8 +43,7 @@ class FindTool(BaseTool):
|
|
|
44
43
|
parts = [f'"{pattern}"']
|
|
45
44
|
if params.path:
|
|
46
45
|
parts.append(f"in {shorten_path(params.path)}")
|
|
47
|
-
|
|
48
|
-
return truncate_text(message)
|
|
46
|
+
return " ".join(parts)
|
|
49
47
|
|
|
50
48
|
async def execute(
|
|
51
49
|
self, params: FindParams, cancel_event: asyncio.Event | None = None
|
|
@@ -11,7 +11,6 @@ from ._tool_utils import (
|
|
|
11
11
|
communicate_or_cancel,
|
|
12
12
|
shorten_path,
|
|
13
13
|
truncate_lines_by_bytes,
|
|
14
|
-
truncate_text,
|
|
15
14
|
)
|
|
16
15
|
from .base import BaseTool
|
|
17
16
|
|
|
@@ -49,8 +48,7 @@ class GrepTool(BaseTool):
|
|
|
49
48
|
parts.append(f"in {shorten_path(params.path)}")
|
|
50
49
|
if params.include:
|
|
51
50
|
parts.append(f"({params.include})")
|
|
52
|
-
|
|
53
|
-
return truncate_text(message)
|
|
51
|
+
return " ".join(parts)
|
|
54
52
|
|
|
55
53
|
async def execute(
|
|
56
54
|
self, params: GrepParams, cancel_event: asyncio.Event | None = None
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import ipaddress
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
from curl_cffi import AsyncSession, CurlOpt
|
|
6
|
+
from html_to_markdown import ConversionOptions, convert
|
|
7
|
+
from lxml import html as lxml_html
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
from readability import Document
|
|
10
|
+
|
|
11
|
+
from ..core.types import ToolResult
|
|
12
|
+
from ._tool_utils import ToolCancelledError, await_task_or_cancel
|
|
13
|
+
from .base import BaseTool
|
|
14
|
+
|
|
15
|
+
MAX_CHARS = 80_000
|
|
16
|
+
MAX_CHARS_PER_LINE = 2000
|
|
17
|
+
MAX_RESPONSE_BYTES = 20_000_000
|
|
18
|
+
REQUEST_TIMEOUT = 15
|
|
19
|
+
|
|
20
|
+
# Only checked on failure paths, so false positives can't suppress real content.
|
|
21
|
+
_CHALLENGE_SIGNATURES = (
|
|
22
|
+
"please wait for verification", # Reddit 200 JS challenge
|
|
23
|
+
"prove your humanity", # Reddit 200 reCAPTCHA gate
|
|
24
|
+
"whoa there, pardner", # Reddit 403 ratelimit page
|
|
25
|
+
"just a moment...", # Cloudflare
|
|
26
|
+
"checking your browser", # Cloudflare (legacy)
|
|
27
|
+
"attention required", # Cloudflare block page
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Inline SVG data URIs would otherwise become base64 noise.
|
|
31
|
+
_CONVERT_OPTIONS = ConversionOptions(skip_images=True)
|
|
32
|
+
|
|
33
|
+
# concat(';', ...) anchors to a property boundary to block url() false positives.
|
|
34
|
+
_HIDDEN_XPATH = (
|
|
35
|
+
"//script | //style | //noscript | //template"
|
|
36
|
+
" | //*[@hidden or @aria-hidden='true']"
|
|
37
|
+
" | //*[contains(concat(';', translate(@style, ' \t\n', '')), ';display:none')]"
|
|
38
|
+
" | //*[contains(concat(';', translate(@style, ' \t\n', '')), ';visibility:hidden')]"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _looks_like_challenge(html: str) -> bool:
|
|
43
|
+
head = html[:4096].lower()
|
|
44
|
+
return any(sig in head for sig in _CHALLENGE_SIGNATURES)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_link_local(ip: str) -> bool:
|
|
48
|
+
try:
|
|
49
|
+
return ipaddress.ip_address(ip).is_link_local
|
|
50
|
+
except ValueError:
|
|
51
|
+
return True # fail closed
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _extract_markdown(html: str) -> str | None:
|
|
55
|
+
if not html:
|
|
56
|
+
return None
|
|
57
|
+
try:
|
|
58
|
+
tree = lxml_html.fromstring(html)
|
|
59
|
+
for node in tree.xpath(_HIDDEN_XPATH):
|
|
60
|
+
if (parent := node.getparent()) is not None:
|
|
61
|
+
parent.remove(node)
|
|
62
|
+
doc_input = tree
|
|
63
|
+
except Exception:
|
|
64
|
+
doc_input = html # fall back to readability's own lenient parser
|
|
65
|
+
summary_html = Document(doc_input).summary()
|
|
66
|
+
if not summary_html or len(summary_html) < 50:
|
|
67
|
+
return None
|
|
68
|
+
return convert(summary_html, _CONVERT_OPTIONS).content or None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class WebFetchParams(BaseModel):
|
|
72
|
+
url: str = Field(description="URL of the web page to fetch and extract content from")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class WebFetchTool(BaseTool):
|
|
76
|
+
name = "web_fetch"
|
|
77
|
+
tool_icon = "%"
|
|
78
|
+
mutating = False
|
|
79
|
+
params = WebFetchParams
|
|
80
|
+
description = (
|
|
81
|
+
"Fetch a web page and extract its main content as clean markdown. "
|
|
82
|
+
"Strips navigation, ads, and boilerplate. "
|
|
83
|
+
"Use web_search first to find relevant URLs (if not provided by the user)."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def format_call(self, params: WebFetchParams) -> str:
|
|
87
|
+
return params.url
|
|
88
|
+
|
|
89
|
+
async def execute(
|
|
90
|
+
self, params: WebFetchParams, cancel_event: asyncio.Event | None = None
|
|
91
|
+
) -> ToolResult:
|
|
92
|
+
scheme = urlparse(params.url).scheme
|
|
93
|
+
if scheme not in ("http", "https"):
|
|
94
|
+
msg = f"Refused: unsupported scheme {scheme!r}"
|
|
95
|
+
return ToolResult(success=False, result=msg, ui_summary=f"[red]{msg}[/red]")
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
async with AsyncSession(
|
|
99
|
+
impersonate="chrome131",
|
|
100
|
+
allow_redirects="safe",
|
|
101
|
+
curl_options={CurlOpt.MAXFILESIZE_LARGE: MAX_RESPONSE_BYTES},
|
|
102
|
+
) as session:
|
|
103
|
+
fetch_task = asyncio.create_task(session.get(params.url, timeout=REQUEST_TIMEOUT))
|
|
104
|
+
response = await await_task_or_cancel(fetch_task, cancel_event)
|
|
105
|
+
except ToolCancelledError:
|
|
106
|
+
return ToolResult(success=False, result="Fetch aborted")
|
|
107
|
+
except Exception as e:
|
|
108
|
+
msg = f"Fetch failed: {e}"
|
|
109
|
+
return ToolResult(success=False, result=msg, ui_summary=f"[red]{msg}[/red]")
|
|
110
|
+
|
|
111
|
+
if _is_link_local(response.primary_ip):
|
|
112
|
+
msg = f"Refused: link-local address ({response.primary_ip or 'unknown'})"
|
|
113
|
+
return ToolResult(success=False, result=msg, ui_summary=f"[red]{msg}[/red]")
|
|
114
|
+
|
|
115
|
+
# Catches decompression bombs (MAXFILESIZE_LARGE only bounds wire bytes).
|
|
116
|
+
if len(response.content) > MAX_RESPONSE_BYTES:
|
|
117
|
+
msg = f"Response too large (>{MAX_RESPONSE_BYTES:,} bytes decoded)"
|
|
118
|
+
return ToolResult(success=False, result=msg, ui_summary=f"[red]{msg}[/red]")
|
|
119
|
+
|
|
120
|
+
if not (200 <= response.status_code < 300):
|
|
121
|
+
status = f"HTTP {response.status_code}"
|
|
122
|
+
msg = (
|
|
123
|
+
f"Site appears to block automated fetchers ({status})"
|
|
124
|
+
if _looks_like_challenge(response.text)
|
|
125
|
+
else status
|
|
126
|
+
)
|
|
127
|
+
return ToolResult(success=False, result=msg, ui_summary=f"[red]{msg}[/red]")
|
|
128
|
+
|
|
129
|
+
html = response.text
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
extract_task = asyncio.create_task(asyncio.to_thread(_extract_markdown, html))
|
|
133
|
+
content = await await_task_or_cancel(extract_task, cancel_event)
|
|
134
|
+
except ToolCancelledError:
|
|
135
|
+
return ToolResult(success=False, result="Extraction aborted")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
msg = f"Extraction failed: {e}"
|
|
138
|
+
return ToolResult(success=False, result=msg, ui_summary=f"[red]{msg}[/red]")
|
|
139
|
+
|
|
140
|
+
if not content:
|
|
141
|
+
msg = (
|
|
142
|
+
"Site appears to block automated fetchers"
|
|
143
|
+
if _looks_like_challenge(html)
|
|
144
|
+
else "Couldn't extract content"
|
|
145
|
+
)
|
|
146
|
+
return ToolResult(success=False, result=msg, ui_summary=f"[red]{msg}[/red]")
|
|
147
|
+
|
|
148
|
+
content = "\n".join(line[:MAX_CHARS_PER_LINE] for line in content.split("\n"))
|
|
149
|
+
|
|
150
|
+
char_count = len(content)
|
|
151
|
+
truncated = char_count > MAX_CHARS
|
|
152
|
+
if truncated:
|
|
153
|
+
cut = content.rfind("\n", 0, MAX_CHARS)
|
|
154
|
+
content = content[: cut if cut > 0 else MAX_CHARS] + "\n\n[content truncated]"
|
|
155
|
+
|
|
156
|
+
ui_summary = f"[dim]({char_count:,} chars)[/dim]"
|
|
157
|
+
if truncated:
|
|
158
|
+
ui_summary += " [yellow](truncated)[/yellow]"
|
|
159
|
+
|
|
160
|
+
return ToolResult(success=True, result=content, ui_summary=ui_summary)
|
|
@@ -312,7 +312,6 @@ async def run_single_turn(
|
|
|
312
312
|
current_state: StreamState | None = None
|
|
313
313
|
stop_reason: StopReason = StopReason.STOP
|
|
314
314
|
interrupted = False
|
|
315
|
-
has_meaningful_output = False
|
|
316
315
|
|
|
317
316
|
def _finalize_current_state(include_empty: bool = True) -> list[StreamEvent]:
|
|
318
317
|
nonlocal current_state, current_tool_call, think_buffer, think_signature, text_buffer
|
|
@@ -437,14 +436,16 @@ async def run_single_turn(
|
|
|
437
436
|
|
|
438
437
|
current_state = StreamState.THINK
|
|
439
438
|
think_buffer.append(t)
|
|
440
|
-
has_meaningful_output = True
|
|
441
439
|
if sig:
|
|
442
440
|
think_signature = sig
|
|
443
441
|
|
|
444
442
|
yield ThinkingDeltaEvent(delta=t)
|
|
445
443
|
|
|
446
444
|
case TextPart(text=t):
|
|
447
|
-
|
|
445
|
+
# Skip whitespace-only text that would start a new (empty)
|
|
446
|
+
# content block — prevents phantom gaps between thinking
|
|
447
|
+
# and tool-call blocks.
|
|
448
|
+
if not t.strip() and current_state != StreamState.TEXT:
|
|
448
449
|
continue
|
|
449
450
|
|
|
450
451
|
if current_state and current_state != StreamState.TEXT:
|
|
@@ -456,14 +457,11 @@ async def run_single_turn(
|
|
|
456
457
|
|
|
457
458
|
current_state = StreamState.TEXT
|
|
458
459
|
text_buffer.append(t)
|
|
459
|
-
if t.strip():
|
|
460
|
-
has_meaningful_output = True
|
|
461
460
|
|
|
462
461
|
yield TextDeltaEvent(delta=t)
|
|
463
462
|
|
|
464
463
|
case ToolCallStart(id=id, name=name, arguments=initial_arguments):
|
|
465
464
|
tool_call_count += 1
|
|
466
|
-
has_meaningful_output = True
|
|
467
465
|
if current_state and current_state != StreamState.TOOL_CALL:
|
|
468
466
|
for finalize_event in _finalize_current_state():
|
|
469
467
|
yield finalize_event
|
|
@@ -61,6 +61,7 @@ from ..llm import (
|
|
|
61
61
|
)
|
|
62
62
|
from ..llm.base import AuthMode
|
|
63
63
|
from ..loop import Agent
|
|
64
|
+
from ..notify import notify
|
|
64
65
|
from ..permissions import ApprovalResponse
|
|
65
66
|
from ..session import Session
|
|
66
67
|
from ..tools import DEFAULT_TOOLS, EXTRA_TOOLS, get_tool, get_tools
|
|
@@ -94,13 +95,14 @@ _CHANGELOG_URL = "https://github.com/0xku/kon/blob/main/CHANGELOG.md"
|
|
|
94
95
|
try:
|
|
95
96
|
VERSION = version(_PYPI_PACKAGE_NAME)
|
|
96
97
|
except PackageNotFoundError:
|
|
97
|
-
VERSION = "0.3.
|
|
98
|
+
VERSION = "0.3.6"
|
|
98
99
|
|
|
99
100
|
_COPILOT_API_TYPES: frozenset[ApiType] = frozenset(
|
|
100
101
|
{ApiType.GITHUB_COPILOT, ApiType.GITHUB_COPILOT_RESPONSES, ApiType.ANTHROPIC_COPILOT}
|
|
101
102
|
)
|
|
102
103
|
|
|
103
104
|
_OPENAI_OAUTH_API_TYPES: frozenset[ApiType] = frozenset({ApiType.OPENAI_CODEX_RESPONSES})
|
|
105
|
+
_NOTIFY_EVENTS = (AgentEndEvent, ToolApprovalEvent)
|
|
104
106
|
|
|
105
107
|
_API_TYPE_BY_PROVIDER: dict[type[BaseProvider], ApiType] = {
|
|
106
108
|
v: k for k, v in API_TYPE_TO_PROVIDER_CLASS.items()
|
|
@@ -846,6 +848,15 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
|
|
|
846
848
|
normal_items = [(display, False) for display, _ in self._pending_queue]
|
|
847
849
|
queue_display.update_items(steer_items + normal_items)
|
|
848
850
|
|
|
851
|
+
def _should_notify_for_event(self, event: object) -> bool:
|
|
852
|
+
if not config.notifications.enabled:
|
|
853
|
+
return False
|
|
854
|
+
if not isinstance(event, _NOTIFY_EVENTS):
|
|
855
|
+
return False
|
|
856
|
+
return not (
|
|
857
|
+
isinstance(event, AgentEndEvent) and event.stop_reason == StopReason.INTERRUPTED
|
|
858
|
+
)
|
|
859
|
+
|
|
849
860
|
async def _run_agent(self, prompt: str) -> None:
|
|
850
861
|
chat = self.query_one("#chat-log", ChatLog)
|
|
851
862
|
status = self.query_one("#status-line", StatusLine)
|
|
@@ -890,6 +901,9 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
|
|
|
890
901
|
async for event in self._agent.run(
|
|
891
902
|
current_prompt, cancel_event=self._cancel_event, steer_event=self._steer_event
|
|
892
903
|
):
|
|
904
|
+
if self._should_notify_for_event(event):
|
|
905
|
+
notify("kon", "Task complete")
|
|
906
|
+
|
|
893
907
|
match event:
|
|
894
908
|
case AgentStartEvent():
|
|
895
909
|
pass
|