kon-coding-agent 0.3.1__tar.gz → 0.3.2__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 (129) hide show
  1. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/CHANGELOG.md +16 -0
  2. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/PKG-INFO +1 -1
  3. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/pyproject.toml +1 -1
  4. kon_coding_agent-0.3.2/scripts/show_themes.py +216 -0
  5. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/config.py +3 -0
  6. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/context/skills.py +6 -2
  7. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/defaults/config.toml +3 -0
  8. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools/_tool_utils.py +4 -0
  9. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools/edit.py +1 -9
  10. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools/find.py +10 -3
  11. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools/grep.py +11 -4
  12. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools/web_fetch.py +2 -3
  13. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/app.py +1 -1
  14. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/blocks.py +6 -3
  15. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/commands.py +12 -2
  16. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/styles.py +2 -2
  17. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/context/test_skills.py +19 -0
  18. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/tools/test_edit_display.py +7 -12
  19. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/uv.lock +1 -1
  20. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/.gitignore +0 -0
  21. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/.kon/skills/kon-release-publish/SKILL.md +0 -0
  22. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/.kon/skills/kon-tmux-test/SKILL.md +0 -0
  23. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/.kon/skills/kon-tmux-test/run-e2e-tests.sh +0 -0
  24. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/.kon/skills/kon-tmux-test/setup-test-project.sh +0 -0
  25. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/.python-version +0 -0
  26. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/AGENTS.md +0 -0
  27. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/LICENSE +0 -0
  28. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/README.md +0 -0
  29. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/docs/architecture-review.md +0 -0
  30. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/docs/images/kon-screenshot.png +0 -0
  31. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/docs/local-models.md +0 -0
  32. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/__init__.py +0 -0
  33. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/context/__init__.py +0 -0
  34. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/context/_xml.py +0 -0
  35. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/context/agent_mds.py +0 -0
  36. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/context/git.py +0 -0
  37. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/context/loader.py +0 -0
  38. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/core/__init__.py +0 -0
  39. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/core/compaction.py +0 -0
  40. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/core/handoff.py +0 -0
  41. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/core/types.py +0 -0
  42. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/defaults/__init__.py +0 -0
  43. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/events.py +0 -0
  44. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/__init__.py +0 -0
  45. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/base.py +0 -0
  46. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/models.py +0 -0
  47. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/oauth/__init__.py +0 -0
  48. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/oauth/copilot.py +0 -0
  49. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/oauth/openai.py +0 -0
  50. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/providers/__init__.py +0 -0
  51. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/providers/anthropic.py +0 -0
  52. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/providers/azure_ai_foundry.py +0 -0
  53. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/providers/copilot.py +0 -0
  54. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/providers/copilot_anthropic.py +0 -0
  55. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/providers/github_copilot_headers.py +0 -0
  56. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/providers/mock.py +0 -0
  57. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/providers/openai_codex_responses.py +0 -0
  58. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/providers/openai_compat.py +0 -0
  59. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/providers/openai_completions.py +0 -0
  60. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/providers/openai_responses.py +0 -0
  61. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/llm/providers/sanitize.py +0 -0
  62. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/loop.py +0 -0
  63. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/metrics.py +0 -0
  64. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/permissions.py +0 -0
  65. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/py.typed +0 -0
  66. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/session.py +0 -0
  67. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/themes.py +0 -0
  68. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools/__init__.py +0 -0
  69. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools/_read_image.py +0 -0
  70. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools/base.py +0 -0
  71. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools/bash.py +0 -0
  72. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools/read.py +0 -0
  73. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools/web_search.py +0 -0
  74. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools/write.py +0 -0
  75. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/tools_manager.py +0 -0
  76. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/turn.py +0 -0
  77. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/__init__.py +0 -0
  78. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/app_protocol.py +0 -0
  79. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/autocomplete.py +0 -0
  80. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/chat.py +0 -0
  81. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/clipboard.py +0 -0
  82. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/export.py +0 -0
  83. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/floating_list.py +0 -0
  84. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/formatting.py +0 -0
  85. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/input.py +0 -0
  86. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/path_complete.py +0 -0
  87. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/prompt_history.py +0 -0
  88. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/selection_mode.py +0 -0
  89. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/session_ui.py +0 -0
  90. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/ui/widgets.py +0 -0
  91. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/src/kon/update_check.py +0 -0
  92. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/conftest.py +0 -0
  93. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/context/test_agents.py +0 -0
  94. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/llm/__init__.py +0 -0
  95. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/llm/test_anthropic_provider.py +0 -0
  96. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/llm/test_azure_ai_foundry_provider.py +0 -0
  97. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/llm/test_mock_provider.py +0 -0
  98. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/llm/test_openai_codex_provider_errors.py +0 -0
  99. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_agentic_loop.py +0 -0
  100. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_cli_provider_resolution.py +0 -0
  101. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_compaction.py +0 -0
  102. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_config_binaries.py +0 -0
  103. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_config_error_fallback.py +0 -0
  104. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_config_injection.py +0 -0
  105. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_config_migration.py +0 -0
  106. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_handoff.py +0 -0
  107. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_launch_warnings.py +0 -0
  108. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_metrics.py +0 -0
  109. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_model_provider_resolution.py +0 -0
  110. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_openai_compat.py +0 -0
  111. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_permissions.py +0 -0
  112. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_session_persistence.py +0 -0
  113. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_session_resume.py +0 -0
  114. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_system_prompt.py +0 -0
  115. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_system_prompt_git_context.py +0 -0
  116. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_update_check.py +0 -0
  117. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/test_update_notice_behavior.py +0 -0
  118. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/tools/test_diff.py +0 -0
  119. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/tools/test_edit.py +0 -0
  120. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/tools/test_read.py +0 -0
  121. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/tools/test_read_image.py +0 -0
  122. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/tools/test_read_image_integration.py +0 -0
  123. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/tools/test_write.py +0 -0
  124. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/ui/test_autocomplete.py +0 -0
  125. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/ui/test_floating_list.py +0 -0
  126. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/ui/test_input_handoff.py +0 -0
  127. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/ui/test_input_paste.py +0 -0
  128. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/ui/test_prompt_history.py +0 -0
  129. {kon_coding_agent-0.3.1 → kon_coding_agent-0.3.2}/tests/ui/test_status_line.py +0 -0
@@ -6,6 +6,22 @@ All notable changes to this project will be documented in this file.
6
6
 
7
7
  - No changes yet.
8
8
 
9
+ ## 0.3.2 - 2026-03-22
10
+
11
+ ### Added
12
+
13
+ - Added a `collapse_thinking` config flag to control thinking block display.
14
+ - Added a Ghostty theme preview script.
15
+
16
+ ### Changed
17
+
18
+ - Improved theme and model picker indicators.
19
+ - Refactored tool display helpers into shared `truncate_text` and `shorten_path` utilities.
20
+
21
+ ### Fixed
22
+
23
+ - Fixed duplicate skill warnings coming from the home directory.
24
+
9
25
  ## 0.3.1 - 2026-03-21
10
26
 
11
27
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kon-coding-agent
3
- Version: 0.3.1
3
+ Version: 0.3.2
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.1"
17
+ version = "0.3.2"
18
18
  description = "Minimal coding agent"
19
19
  readme = "README.md"
20
20
  requires-python = ">=3.12"
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import subprocess
5
+ import sys
6
+ import time
7
+ from pathlib import Path
8
+
9
+ KON_THEME_TO_GHOSTTY_THEME = {
10
+ "catppuccin-latte": "Catppuccin Latte",
11
+ "catppuccin-mocha": "Catppuccin Mocha",
12
+ "dracula": "Dracula",
13
+ "github-dark": "GitHub Dark",
14
+ "github-light": "GitHub Light Default",
15
+ "gruvbox-dark": "Gruvbox Dark",
16
+ "gruvbox-light": "Gruvbox Light",
17
+ "nord": "Nord",
18
+ "one-dark": "Atom One Dark",
19
+ "one-light": "Atom One Light",
20
+ "solarized-dark": "Builtin Solarized Dark",
21
+ "solarized-light": "Builtin Solarized Light",
22
+ "tokyo-day": "TokyoNight Day",
23
+ "tokyo-night": "TokyoNight Night",
24
+ }
25
+
26
+
27
+ def parse_args() -> argparse.Namespace:
28
+ parser = argparse.ArgumentParser()
29
+ parser.add_argument(
30
+ "--project-dir",
31
+ type=Path,
32
+ default=Path.cwd(),
33
+ help="Directory to open before running `uv run kon -c`.",
34
+ )
35
+ parser.add_argument(
36
+ "--ghostty-config",
37
+ type=Path,
38
+ default=Path.home() / ".config/ghostty/config",
39
+ help="Ghostty config file to rewrite during previews.",
40
+ )
41
+ parser.add_argument(
42
+ "--kon-config",
43
+ type=Path,
44
+ default=Path.home() / ".kon/config.toml",
45
+ help="Kon config file to rewrite during previews.",
46
+ )
47
+ parser.add_argument(
48
+ "--duration", type=float, default=10.0, help="How long each preview stays open in seconds."
49
+ )
50
+ parser.add_argument(
51
+ "--pause", type=float, default=1.5, help="Pause between previews in seconds."
52
+ )
53
+ parser.add_argument(
54
+ "--ghostty-app",
55
+ type=Path,
56
+ default=Path("/Applications/Ghostty.app"),
57
+ help="Path to Ghostty.app for launching new macOS instances.",
58
+ )
59
+ return parser.parse_args()
60
+
61
+
62
+ def list_ghostty_themes() -> set[str]:
63
+ result = subprocess.run(
64
+ ["ghostty", "+list-themes"], check=True, capture_output=True, text=True
65
+ )
66
+ themes = set()
67
+ for line in result.stdout.splitlines():
68
+ theme = line.strip()
69
+ if not theme:
70
+ continue
71
+ if theme.endswith("(resources)"):
72
+ theme = theme[: -len("(resources)")].rstrip()
73
+ themes.add(theme)
74
+ return themes
75
+
76
+
77
+ def ensure_theme_mapping_is_valid(available_themes: set[str]) -> None:
78
+ missing = {
79
+ kon_theme: ghostty_theme
80
+ for kon_theme, ghostty_theme in KON_THEME_TO_GHOSTTY_THEME.items()
81
+ if ghostty_theme not in available_themes
82
+ }
83
+ if missing:
84
+ lines = ["Missing Ghostty theme mappings:"]
85
+ for kon_theme, ghostty_theme in missing.items():
86
+ lines.append(f" {kon_theme} -> {ghostty_theme}")
87
+ raise RuntimeError("\n".join(lines))
88
+
89
+
90
+ def replace_or_append_theme_line(config_text: str, theme_name: str) -> str:
91
+ lines = config_text.splitlines()
92
+ in_ui_section = False
93
+ ui_section_found = False
94
+
95
+ for index, line in enumerate(lines):
96
+ stripped = line.strip()
97
+ if stripped.startswith("[") and stripped.endswith("]"):
98
+ if stripped == "[ui]":
99
+ in_ui_section = True
100
+ ui_section_found = True
101
+ continue
102
+ if in_ui_section:
103
+ lines.insert(index, f'theme = "{theme_name}"')
104
+ return "\n".join(lines) + "\n"
105
+ in_ui_section = False
106
+ continue
107
+
108
+ if in_ui_section and stripped.startswith("theme") and "=" in stripped:
109
+ lines[index] = f'theme = "{theme_name}"'
110
+ return "\n".join(lines) + "\n"
111
+
112
+ if ui_section_found:
113
+ lines.append(f'theme = "{theme_name}"')
114
+ return "\n".join(lines) + "\n"
115
+
116
+ suffix = "\n" if config_text.endswith("\n") or not config_text else "\n\n"
117
+ return f'{config_text}{suffix}[ui]\ntheme = "{theme_name}"\n'
118
+
119
+
120
+ def write_ghostty_config(config_path: Path, theme_name: str) -> None:
121
+ config_path.parent.mkdir(parents=True, exist_ok=True)
122
+ config_path.write_text(f"theme = {theme_name}\n", encoding="utf-8")
123
+
124
+
125
+ def write_kon_theme(config_path: Path, theme_name: str) -> None:
126
+ config_path.parent.mkdir(parents=True, exist_ok=True)
127
+ original = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
128
+ updated = replace_or_append_theme_line(original, theme_name)
129
+ config_path.write_text(updated, encoding="utf-8")
130
+
131
+
132
+ def sh_quote(value: str) -> str:
133
+ return "'" + value.replace("'", "'\"'\"'") + "'"
134
+
135
+
136
+ def launch_preview(ghostty_app: Path, project_dir: Path, duration: float) -> None:
137
+ trap_cmd = (
138
+ 'trap \'test -n "$kon_pid" && kill -TERM "$kon_pid" 2>/dev/null || true\' EXIT INT TERM'
139
+ )
140
+ shell_script = (
141
+ f"cd {sh_quote(str(project_dir))} && "
142
+ "kon_pid='' && "
143
+ f"{trap_cmd} && "
144
+ "uv run kon -c & kon_pid=$! && "
145
+ f"sleep {duration} && "
146
+ 'kill -TERM "$kon_pid" 2>/dev/null || true && '
147
+ 'wait "$kon_pid" 2>/dev/null || true'
148
+ )
149
+ subprocess.run(
150
+ [
151
+ "open",
152
+ "-na",
153
+ str(ghostty_app),
154
+ "--args",
155
+ f"--working-directory={project_dir}",
156
+ "-e",
157
+ "sh",
158
+ "-lc",
159
+ shell_script,
160
+ ],
161
+ check=True,
162
+ )
163
+
164
+
165
+ def main() -> int:
166
+ args = parse_args()
167
+ project_dir = args.project_dir.expanduser().resolve()
168
+ ghostty_config_path = args.ghostty_config.expanduser()
169
+ kon_config_path = args.kon_config.expanduser()
170
+ ghostty_app = args.ghostty_app.expanduser()
171
+
172
+ if not project_dir.exists():
173
+ raise FileNotFoundError(f"Project directory does not exist: {project_dir}")
174
+ if not ghostty_app.exists():
175
+ raise FileNotFoundError(f"Ghostty app not found: {ghostty_app}")
176
+
177
+ available_themes = list_ghostty_themes()
178
+ ensure_theme_mapping_is_valid(available_themes)
179
+
180
+ original_ghostty_config = (
181
+ ghostty_config_path.read_text(encoding="utf-8") if ghostty_config_path.exists() else None
182
+ )
183
+ original_kon_config = (
184
+ kon_config_path.read_text(encoding="utf-8") if kon_config_path.exists() else None
185
+ )
186
+
187
+ try:
188
+ for kon_theme, ghostty_theme in KON_THEME_TO_GHOSTTY_THEME.items():
189
+ print(f"Previewing kon={kon_theme} ghostty={ghostty_theme}")
190
+ write_kon_theme(kon_config_path, kon_theme)
191
+ write_ghostty_config(ghostty_config_path, ghostty_theme)
192
+ launch_preview(ghostty_app, project_dir, args.duration)
193
+ time.sleep(args.duration + args.pause + 2)
194
+ finally:
195
+ if original_kon_config is None:
196
+ if kon_config_path.exists():
197
+ kon_config_path.unlink()
198
+ else:
199
+ kon_config_path.write_text(original_kon_config, encoding="utf-8")
200
+
201
+ if original_ghostty_config is None:
202
+ if ghostty_config_path.exists():
203
+ ghostty_config_path.unlink()
204
+ else:
205
+ ghostty_config_path.write_text(original_ghostty_config, encoding="utf-8")
206
+
207
+ print("Done. Restored Ghostty and kon config files.")
208
+ return 0
209
+
210
+
211
+ if __name__ == "__main__":
212
+ try:
213
+ raise SystemExit(main())
214
+ except KeyboardInterrupt as exc:
215
+ print("Interrupted.", file=sys.stderr)
216
+ raise SystemExit(130) from exc
@@ -42,6 +42,9 @@ class MetaConfig(BaseModel):
42
42
 
43
43
  class UIConfig(BaseModel):
44
44
  theme: str = "gruvbox-dark"
45
+ # When true, finalized thinking blocks are collapsed to a single line summary.
46
+ # Set to false to always show the full thinking content.
47
+ collapse_thinking: bool = True
45
48
 
46
49
  @field_validator("theme")
47
50
  @classmethod
@@ -218,8 +218,12 @@ def load_skills(cwd: str | None = None) -> LoadSkillsResult:
218
218
  else:
219
219
  skill_map[skill.name] = skill
220
220
 
221
- add_skills(_load_skills_from_dir(resolved_cwd / CONFIG_DIR_NAME / "skills"))
222
- add_skills(_load_skills_from_dir(get_config_dir() / "skills"))
221
+ local_skills_dir = (resolved_cwd / CONFIG_DIR_NAME / "skills").resolve(strict=False)
222
+ global_skills_dir = (get_config_dir() / "skills").resolve(strict=False)
223
+
224
+ add_skills(_load_skills_from_dir(local_skills_dir))
225
+ if global_skills_dir != local_skills_dir:
226
+ add_skills(_load_skills_from_dir(global_skills_dir))
223
227
 
224
228
  return LoadSkillsResult(skills=list(skill_map.values()), warnings=all_warnings)
225
229
 
@@ -53,6 +53,9 @@ extra = ["web_search", "web_fetch"]
53
53
 
54
54
  [ui]
55
55
  theme = "gruvbox-dark"
56
+ # When true, finalized thinking blocks are collapsed to a single line summary.
57
+ # Set to false to always show the full thinking content.
58
+ collapse_thinking = true
56
59
 
57
60
  [permissions]
58
61
  # Approval behavior for mutating tools.
@@ -83,6 +83,10 @@ def shorten_path(path: str) -> str:
83
83
  return path
84
84
 
85
85
 
86
+ def truncate_text(text: str, n: int = 80) -> str:
87
+ return text[:77] + "..." if len(text) > n else text
88
+
89
+
86
90
  def truncate_lines_by_bytes(
87
91
  lines: list[str], max_output_bytes: int, marker: str = "[output truncated]"
88
92
  ) -> tuple[str, bool]:
@@ -131,14 +131,6 @@ def generate_diff(
131
131
  return "\n".join(output), added, removed
132
132
 
133
133
 
134
- def truncate_diff_line(line: str, max_chars: int = 105) -> str:
135
- if len(line) <= max_chars:
136
- return line
137
- if max_chars <= 3:
138
- return "." * max_chars
139
- return f"{line[: max_chars - 3]}..."
140
-
141
-
142
134
  def format_diff_display(diff: str) -> str:
143
135
  colors = config.ui.colors
144
136
  lines = diff.split("\n")
@@ -148,7 +140,7 @@ def format_diff_display(diff: str) -> str:
148
140
  if not line:
149
141
  continue
150
142
 
151
- truncated = truncate_diff_line(line)
143
+ truncated = line[:102] + "..." if len(line) > 105 else line
152
144
  escaped = truncated.replace("[", "\\[")
153
145
 
154
146
  if line.startswith("-"):
@@ -5,7 +5,13 @@ from pydantic import BaseModel, Field
5
5
 
6
6
  from ..core.types import ToolResult
7
7
  from ..tools_manager import ensure_tool
8
- from ._tool_utils import ToolCancelledError, communicate_or_cancel, truncate_lines_by_bytes
8
+ from ._tool_utils import (
9
+ ToolCancelledError,
10
+ communicate_or_cancel,
11
+ shorten_path,
12
+ truncate_lines_by_bytes,
13
+ truncate_text,
14
+ )
9
15
  from .base import BaseTool
10
16
 
11
17
  MAX_RESULTS = 100
@@ -37,8 +43,9 @@ class FindTool(BaseTool):
37
43
  pattern = params.pattern.replace('"', '\\"')
38
44
  parts = [f'"{pattern}"']
39
45
  if params.path:
40
- parts.append(f"in {params.path}")
41
- return " ".join(parts)
46
+ parts.append(f"in {shorten_path(params.path)}")
47
+ message = " ".join(parts)
48
+ return truncate_text(message)
42
49
 
43
50
  async def execute(
44
51
  self, params: FindParams, cancel_event: asyncio.Event | None = None
@@ -6,7 +6,13 @@ from pydantic import BaseModel, Field
6
6
 
7
7
  from ..core.types import ToolResult
8
8
  from ..tools_manager import ensure_tool
9
- from ._tool_utils import ToolCancelledError, communicate_or_cancel, truncate_lines_by_bytes
9
+ from ._tool_utils import (
10
+ ToolCancelledError,
11
+ communicate_or_cancel,
12
+ shorten_path,
13
+ truncate_lines_by_bytes,
14
+ truncate_text,
15
+ )
10
16
  from .base import BaseTool
11
17
 
12
18
  MAX_MATCHES = 100
@@ -40,10 +46,11 @@ class GrepTool(BaseTool):
40
46
  pattern = params.pattern.replace('"', '\\"')
41
47
  parts = [f'"{pattern}"']
42
48
  if params.path:
43
- parts.append(f"in {params.path}")
49
+ parts.append(f"in {shorten_path(params.path)}")
44
50
  if params.include:
45
- parts.append(f"--include={params.include}")
46
- return " ".join(parts)
51
+ parts.append(f"({params.include})")
52
+ message = " ".join(parts)
53
+ return truncate_text(message)
47
54
 
48
55
  async def execute(
49
56
  self, params: GrepParams, cancel_event: asyncio.Event | None = None
@@ -6,7 +6,7 @@ from trafilatura import extract, fetch_url
6
6
  from trafilatura.settings import DEFAULT_CONFIG
7
7
 
8
8
  from ..core.types import ToolResult
9
- from ._tool_utils import ToolCancelledError, await_task_or_cancel
9
+ from ._tool_utils import ToolCancelledError, await_task_or_cancel, truncate_text
10
10
  from .base import BaseTool
11
11
 
12
12
  MAX_CHARS = 80_000
@@ -33,8 +33,7 @@ class WebFetchTool(BaseTool):
33
33
  )
34
34
 
35
35
  def format_call(self, params: WebFetchParams) -> str:
36
- url = params.url
37
- return url[:77] + "..." if len(url) > 80 else url
36
+ return truncate_text(params.url)
38
37
 
39
38
  async def execute(
40
39
  self, params: WebFetchParams, cancel_event: asyncio.Event | None = None
@@ -90,7 +90,7 @@ _CHANGELOG_URL = "https://github.com/0xku/kon/blob/main/CHANGELOG.md"
90
90
  try:
91
91
  VERSION = version(_PYPI_PACKAGE_NAME)
92
92
  except PackageNotFoundError:
93
- VERSION = "0.3.1"
93
+ VERSION = "0.3.2"
94
94
 
95
95
  _COPILOT_API_TYPES: frozenset[ApiType] = frozenset(
96
96
  {ApiType.GITHUB_COPILOT, ApiType.GITHUB_COPILOT_RESPONSES, ApiType.ANTHROPIC_COPILOT}
@@ -83,7 +83,7 @@ class ThinkingBlock(_StreamingMarkdownMixin, Static):
83
83
  self.add_class("thinking-block")
84
84
 
85
85
  def compose(self) -> ComposeResult:
86
- if self._finalized and self._content:
86
+ if self._finalized and self._content and config.ui.collapse_thinking:
87
87
  yield Label(self._format_collapsed(), id="thinking-content", markup=False)
88
88
  else:
89
89
  yield Label(self._content, id="thinking-content", markup=False)
@@ -114,13 +114,16 @@ class ThinkingBlock(_StreamingMarkdownMixin, Static):
114
114
  self.call_after_refresh(self._do_finalize)
115
115
 
116
116
  def _do_finalize(self) -> None:
117
- if self._content:
117
+ if self._content and config.ui.collapse_thinking:
118
118
  self.label.update(self._format_collapsed())
119
119
 
120
120
  def set_content(self, text: str) -> None:
121
121
  self._content = text
122
122
  self._finalized = True
123
- self.label.update(self._format_collapsed())
123
+ if config.ui.collapse_thinking:
124
+ self.label.update(self._format_collapsed())
125
+ else:
126
+ self.label.update(text)
124
127
 
125
128
 
126
129
  class ContentBlock(_StreamingMarkdownMixin, Static):
@@ -175,6 +175,8 @@ Extra tools:
175
175
  parts = [m.provider]
176
176
  if not m.supports_images:
177
177
  parts.append("[no-vision]")
178
+ if m.id == self._model and m.provider == self._model_provider:
179
+ parts.append("✓")
178
180
  caption = " ".join(parts)
179
181
  items.append(ListItem(value=m, label=m.id, description=caption))
180
182
 
@@ -199,8 +201,13 @@ Extra tools:
199
201
  chat.add_info_message(str(e), error=True)
200
202
  return
201
203
 
204
+ current_theme = config.ui.theme
202
205
  items = [
203
- ListItem(value=theme_id, label=label, description=theme_id)
206
+ ListItem(
207
+ value=theme_id,
208
+ label=label,
209
+ description=f"{theme_id} ✓" if theme_id == current_theme else theme_id,
210
+ )
204
211
  for theme_id, label in get_theme_options()
205
212
  ]
206
213
 
@@ -218,7 +225,10 @@ Extra tools:
218
225
  set_theme(theme_id)
219
226
  self._apply_theme(theme_id)
220
227
  chat = self.query_one("#chat-log", ChatLog)
221
- chat.add_info_message(f"Theme changed to {theme_id}")
228
+ chat.add_info_message(
229
+ f"Theme changed to {theme_id}. Full theme refresh applies when kon is restarted.",
230
+ warning=True,
231
+ )
222
232
 
223
233
  def _select_model(self, model) -> None:
224
234
  chat = self.query_one("#chat-log", ChatLog)
@@ -77,8 +77,8 @@ Screen {{
77
77
  .update-available-block {{
78
78
  padding: 0 1;
79
79
  margin: 1 0 0 0;
80
- border-top: solid yellow;
81
- border-bottom: solid yellow;
80
+ border-top: solid {colors.notice};
81
+ border-bottom: solid {colors.notice};
82
82
  }}
83
83
 
84
84
  /* Launch warnings */
@@ -324,6 +324,25 @@ description: Global version
324
324
  )
325
325
  assert collision.message == expected
326
326
 
327
+ def test_skips_duplicate_global_dir_when_cwd_is_home(self, tmp_path, monkeypatch):
328
+ home_dir = tmp_path / "home"
329
+ skill_dir = home_dir / ".kon" / "skills" / "noc"
330
+ skill_dir.mkdir(parents=True)
331
+ (skill_dir / "SKILL.md").write_text("""---
332
+ name: noc
333
+ description: Planning-only mode
334
+ ---
335
+ """)
336
+
337
+ monkeypatch.setattr("kon.context.skills.get_config_dir", lambda: home_dir / ".kon")
338
+
339
+ result = load_skills(str(home_dir))
340
+
341
+ assert len(result.skills) == 1
342
+ assert result.skills[0].name == "noc"
343
+ assert result.skills[0].path == str(skill_dir / "SKILL.md")
344
+ assert result.warnings == []
345
+
327
346
  def test_empty_when_no_skill_directories(self, tmp_path, monkeypatch):
328
347
  repo = tmp_path / "repo"
329
348
  repo.mkdir()
@@ -1,20 +1,15 @@
1
1
  from kon import config
2
- from kon.tools.edit import format_diff_display, truncate_diff_line
2
+ from kon.tools.edit import format_diff_display
3
3
 
4
4
 
5
- def test_truncate_diff_line_does_not_truncate_short_line() -> None:
6
- line = "+2 short line"
7
- assert truncate_diff_line(line, max_chars=90) == line
5
+ def test_format_diff_display_short_lines_not_truncated() -> None:
6
+ short = "+2 short line"
7
+ display = format_diff_display(short)
8
+ assert "..." not in display
9
+ assert "short line" in display
8
10
 
9
11
 
10
- def test_truncate_diff_line_truncates_long_line_with_ellipsis() -> None:
11
- line = "+2 " + "x" * 200
12
- truncated = truncate_diff_line(line, max_chars=90)
13
- assert len(truncated) == 90
14
- assert truncated.endswith("...")
15
-
16
-
17
- def test_format_diff_display_truncates_and_keeps_color_markup() -> None:
12
+ def test_format_diff_display_truncates_long_lines() -> None:
18
13
  long_added = "+2 " + "x" * 200
19
14
  long_removed = "-2 " + "y" * 200
20
15
 
@@ -714,7 +714,7 @@ wheels = [
714
714
 
715
715
  [[package]]
716
716
  name = "kon-coding-agent"
717
- version = "0.3.1"
717
+ version = "0.3.2"
718
718
  source = { editable = "." }
719
719
  dependencies = [
720
720
  { name = "aiofiles" },