kon-coding-agent 0.3.4__tar.gz → 0.3.5__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 (138) hide show
  1. kon_coding_agent-0.3.5/.github/workflows/test.yml +33 -0
  2. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/.kon/skills/kon-release-publish/SKILL.md +16 -9
  3. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/AGENTS.md +1 -0
  4. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/CHANGELOG.md +64 -0
  5. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/PKG-INFO +1 -1
  6. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/pyproject.toml +1 -1
  7. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/config.py +1 -0
  8. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/defaults/config.toml +3 -0
  9. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/base.py +1 -1
  10. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/providers/openai_completions.py +7 -1
  11. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/providers/openai_responses.py +3 -0
  12. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/session.py +144 -13
  13. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools/edit.py +1 -1
  14. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/app.py +22 -21
  15. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/chat.py +17 -2
  16. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/commands.py +26 -72
  17. kon_coding_agent-0.3.5/src/kon/ui/export.py +693 -0
  18. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/floating_list.py +70 -8
  19. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/session_ui.py +14 -52
  20. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/widgets.py +116 -42
  21. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/context/test_skills.py +1 -1
  22. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_handoff.py +4 -6
  23. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_launch_warnings.py +1 -1
  24. kon_coding_agent-0.3.5/tests/test_session_queries.py +61 -0
  25. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_session_resume.py +1 -5
  26. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/tools/test_edit_display.py +2 -2
  27. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/uv.lock +1 -1
  28. kon_coding_agent-0.3.4/src/kon/ui/export.py +0 -330
  29. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/.gitignore +0 -0
  30. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/.kon/skills/kon-tmux-test/SKILL.md +0 -0
  31. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/.kon/skills/kon-tmux-test/run-e2e-tests.sh +0 -0
  32. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/.kon/skills/kon-tmux-test/setup-test-project.sh +0 -0
  33. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/.python-version +0 -0
  34. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/LICENSE +0 -0
  35. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/README.md +0 -0
  36. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/docs/architecture-review.md +0 -0
  37. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/docs/images/kon-screenshot.png +0 -0
  38. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/docs/local-models.md +0 -0
  39. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/scripts/show_themes.py +0 -0
  40. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/__init__.py +0 -0
  41. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/async_utils.py +0 -0
  42. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/builtin_skills/init/SKILL.md +0 -0
  43. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/context/__init__.py +0 -0
  44. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/context/_xml.py +0 -0
  45. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/context/agent_mds.py +0 -0
  46. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/context/git.py +0 -0
  47. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/context/loader.py +0 -0
  48. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/context/skills.py +0 -0
  49. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/core/__init__.py +0 -0
  50. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/core/compaction.py +0 -0
  51. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/core/handoff.py +0 -0
  52. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/core/types.py +0 -0
  53. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/defaults/__init__.py +0 -0
  54. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/events.py +0 -0
  55. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/__init__.py +0 -0
  56. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/models.py +0 -0
  57. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/oauth/__init__.py +0 -0
  58. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/oauth/copilot.py +0 -0
  59. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/oauth/openai.py +0 -0
  60. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/providers/__init__.py +0 -0
  61. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/providers/anthropic.py +0 -0
  62. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/providers/azure_ai_foundry.py +0 -0
  63. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/providers/copilot.py +0 -0
  64. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/providers/copilot_anthropic.py +0 -0
  65. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/providers/github_copilot_headers.py +0 -0
  66. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/providers/mock.py +0 -0
  67. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/providers/openai_codex_responses.py +0 -0
  68. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/providers/openai_compat.py +0 -0
  69. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/llm/providers/sanitize.py +0 -0
  70. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/loop.py +0 -0
  71. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/permissions.py +0 -0
  72. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/py.typed +0 -0
  73. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/themes.py +0 -0
  74. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools/__init__.py +0 -0
  75. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools/_read_image.py +0 -0
  76. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools/_tool_utils.py +0 -0
  77. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools/base.py +0 -0
  78. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools/bash.py +0 -0
  79. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools/find.py +0 -0
  80. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools/grep.py +0 -0
  81. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools/read.py +0 -0
  82. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools/web_fetch.py +0 -0
  83. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools/web_search.py +0 -0
  84. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools/write.py +0 -0
  85. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/tools_manager.py +0 -0
  86. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/turn.py +0 -0
  87. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/__init__.py +0 -0
  88. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/app_protocol.py +0 -0
  89. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/autocomplete.py +0 -0
  90. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/blocks.py +0 -0
  91. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/clipboard.py +0 -0
  92. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/formatting.py +0 -0
  93. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/input.py +0 -0
  94. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/path_complete.py +0 -0
  95. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/prompt_history.py +0 -0
  96. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/selection_mode.py +0 -0
  97. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/ui/styles.py +0 -0
  98. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/src/kon/update_check.py +0 -0
  99. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/conftest.py +0 -0
  100. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/context/test_agents.py +0 -0
  101. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/llm/__init__.py +0 -0
  102. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/llm/test_anthropic_provider.py +0 -0
  103. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/llm/test_azure_ai_foundry_provider.py +0 -0
  104. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/llm/test_mock_provider.py +0 -0
  105. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/llm/test_openai_codex_provider_errors.py +0 -0
  106. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/llm/test_openai_oauth.py +0 -0
  107. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_agentic_loop.py +0 -0
  108. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_cli_auth_flags.py +0 -0
  109. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_cli_provider_resolution.py +0 -0
  110. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_compaction.py +0 -0
  111. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_config_binaries.py +0 -0
  112. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_config_error_fallback.py +0 -0
  113. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_config_injection.py +0 -0
  114. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_config_migration.py +0 -0
  115. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_handoff_link_interrupt.py +0 -0
  116. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_local_auth_config.py +0 -0
  117. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_model_provider_resolution.py +0 -0
  118. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_openai_compat.py +0 -0
  119. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_permissions.py +0 -0
  120. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_session_persistence.py +0 -0
  121. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_system_prompt.py +0 -0
  122. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_system_prompt_git_context.py +0 -0
  123. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_tools_manager.py +0 -0
  124. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_update_check.py +0 -0
  125. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/test_update_notice_behavior.py +0 -0
  126. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/tools/test_diff.py +0 -0
  127. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/tools/test_edit.py +0 -0
  128. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/tools/test_read.py +0 -0
  129. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/tools/test_read_image.py +0 -0
  130. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/tools/test_read_image_integration.py +0 -0
  131. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/tools/test_subprocess_cancellation.py +0 -0
  132. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/tools/test_write.py +0 -0
  133. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/ui/test_autocomplete.py +0 -0
  134. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/ui/test_floating_list.py +0 -0
  135. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/ui/test_input_handoff.py +0 -0
  136. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/ui/test_input_paste.py +0 -0
  137. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/ui/test_prompt_history.py +0 -0
  138. {kon_coding_agent-0.3.4 → kon_coding_agent-0.3.5}/tests/ui/test_status_line.py +0 -0
@@ -0,0 +1,33 @@
1
+ name: Test
2
+
3
+ on: [push, pull_request]
4
+
5
+ permissions:
6
+ contents: read
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.12","3.13", "3.14"]
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+ - name: Set up Python ${{ matrix.python-version }}
17
+ uses: actions/setup-python@v4
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ cache: pip
21
+ cache-dependency-path: pyproject.toml
22
+ - name: Cache models
23
+ uses: actions/cache@v3
24
+ with:
25
+ path: ~/.cache
26
+ key: ${{ runner.os }}-torch-
27
+ - name: Install dependencies
28
+ run: |
29
+ pip install . --group dev
30
+ - name: Run tests
31
+ run: |
32
+ python -m pytest -s
33
+
@@ -33,38 +33,44 @@ Use this skill when the user asks to cut a new Kon version, tag it, publish to P
33
33
  - `git status --short --branch` must be clean (or confirm with user)
34
34
  - `git tag --list` and `git log --oneline <prev_tag>..HEAD` to summarize changes
35
35
 
36
- 2. **Version bump**
36
+ 2. **Update CHANGELOG.md**
37
+ - Replace the `## [Unreleased]` section's `- No changes yet.` with a new versioned heading: `## <version> - YYYY-MM-DD`
38
+ - Use `git log --oneline <prev_tag>..HEAD` to categorize changes into `### Added`, `### Changed`, `### Fixed` sections
39
+ - Credit external contributors with `- @username`
40
+ - Commit message: `docs: update changelog for <version>`
41
+
42
+ 3. **Version bump**
37
43
  - Update version in all 3 files above
38
44
 
39
- 3. **Quality gates**
45
+ 4. **Quality gates**
40
46
  - `uv run ruff format .`
41
47
  - `uv run ruff check .`
42
48
  - `uv run pyright .`
43
49
  - `uv run pytest`
44
50
 
45
- 4. **Commit**
51
+ 5. **Commit**
46
52
  - Commit message: `build: bump version to <version>`
47
53
 
48
- 5. **Tag**
54
+ 6. **Tag**
49
55
  - Annotated tag: `git tag -a v<version> -m "v<version> ..."`
50
- - Include concise changes since previous tag bullets
56
+ - Include concise "changes since previous tag" bullets
51
57
 
52
- 6. **Push**
58
+ 7. **Push**
53
59
  - `git push origin main`
54
60
  - `git push origin v<version>`
55
61
 
56
- 7. **Build + verify artifacts**
62
+ 8. **Build + verify artifacts**
57
63
  - `rm -rf dist && uv build`
58
64
  - `uv run python -m twine check dist/*`
59
65
 
60
- 8. **Publish to PyPI**
66
+ 9. **Publish to PyPI**
61
67
  - Prefer token file if present (example `~/.pypi-token`):
62
68
  - `TWINE_USERNAME=__token__ TWINE_PASSWORD="$(< ~/.pypi-token)" uv run python -m twine upload dist/*`
63
69
  - Verify:
64
70
  - `https://pypi.org/project/kon-coding-agent/<version>/`
65
71
  - `https://pypi.org/pypi/kon-coding-agent/json` reports latest version
66
72
 
67
- 9. **Create GitHub release**
73
+ 10. **Create GitHub release**
68
74
  - If token exists at `~/.github-token`, call Releases API:
69
75
  - `POST /repos/<owner>/<repo>/releases` with:
70
76
  - `tag_name: v<version>`
@@ -83,6 +89,7 @@ Use this skill when the user asks to cut a new Kon version, tag it, publish to P
83
89
 
84
90
  ## Output checklist to report
85
91
 
92
+ - Changelog updated for `<version>`
86
93
  - Version bumped in all files
87
94
  - Checks passed
88
95
  - Commit hash
@@ -4,6 +4,7 @@
4
4
 
5
5
  - Don't add trivial docstrings. Only add docstrings when explaining complex functionality.
6
6
  - This project uses `uv`. Run `uv run ruff format .` after editing or creating any files.
7
+ - If generating and running a Python script, use `uv run python` instead of `python`.
7
8
 
8
9
  ## Testing
9
10
 
@@ -6,6 +6,70 @@ All notable changes to this project will be documented in this file.
6
6
 
7
7
  - No changes yet.
8
8
 
9
+ ## 0.3.5 - 2026-04-18
10
+
11
+ ### Added
12
+
13
+ - Standalone session HTML export with self-contained styling.
14
+ - Configurable request timeout for API calls - @jspruit.
15
+ - GitHub CI tests for Python 3.12–3.13 - @sukhbinder.
16
+ - Width-aware popup lists and queue display.
17
+
18
+ ### Changed
19
+
20
+ - Highlight color applied to the second column in floating lists.
21
+ - Batch scroll during streaming, cache query_one lookups, pause spinner timer when idle.
22
+ - Diff line length capped at 200 characters.
23
+
24
+ ### Fixed
25
+
26
+ - Persist thinking level in session header and change all defaults to high.
27
+ - Normalize OpenAI provider imports.
28
+ - Widen resume popup labels.
29
+ - Remove unused UI app import.
30
+
31
+ ### Performance
32
+
33
+ - Added `gc.freeze()` and `PAUSE_GC_ON_SCROLL` to reduce GC stutters.
34
+
35
+ ## 0.3.4 - 2026-04-10
36
+
37
+ ### Fixed
38
+
39
+ - Fixed Windows UTF-8 encoding errors in file operations - @sukhbinder.
40
+ - Fixed local Gemma model thinking block compatibility.
41
+ - Removed duplicate force-include for builtin skills in build config.
42
+
43
+ ### Changed
44
+
45
+ - Updated local model documentation.
46
+
47
+ ## 0.3.3 - 2026-04-08
48
+
49
+ ### Added
50
+
51
+ - Added queued agent steering between turns - @0xku.
52
+ - Added bundled `/init` slash command for project scaffolding - @0xku.
53
+ - Added help info for queue and steer queue commands.
54
+ - Added GLM-5.1 support for zai provider.
55
+
56
+ ### Changed
57
+
58
+ - Improved read tool directory listings.
59
+ - Updated README with steer queue documentation.
60
+ - Added `$` icon for bash, `%` for web tools, and `←` for edit tool.
61
+ - Used muted color for shortcut key hints in exit/delete prompts.
62
+
63
+ ### Fixed
64
+
65
+ - Let ESC interrupt retry backoff immediately - @0xku.
66
+ - Fixed OpenAI login stdin leak by removing orphaned thread - @Meltedd.
67
+ - Fixed OpenAI and Anthropic local compat with auth flags.
68
+ - Fixed interrupt handling before handoff thread switch.
69
+ - Fixed subprocess communication drain on cancellation/timeout.
70
+ - Added zipfile path traversal validation.
71
+ - Removed token throughput metrics.
72
+
9
73
  ## 0.3.2 - 2026-03-22
10
74
 
11
75
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kon-coding-agent
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Summary: Minimal coding agent
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -14,7 +14,7 @@ default = true
14
14
 
15
15
  [project]
16
16
  name = "kon-coding-agent"
17
- version = "0.3.4"
17
+ version = "0.3.5"
18
18
  description = "Minimal coding agent"
19
19
  readme = "README.md"
20
20
  requires-python = ">=3.12"
@@ -76,6 +76,7 @@ class LLMConfig(BaseModel):
76
76
  default_thinking_level: str
77
77
  system_prompt: SystemPromptConfig
78
78
  tool_call_idle_timeout_seconds: float = 180
79
+ request_timeout_seconds: float = 600
79
80
  auth: AuthConfig = AuthConfig()
80
81
 
81
82
 
@@ -11,6 +11,9 @@ default_thinking_level = "high"
11
11
  # Abort a tool call if it stays idle for this long during a turn.
12
12
  # Helps prevent stalled tool executions from hanging the agent loop.
13
13
  tool_call_idle_timeout_seconds = 180
14
+ # HTTP request timeout for LLM API calls (in seconds).
15
+ # Local models (e.g. llama.cpp) may need a higher value for long compaction requests.
16
+ request_timeout_seconds = 600
14
17
 
15
18
  [llm.auth]
16
19
  # Auth policy for OpenAI-compatible and Anthropic-compatible endpoints.
@@ -77,7 +77,7 @@ class ProviderConfig:
77
77
  model: str = ""
78
78
  max_tokens: int = 8192
79
79
  temperature: float | None = None
80
- thinking_level: str = "medium"
80
+ thinking_level: str = "high"
81
81
  provider: str | None = None
82
82
  session_id: str | None = None
83
83
  openai_compat_auth_mode: AuthMode = "auto"
@@ -10,6 +10,8 @@ from openai.types.chat import (
10
10
  ChatCompletionToolParam,
11
11
  )
12
12
 
13
+ from kon import config as kon_config
14
+
13
15
  from ...core.types import (
14
16
  AssistantMessage,
15
17
  ImageContent,
@@ -99,7 +101,11 @@ class OpenAICompletionsProvider(BaseProvider):
99
101
  "Set OPENAI_API_KEY or ZAI_API_KEY environment variable, "
100
102
  'or configure llm.auth.openai_compat = "auto"/"none" for local endpoints.'
101
103
  )
102
- self._client = AsyncOpenAI(api_key=api_key, base_url=config.base_url)
104
+ self._client = AsyncOpenAI(
105
+ api_key=api_key,
106
+ base_url=config.base_url,
107
+ timeout=kon_config.llm.request_timeout_seconds,
108
+ )
103
109
  self._compat = _detect_compat(
104
110
  config.provider or "", config.base_url or "", config.model or ""
105
111
  )
@@ -4,6 +4,8 @@ from typing import Any
4
4
 
5
5
  from openai import APIStatusError, AsyncOpenAI, RateLimitError
6
6
 
7
+ from kon import config as kon_config
8
+
7
9
  from ...core.types import (
8
10
  AssistantMessage,
9
11
  ImageContent,
@@ -51,6 +53,7 @@ class OpenAIResponsesProvider(BaseProvider):
51
53
  api_key=self.config.api_key,
52
54
  base_url=self.config.base_url,
53
55
  default_headers=self._headers,
56
+ timeout=kon_config.llm.request_timeout_seconds,
54
57
  )
55
58
  return self._client
56
59
 
@@ -6,15 +6,18 @@ with a type field. The first line is always the session header.
6
6
 
7
7
  Structure:
8
8
  {"type": "header", "id": "...", "version": 1, "timestamp": "...",
9
- "cwd": "...", "system_prompt": "..."}
9
+ "cwd": "...", "system_prompt": "...", "tools": ["read", "edit", ...]}
10
10
  {"type": "message", "id": "...", "parent_id": "...", "timestamp": "...", "message": {...}}
11
11
  {"type": "message", "id": "...", "parent_id": "...", "timestamp": "...", "message": {...}}
12
12
  ...
13
13
  """
14
14
 
15
+ from __future__ import annotations
16
+
15
17
  import json
16
18
  import re
17
19
  import uuid
20
+ from dataclasses import dataclass
18
21
  from datetime import UTC, datetime
19
22
  from pathlib import Path
20
23
  from typing import Any, Literal
@@ -23,7 +26,15 @@ from pydantic import BaseModel
23
26
 
24
27
  from kon import CONFIG_DIR_NAME
25
28
 
26
- from .core.types import AssistantMessage, Message, StopReason, TextContent, UserMessage
29
+ from .core.types import (
30
+ AssistantMessage,
31
+ Message,
32
+ StopReason,
33
+ TextContent,
34
+ ToolCall,
35
+ ToolResultMessage,
36
+ UserMessage,
37
+ )
27
38
 
28
39
  CURRENT_VERSION = 1
29
40
  _SKILL_TRIGGER_HEADER_RE = re.compile(r"^\[([a-z0-9-]+)\]\s*$")
@@ -40,6 +51,8 @@ class SessionHeader(BaseModel):
40
51
  timestamp: str
41
52
  cwd: str
42
53
  system_prompt: str | None = None
54
+ tools: list[str] | None = None
55
+ initial_thinking_level: str = "high"
43
56
 
44
57
 
45
58
  class EntryBase(BaseModel):
@@ -107,6 +120,36 @@ class SessionInfo(BaseModel):
107
120
  first_message: str
108
121
 
109
122
 
123
+ @dataclass(frozen=True)
124
+ class SessionTokenTotals:
125
+ input_tokens: int = 0
126
+ output_tokens: int = 0
127
+ context_tokens: int = 0
128
+ cache_read_tokens: int = 0
129
+ cache_write_tokens: int = 0
130
+
131
+ @property
132
+ def total_tokens(self) -> int:
133
+ return (
134
+ self.input_tokens
135
+ + self.output_tokens
136
+ + self.cache_read_tokens
137
+ + self.cache_write_tokens
138
+ )
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class SessionMessageCounts:
143
+ user_messages: int = 0
144
+ assistant_messages: int = 0
145
+ tool_calls: int = 0
146
+ tool_results: int = 0
147
+
148
+ @property
149
+ def total_messages(self) -> int:
150
+ return self.user_messages + self.assistant_messages
151
+
152
+
110
153
  class Session:
111
154
  """
112
155
  Manages conversation persistence as append-only JSONL.
@@ -148,7 +191,7 @@ class Session:
148
191
  persist: bool = True,
149
192
  initial_provider: str | None = None,
150
193
  initial_model_id: str | None = None,
151
- initial_thinking_level: str = "medium",
194
+ initial_thinking_level: str = "high",
152
195
  ):
153
196
  self._id = session_id
154
197
  self._cwd = cwd
@@ -186,6 +229,14 @@ class Session:
186
229
  def system_prompt(self) -> str | None:
187
230
  return self._header.system_prompt if self._header else None
188
231
 
232
+ @property
233
+ def tools(self) -> list[str] | None:
234
+ return self._header.tools if self._header else None
235
+
236
+ @property
237
+ def created_at(self) -> str | None:
238
+ return self._header.timestamp if self._header else None
239
+
189
240
  @property
190
241
  def leaf_id(self) -> str | None:
191
242
  return self._leaf_id
@@ -388,6 +439,72 @@ class Session:
388
439
 
389
440
  return None
390
441
 
442
+ def token_totals(self) -> SessionTokenTotals:
443
+ input_tokens = 0
444
+ output_tokens = 0
445
+ cache_read_tokens = 0
446
+ cache_write_tokens = 0
447
+ context_tokens = 0
448
+
449
+ for entry in self._entries:
450
+ if isinstance(entry, MessageEntry) and isinstance(entry.message, AssistantMessage):
451
+ usage = entry.message.usage
452
+ if usage is None:
453
+ continue
454
+ input_tokens += usage.input_tokens
455
+ output_tokens += usage.output_tokens
456
+ cache_read_tokens += usage.cache_read_tokens
457
+ cache_write_tokens += usage.cache_write_tokens
458
+ context_tokens = (
459
+ usage.input_tokens
460
+ + usage.output_tokens
461
+ + usage.cache_read_tokens
462
+ + usage.cache_write_tokens
463
+ )
464
+
465
+ return SessionTokenTotals(
466
+ input_tokens=input_tokens,
467
+ output_tokens=output_tokens,
468
+ context_tokens=context_tokens,
469
+ cache_read_tokens=cache_read_tokens,
470
+ cache_write_tokens=cache_write_tokens,
471
+ )
472
+
473
+ def file_changes_summary(self) -> dict[str, tuple[int, int]]:
474
+ file_changes: dict[str, tuple[int, int]] = {}
475
+ for entry in self._entries:
476
+ if isinstance(entry, MessageEntry) and isinstance(entry.message, ToolResultMessage):
477
+ fc = entry.message.file_changes
478
+ if fc:
479
+ prev_added, prev_removed = file_changes.get(fc.path, (0, 0))
480
+ file_changes[fc.path] = (prev_added + fc.added, prev_removed + fc.removed)
481
+ return file_changes
482
+
483
+ def message_counts(self) -> SessionMessageCounts:
484
+ user_messages = 0
485
+ assistant_messages = 0
486
+ tool_calls = 0
487
+ tool_results = 0
488
+
489
+ for entry in self._entries:
490
+ if not isinstance(entry, MessageEntry):
491
+ continue
492
+ message = entry.message
493
+ if isinstance(message, UserMessage):
494
+ user_messages += 1
495
+ elif isinstance(message, AssistantMessage):
496
+ assistant_messages += 1
497
+ tool_calls += sum(1 for part in message.content if isinstance(part, ToolCall))
498
+ elif isinstance(message, ToolResultMessage):
499
+ tool_results += 1
500
+
501
+ return SessionMessageCounts(
502
+ user_messages=user_messages,
503
+ assistant_messages=assistant_messages,
504
+ tool_calls=tool_calls,
505
+ tool_results=tool_results,
506
+ )
507
+
391
508
  @property
392
509
  def name(self) -> str | None:
393
510
  for entry in reversed(self._entries):
@@ -435,9 +552,10 @@ class Session:
435
552
  persist: bool = True,
436
553
  provider: str | None = None,
437
554
  model_id: str | None = None,
438
- thinking_level: str = "medium",
555
+ thinking_level: str = "high",
439
556
  system_prompt: str | None = None,
440
- ) -> "Session":
557
+ tools: list[str] | None = None,
558
+ ) -> Session:
441
559
  session_id = str(uuid.uuid4())
442
560
  timestamp = _now_iso()
443
561
 
@@ -450,7 +568,12 @@ class Session:
450
568
  initial_thinking_level=thinking_level,
451
569
  )
452
570
  session._header = SessionHeader(
453
- id=session_id, timestamp=timestamp, cwd=cwd, system_prompt=system_prompt
571
+ id=session_id,
572
+ timestamp=timestamp,
573
+ cwd=cwd,
574
+ system_prompt=system_prompt,
575
+ tools=tools,
576
+ initial_thinking_level=thinking_level,
454
577
  )
455
578
 
456
579
  if persist:
@@ -461,7 +584,7 @@ class Session:
461
584
  return session
462
585
 
463
586
  @classmethod
464
- def load(cls, path: Path | str) -> "Session":
587
+ def load(cls, path: Path | str) -> Session:
465
588
  path = Path(path)
466
589
  if not path.exists():
467
590
  raise FileNotFoundError(f"Session file not found: {path}")
@@ -500,7 +623,13 @@ class Session:
500
623
  if not header:
501
624
  raise ValueError(f"Invalid session file (no header): {path}")
502
625
 
503
- session = cls(session_id=header.id, cwd=header.cwd, session_file=path, persist=True)
626
+ session = cls(
627
+ session_id=header.id,
628
+ cwd=header.cwd,
629
+ session_file=path,
630
+ persist=True,
631
+ initial_thinking_level=header.initial_thinking_level,
632
+ )
504
633
  session._header = header
505
634
  session._entries = entries
506
635
  session._by_id = {e.id: e for e in entries}
@@ -516,9 +645,9 @@ class Session:
516
645
  cwd: str,
517
646
  provider: str | None = None,
518
647
  model_id: str | None = None,
519
- thinking_level: str = "medium",
648
+ thinking_level: str = "high",
520
649
  system_prompt: str | None = None,
521
- ) -> "Session":
650
+ ) -> Session:
522
651
  sessions_dir = cls.get_sessions_dir(cwd)
523
652
 
524
653
  jsonl_files = list(sessions_dir.glob("*.jsonl"))
@@ -535,7 +664,7 @@ class Session:
535
664
  return cls.load(most_recent)
536
665
 
537
666
  @classmethod
538
- def continue_by_id(cls, cwd: str, session_id: str) -> "Session":
667
+ def continue_by_id(cls, cwd: str, session_id: str) -> Session:
539
668
  normalized_id = session_id.strip().lower()
540
669
  if not normalized_id:
541
670
  raise ValueError("Session ID cannot be empty")
@@ -652,9 +781,10 @@ class Session:
652
781
  cwd: str = ".",
653
782
  provider: str | None = None,
654
783
  model_id: str | None = None,
655
- thinking_level: str = "medium",
784
+ thinking_level: str = "high",
656
785
  system_prompt: str | None = None,
657
- ) -> "Session":
786
+ tools: list[str] | None = None,
787
+ ) -> Session:
658
788
  return cls.create(
659
789
  cwd,
660
790
  persist=False,
@@ -662,4 +792,5 @@ class Session:
662
792
  model_id=model_id,
663
793
  thinking_level=thinking_level,
664
794
  system_prompt=system_prompt,
795
+ tools=tools,
665
796
  )
@@ -140,7 +140,7 @@ def format_diff_display(diff: str) -> str:
140
140
  if not line:
141
141
  continue
142
142
 
143
- truncated = line[:102] + "..." if len(line) > 105 else line
143
+ truncated = line[:200] + "..." if len(line) > 203 else line
144
144
  escaped = truncated.replace("[", "\\[")
145
145
 
146
146
  if line.startswith("-"):
@@ -60,7 +60,7 @@ from ..llm import (
60
60
  resolve_provider_api_type,
61
61
  )
62
62
  from ..llm.base import AuthMode
63
- from ..loop import Agent, build_system_prompt
63
+ from ..loop import Agent
64
64
  from ..permissions import ApprovalResponse
65
65
  from ..session import Session
66
66
  from ..tools import DEFAULT_TOOLS, EXTRA_TOOLS, get_tool, get_tools
@@ -94,7 +94,7 @@ _CHANGELOG_URL = "https://github.com/0xku/kon/blob/main/CHANGELOG.md"
94
94
  try:
95
95
  VERSION = version(_PYPI_PACKAGE_NAME)
96
96
  except PackageNotFoundError:
97
- VERSION = "0.3.4"
97
+ VERSION = "0.3.5"
98
98
 
99
99
  _COPILOT_API_TYPES: frozenset[ApiType] = frozenset(
100
100
  {ApiType.GITHUB_COPILOT, ApiType.GITHUB_COPILOT_RESPONSES, ApiType.ANTHROPIC_COPILOT}
@@ -117,6 +117,7 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
117
117
  CSS = get_styles()
118
118
  TITLE = "kon"
119
119
  VERSION = VERSION
120
+ PAUSE_GC_ON_SCROLL = True
120
121
 
121
122
  BINDINGS: ClassVar[list] = [
122
123
  ("ctrl+c", "handle_ctrl_c", "Clear"),
@@ -324,7 +325,7 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
324
325
  provider=self._model_provider,
325
326
  model_id=self._model,
326
327
  thinking_level=self._thinking_level,
327
- system_prompt=build_system_prompt(self._cwd, tools=self._tools),
328
+ system_prompt=self._resolve_system_prompt(None),
328
329
  )
329
330
  except Exception as e:
330
331
  self._add_launch_warning(str(e), severity="error")
@@ -375,7 +376,7 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
375
376
  if self._provider:
376
377
  valid_levels = self._provider.thinking_levels
377
378
  if self._thinking_level not in valid_levels:
378
- self._thinking_level = valid_levels[0] if valid_levels else "medium"
379
+ self._thinking_level = valid_levels[0] if valid_levels else "high"
379
380
  self._provider.set_thinking_level(self._thinking_level)
380
381
 
381
382
  self._session_start_time = time.time()
@@ -388,13 +389,13 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
388
389
  else (self._provider.name if self._provider else self._model_provider)
389
390
  )
390
391
  self._model_provider = model_provider
391
- system_prompt = build_system_prompt(self._cwd, tools=self._tools)
392
392
  self._session = Session.create(
393
393
  self._cwd,
394
394
  provider=model_provider,
395
395
  model_id=self._model,
396
396
  thinking_level=self._thinking_level,
397
- system_prompt=system_prompt,
397
+ system_prompt=self._resolve_system_prompt(None),
398
+ tools=[t.name for t in self._tools],
398
399
  )
399
400
  if model_provider:
400
401
  self._session.append_model_change(model_provider, self._model, base_url)
@@ -402,15 +403,12 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
402
403
  # Create Agent once — it owns context + system prompt (stable across queries
403
404
  # for prompt-prefix caching on llama-server and similar engines).
404
405
  if self._provider is not None and self._session is not None:
405
- system_prompt = self._session.system_prompt or build_system_prompt(
406
- self._cwd, tools=self._tools
407
- )
408
406
  self._agent = Agent(
409
407
  provider=self._provider,
410
408
  tools=self._tools,
411
409
  session=self._session,
412
410
  cwd=self._cwd,
413
- system_prompt=system_prompt,
411
+ system_prompt=self._resolve_system_prompt(self._session),
414
412
  )
415
413
 
416
414
  self._sync_slash_commands()
@@ -435,8 +433,6 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
435
433
  self._flush_launch_warnings(chat)
436
434
 
437
435
  info_bar = self.query_one("#info-bar", InfoBar)
438
- if self._session:
439
- info_bar.set_session_id(self._session.id[:8])
440
436
  model_provider = (
441
437
  model_info.provider
442
438
  if model_info
@@ -453,11 +449,15 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
453
449
  and self._session.entries
454
450
  ):
455
451
  self._render_session_entries(self._session)
456
- input_t, output_t, context_t, cache_read_t, cache_write_t = (
457
- self._calculate_session_tokens(self._session)
452
+ token_totals = self._session.token_totals()
453
+ info_bar.set_tokens(
454
+ token_totals.input_tokens,
455
+ token_totals.output_tokens,
456
+ token_totals.context_tokens,
457
+ token_totals.cache_read_tokens,
458
+ token_totals.cache_write_tokens,
458
459
  )
459
- info_bar.set_tokens(input_t, output_t, context_t, cache_read_t, cache_write_t)
460
- info_bar.set_file_changes(self._calculate_session_file_changes(self._session))
460
+ info_bar.set_file_changes(self._session.file_changes_summary())
461
461
  chat.add_info_message("Resumed session")
462
462
 
463
463
  if self._provider and self._session:
@@ -467,6 +467,10 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
467
467
  self._show_pending_update_notice_if_idle()
468
468
  input_box.focus()
469
469
 
470
+ import gc
471
+
472
+ gc.freeze()
473
+
470
474
  async def _collect_file_paths(self) -> None:
471
475
  """Collect file paths using glob (fallback when fd is unavailable)."""
472
476
  patterns = [
@@ -853,15 +857,12 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
853
857
  return
854
858
 
855
859
  if self._agent is None:
856
- system_prompt = self._session.system_prompt or build_system_prompt(
857
- self._cwd, tools=self._tools
858
- )
859
860
  self._agent = Agent(
860
861
  provider=self._provider,
861
862
  tools=self._tools,
862
863
  session=self._session,
863
864
  cwd=self._cwd,
864
- system_prompt=system_prompt,
865
+ system_prompt=self._resolve_system_prompt(self._session),
865
866
  )
866
867
 
867
868
  current_prompt = prompt
@@ -1180,7 +1181,7 @@ def main():
1180
1181
 
1181
1182
  if app._session:
1182
1183
  session_id = app._session.id
1183
- file_changes = SessionUIMixin._calculate_session_file_changes(app._session) or None
1184
+ file_changes = app._session.file_changes_summary() or None
1184
1185
  if app._session_start_time is not None:
1185
1186
  duration = time.time() - app._session_start_time
1186
1187