kon-coding-agent 0.2.2__tar.gz → 0.2.4__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 (105) hide show
  1. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/PKG-INFO +7 -7
  2. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/README.md +6 -6
  3. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/pyproject.toml +1 -1
  4. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/config.py +1 -0
  5. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/defaults/config.toml +1 -0
  6. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/loop.py +20 -16
  7. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/turn.py +0 -1
  8. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/app.py +56 -30
  9. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/app_protocol.py +2 -4
  10. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/commands.py +8 -12
  11. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/input.py +6 -25
  12. kon_coding_agent-0.2.4/src/kon/ui/prompt_history.py +98 -0
  13. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/session_ui.py +4 -8
  14. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/test_agentic_loop.py +3 -2
  15. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/ui/test_input_paste.py +1 -1
  16. kon_coding_agent-0.2.4/tests/ui/test_prompt_history.py +98 -0
  17. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/uv.lock +1 -1
  18. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/.gitignore +0 -0
  19. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/.kon/skills/kon-release-publish/SKILL.md +0 -0
  20. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/.kon/skills/kon-tmux-test/SKILL.md +0 -0
  21. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/.kon/skills/kon-tmux-test/run-e2e-tests.sh +0 -0
  22. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/.kon/skills/kon-tmux-test/setup-test-project.sh +0 -0
  23. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/.python-version +0 -0
  24. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/AGENTS.md +0 -0
  25. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/LICENSE +0 -0
  26. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/LOCAL.md +0 -0
  27. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/TODO.md +0 -0
  28. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/scripts/test_models.py +0 -0
  29. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/scripts/test_thinking_blocks.py +0 -0
  30. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/__init__.py +0 -0
  31. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/context/__init__.py +0 -0
  32. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/context/agents.py +0 -0
  33. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/context/loader.py +0 -0
  34. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/context/shared.py +0 -0
  35. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/context/skills.py +0 -0
  36. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/core/__init__.py +0 -0
  37. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/core/compaction.py +0 -0
  38. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/core/types.py +0 -0
  39. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/defaults/__init__.py +0 -0
  40. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/events.py +0 -0
  41. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/__init__.py +0 -0
  42. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/base.py +0 -0
  43. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/models.py +0 -0
  44. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/oauth/__init__.py +0 -0
  45. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/oauth/copilot.py +0 -0
  46. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/oauth/openai.py +0 -0
  47. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/providers/__init__.py +0 -0
  48. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/providers/anthropic.py +0 -0
  49. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/providers/copilot.py +0 -0
  50. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/providers/copilot_anthropic.py +0 -0
  51. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/providers/github_copilot_headers.py +0 -0
  52. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/providers/mock.py +0 -0
  53. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/providers/openai_codex_responses.py +0 -0
  54. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/providers/openai_completions.py +0 -0
  55. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/providers/openai_responses.py +0 -0
  56. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/llm/providers/sanitize.py +0 -0
  57. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/py.typed +0 -0
  58. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/session.py +0 -0
  59. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/shared.py +0 -0
  60. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/tools/__init__.py +0 -0
  61. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/tools/_read_image.py +0 -0
  62. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/tools/base.py +0 -0
  63. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/tools/bash.py +0 -0
  64. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/tools/edit.py +0 -0
  65. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/tools/find.py +0 -0
  66. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/tools/grep.py +0 -0
  67. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/tools/read.py +0 -0
  68. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/tools/write.py +0 -0
  69. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/tools_manager.py +0 -0
  70. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/__init__.py +0 -0
  71. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/autocomplete.py +0 -0
  72. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/blocks.py +0 -0
  73. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/chat.py +0 -0
  74. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/clipboard.py +0 -0
  75. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/export.py +0 -0
  76. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/floating_list.py +0 -0
  77. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/formatting.py +0 -0
  78. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/path_complete.py +0 -0
  79. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/selection_mode.py +0 -0
  80. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/styles.py +0 -0
  81. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/ui/widgets.py +0 -0
  82. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/src/kon/update_check.py +0 -0
  83. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/conftest.py +0 -0
  84. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/context/test_agents.py +0 -0
  85. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/context/test_skills.py +0 -0
  86. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/llm/__init__.py +0 -0
  87. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/llm/test_mock_provider.py +0 -0
  88. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/test_cli_provider_resolution.py +0 -0
  89. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/test_compaction.py +0 -0
  90. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/test_config_binaries.py +0 -0
  91. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/test_config_error_fallback.py +0 -0
  92. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/test_config_injection.py +0 -0
  93. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/test_model_provider_resolution.py +0 -0
  94. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/test_session_persistence.py +0 -0
  95. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/test_system_prompt.py +0 -0
  96. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/test_update_check.py +0 -0
  97. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/test_update_notice_behavior.py +0 -0
  98. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/tools/test_diff.py +0 -0
  99. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/tools/test_edit.py +0 -0
  100. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/tools/test_read.py +0 -0
  101. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/tools/test_read_image.py +0 -0
  102. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/tools/test_read_image_integration.py +0 -0
  103. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/tools/test_write.py +0 -0
  104. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/ui/test_autocomplete.py +0 -0
  105. {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.4}/tests/ui/test_floating_list.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kon-coding-agent
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Minimal coding agent
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -35,12 +35,9 @@ $ fd . | cut -d/ -f1 | sort | uniq -c | sort -rn
35
35
  108 kon
36
36
  ```
37
37
 
38
- ## Setup
39
-
40
- ### Warning
38
+ [Kon](https://bleach.fandom.com/wiki/Kon) is inspired from Bleach, a artificial soul
41
39
 
42
- > [!WARNING]
43
- > Platform support: macOS and Linux are supported; Windows is not tested yet.
40
+ ## Setup
44
41
 
45
42
  ### Prerequisites
46
43
 
@@ -57,11 +54,14 @@ This installs `kon` globally as a CLI tool.
57
54
  ### Install from source (advanced)
58
55
 
59
56
  ```bash
60
- git clone <repository-url>
57
+ git clone https://github.com/kuutsav/kon
61
58
  cd kon
62
59
  uv tool install .
63
60
  ```
64
61
 
62
+ > [!WARNING]
63
+ > Platform support: macOS and Linux are supported; Windows is not tested yet.
64
+
65
65
  ### Run
66
66
 
67
67
  ```bash
@@ -19,12 +19,9 @@ $ fd . | cut -d/ -f1 | sort | uniq -c | sort -rn
19
19
  108 kon
20
20
  ```
21
21
 
22
- ## Setup
23
-
24
- ### Warning
22
+ [Kon](https://bleach.fandom.com/wiki/Kon) is inspired from Bleach, a artificial soul
25
23
 
26
- > [!WARNING]
27
- > Platform support: macOS and Linux are supported; Windows is not tested yet.
24
+ ## Setup
28
25
 
29
26
  ### Prerequisites
30
27
 
@@ -41,11 +38,14 @@ This installs `kon` globally as a CLI tool.
41
38
  ### Install from source (advanced)
42
39
 
43
40
  ```bash
44
- git clone <repository-url>
41
+ git clone https://github.com/kuutsav/kon
45
42
  cd kon
46
43
  uv tool install .
47
44
  ```
48
45
 
46
+ > [!WARNING]
47
+ > Platform support: macOS and Linux are supported; Windows is not tested yet.
48
+
49
49
  ### Run
50
50
 
51
51
  ```bash
@@ -14,7 +14,7 @@ default = true
14
14
 
15
15
  [project]
16
16
  name = "kon-coding-agent"
17
- version = "0.2.2"
17
+ version = "0.2.4"
18
18
  description = "Minimal coding agent"
19
19
  readme = "README.md"
20
20
  requires-python = ">=3.12"
@@ -57,6 +57,7 @@ class UIConfig(BaseModel):
57
57
  class LLMConfig(BaseModel):
58
58
  default_provider: str
59
59
  default_model: str
60
+ default_base_url: str = ""
60
61
  default_thinking_level: str
61
62
  system_prompt: str
62
63
  tool_call_idle_timeout_seconds: float = 60
@@ -1,6 +1,7 @@
1
1
  [llm]
2
2
  default_provider = "openai-codex" # "zhipu", "github-copilot", "openai-codex"
3
3
  default_model = "gpt-5.3-codex"
4
+ default_base_url = ""
4
5
  default_thinking_level = "high"
5
6
  tool_call_idle_timeout_seconds = 120
6
7
  system_prompt = """You are an expert coding assistant called `Kon`.
@@ -46,14 +46,6 @@ from .turn import run_single_turn
46
46
 
47
47
 
48
48
  def build_system_prompt(cwd: str, context: Context | None = None) -> str:
49
- """
50
- Build the system prompt with tools, guidelines, context files, and skills.
51
-
52
- Args:
53
- cwd: Working directory
54
- context: Pre-loaded context (agents files and skills). If None, will load fresh.
55
- """
56
-
57
49
  now = datetime.now()
58
50
  date_time = now.strftime("%A, %B %d, %Y at %I:%M %p %Z").strip()
59
51
 
@@ -77,9 +69,6 @@ def build_system_prompt(cwd: str, context: Context | None = None) -> str:
77
69
  @dataclass
78
70
  class AgentConfig:
79
71
  max_turns: int | None = None
80
- system_prompt: str | None = None
81
- cwd: str | None = None
82
- context: Context | None = None
83
72
  context_window: int | None = None
84
73
  max_output_tokens: int | None = None
85
74
 
@@ -90,14 +79,32 @@ class Agent:
90
79
  provider: BaseProvider,
91
80
  tools: list[BaseTool],
92
81
  session: Session,
82
+ cwd: str | None = None,
83
+ context: Context | None = None,
84
+ system_prompt: str | None = None,
93
85
  config: AgentConfig | None = None,
94
86
  ):
95
87
  self.provider = provider
96
88
  self.tools = tools
97
89
  self.session = session
98
90
  self.config = config or AgentConfig()
91
+ self._cwd = cwd or os.getcwd()
92
+ self._context = context or Context.load(self._cwd)
93
+ self._system_prompt = system_prompt or build_system_prompt(self._cwd, self._context)
99
94
  self._run_usage = Usage()
100
95
 
96
+ @property
97
+ def context(self) -> Context:
98
+ return self._context
99
+
100
+ @property
101
+ def system_prompt(self) -> str:
102
+ return self._system_prompt
103
+
104
+ def reload_context(self) -> None:
105
+ self._context = Context.load(self._cwd)
106
+ self._system_prompt = build_system_prompt(self._cwd, self._context)
107
+
101
108
  @property
102
109
  def messages(self) -> list[Message]:
103
110
  return self.session.messages
@@ -131,10 +138,7 @@ class Agent:
131
138
  stop_reason = StopReason.STOP
132
139
  was_interrupted = False
133
140
 
134
- # Build system prompt ONCE before the loop to enable prompt caching
135
- # If we don't do this date_time might change during execution invalidating cache
136
- cwd = self.config.cwd or os.getcwd()
137
- system_prompt = self.config.system_prompt or build_system_prompt(cwd, self.config.context)
141
+ system_prompt = self._system_prompt
138
142
 
139
143
  try:
140
144
  max_turns = (
@@ -225,7 +229,7 @@ class Agent:
225
229
  return
226
230
 
227
231
  context_window = self.config.context_window or kon_config.agent.default_context_window
228
- max_output = self.config.max_output_tokens or self.provider.config.max_tokens
232
+ max_output = self.config.max_output_tokens or self.provider.config.max_tokens or 0
229
233
  buffer_tokens = kon_config.compaction.buffer_tokens
230
234
 
231
235
  if not is_overflow(last_usage, context_window, max_output, buffer_tokens):
@@ -77,7 +77,6 @@ _TOOL_ARGS_TOKEN_CHUNK_UPDATE_INTERVAL = 4
77
77
 
78
78
 
79
79
  def _count_tokens(text: str) -> int:
80
- """Estimate token count from text (approx 4 chars per token)."""
81
80
  return len(text) // 4
82
81
 
83
82
 
@@ -11,14 +11,13 @@ from pathlib import Path
11
11
  from typing import ClassVar
12
12
 
13
13
  from rich.console import Console
14
- from textual import on
14
+ from textual import events, on
15
15
  from textual.app import App, ComposeResult
16
16
  from textual.binding import Binding
17
17
 
18
18
  from kon import config, consume_config_warnings, update_available_binaries
19
19
  from kon.tools_manager import ensure_tools
20
20
 
21
- from ..context import Context
22
21
  from ..core.types import StopReason
23
22
  from ..events import (
24
23
  AgentEndEvent,
@@ -54,7 +53,7 @@ from ..llm import (
54
53
  is_openai_logged_in,
55
54
  resolve_provider_api_type,
56
55
  )
57
- from ..loop import Agent, AgentConfig
56
+ from ..loop import Agent
58
57
  from ..session import Session
59
58
  from ..tools import DEFAULT_TOOLS, get_tools
60
59
  from ..update_check import get_newer_pypi_version
@@ -85,7 +84,7 @@ _PYPI_PACKAGE_NAME = _get_package_name()
85
84
  try:
86
85
  VERSION = version(_PYPI_PACKAGE_NAME)
87
86
  except PackageNotFoundError:
88
- VERSION = "0.2.2"
87
+ VERSION = "0.2.4"
89
88
 
90
89
  _COPILOT_API_TYPES: frozenset[ApiType] = frozenset(
91
90
  {ApiType.GITHUB_COPILOT, ApiType.GITHUB_COPILOT_RESPONSES, ApiType.ANTHROPIC_COPILOT}
@@ -137,7 +136,7 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
137
136
  else (config.llm.default_provider if model is None else None)
138
137
  )
139
138
  self._api_key = api_key
140
- self._base_url = base_url
139
+ self._base_url = base_url or config.llm.default_base_url or None
141
140
  self._resume_session = resume_session
142
141
  self._continue_recent = continue_recent
143
142
  self._thinking_level = thinking_level or config.llm.default_thinking_level
@@ -159,7 +158,6 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
159
158
  self._provider: BaseProvider | None = None
160
159
  self._session: Session | None = None
161
160
  self._agent: Agent | None = None
162
- self._project_context: Context | None = None
163
161
 
164
162
  self._pending_update_notice_version: str | None = None
165
163
  self._update_notice_shown = False
@@ -192,6 +190,12 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
192
190
  def _get_provider_api_type(self, provider: BaseProvider) -> ApiType:
193
191
  return _API_TYPE_BY_PROVIDER.get(type(provider), ApiType.OPENAI_COMPLETIONS)
194
192
 
193
+ @on(events.TextSelected)
194
+ def _on_text_selected(self) -> None:
195
+ selection = self.screen.get_selected_text()
196
+ if selection:
197
+ self.copy_to_clipboard(selection)
198
+
195
199
  def on_mount(self) -> None:
196
200
  if config.binaries.fd:
197
201
  self._fd_path = shutil.which("fd") or shutil.which("fdfind")
@@ -254,21 +258,25 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
254
258
  thinking_level=self._thinking_level,
255
259
  )
256
260
 
261
+ provider_error: str | None = None
257
262
  try:
258
263
  self._provider = self._create_provider(api_type, provider_config)
259
264
  except ValueError as e:
260
- chat = self.query_one("#chat-log", ChatLog)
261
- chat.add_info_message(str(e), error=True)
262
- return
265
+ provider_error = str(e)
263
266
 
264
- valid_levels = self._provider.thinking_levels
265
- if self._thinking_level not in valid_levels:
266
- self._thinking_level = valid_levels[0] if valid_levels else "medium"
267
- self._provider.set_thinking_level(self._thinking_level)
267
+ if self._provider:
268
+ valid_levels = self._provider.thinking_levels
269
+ if self._thinking_level not in valid_levels:
270
+ self._thinking_level = valid_levels[0] if valid_levels else "medium"
271
+ self._provider.set_thinking_level(self._thinking_level)
268
272
 
269
273
  if not self._continue_recent and not self._resume_session:
270
274
  selected_model = get_model(self._model, self._model_provider)
271
- model_provider = selected_model.provider if selected_model else self._provider.name
275
+ model_provider = (
276
+ selected_model.provider
277
+ if selected_model
278
+ else (self._provider.name if self._provider else self._model_provider)
279
+ )
272
280
  self._model_provider = model_provider
273
281
  self._session = Session.create(
274
282
  self._cwd,
@@ -276,19 +284,31 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
276
284
  model_id=self._model,
277
285
  thinking_level=self._thinking_level,
278
286
  )
279
- self._session.append_model_change(model_provider, self._model, base_url)
280
-
281
- self._project_context = Context.load(self._cwd)
282
- # TODO: Surface self._project_context.skill_warnings in UI (e.g. chat info/error messages)
283
- # so skill load/validation issues are visible to users.
287
+ if model_provider:
288
+ self._session.append_model_change(model_provider, self._model, base_url)
289
+
290
+ # Create Agent once it owns context + system prompt (stable across queries
291
+ # for prompt-prefix caching on llama-server and similar engines).
292
+ if self._provider is not None and self._session is not None:
293
+ self._agent = Agent(
294
+ provider=self._provider,
295
+ tools=get_tools(DEFAULT_TOOLS),
296
+ session=self._session,
297
+ cwd=self._cwd,
298
+ )
284
299
 
285
300
  chat = self.query_one("#chat-log", ChatLog)
286
301
  chat.add_session_info(VERSION)
287
302
 
288
- chat.add_loaded_resources(
289
- context_paths=[format_path(f.path) for f in self._project_context.agents_files],
290
- skill_paths=[format_path(s.file_path) for s in self._project_context.skills],
291
- )
303
+ if self._agent:
304
+ # TODO: Surface self._agent.context.skill_warnings in UI
305
+ chat.add_loaded_resources(
306
+ context_paths=[format_path(f.path) for f in self._agent.context.agents_files],
307
+ skill_paths=[format_path(s.file_path) for s in self._agent.context.skills],
308
+ )
309
+
310
+ if provider_error:
311
+ chat.add_info_message(provider_error, error=True)
292
312
 
293
313
  for warning in consume_config_warnings():
294
314
  chat.add_info_message(warning, warning=True)
@@ -572,6 +592,14 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
572
592
  self._is_running = False
573
593
  return
574
594
 
595
+ if self._agent is None:
596
+ self._agent = Agent(
597
+ provider=self._provider,
598
+ tools=get_tools(DEFAULT_TOOLS),
599
+ session=self._session,
600
+ cwd=self._cwd,
601
+ )
602
+
575
603
  current_prompt = prompt
576
604
 
577
605
  while True:
@@ -583,14 +611,12 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
583
611
  if self._interrupt_requested:
584
612
  self._cancel_event.set()
585
613
 
586
- tools = get_tools(DEFAULT_TOOLS)
587
614
  model_info = get_model(self._model, self._model_provider)
588
- agent_config = AgentConfig(
589
- system_prompt=self._get_system_prompt(),
590
- context_window=model_info.context_window if model_info else None,
591
- max_output_tokens=model_info.max_tokens if model_info else None,
592
- )
593
- self._agent = Agent(self._provider, tools, self._session, agent_config)
615
+ self._agent.provider = self._provider
616
+ self._agent.session = self._session
617
+ self._agent.tools = get_tools(DEFAULT_TOOLS)
618
+ self._agent.config.context_window = model_info.context_window if model_info else None
619
+ self._agent.config.max_output_tokens = model_info.max_tokens if model_info else None
594
620
 
595
621
  status.set_status("working")
596
622
 
@@ -1,6 +1,5 @@
1
- from typing import Protocol
1
+ from typing import Any, Protocol
2
2
 
3
- from ..context import Context
4
3
  from ..llm import ApiType, BaseProvider, ProviderConfig
5
4
  from ..session import Session
6
5
  from .selection_mode import SelectionMode
@@ -20,7 +19,7 @@ class Kon(Protocol):
20
19
  _selection_mode: SelectionMode | None
21
20
  _provider: BaseProvider | None
22
21
  _session: Session | None
23
- _project_context: Context | None
22
+ _agent: Any
24
23
 
25
24
  # Methods expected by mixins
26
25
  def exit(self) -> None: ...
@@ -32,4 +31,3 @@ class Kon(Protocol):
32
31
  # Methods expected by SessionUIMixin
33
32
  def _get_provider_api_type(self, provider: BaseProvider) -> ApiType: ...
34
33
  def _create_provider(self, api_type: ApiType, config: ProviderConfig) -> BaseProvider: ...
35
- def _get_system_prompt(self) -> str: ...
@@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from kon import config
8
8
 
9
- from ..context import Context
10
9
  from ..core.compaction import generate_summary
11
10
  from ..core.types import AssistantMessage, ToolCall, ToolResultMessage
12
11
  from ..llm import (
@@ -43,7 +42,7 @@ class CommandsMixin:
43
42
  _api_key: str | None
44
43
  _provider: BaseProvider | None
45
44
  _session: Session | None
46
- _project_context: Context | None
45
+ _agent: Any
47
46
  _is_running: bool
48
47
 
49
48
  # Methods from App - declared for type checking
@@ -59,7 +58,6 @@ class CommandsMixin:
59
58
 
60
59
  def _get_provider_api_type(self, provider: BaseProvider) -> ApiType: ...
61
60
  def _create_provider(self, api_type: ApiType, config: ProviderConfig) -> BaseProvider: ...
62
- def _get_system_prompt(self) -> str: ...
63
61
 
64
62
  def _handle_command(self, text: str) -> bool:
65
63
  parts = text[1:].split(maxsplit=1)
@@ -242,8 +240,6 @@ Keybindings:
242
240
  self.run_worker(self._do_new_conversation(chat, info_bar, status), exclusive=False)
243
241
 
244
242
  async def _do_new_conversation(self, chat: ChatLog, info_bar, status) -> None:
245
- from kon.context import Context
246
-
247
243
  await chat.remove_all_children()
248
244
 
249
245
  status.reset()
@@ -255,12 +251,12 @@ Keybindings:
255
251
 
256
252
  chat.add_session_info(getattr(self, "VERSION", ""))
257
253
 
258
- self._project_context = Context.load(self._cwd)
259
- # TODO: Surface self._project_context.skill_warnings in UI (e.g. chat info/error messages)
260
- # so skill load/validation issues are visible to users.
254
+ self._agent.reload_context()
255
+ self._agent.session = self._session
256
+ # TODO: Surface self._agent.context.skill_warnings in UI
261
257
  chat.add_loaded_resources(
262
- context_paths=[format_path(f.path) for f in self._project_context.agents_files],
263
- skill_paths=[format_path(s.file_path) for s in self._project_context.skills],
258
+ context_paths=[format_path(f.path) for f in self._agent.context.agents_files],
259
+ skill_paths=[format_path(s.file_path) for s in self._agent.context.skills],
264
260
  )
265
261
 
266
262
  chat.add_info_message("Started new conversation")
@@ -510,7 +506,7 @@ Keybindings:
510
506
  chat.add_info_message("Session has no messages to export")
511
507
  return
512
508
 
513
- system_prompt = self._get_system_prompt()
509
+ system_prompt = self._agent.system_prompt
514
510
  tools = get_tools(DEFAULT_TOOLS)
515
511
 
516
512
  provider_name = self._provider.name if self._provider else "unknown"
@@ -585,7 +581,7 @@ Keybindings:
585
581
 
586
582
  try:
587
583
  summary = await generate_summary(
588
- self._session.all_messages, self._provider, system_prompt=self._get_system_prompt()
584
+ self._session.all_messages, self._provider, system_prompt=self._agent.system_prompt
589
585
  )
590
586
  self._session.append_compaction(
591
587
  summary=summary,
@@ -21,6 +21,7 @@ from .autocomplete import (
21
21
  )
22
22
  from .floating_list import ListItem
23
23
  from .path_complete import PathComplete
24
+ from .prompt_history import PromptHistory
24
25
 
25
26
  if TYPE_CHECKING:
26
27
  pass
@@ -93,9 +94,7 @@ class InputBox(Vertical):
93
94
  ) -> None:
94
95
  super().__init__(id=id, classes=classes)
95
96
  self._cwd = cwd or os.getcwd()
96
- self._history: list[str] = []
97
- self._history_index: int = -1
98
- self._history_temp: str = ""
97
+ self._history = PromptHistory()
99
98
 
100
99
  # Autocomplete providers
101
100
  self._slash_provider = SlashCommandProvider(DEFAULT_COMMANDS.copy())
@@ -460,33 +459,15 @@ class InputBox(Vertical):
460
459
  # -------------------------------------------------------------------------
461
460
 
462
461
  def _add_to_history(self, text: str) -> None:
463
- if text and (not self._history or self._history[-1] != text):
464
- self._history.append(text)
465
- self._history_index = -1
466
- self._history_temp = ""
462
+ self._history.append(text)
467
463
 
468
464
  def _history_navigate(self, direction: int) -> None:
469
- if not self._history:
470
- return
471
-
472
465
  textarea = self.query_one("#input-textarea", TextArea)
473
-
474
- if self._history_index == -1:
475
- self._history_temp = textarea.text
476
-
477
- new_index = self._history_index + direction
478
-
479
- if new_index < -1 or new_index >= len(self._history):
466
+ result = self._history.navigate(direction, textarea.text)
467
+ if result is None:
480
468
  return
481
-
482
- self._history_index = new_index
483
-
484
469
  textarea.clear()
485
- if self._history_index == -1:
486
- textarea.insert(self._history_temp)
487
- else:
488
- history_item = self._history[-(self._history_index + 1)]
489
- textarea.insert(history_item)
470
+ textarea.insert(result)
490
471
 
491
472
  # -------------------------------------------------------------------------
492
473
  # Messages
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from kon import CONFIG_DIR_NAME
7
+
8
+ MAX_HISTORY_ENTRIES = 50
9
+
10
+
11
+ def _history_path() -> Path:
12
+ return Path.home() / CONFIG_DIR_NAME / "prompt-history.jsonl"
13
+
14
+
15
+ class PromptHistory:
16
+ def __init__(self) -> None:
17
+ self._entries: list[str] = []
18
+ self._index: int = 0
19
+ self._draft: str = ""
20
+ self._load()
21
+
22
+ def _load(self) -> None:
23
+ path = _history_path()
24
+ if not path.exists():
25
+ return
26
+ try:
27
+ text = path.read_text(encoding="utf-8")
28
+ except OSError:
29
+ return
30
+ lines: list[str] = []
31
+ for line in text.strip().split("\n"):
32
+ if not line:
33
+ continue
34
+ try:
35
+ entry = json.loads(line)
36
+ except (json.JSONDecodeError, ValueError):
37
+ continue
38
+ if isinstance(entry, str) and entry:
39
+ lines.append(entry)
40
+ self._entries = lines[-MAX_HISTORY_ENTRIES:]
41
+ if len(lines) > MAX_HISTORY_ENTRIES:
42
+ self._rewrite()
43
+
44
+ def _rewrite(self) -> None:
45
+ path = _history_path()
46
+ path.parent.mkdir(parents=True, exist_ok=True)
47
+ try:
48
+ content = "\n".join(json.dumps(e) for e in self._entries) + "\n"
49
+ path.write_text(content, encoding="utf-8")
50
+ except OSError:
51
+ pass
52
+
53
+ def _append_to_file(self, entry: str) -> None:
54
+ path = _history_path()
55
+ path.parent.mkdir(parents=True, exist_ok=True)
56
+ try:
57
+ with path.open("a", encoding="utf-8") as f:
58
+ f.write(json.dumps(entry) + "\n")
59
+ except OSError:
60
+ pass
61
+
62
+ def append(self, text: str) -> None:
63
+ if not text:
64
+ return
65
+ if self._entries and self._entries[-1] == text:
66
+ self._reset_index()
67
+ return
68
+ self._entries.append(text)
69
+ trimmed = len(self._entries) > MAX_HISTORY_ENTRIES
70
+ if trimmed:
71
+ self._entries = self._entries[-MAX_HISTORY_ENTRIES:]
72
+ self._rewrite()
73
+ else:
74
+ self._append_to_file(text)
75
+ self._reset_index()
76
+
77
+ def _reset_index(self) -> None:
78
+ self._index = 0
79
+ self._draft = ""
80
+
81
+ def navigate(self, direction: int, current_text: str) -> str | None:
82
+ if not self._entries:
83
+ return None
84
+
85
+ if self._index == 0:
86
+ self._draft = current_text
87
+
88
+ new_index = self._index + direction
89
+
90
+ if new_index > 0 or abs(new_index) > len(self._entries):
91
+ return None
92
+
93
+ self._index = new_index
94
+
95
+ if self._index == 0:
96
+ return self._draft
97
+
98
+ return self._entries[self._index]
@@ -4,7 +4,6 @@ import json
4
4
  from pathlib import Path
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
- from ..context import Context
8
7
  from ..core.types import (
9
8
  AssistantMessage,
10
9
  ImageContent,
@@ -15,7 +14,6 @@ from ..core.types import (
15
14
  UserMessage,
16
15
  )
17
16
  from ..llm import ApiType, BaseProvider, ProviderConfig, get_max_tokens, get_model
18
- from ..loop import build_system_prompt
19
17
  from ..session import CompactionEntry, CustomMessageEntry, MessageEntry, Session
20
18
  from ..tools import tools_by_name
21
19
  from .chat import ChatLog
@@ -26,7 +24,7 @@ from .widgets import InfoBar, StatusLine, format_path
26
24
  class SessionUIMixin:
27
25
  # Attributes provided by the App subclass
28
26
  _cwd: str
29
- _project_context: Context | None
27
+ _agent: Any
30
28
  _hide_thinking: bool
31
29
  _session: Session | None
32
30
  _current_block_type: str | None
@@ -43,8 +41,6 @@ class SessionUIMixin:
43
41
  # Methods from other mixins/main class
44
42
  def _get_provider_api_type(self, provider: BaseProvider) -> ApiType: ...
45
43
  def _create_provider(self, api_type: ApiType, config: ProviderConfig) -> BaseProvider: ...
46
- def _get_system_prompt(self) -> str:
47
- return build_system_prompt(self._cwd, self._project_context)
48
44
 
49
45
  def _extract_text_content(self, content: str | list[TextContent | ImageContent]) -> str:
50
46
  if isinstance(content, str):
@@ -220,10 +216,10 @@ class SessionUIMixin:
220
216
 
221
217
  chat.add_session_info(getattr(self, "VERSION", ""))
222
218
 
223
- if self._project_context:
219
+ if self._agent:
224
220
  chat.add_loaded_resources(
225
- context_paths=[format_path(f.path) for f in self._project_context.agents_files],
226
- skill_paths=[format_path(s.file_path) for s in self._project_context.skills],
221
+ context_paths=[format_path(f.path) for f in self._agent.context.agents_files],
222
+ skill_paths=[format_path(s.file_path) for s in self._agent.context.skills],
227
223
  )
228
224
 
229
225
  self._render_session_entries(session)
@@ -146,7 +146,8 @@ async def test_agent_system_prompt(tools, in_memory_session):
146
146
  provider,
147
147
  tools,
148
148
  in_memory_session,
149
- config=AgentConfig(system_prompt="Custom system prompt", max_turns=1),
149
+ system_prompt="Custom system prompt",
150
+ config=AgentConfig(max_turns=1),
150
151
  )
151
152
 
152
153
  events = []
@@ -209,7 +210,7 @@ async def test_agent_custom_cwd(tools):
209
210
  provider = MockProvider(scenario="simple_text")
210
211
  session = Session.in_memory(cwd="/custom/path")
211
212
 
212
- agent = Agent(provider, tools, session, config=AgentConfig(cwd="/custom/path"))
213
+ agent = Agent(provider, tools, session, cwd="/custom/path")
213
214
 
214
215
  events = []
215
216
  async for event in agent.run("Where am I?"):
@@ -70,4 +70,4 @@ def test_submit_keeps_display_text_but_expands_query_text() -> None:
70
70
  assert input_box._fake_textarea.cleared is True
71
71
  assert input_box._pastes == {}
72
72
  assert input_box._paste_counter == 0
73
- assert input_box._history[-1] == f"prefix {pasted} suffix"
73
+ assert input_box._history._entries[-1] == f"prefix {pasted} suffix"
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ import pytest
6
+
7
+ from kon.ui import prompt_history as ph
8
+ from kon.ui.prompt_history import MAX_HISTORY_ENTRIES, PromptHistory
9
+
10
+
11
+ @pytest.fixture(autouse=True)
12
+ def _isolate_history(tmp_path, monkeypatch):
13
+ history_file = tmp_path / "prompt-history.jsonl"
14
+ monkeypatch.setattr(ph, "_history_path", lambda: history_file)
15
+ return history_file
16
+
17
+
18
+ def test_append_and_navigate():
19
+ h = PromptHistory()
20
+ h.append("hello")
21
+ h.append("world")
22
+
23
+ assert h.navigate(-1, "") == "world"
24
+ assert h.navigate(-1, "world") == "hello"
25
+ assert h.navigate(1, "hello") == "world"
26
+ assert h.navigate(1, "world") == ""
27
+
28
+
29
+ def test_navigate_empty():
30
+ h = PromptHistory()
31
+ assert h.navigate(-1, "") is None
32
+
33
+
34
+ def test_navigate_preserves_draft():
35
+ h = PromptHistory()
36
+ h.append("old")
37
+
38
+ result = h.navigate(-1, "my draft")
39
+ assert result == "old"
40
+ result = h.navigate(1, "old")
41
+ assert result == "my draft"
42
+
43
+
44
+ def test_navigate_bounds():
45
+ h = PromptHistory()
46
+ h.append("only")
47
+
48
+ assert h.navigate(-1, "") == "only"
49
+ assert h.navigate(-1, "only") is None
50
+ h._reset_index()
51
+ assert h.navigate(1, "") is None
52
+
53
+
54
+ def test_dedup_consecutive():
55
+ h = PromptHistory()
56
+ h.append("same")
57
+ h.append("same")
58
+ assert len(h._entries) == 1
59
+
60
+
61
+ def test_persistence(tmp_path):
62
+ h1 = PromptHistory()
63
+ h1.append("first")
64
+ h1.append("second")
65
+
66
+ h2 = PromptHistory()
67
+ assert h2._entries == ["first", "second"]
68
+
69
+
70
+ def test_max_entries_trim(tmp_path):
71
+ h = PromptHistory()
72
+ for i in range(MAX_HISTORY_ENTRIES + 10):
73
+ h.append(f"entry-{i}")
74
+
75
+ assert len(h._entries) == MAX_HISTORY_ENTRIES
76
+ assert h._entries[0] == "entry-10"
77
+ assert h._entries[-1] == f"entry-{MAX_HISTORY_ENTRIES + 9}"
78
+
79
+ h2 = PromptHistory()
80
+ assert len(h2._entries) == MAX_HISTORY_ENTRIES
81
+
82
+
83
+ def test_corrupt_lines_ignored(tmp_path):
84
+ path = ph._history_path()
85
+ path.parent.mkdir(parents=True, exist_ok=True)
86
+ path.write_text(
87
+ json.dumps("good")
88
+ + "\n"
89
+ + "not valid json\n"
90
+ + json.dumps(42)
91
+ + "\n"
92
+ + json.dumps("also good")
93
+ + "\n",
94
+ encoding="utf-8",
95
+ )
96
+
97
+ h = PromptHistory()
98
+ assert h._entries == ["good", "also good"]
@@ -636,7 +636,7 @@ wheels = [
636
636
 
637
637
  [[package]]
638
638
  name = "kon-coding-agent"
639
- version = "0.2.2"
639
+ version = "0.2.3"
640
640
  source = { editable = "." }
641
641
  dependencies = [
642
642
  { name = "aiofiles" },