kon-coding-agent 0.2.2__tar.gz → 0.2.3__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.2.2 → kon_coding_agent-0.2.3}/PKG-INFO +7 -7
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/README.md +6 -6
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/pyproject.toml +1 -1
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/turn.py +0 -1
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/app.py +25 -11
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/input.py +6 -25
- kon_coding_agent-0.2.3/src/kon/ui/prompt_history.py +98 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/ui/test_input_paste.py +1 -1
- kon_coding_agent-0.2.3/tests/ui/test_prompt_history.py +98 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/uv.lock +1 -1
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/.gitignore +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/.kon/skills/kon-release-publish/SKILL.md +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/.kon/skills/kon-tmux-test/SKILL.md +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/.kon/skills/kon-tmux-test/run-e2e-tests.sh +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/.kon/skills/kon-tmux-test/setup-test-project.sh +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/.python-version +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/AGENTS.md +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/LICENSE +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/LOCAL.md +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/TODO.md +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/scripts/test_models.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/scripts/test_thinking_blocks.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/__init__.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/config.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/context/__init__.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/context/agents.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/context/loader.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/context/shared.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/context/skills.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/core/__init__.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/core/compaction.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/core/types.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/defaults/__init__.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/defaults/config.toml +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/events.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/__init__.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/base.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/models.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/oauth/__init__.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/oauth/copilot.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/oauth/openai.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/__init__.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/anthropic.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/copilot.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/copilot_anthropic.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/github_copilot_headers.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/mock.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/openai_codex_responses.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/openai_completions.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/openai_responses.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/sanitize.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/loop.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/py.typed +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/session.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/shared.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/tools/__init__.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/tools/_read_image.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/tools/base.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/tools/bash.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/tools/edit.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/tools/find.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/tools/grep.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/tools/read.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/tools/write.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/tools_manager.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/__init__.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/app_protocol.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/autocomplete.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/blocks.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/chat.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/clipboard.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/commands.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/export.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/floating_list.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/formatting.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/path_complete.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/selection_mode.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/session_ui.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/styles.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/ui/widgets.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/update_check.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/conftest.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/context/test_agents.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/context/test_skills.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/llm/__init__.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/llm/test_mock_provider.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/test_agentic_loop.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/test_cli_provider_resolution.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/test_compaction.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/test_config_binaries.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/test_config_error_fallback.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/test_config_injection.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/test_model_provider_resolution.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/test_session_persistence.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/test_system_prompt.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/test_update_check.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/test_update_notice_behavior.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/tools/test_diff.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/tools/test_edit.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/tools/test_read.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/tools/test_read_image.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/tools/test_read_image_integration.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/tools/test_write.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/ui/test_autocomplete.py +0 -0
- {kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/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.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
### Warning
|
|
38
|
+
[Kon](https://bleach.fandom.com/wiki/Kon) is inspired from Bleach, a artificial soul
|
|
41
39
|
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
### Warning
|
|
22
|
+
[Kon](https://bleach.fandom.com/wiki/Kon) is inspired from Bleach, a artificial soul
|
|
25
23
|
|
|
26
|
-
|
|
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
|
|
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
|
|
@@ -11,7 +11,7 @@ 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
|
|
|
@@ -85,7 +85,7 @@ _PYPI_PACKAGE_NAME = _get_package_name()
|
|
|
85
85
|
try:
|
|
86
86
|
VERSION = version(_PYPI_PACKAGE_NAME)
|
|
87
87
|
except PackageNotFoundError:
|
|
88
|
-
VERSION = "0.2.
|
|
88
|
+
VERSION = "0.2.3"
|
|
89
89
|
|
|
90
90
|
_COPILOT_API_TYPES: frozenset[ApiType] = frozenset(
|
|
91
91
|
{ApiType.GITHUB_COPILOT, ApiType.GITHUB_COPILOT_RESPONSES, ApiType.ANTHROPIC_COPILOT}
|
|
@@ -192,6 +192,12 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
|
|
|
192
192
|
def _get_provider_api_type(self, provider: BaseProvider) -> ApiType:
|
|
193
193
|
return _API_TYPE_BY_PROVIDER.get(type(provider), ApiType.OPENAI_COMPLETIONS)
|
|
194
194
|
|
|
195
|
+
@on(events.TextSelected)
|
|
196
|
+
def _on_text_selected(self) -> None:
|
|
197
|
+
selection = self.screen.get_selected_text()
|
|
198
|
+
if selection:
|
|
199
|
+
self.copy_to_clipboard(selection)
|
|
200
|
+
|
|
195
201
|
def on_mount(self) -> None:
|
|
196
202
|
if config.binaries.fd:
|
|
197
203
|
self._fd_path = shutil.which("fd") or shutil.which("fdfind")
|
|
@@ -254,21 +260,25 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
|
|
|
254
260
|
thinking_level=self._thinking_level,
|
|
255
261
|
)
|
|
256
262
|
|
|
263
|
+
provider_error: str | None = None
|
|
257
264
|
try:
|
|
258
265
|
self._provider = self._create_provider(api_type, provider_config)
|
|
259
266
|
except ValueError as e:
|
|
260
|
-
|
|
261
|
-
chat.add_info_message(str(e), error=True)
|
|
262
|
-
return
|
|
267
|
+
provider_error = str(e)
|
|
263
268
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
self._thinking_level
|
|
267
|
-
|
|
269
|
+
if self._provider:
|
|
270
|
+
valid_levels = self._provider.thinking_levels
|
|
271
|
+
if self._thinking_level not in valid_levels:
|
|
272
|
+
self._thinking_level = valid_levels[0] if valid_levels else "medium"
|
|
273
|
+
self._provider.set_thinking_level(self._thinking_level)
|
|
268
274
|
|
|
269
275
|
if not self._continue_recent and not self._resume_session:
|
|
270
276
|
selected_model = get_model(self._model, self._model_provider)
|
|
271
|
-
model_provider =
|
|
277
|
+
model_provider = (
|
|
278
|
+
selected_model.provider
|
|
279
|
+
if selected_model
|
|
280
|
+
else (self._provider.name if self._provider else self._model_provider)
|
|
281
|
+
)
|
|
272
282
|
self._model_provider = model_provider
|
|
273
283
|
self._session = Session.create(
|
|
274
284
|
self._cwd,
|
|
@@ -276,7 +286,8 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
|
|
|
276
286
|
model_id=self._model,
|
|
277
287
|
thinking_level=self._thinking_level,
|
|
278
288
|
)
|
|
279
|
-
|
|
289
|
+
if model_provider:
|
|
290
|
+
self._session.append_model_change(model_provider, self._model, base_url)
|
|
280
291
|
|
|
281
292
|
self._project_context = Context.load(self._cwd)
|
|
282
293
|
# TODO: Surface self._project_context.skill_warnings in UI (e.g. chat info/error messages)
|
|
@@ -290,6 +301,9 @@ class Kon(CommandsMixin, SessionUIMixin, App[None]):
|
|
|
290
301
|
skill_paths=[format_path(s.file_path) for s in self._project_context.skills],
|
|
291
302
|
)
|
|
292
303
|
|
|
304
|
+
if provider_error:
|
|
305
|
+
chat.add_info_message(provider_error, error=True)
|
|
306
|
+
|
|
293
307
|
for warning in consume_config_warnings():
|
|
294
308
|
chat.add_info_message(warning, warning=True)
|
|
295
309
|
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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]
|
|
@@ -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"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/.kon/skills/kon-tmux-test/run-e2e-tests.sh
RENAMED
|
File without changes
|
{kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/.kon/skills/kon-tmux-test/setup-test-project.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/copilot_anthropic.py
RENAMED
|
File without changes
|
{kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/github_copilot_headers.py
RENAMED
|
File without changes
|
|
File without changes
|
{kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/openai_codex_responses.py
RENAMED
|
File without changes
|
{kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/src/kon/llm/providers/openai_completions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kon_coding_agent-0.2.2 → kon_coding_agent-0.2.3}/tests/tools/test_read_image_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|