akernel-runtime 0.1.4__tar.gz → 0.1.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- akernel_runtime-0.1.6/.github/release-notes/v0.1.5.md +31 -0
- akernel_runtime-0.1.6/.github/release-notes/v0.1.6.md +33 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/CHANGELOG.md +20 -0
- {akernel_runtime-0.1.4/src/akernel_runtime.egg-info → akernel_runtime-0.1.6}/PKG-INFO +1 -1
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/packages/npm/akernel/package.json +1 -1
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/pyproject.toml +1 -1
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6/src/akernel_runtime.egg-info}/PKG-INFO +1 -1
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/SOURCES.txt +2 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/__init__.py +1 -1
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/cli.py +222 -53
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/providers.py +7 -4
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/tests/test_runtime.py +95 -6
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.env.example +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/pull_request_template.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.0.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.1.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.2.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.3.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.4.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/workflows/ci.yml +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/workflows/release.yml +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/CODE_OF_CONDUCT.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/CONTRIBUTING.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/LICENSE +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/MANIFEST.in +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/NOTICE +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/README.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/SECURITY.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/00-vision.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/01-architecture.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/02-execution-plan.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/03-cli-mvp.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/04-evaluation.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/05-local-wake.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/06-skill-compiler.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/07-release-and-ci.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/08-open-source-plan.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/09-product-roadmap.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/10-benchmark-evidence.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/11-publishing-setup.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/phase2/01-routing.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/phase2/02-memory.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/phase2/03-budget-profiles.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/scale/01-context-pressure.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/scale/02-agent-editing.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/scale/03-global-memory-marketplace.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/evals/phase2.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/marketplace/skills/index.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/skills/context_budget.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/skills/edit_file.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/skills/markdown/context_budget.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/packages/npm/akernel/README.md +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/packages/npm/akernel/bin/akernel.js +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/scripts/install_remote.ps1 +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/scripts/release_check.ps1 +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/setup.cfg +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/setup.cmd +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/setup.ps1 +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/dependency_links.txt +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/entry_points.txt +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/requires.txt +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/top_level.txt +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/__main__.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/agent_reports.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/benchmarks.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/budget.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/context.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/evals.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/global_memory.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/loop.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/context_budget.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/context_compaction.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/edit_file.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/index.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/long_task_planning.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/multi_file_bugfix.json +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/memory.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/models.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/planner.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/policy.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/project.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/report_costs.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/runner.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/skills.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/state_writer.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/storage.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/tasks.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/text.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/tokenizer.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/tools.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/verifier.py +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/wake.cmd +0 -0
- {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/wake.ps1 +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Context Kernel v0.1.5
|
|
2
|
+
|
|
3
|
+
This patch fixes a critical interactive TUI startup bug.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Python:
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
python -m pip install --user --upgrade akernel-runtime
|
|
11
|
+
akernel setup
|
|
12
|
+
akernel
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
npm launcher:
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
npm install -g @context-akernel/akernel
|
|
19
|
+
akernel setup
|
|
20
|
+
akernel
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Fixes
|
|
24
|
+
|
|
25
|
+
- Fixed TUI chat sessions exiting on the first normal user message.
|
|
26
|
+
- Stabilized `.env` lookup so the launcher project root is used before unrelated parent-directory `.env` files.
|
|
27
|
+
|
|
28
|
+
## Verification
|
|
29
|
+
|
|
30
|
+
- Added a forced `--ui tui` regression test that sends a task and confirms the agent loop completes.
|
|
31
|
+
- Ran the full Python test suite.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Context Kernel v0.1.6
|
|
2
|
+
|
|
3
|
+
This release improves the interactive `akernel` experience with a cleaner terminal workspace, scrollback-friendly history, and file search through `@`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Python:
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
python -m pip install --user --upgrade akernel-runtime
|
|
11
|
+
akernel setup
|
|
12
|
+
akernel
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
npm launcher:
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
npm install -g @context-akernel/akernel
|
|
19
|
+
akernel setup
|
|
20
|
+
akernel
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Highlights
|
|
24
|
+
|
|
25
|
+
- Default TUI no longer takes over the terminal alternate screen, so normal terminal scrollback works.
|
|
26
|
+
- Added `/up`, `/down`, and `/latest` for keyboard-driven transcript viewport control.
|
|
27
|
+
- Added `@` file search: use `@`, `@query`, then `@1`, `@2`, etc. to attach a listed file.
|
|
28
|
+
- Refined the TUI layout with cleaner sections, lighter visual density, and better CJK/wide-character alignment.
|
|
29
|
+
|
|
30
|
+
## Verification
|
|
31
|
+
|
|
32
|
+
- Added regression tests for TUI history viewport rendering and numbered `@` file attachment.
|
|
33
|
+
- Ran the full Python test suite.
|
|
@@ -8,6 +8,26 @@ The project follows a pragmatic pre-1.0 changelog: breaking changes may occur, b
|
|
|
8
8
|
|
|
9
9
|
No changes yet.
|
|
10
10
|
|
|
11
|
+
## 0.1.6 - 2026-05-14
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added TUI history viewport controls with `/up`, `/down`, and `/latest`.
|
|
16
|
+
- Added `@` file search for the current workspace, including numbered follow-up attachment commands such as `@1`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Redesigned the interactive TUI into a cleaner, lower-density terminal workspace.
|
|
21
|
+
- Made the default TUI scrollback-friendly by avoiding alt-screen takeover unless `AKERNEL_ALT_SCREEN=1` is set.
|
|
22
|
+
- Improved CJK/wide-character alignment in TUI rows and truncation.
|
|
23
|
+
|
|
24
|
+
## 0.1.5 - 2026-05-12
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Fixed TUI chat sessions exiting on the first user message because the interactive state holder was not initialized.
|
|
29
|
+
- Made project-root `.env` fallback stable when a user-level parent directory also contains a `.env` file.
|
|
30
|
+
|
|
11
31
|
## 0.1.4 - 2026-05-12
|
|
12
32
|
|
|
13
33
|
### Changed
|
|
@@ -20,6 +20,8 @@ wake.ps1
|
|
|
20
20
|
.github/release-notes/v0.1.2.md
|
|
21
21
|
.github/release-notes/v0.1.3.md
|
|
22
22
|
.github/release-notes/v0.1.4.md
|
|
23
|
+
.github/release-notes/v0.1.5.md
|
|
24
|
+
.github/release-notes/v0.1.6.md
|
|
23
25
|
.github/workflows/ci.yml
|
|
24
26
|
.github/workflows/release.yml
|
|
25
27
|
docs/00-vision.md
|
|
@@ -9,6 +9,7 @@ import os
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
import shutil
|
|
11
11
|
import sys
|
|
12
|
+
import unicodedata
|
|
12
13
|
from typing import Any
|
|
13
14
|
|
|
14
15
|
from .agent_reports import build_agent_cost_report, load_agent_report, render_agent_cost_report
|
|
@@ -1065,7 +1066,7 @@ def cmd_chat(args: argparse.Namespace) -> None:
|
|
|
1065
1066
|
task_id = task["id"]
|
|
1066
1067
|
|
|
1067
1068
|
last_report: dict[str, Any] | None = None
|
|
1068
|
-
state: dict[str, Any] = {"last_report": None}
|
|
1069
|
+
state: dict[str, Any] = {"last_report": None, "file_matches": []}
|
|
1069
1070
|
pending_context: list[str] = []
|
|
1070
1071
|
if resolve_chat_ui(args) == "tui":
|
|
1071
1072
|
run_chat_loop_tui(workspace, tasks, task_id, args)
|
|
@@ -1101,7 +1102,7 @@ def cmd_chat(args: argparse.Namespace) -> None:
|
|
|
1101
1102
|
continue
|
|
1102
1103
|
request = pasted
|
|
1103
1104
|
elif request.startswith("@"):
|
|
1104
|
-
|
|
1105
|
+
attach_chat_file_command(workspace, tasks, task_id, request[1:].strip(), pending_context, state)
|
|
1105
1106
|
continue
|
|
1106
1107
|
elif request.startswith("!"):
|
|
1107
1108
|
run_chat_command(workspace, tasks, task_id, request[1:].strip(), pending_context)
|
|
@@ -1214,19 +1215,25 @@ def run_chat_loop_tui(
|
|
|
1214
1215
|
args: argparse.Namespace,
|
|
1215
1216
|
) -> None:
|
|
1216
1217
|
last_report: dict[str, Any] | None = None
|
|
1218
|
+
state: dict[str, Any] = {"last_report": None, "scroll_offset": 0, "file_matches": []}
|
|
1217
1219
|
pending_context: list[str] = []
|
|
1218
1220
|
transcript: list[dict[str, str]] = [
|
|
1219
1221
|
{
|
|
1220
1222
|
"role": "system",
|
|
1221
1223
|
"title": "Welcome",
|
|
1222
|
-
"text": "Describe a task,
|
|
1224
|
+
"text": "Describe a task, search files with @query, run safe commands with !command, or type /help.",
|
|
1223
1225
|
}
|
|
1224
1226
|
]
|
|
1225
|
-
use_alt_screen =
|
|
1227
|
+
use_alt_screen = (
|
|
1228
|
+
sys.stdout.isatty()
|
|
1229
|
+
and os.environ.get("AKERNEL_ALT_SCREEN", "").strip().lower() in {"1", "true", "yes"}
|
|
1230
|
+
and not os.environ.get("AKERNEL_NO_ALT_SCREEN")
|
|
1231
|
+
)
|
|
1226
1232
|
if use_alt_screen:
|
|
1227
1233
|
print("\033[?1049h", end="")
|
|
1234
|
+
state["scrollback_mode"] = not use_alt_screen
|
|
1228
1235
|
try:
|
|
1229
|
-
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="ready")
|
|
1236
|
+
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="ready", state=state, clear=use_alt_screen)
|
|
1230
1237
|
while True:
|
|
1231
1238
|
try:
|
|
1232
1239
|
request = input(tui_prompt(args)).strip()
|
|
@@ -1234,17 +1241,18 @@ def run_chat_loop_tui(
|
|
|
1234
1241
|
break
|
|
1235
1242
|
except KeyboardInterrupt:
|
|
1236
1243
|
transcript.append({"role": "system", "title": "Interrupted", "text": "Keyboard interrupt received."})
|
|
1237
|
-
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="interrupted")
|
|
1244
|
+
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="interrupted", state=state, clear=use_alt_screen)
|
|
1238
1245
|
break
|
|
1239
1246
|
if not request:
|
|
1240
|
-
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="ready")
|
|
1247
|
+
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="ready", state=state, clear=use_alt_screen)
|
|
1241
1248
|
continue
|
|
1242
1249
|
lowered = request.lower()
|
|
1243
1250
|
if lowered in {"/exit", "/quit", "exit", "quit"}:
|
|
1244
1251
|
break
|
|
1245
1252
|
if lowered == "/clear":
|
|
1246
1253
|
transcript.clear()
|
|
1247
|
-
|
|
1254
|
+
state["scroll_offset"] = 0
|
|
1255
|
+
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="cleared", state=state, clear=use_alt_screen)
|
|
1248
1256
|
continue
|
|
1249
1257
|
state["last_report"] = last_report
|
|
1250
1258
|
if handle_tui_command(
|
|
@@ -1259,13 +1267,14 @@ def run_chat_loop_tui(
|
|
|
1259
1267
|
state=state,
|
|
1260
1268
|
):
|
|
1261
1269
|
last_report = state.get("last_report")
|
|
1262
|
-
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="ready")
|
|
1270
|
+
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="ready", state=state, clear=use_alt_screen)
|
|
1263
1271
|
continue
|
|
1264
1272
|
|
|
1265
1273
|
request_for_agent = merge_pending_context(request, pending_context)
|
|
1266
1274
|
pending_context.clear()
|
|
1275
|
+
state["scroll_offset"] = 0
|
|
1267
1276
|
transcript.append({"role": "user", "title": "You", "text": request_for_agent})
|
|
1268
|
-
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="running")
|
|
1277
|
+
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="running", state=state, clear=use_alt_screen)
|
|
1269
1278
|
last_report = AgentLoop(workspace).run(
|
|
1270
1279
|
request_for_agent,
|
|
1271
1280
|
provider_name=args.provider,
|
|
@@ -1283,7 +1292,7 @@ def run_chat_loop_tui(
|
|
|
1283
1292
|
expect_json=args.expect_json,
|
|
1284
1293
|
)
|
|
1285
1294
|
transcript.append({"role": "assistant", "title": "Assistant", "text": format_tui_report(last_report)})
|
|
1286
|
-
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="ready")
|
|
1295
|
+
render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="ready", state=state, clear=use_alt_screen)
|
|
1287
1296
|
finally:
|
|
1288
1297
|
if use_alt_screen:
|
|
1289
1298
|
print("\033[?1049l", end="")
|
|
@@ -1303,6 +1312,15 @@ def handle_tui_command(
|
|
|
1303
1312
|
state: dict[str, Any],
|
|
1304
1313
|
) -> bool:
|
|
1305
1314
|
last_report = state.get("last_report")
|
|
1315
|
+
if lowered in {"/up", "/pageup", "/pgup"}:
|
|
1316
|
+
state["scroll_offset"] = int(state.get("scroll_offset", 0)) + 12
|
|
1317
|
+
return True
|
|
1318
|
+
if lowered in {"/down", "/pagedown", "/pgdn"}:
|
|
1319
|
+
state["scroll_offset"] = max(0, int(state.get("scroll_offset", 0)) - 12)
|
|
1320
|
+
return True
|
|
1321
|
+
if lowered in {"/latest", "/bottom"}:
|
|
1322
|
+
state["scroll_offset"] = 0
|
|
1323
|
+
return True
|
|
1306
1324
|
if lowered == "/help":
|
|
1307
1325
|
transcript.append({"role": "system", "title": "Help", "text": format_chat_help_text()})
|
|
1308
1326
|
return True
|
|
@@ -1355,7 +1373,7 @@ def handle_tui_command(
|
|
|
1355
1373
|
transcript.append({"role": "assistant", "title": "Assistant", "text": format_tui_report(report)})
|
|
1356
1374
|
return True
|
|
1357
1375
|
if request.startswith("@"):
|
|
1358
|
-
text = capture_chat_output(lambda:
|
|
1376
|
+
text = capture_chat_output(lambda: attach_chat_file_command(workspace, tasks, task_id, request[1:].strip(), pending_context, state))
|
|
1359
1377
|
transcript.append({"role": "system", "title": "Attach File", "text": text})
|
|
1360
1378
|
return True
|
|
1361
1379
|
if request.startswith("!"):
|
|
@@ -1385,8 +1403,12 @@ def format_chat_help_text() -> str:
|
|
|
1385
1403
|
("/task", "print the current task session JSON"),
|
|
1386
1404
|
("/runs", "list recent agent runs"),
|
|
1387
1405
|
("/cost", "print the last agent run cost report"),
|
|
1406
|
+
("/up", "show older transcript lines in the viewport"),
|
|
1407
|
+
("/down", "move the viewport back toward latest messages"),
|
|
1408
|
+
("/latest", "jump to the newest transcript lines"),
|
|
1388
1409
|
("/clear", "clear the transcript"),
|
|
1389
1410
|
("/exit", "leave the interactive session"),
|
|
1411
|
+
("@query", "search current workspace files; use @1, @2... to attach a listed match"),
|
|
1390
1412
|
]
|
|
1391
1413
|
return "\n".join(f"{name:<10} {description}" for name, description in rows)
|
|
1392
1414
|
|
|
@@ -1404,9 +1426,12 @@ def render_chat_tui_screen(
|
|
|
1404
1426
|
pending_context: list[str],
|
|
1405
1427
|
*,
|
|
1406
1428
|
status: str,
|
|
1429
|
+
state: dict[str, Any] | None = None,
|
|
1430
|
+
clear: bool = True,
|
|
1407
1431
|
) -> None:
|
|
1408
|
-
screen = build_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status=status)
|
|
1409
|
-
|
|
1432
|
+
screen = build_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status=status, state=state)
|
|
1433
|
+
prefix = "\033[2J\033[H" if clear else "\n"
|
|
1434
|
+
print(prefix + screen + ("\n" if not clear else ""), end="")
|
|
1410
1435
|
|
|
1411
1436
|
|
|
1412
1437
|
def build_chat_tui_screen(
|
|
@@ -1418,9 +1443,12 @@ def build_chat_tui_screen(
|
|
|
1418
1443
|
pending_context: list[str],
|
|
1419
1444
|
*,
|
|
1420
1445
|
status: str,
|
|
1446
|
+
state: dict[str, Any] | None = None,
|
|
1421
1447
|
) -> str:
|
|
1422
1448
|
width = chat_width()
|
|
1423
1449
|
height = max(24, shutil.get_terminal_size((width, 32)).lines)
|
|
1450
|
+
if (state or {}).get("scrollback_mode"):
|
|
1451
|
+
height = min(height, 22)
|
|
1424
1452
|
right_width = min(40, max(32, width // 3))
|
|
1425
1453
|
left_width = max(46, width - right_width - 3)
|
|
1426
1454
|
header = tui_header_lines(workspace, args, last_report, status=status, width=width)
|
|
@@ -1429,12 +1457,13 @@ def build_chat_tui_screen(
|
|
|
1429
1457
|
lines = header
|
|
1430
1458
|
body = tui_body_lines(transcript, left_width, status=status)
|
|
1431
1459
|
side = tui_sidebar_lines(workspace, task_id, args, last_report, pending_context, right_width)
|
|
1432
|
-
|
|
1460
|
+
scroll_offset = max(0, int((state or {}).get("scroll_offset", 0)))
|
|
1461
|
+
body = slice_tui_body(body, body_height, scroll_offset, left_width)
|
|
1433
1462
|
side = side[:body_height]
|
|
1434
1463
|
for index in range(body_height):
|
|
1435
1464
|
left = body[index] if index < len(body) else ""
|
|
1436
1465
|
right = side[index] if index < len(side) else ""
|
|
1437
|
-
lines.append(f"{left
|
|
1466
|
+
lines.append(f"{pad_display(left, left_width)} {chat_color('|', 'dim')} {pad_display(right, right_width)}")
|
|
1438
1467
|
lines.extend(footer)
|
|
1439
1468
|
return "\n".join(lines)
|
|
1440
1469
|
|
|
@@ -1450,10 +1479,10 @@ def tui_header_lines(
|
|
|
1450
1479
|
status_label = status.upper()
|
|
1451
1480
|
status_color = "green" if status == "ready" else "yellow" if status == "running" else "cyan"
|
|
1452
1481
|
tokens = 0 if not last_report else last_report.get("totals", {}).get("total_tokens", 0)
|
|
1453
|
-
title = f"
|
|
1482
|
+
title = f" AKERNEL // {status_label} "
|
|
1454
1483
|
subtitle = (
|
|
1455
|
-
f"{compact_path(workspace.root)}
|
|
1456
|
-
f"primary
|
|
1484
|
+
f"{compact_path(workspace.root)} | provider {args.provider} | "
|
|
1485
|
+
f"primary {primary_model(args)} | aux {auxiliary_model(args)} | tokens {tokens}"
|
|
1457
1486
|
)
|
|
1458
1487
|
return [
|
|
1459
1488
|
chat_color(tui_rule(title, width), status_color, bold=True),
|
|
@@ -1465,13 +1494,13 @@ def tui_header_lines(
|
|
|
1465
1494
|
def tui_footer_lines(width: int) -> list[str]:
|
|
1466
1495
|
return [
|
|
1467
1496
|
tui_rule(" Input ", width),
|
|
1468
|
-
truncate_line("Type a task.
|
|
1497
|
+
truncate_line("Type a task. @ finds files, @1 attaches a match, /up and /down move transcript view, /latest returns to now.", width),
|
|
1469
1498
|
"",
|
|
1470
1499
|
]
|
|
1471
1500
|
|
|
1472
1501
|
|
|
1473
1502
|
def tui_command_strip(width: int) -> str:
|
|
1474
|
-
commands = " /help /status /model /compact /runs /cost @file !cmd "
|
|
1503
|
+
commands = " /help /status /model /compact /runs /cost /up /down @file !cmd "
|
|
1475
1504
|
return chat_color(truncate_line(commands.center(width, "-"), width), "dim")
|
|
1476
1505
|
|
|
1477
1506
|
|
|
@@ -1479,25 +1508,24 @@ def tui_body_lines(transcript: list[dict[str, str]], width: int, *, status: str
|
|
|
1479
1508
|
if not transcript:
|
|
1480
1509
|
return ["No messages yet. Start with one concrete task."]
|
|
1481
1510
|
lines: list[str] = []
|
|
1482
|
-
lines.append(f"
|
|
1483
|
-
lines.append("-" * min(width,
|
|
1511
|
+
lines.append(f"Conversation [{status}]")
|
|
1512
|
+
lines.append("-" * min(width, 24))
|
|
1484
1513
|
for item in transcript:
|
|
1485
1514
|
title = item.get("title", item.get("role", "message"))
|
|
1486
1515
|
role = item.get("role", "system")
|
|
1487
1516
|
label = tui_role_label(role, title)
|
|
1488
1517
|
lines.append("")
|
|
1489
|
-
lines.append(truncate_line(f"
|
|
1490
|
-
prefix = "
|
|
1518
|
+
lines.append(truncate_line(f"{label}", width))
|
|
1519
|
+
prefix = " " if role != "user" else "> "
|
|
1491
1520
|
for line in wrap_plain(item.get("text", ""), width=max(20, width - len(prefix))).splitlines():
|
|
1492
1521
|
lines.append(truncate_line(prefix + line, width))
|
|
1493
|
-
lines.append("")
|
|
1494
1522
|
return lines
|
|
1495
1523
|
|
|
1496
1524
|
|
|
1497
1525
|
def tui_role_label(role: str, title: str) -> str:
|
|
1498
1526
|
labels = {
|
|
1499
1527
|
"user": "YOU",
|
|
1500
|
-
"assistant": "
|
|
1528
|
+
"assistant": "AKERNEL",
|
|
1501
1529
|
"system": "SYSTEM",
|
|
1502
1530
|
}
|
|
1503
1531
|
base = labels.get(role, role.upper())
|
|
@@ -1512,23 +1540,23 @@ def tui_sidebar_lines(
|
|
|
1512
1540
|
pending_context: list[str],
|
|
1513
1541
|
width: int,
|
|
1514
1542
|
) -> list[str]:
|
|
1515
|
-
rows = tui_section("
|
|
1543
|
+
rows = tui_section("Session", width)
|
|
1516
1544
|
rows.extend(
|
|
1517
1545
|
[
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1546
|
+
tui_kv("provider", args.provider, width),
|
|
1547
|
+
tui_kv("profile", getattr(args, "profile", DEFAULT_PROFILE), width),
|
|
1548
|
+
tui_kv("routing", getattr(args, "model_routing", "auto"), width),
|
|
1549
|
+
tui_kv("steps", getattr(args, "max_steps", "?"), width),
|
|
1550
|
+
tui_kv("attached", len(pending_context), width),
|
|
1523
1551
|
"",
|
|
1524
1552
|
]
|
|
1525
1553
|
)
|
|
1526
|
-
rows.extend(tui_section("
|
|
1554
|
+
rows.extend(tui_section("Models", width))
|
|
1527
1555
|
rows.extend(
|
|
1528
1556
|
[
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1557
|
+
tui_kv("primary", primary_model(args), width),
|
|
1558
|
+
tui_kv("aux", auxiliary_model(args), width),
|
|
1559
|
+
tui_kv("review", getattr(args, "aux_review", "auto"), width),
|
|
1532
1560
|
"",
|
|
1533
1561
|
]
|
|
1534
1562
|
)
|
|
@@ -1548,22 +1576,26 @@ def tui_sidebar_lines(
|
|
|
1548
1576
|
|
|
1549
1577
|
|
|
1550
1578
|
def tui_section(title: str, width: int) -> list[str]:
|
|
1551
|
-
label = f"
|
|
1552
|
-
return [label, "-" * min(width, len(label) +
|
|
1579
|
+
label = f"{title}"
|
|
1580
|
+
return [label, "-" * min(width, max(10, len(label) + 4))]
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
def tui_kv(key: str, value: Any, width: int) -> str:
|
|
1584
|
+
return truncate_line(f"{key:<9} {value}", width)
|
|
1553
1585
|
|
|
1554
1586
|
|
|
1555
1587
|
def tui_task_panel(workspace: Workspace, task_id: str, width: int) -> list[str]:
|
|
1556
|
-
rows = tui_section("
|
|
1588
|
+
rows = tui_section("Task", width)
|
|
1557
1589
|
try:
|
|
1558
1590
|
task = TaskStore(workspace).get(task_id)
|
|
1559
1591
|
except (KeyError, FileNotFoundError):
|
|
1560
|
-
rows.extend([
|
|
1592
|
+
rows.extend([tui_kv("id", task_id, width), tui_kv("status", "unknown", width), ""])
|
|
1561
1593
|
return rows
|
|
1562
1594
|
rows.extend(
|
|
1563
1595
|
[
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1596
|
+
tui_kv("id", task.get("id", task_id), width),
|
|
1597
|
+
tui_kv("status", task.get("status", "unknown"), width),
|
|
1598
|
+
tui_kv("title", task.get("title", ""), width),
|
|
1567
1599
|
]
|
|
1568
1600
|
)
|
|
1569
1601
|
plan = task.get("plan")
|
|
@@ -1571,26 +1603,26 @@ def tui_task_panel(workspace: Workspace, task_id: str, width: int) -> list[str]:
|
|
|
1571
1603
|
progress = plan.get("milestones", [])
|
|
1572
1604
|
completed = sum(1 for item in progress if item.get("status") == "completed")
|
|
1573
1605
|
active = next((item for item in progress if item.get("status") == "active"), None)
|
|
1574
|
-
rows.append(f"
|
|
1606
|
+
rows.append(tui_kv("plan", f"{completed}/{len(progress)} done", width))
|
|
1575
1607
|
if active:
|
|
1576
|
-
rows.append(
|
|
1608
|
+
rows.append(tui_kv("active", f"{active.get('id')} {active.get('title', '')}", width))
|
|
1577
1609
|
rows.append("")
|
|
1578
1610
|
return rows
|
|
1579
1611
|
|
|
1580
1612
|
|
|
1581
1613
|
def tui_last_run_panel(report: dict[str, Any], width: int) -> list[str]:
|
|
1582
|
-
rows = tui_section("Last Run
|
|
1614
|
+
rows = tui_section("Last Run", width)
|
|
1583
1615
|
rows.extend(
|
|
1584
1616
|
[
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1617
|
+
tui_kv("id", report.get("id"), width),
|
|
1618
|
+
tui_kv("status", report.get("status"), width),
|
|
1619
|
+
tui_kv("tokens", report.get("totals", {}).get("total_tokens", 0), width),
|
|
1588
1620
|
]
|
|
1589
1621
|
)
|
|
1590
1622
|
steps = report.get("steps", [])
|
|
1591
1623
|
if steps:
|
|
1592
1624
|
compact_actions = " -> ".join(str((step.get("action") or {}).get("action") or "none") for step in steps)
|
|
1593
|
-
rows.append(
|
|
1625
|
+
rows.append(tui_kv("actions", compact_actions, width))
|
|
1594
1626
|
for step in steps[:4]:
|
|
1595
1627
|
action = str((step.get("action") or {}).get("action") or "none")
|
|
1596
1628
|
ok = "ok" if step.get("verifier_ok", True) else "check"
|
|
@@ -1600,6 +1632,18 @@ def tui_last_run_panel(report: dict[str, Any], width: int) -> list[str]:
|
|
|
1600
1632
|
return rows
|
|
1601
1633
|
|
|
1602
1634
|
|
|
1635
|
+
def slice_tui_body(lines: list[str], height: int, scroll_offset: int, width: int) -> list[str]:
|
|
1636
|
+
if len(lines) <= height:
|
|
1637
|
+
return lines
|
|
1638
|
+
offset = max(0, min(scroll_offset, len(lines) - height))
|
|
1639
|
+
end = len(lines) - offset
|
|
1640
|
+
start = max(0, end - height)
|
|
1641
|
+
window = lines[start:end]
|
|
1642
|
+
if offset:
|
|
1643
|
+
window = [truncate_line(f"History view: {offset} line(s) above latest. Use /down or /latest.", width)] + window[1:]
|
|
1644
|
+
return window
|
|
1645
|
+
|
|
1646
|
+
|
|
1603
1647
|
def format_tui_report(report: dict[str, Any]) -> str:
|
|
1604
1648
|
actions = " -> ".join(str((step.get("action") or {}).get("action") or "none") for step in report.get("steps", []))
|
|
1605
1649
|
parts = [
|
|
@@ -1634,7 +1678,34 @@ def wrap_plain(text: str, *, width: int) -> str:
|
|
|
1634
1678
|
|
|
1635
1679
|
def truncate_line(text: str, width: int) -> str:
|
|
1636
1680
|
value = str(text)
|
|
1637
|
-
|
|
1681
|
+
if display_width(value) <= width:
|
|
1682
|
+
return value
|
|
1683
|
+
if width <= 3:
|
|
1684
|
+
return "." * max(0, width)
|
|
1685
|
+
result = ""
|
|
1686
|
+
used = 0
|
|
1687
|
+
for char in value:
|
|
1688
|
+
char_width = char_display_width(char)
|
|
1689
|
+
if used + char_width > width - 3:
|
|
1690
|
+
break
|
|
1691
|
+
result += char
|
|
1692
|
+
used += char_width
|
|
1693
|
+
return result + "..."
|
|
1694
|
+
|
|
1695
|
+
|
|
1696
|
+
def pad_display(text: str, width: int) -> str:
|
|
1697
|
+
value = truncate_line(text, width)
|
|
1698
|
+
return value + " " * max(0, width - display_width(value))
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
def display_width(text: str) -> int:
|
|
1702
|
+
return sum(char_display_width(char) for char in str(text))
|
|
1703
|
+
|
|
1704
|
+
|
|
1705
|
+
def char_display_width(char: str) -> int:
|
|
1706
|
+
if unicodedata.combining(char):
|
|
1707
|
+
return 0
|
|
1708
|
+
return 2 if unicodedata.east_asian_width(char) in {"F", "W"} else 1
|
|
1638
1709
|
|
|
1639
1710
|
|
|
1640
1711
|
def print_chat_turn_start(request: str, args: argparse.Namespace) -> None:
|
|
@@ -1691,11 +1762,14 @@ def print_chat_help() -> None:
|
|
|
1691
1762
|
("/config", "show setup and environment guidance"),
|
|
1692
1763
|
("/compact", "show the compact task brief used for resume context"),
|
|
1693
1764
|
("/paste", "enter a multi-line task; finish with /end"),
|
|
1694
|
-
("@
|
|
1765
|
+
("@query", "search workspace files; use @1, @2... to attach a listed match"),
|
|
1695
1766
|
("!command", "run a policy-checked command and attach its summary"),
|
|
1696
1767
|
("/task", "print the current task session JSON"),
|
|
1697
1768
|
("/runs", "list recent agent runs"),
|
|
1698
1769
|
("/cost", "print the last agent run cost report"),
|
|
1770
|
+
("/up", "show older transcript lines in the TUI viewport"),
|
|
1771
|
+
("/down", "move the TUI viewport back toward latest messages"),
|
|
1772
|
+
("/latest", "jump the TUI viewport to the latest messages"),
|
|
1699
1773
|
("/clear", "clear and redraw the session header"),
|
|
1700
1774
|
("/exit", "leave the interactive session"),
|
|
1701
1775
|
],
|
|
@@ -1722,6 +1796,101 @@ def print_recent_agent_runs(workspace: Workspace, *, limit: int) -> None:
|
|
|
1722
1796
|
)
|
|
1723
1797
|
|
|
1724
1798
|
|
|
1799
|
+
IGNORED_FILE_FINDER_DIRS = {
|
|
1800
|
+
".git",
|
|
1801
|
+
".hg",
|
|
1802
|
+
".svn",
|
|
1803
|
+
".akernel",
|
|
1804
|
+
".venv",
|
|
1805
|
+
"venv",
|
|
1806
|
+
"node_modules",
|
|
1807
|
+
"__pycache__",
|
|
1808
|
+
".pytest_cache",
|
|
1809
|
+
"dist",
|
|
1810
|
+
"build",
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
def attach_chat_file_command(
|
|
1815
|
+
workspace: Workspace,
|
|
1816
|
+
tasks: TaskStore,
|
|
1817
|
+
task_id: str,
|
|
1818
|
+
query: str,
|
|
1819
|
+
pending_context: list[str],
|
|
1820
|
+
state: dict[str, Any] | None = None,
|
|
1821
|
+
) -> None:
|
|
1822
|
+
state = state if state is not None else {}
|
|
1823
|
+
query = query.strip().strip('"').strip("'")
|
|
1824
|
+
if query.isdigit() and state.get("file_matches"):
|
|
1825
|
+
matches = list(state.get("file_matches") or [])
|
|
1826
|
+
index = int(query) - 1
|
|
1827
|
+
if 0 <= index < len(matches):
|
|
1828
|
+
attach_chat_file(workspace, tasks, task_id, str(matches[index]), pending_context)
|
|
1829
|
+
return
|
|
1830
|
+
chat_notice("File Search", f"No cached match @{query}. Use @ to list files again.")
|
|
1831
|
+
return
|
|
1832
|
+
|
|
1833
|
+
if query and (workspace.root / query).is_file():
|
|
1834
|
+
attach_chat_file(workspace, tasks, task_id, query, pending_context)
|
|
1835
|
+
return
|
|
1836
|
+
|
|
1837
|
+
matches = find_workspace_files(workspace.root, query, limit=12)
|
|
1838
|
+
state["file_matches"] = matches
|
|
1839
|
+
if not matches:
|
|
1840
|
+
hint = "Try @readme, @pyproject, or a filename fragment."
|
|
1841
|
+
chat_notice("File Search", f"No files matched `{query or '*'}`. {hint}")
|
|
1842
|
+
return
|
|
1843
|
+
if query and len(matches) == 1:
|
|
1844
|
+
attach_chat_file(workspace, tasks, task_id, matches[0], pending_context)
|
|
1845
|
+
return
|
|
1846
|
+
|
|
1847
|
+
print("")
|
|
1848
|
+
print(chat_color("[ File Search ]", "cyan", bold=True))
|
|
1849
|
+
print(wrap_chat_text("Type @1, @2, ... to attach a result, or keep typing a narrower @query.", indent=" "))
|
|
1850
|
+
for index, path in enumerate(matches, start=1):
|
|
1851
|
+
print(f" @{index:<2} {path}")
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
def find_workspace_files(root: Path, query: str, *, limit: int = 12, max_scan: int = 2500) -> list[str]:
|
|
1855
|
+
normalized_query = query.casefold().replace("\\", "/")
|
|
1856
|
+
candidates: list[tuple[int, str]] = []
|
|
1857
|
+
scanned = 0
|
|
1858
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
1859
|
+
dirnames[:] = [
|
|
1860
|
+
name
|
|
1861
|
+
for name in dirnames
|
|
1862
|
+
if name not in IGNORED_FILE_FINDER_DIRS and not name.startswith(".mypy_cache")
|
|
1863
|
+
]
|
|
1864
|
+
for filename in filenames:
|
|
1865
|
+
scanned += 1
|
|
1866
|
+
if scanned > max_scan:
|
|
1867
|
+
break
|
|
1868
|
+
path = Path(dirpath) / filename
|
|
1869
|
+
try:
|
|
1870
|
+
relative = path.relative_to(root).as_posix()
|
|
1871
|
+
except ValueError:
|
|
1872
|
+
continue
|
|
1873
|
+
haystack = relative.casefold()
|
|
1874
|
+
name = filename.casefold()
|
|
1875
|
+
if normalized_query and normalized_query not in haystack:
|
|
1876
|
+
continue
|
|
1877
|
+
score = 0
|
|
1878
|
+
if normalized_query:
|
|
1879
|
+
if name == normalized_query:
|
|
1880
|
+
score -= 40
|
|
1881
|
+
elif name.startswith(normalized_query):
|
|
1882
|
+
score -= 25
|
|
1883
|
+
elif haystack.startswith(normalized_query):
|
|
1884
|
+
score -= 15
|
|
1885
|
+
score += haystack.find(normalized_query)
|
|
1886
|
+
score += relative.count("/") * 2
|
|
1887
|
+
score += len(relative) // 20
|
|
1888
|
+
candidates.append((score, relative))
|
|
1889
|
+
if scanned > max_scan:
|
|
1890
|
+
break
|
|
1891
|
+
return [path for _, path in sorted(candidates, key=lambda item: (item[0], item[1]))[:limit]]
|
|
1892
|
+
|
|
1893
|
+
|
|
1725
1894
|
def attach_chat_file(
|
|
1726
1895
|
workspace: Workspace,
|
|
1727
1896
|
tasks: TaskStore,
|
|
@@ -848,15 +848,18 @@ def env_value(name: str) -> str | None:
|
|
|
848
848
|
|
|
849
849
|
|
|
850
850
|
def project_env_values() -> dict[str, str]:
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
return parse_env_file(path)
|
|
851
|
+
cwd_env = Path.cwd() / ".env"
|
|
852
|
+
if cwd_env.exists():
|
|
853
|
+
return parse_env_file(cwd_env)
|
|
855
854
|
project_root = os.environ.get("AKERNEL_PROJECT_ROOT") or os.environ.get("CONTEXT_KERNEL_PROJECT_ROOT")
|
|
856
855
|
if project_root:
|
|
857
856
|
path = Path(project_root) / ".env"
|
|
858
857
|
if path.exists():
|
|
859
858
|
return parse_env_file(path)
|
|
859
|
+
for directory in Path.cwd().parents:
|
|
860
|
+
path = directory / ".env"
|
|
861
|
+
if path.exists():
|
|
862
|
+
return parse_env_file(path)
|
|
860
863
|
return {}
|
|
861
864
|
|
|
862
865
|
|
|
@@ -1323,10 +1323,99 @@ class RuntimeTests(unittest.TestCase):
|
|
|
1323
1323
|
status="ready",
|
|
1324
1324
|
)
|
|
1325
1325
|
|
|
1326
|
-
self.assertIn("
|
|
1327
|
-
self.assertIn("provider
|
|
1326
|
+
self.assertIn("AKERNEL // READY", screen)
|
|
1327
|
+
self.assertIn("provider mock", screen)
|
|
1328
1328
|
self.assertIn("Last Run", screen)
|
|
1329
|
-
self.assertIn("actions
|
|
1329
|
+
self.assertIn("actions respond", screen)
|
|
1330
|
+
|
|
1331
|
+
def test_tui_chat_runs_agent_loop_after_user_message(self) -> None:
|
|
1332
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1333
|
+
workspace = Workspace(Path(tmp))
|
|
1334
|
+
workspace.init()
|
|
1335
|
+
|
|
1336
|
+
with patch("builtins.input", side_effect=["Continue the runtime work", "/exit"]):
|
|
1337
|
+
with patch("sys.stdout", new=io.StringIO()) as stdout:
|
|
1338
|
+
main(
|
|
1339
|
+
[
|
|
1340
|
+
"--workspace",
|
|
1341
|
+
str(workspace.root),
|
|
1342
|
+
"chat",
|
|
1343
|
+
"--provider",
|
|
1344
|
+
"mock",
|
|
1345
|
+
"--max-steps",
|
|
1346
|
+
"1",
|
|
1347
|
+
"--ui",
|
|
1348
|
+
"tui",
|
|
1349
|
+
]
|
|
1350
|
+
)
|
|
1351
|
+
|
|
1352
|
+
output = stdout.getvalue()
|
|
1353
|
+
reports = list(workspace.agent_runs_dir.glob("*.json"))
|
|
1354
|
+
|
|
1355
|
+
self.assertEqual(len(reports), 1)
|
|
1356
|
+
self.assertIn("AKERNEL // READY", output)
|
|
1357
|
+
self.assertIn("Assistant", output)
|
|
1358
|
+
self.assertIn("Mock agent response", output)
|
|
1359
|
+
self.assertIn("bye", output)
|
|
1360
|
+
|
|
1361
|
+
def test_tui_screen_can_render_older_history_window(self) -> None:
|
|
1362
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1363
|
+
workspace = Workspace(Path(tmp))
|
|
1364
|
+
workspace.init()
|
|
1365
|
+
args = type(
|
|
1366
|
+
"Args",
|
|
1367
|
+
(),
|
|
1368
|
+
{
|
|
1369
|
+
"provider": "mock",
|
|
1370
|
+
"model": None,
|
|
1371
|
+
"aux_model": "gpt-5.3-codex",
|
|
1372
|
+
"profile": "balanced",
|
|
1373
|
+
"max_steps": 3,
|
|
1374
|
+
"model_routing": "auto",
|
|
1375
|
+
"aux_review": "auto",
|
|
1376
|
+
},
|
|
1377
|
+
)()
|
|
1378
|
+
transcript = [
|
|
1379
|
+
{"role": "user", "title": "You", "text": f"message {index}"}
|
|
1380
|
+
for index in range(20)
|
|
1381
|
+
]
|
|
1382
|
+
|
|
1383
|
+
screen = build_chat_tui_screen(
|
|
1384
|
+
workspace,
|
|
1385
|
+
"task123",
|
|
1386
|
+
args,
|
|
1387
|
+
transcript,
|
|
1388
|
+
None,
|
|
1389
|
+
[],
|
|
1390
|
+
status="ready",
|
|
1391
|
+
state={"scroll_offset": 10},
|
|
1392
|
+
)
|
|
1393
|
+
|
|
1394
|
+
self.assertIn("History view", screen)
|
|
1395
|
+
self.assertIn("/down or /latest", screen)
|
|
1396
|
+
|
|
1397
|
+
def test_chat_file_search_lists_and_attaches_numbered_match(self) -> None:
|
|
1398
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1399
|
+
root = Path(tmp)
|
|
1400
|
+
(root / "alpha.txt").write_text("alpha context works", encoding="utf-8")
|
|
1401
|
+
(root / "beta.txt").write_text("beta context", encoding="utf-8")
|
|
1402
|
+
workspace = Workspace(root)
|
|
1403
|
+
workspace.init()
|
|
1404
|
+
|
|
1405
|
+
with patch("builtins.input", side_effect=["@", "@1", "Use the attached file", "/exit"]):
|
|
1406
|
+
with patch("sys.stdout", new=io.StringIO()) as stdout:
|
|
1407
|
+
main(["--workspace", str(workspace.root), "chat", "--provider", "mock", "--max-steps", "1"])
|
|
1408
|
+
|
|
1409
|
+
output = stdout.getvalue()
|
|
1410
|
+
reports = list(workspace.agent_runs_dir.glob("*.json"))
|
|
1411
|
+
tool_traces = list(workspace.tool_traces_dir.glob("*.json"))
|
|
1412
|
+
|
|
1413
|
+
self.assertEqual(len(reports), 1)
|
|
1414
|
+
self.assertGreaterEqual(len(tool_traces), 1)
|
|
1415
|
+
self.assertIn("File Search", output)
|
|
1416
|
+
self.assertIn("@1", output)
|
|
1417
|
+
self.assertIn("Attached File", output)
|
|
1418
|
+
self.assertIn("Mock agent response", output)
|
|
1330
1419
|
|
|
1331
1420
|
def test_tui_screen_surfaces_task_plan_and_command_strip(self) -> None:
|
|
1332
1421
|
with tempfile.TemporaryDirectory() as tmp:
|
|
@@ -1363,9 +1452,9 @@ class RuntimeTests(unittest.TestCase):
|
|
|
1363
1452
|
|
|
1364
1453
|
self.assertIn("AKERNEL // READY", screen)
|
|
1365
1454
|
self.assertIn("/compact", screen)
|
|
1366
|
-
self.assertIn("
|
|
1367
|
-
self.assertIn("plan
|
|
1368
|
-
self.assertIn("active
|
|
1455
|
+
self.assertIn("Task", screen)
|
|
1456
|
+
self.assertIn("plan", screen)
|
|
1457
|
+
self.assertIn("active", screen)
|
|
1369
1458
|
|
|
1370
1459
|
def test_bare_akernel_starts_chat_and_initializes_default_workspace(self) -> None:
|
|
1371
1460
|
with tempfile.TemporaryDirectory() as tmp:
|
|
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
|
{akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/phase2/03-budget-profiles.json
RENAMED
|
File without changes
|
{akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/scale/01-context-pressure.json
RENAMED
|
File without changes
|
{akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/scale/02-agent-editing.json
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
|
{akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/entry_points.txt
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
|