akernel-runtime 0.1.5__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.
Files changed (96) hide show
  1. akernel_runtime-0.1.6/.github/release-notes/v0.1.6.md +33 -0
  2. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/CHANGELOG.md +13 -0
  3. {akernel_runtime-0.1.5/src/akernel_runtime.egg-info → akernel_runtime-0.1.6}/PKG-INFO +1 -1
  4. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/packages/npm/akernel/package.json +1 -1
  5. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/pyproject.toml +1 -1
  6. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6/src/akernel_runtime.egg-info}/PKG-INFO +1 -1
  7. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/SOURCES.txt +1 -0
  8. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/__init__.py +1 -1
  9. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/cli.py +222 -54
  10. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/tests/test_runtime.py +66 -7
  11. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/.env.example +0 -0
  12. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  13. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  14. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/.github/pull_request_template.md +0 -0
  15. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.0.md +0 -0
  16. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.1.md +0 -0
  17. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.2.md +0 -0
  18. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.3.md +0 -0
  19. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.4.md +0 -0
  20. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.5.md +0 -0
  21. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/.github/workflows/ci.yml +0 -0
  22. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/.github/workflows/release.yml +0 -0
  23. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/CODE_OF_CONDUCT.md +0 -0
  24. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/CONTRIBUTING.md +0 -0
  25. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/LICENSE +0 -0
  26. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/MANIFEST.in +0 -0
  27. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/NOTICE +0 -0
  28. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/README.md +0 -0
  29. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/SECURITY.md +0 -0
  30. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/docs/00-vision.md +0 -0
  31. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/docs/01-architecture.md +0 -0
  32. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/docs/02-execution-plan.md +0 -0
  33. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/docs/03-cli-mvp.md +0 -0
  34. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/docs/04-evaluation.md +0 -0
  35. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/docs/05-local-wake.md +0 -0
  36. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/docs/06-skill-compiler.md +0 -0
  37. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/docs/07-release-and-ci.md +0 -0
  38. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/docs/08-open-source-plan.md +0 -0
  39. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/docs/09-product-roadmap.md +0 -0
  40. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/docs/10-benchmark-evidence.md +0 -0
  41. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/docs/11-publishing-setup.md +0 -0
  42. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/examples/benchmarks/phase2/01-routing.json +0 -0
  43. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/examples/benchmarks/phase2/02-memory.json +0 -0
  44. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/examples/benchmarks/phase2/03-budget-profiles.json +0 -0
  45. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/examples/benchmarks/scale/01-context-pressure.json +0 -0
  46. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/examples/benchmarks/scale/02-agent-editing.json +0 -0
  47. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/examples/benchmarks/scale/03-global-memory-marketplace.json +0 -0
  48. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/examples/evals/phase2.json +0 -0
  49. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/examples/marketplace/skills/index.json +0 -0
  50. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/examples/skills/context_budget.json +0 -0
  51. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/examples/skills/edit_file.json +0 -0
  52. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/examples/skills/markdown/context_budget.md +0 -0
  53. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/packages/npm/akernel/README.md +0 -0
  54. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/packages/npm/akernel/bin/akernel.js +0 -0
  55. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/scripts/install_remote.ps1 +0 -0
  56. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/scripts/release_check.ps1 +0 -0
  57. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/setup.cfg +0 -0
  58. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/setup.cmd +0 -0
  59. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/setup.ps1 +0 -0
  60. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/dependency_links.txt +0 -0
  61. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/entry_points.txt +0 -0
  62. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/requires.txt +0 -0
  63. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/top_level.txt +0 -0
  64. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/__main__.py +0 -0
  65. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/agent_reports.py +0 -0
  66. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/benchmarks.py +0 -0
  67. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/budget.py +0 -0
  68. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/context.py +0 -0
  69. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/evals.py +0 -0
  70. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/global_memory.py +0 -0
  71. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/loop.py +0 -0
  72. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/marketplace.py +0 -0
  73. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/context_budget.json +0 -0
  74. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/context_compaction.json +0 -0
  75. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/edit_file.json +0 -0
  76. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/index.json +0 -0
  77. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/long_task_planning.json +0 -0
  78. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/multi_file_bugfix.json +0 -0
  79. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/memory.py +0 -0
  80. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/models.py +0 -0
  81. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/planner.py +0 -0
  82. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/policy.py +0 -0
  83. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/project.py +0 -0
  84. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/providers.py +0 -0
  85. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/report_costs.py +0 -0
  86. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/runner.py +0 -0
  87. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/skills.py +0 -0
  88. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/state_writer.py +0 -0
  89. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/storage.py +0 -0
  90. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/tasks.py +0 -0
  91. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/text.py +0 -0
  92. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/tokenizer.py +0 -0
  93. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/tools.py +0 -0
  94. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/src/context_kernel/verifier.py +0 -0
  95. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/wake.cmd +0 -0
  96. {akernel_runtime-0.1.5 → akernel_runtime-0.1.6}/wake.ps1 +0 -0
@@ -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,19 @@ 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
+
11
24
  ## 0.1.5 - 2026-05-12
12
25
 
13
26
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akernel-runtime
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: Agent Kernel: a CLI-first context-native agent runtime prototype.
5
5
  Author: Context Kernel contributors
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-akernel/akernel",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "npm launcher wrapper for the Context Kernel akernel CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/huanxin0825-ctrl/context-akernel#readme",
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "akernel-runtime"
7
- version = "0.1.5"
7
+ version = "0.1.6"
8
8
  description = "Agent Kernel: a CLI-first context-native agent runtime prototype."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akernel-runtime
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: Agent Kernel: a CLI-first context-native agent runtime prototype.
5
5
  Author: Context Kernel contributors
6
6
  License-Expression: Apache-2.0
@@ -21,6 +21,7 @@ wake.ps1
21
21
  .github/release-notes/v0.1.3.md
22
22
  .github/release-notes/v0.1.4.md
23
23
  .github/release-notes/v0.1.5.md
24
+ .github/release-notes/v0.1.6.md
24
25
  .github/workflows/ci.yml
25
26
  .github/workflows/release.yml
26
27
  docs/00-vision.md
@@ -1,3 +1,3 @@
1
1
  """Context Kernel CLI-first agent runtime prototype."""
2
2
 
3
- __version__ = "0.1.5"
3
+ __version__ = "0.1.6"
@@ -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
- attach_chat_file(workspace, tasks, task_id, request[1:].strip(), pending_context)
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,20 +1215,25 @@ def run_chat_loop_tui(
1214
1215
  args: argparse.Namespace,
1215
1216
  ) -> None:
1216
1217
  last_report: dict[str, Any] | None = None
1217
- state: dict[str, Any] = {"last_report": None}
1218
+ state: dict[str, Any] = {"last_report": None, "scroll_offset": 0, "file_matches": []}
1218
1219
  pending_context: list[str] = []
1219
1220
  transcript: list[dict[str, str]] = [
1220
1221
  {
1221
1222
  "role": "system",
1222
1223
  "title": "Welcome",
1223
- "text": "Describe a task, attach files with @path, run safe commands with !command, or type /help.",
1224
+ "text": "Describe a task, search files with @query, run safe commands with !command, or type /help.",
1224
1225
  }
1225
1226
  ]
1226
- use_alt_screen = sys.stdout.isatty() and not os.environ.get("AKERNEL_NO_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
+ )
1227
1232
  if use_alt_screen:
1228
1233
  print("\033[?1049h", end="")
1234
+ state["scrollback_mode"] = not use_alt_screen
1229
1235
  try:
1230
- 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)
1231
1237
  while True:
1232
1238
  try:
1233
1239
  request = input(tui_prompt(args)).strip()
@@ -1235,17 +1241,18 @@ def run_chat_loop_tui(
1235
1241
  break
1236
1242
  except KeyboardInterrupt:
1237
1243
  transcript.append({"role": "system", "title": "Interrupted", "text": "Keyboard interrupt received."})
1238
- 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)
1239
1245
  break
1240
1246
  if not request:
1241
- 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)
1242
1248
  continue
1243
1249
  lowered = request.lower()
1244
1250
  if lowered in {"/exit", "/quit", "exit", "quit"}:
1245
1251
  break
1246
1252
  if lowered == "/clear":
1247
1253
  transcript.clear()
1248
- render_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status="cleared")
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)
1249
1256
  continue
1250
1257
  state["last_report"] = last_report
1251
1258
  if handle_tui_command(
@@ -1260,13 +1267,14 @@ def run_chat_loop_tui(
1260
1267
  state=state,
1261
1268
  ):
1262
1269
  last_report = state.get("last_report")
1263
- 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)
1264
1271
  continue
1265
1272
 
1266
1273
  request_for_agent = merge_pending_context(request, pending_context)
1267
1274
  pending_context.clear()
1275
+ state["scroll_offset"] = 0
1268
1276
  transcript.append({"role": "user", "title": "You", "text": request_for_agent})
1269
- 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)
1270
1278
  last_report = AgentLoop(workspace).run(
1271
1279
  request_for_agent,
1272
1280
  provider_name=args.provider,
@@ -1284,7 +1292,7 @@ def run_chat_loop_tui(
1284
1292
  expect_json=args.expect_json,
1285
1293
  )
1286
1294
  transcript.append({"role": "assistant", "title": "Assistant", "text": format_tui_report(last_report)})
1287
- 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)
1288
1296
  finally:
1289
1297
  if use_alt_screen:
1290
1298
  print("\033[?1049l", end="")
@@ -1304,6 +1312,15 @@ def handle_tui_command(
1304
1312
  state: dict[str, Any],
1305
1313
  ) -> bool:
1306
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
1307
1324
  if lowered == "/help":
1308
1325
  transcript.append({"role": "system", "title": "Help", "text": format_chat_help_text()})
1309
1326
  return True
@@ -1356,7 +1373,7 @@ def handle_tui_command(
1356
1373
  transcript.append({"role": "assistant", "title": "Assistant", "text": format_tui_report(report)})
1357
1374
  return True
1358
1375
  if request.startswith("@"):
1359
- text = capture_chat_output(lambda: attach_chat_file(workspace, tasks, task_id, request[1:].strip(), pending_context))
1376
+ text = capture_chat_output(lambda: attach_chat_file_command(workspace, tasks, task_id, request[1:].strip(), pending_context, state))
1360
1377
  transcript.append({"role": "system", "title": "Attach File", "text": text})
1361
1378
  return True
1362
1379
  if request.startswith("!"):
@@ -1386,8 +1403,12 @@ def format_chat_help_text() -> str:
1386
1403
  ("/task", "print the current task session JSON"),
1387
1404
  ("/runs", "list recent agent runs"),
1388
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"),
1389
1409
  ("/clear", "clear the transcript"),
1390
1410
  ("/exit", "leave the interactive session"),
1411
+ ("@query", "search current workspace files; use @1, @2... to attach a listed match"),
1391
1412
  ]
1392
1413
  return "\n".join(f"{name:<10} {description}" for name, description in rows)
1393
1414
 
@@ -1405,9 +1426,12 @@ def render_chat_tui_screen(
1405
1426
  pending_context: list[str],
1406
1427
  *,
1407
1428
  status: str,
1429
+ state: dict[str, Any] | None = None,
1430
+ clear: bool = True,
1408
1431
  ) -> None:
1409
- screen = build_chat_tui_screen(workspace, task_id, args, transcript, last_report, pending_context, status=status)
1410
- print("\033[2J\033[H" + screen, end="")
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="")
1411
1435
 
1412
1436
 
1413
1437
  def build_chat_tui_screen(
@@ -1419,9 +1443,12 @@ def build_chat_tui_screen(
1419
1443
  pending_context: list[str],
1420
1444
  *,
1421
1445
  status: str,
1446
+ state: dict[str, Any] | None = None,
1422
1447
  ) -> str:
1423
1448
  width = chat_width()
1424
1449
  height = max(24, shutil.get_terminal_size((width, 32)).lines)
1450
+ if (state or {}).get("scrollback_mode"):
1451
+ height = min(height, 22)
1425
1452
  right_width = min(40, max(32, width // 3))
1426
1453
  left_width = max(46, width - right_width - 3)
1427
1454
  header = tui_header_lines(workspace, args, last_report, status=status, width=width)
@@ -1430,12 +1457,13 @@ def build_chat_tui_screen(
1430
1457
  lines = header
1431
1458
  body = tui_body_lines(transcript, left_width, status=status)
1432
1459
  side = tui_sidebar_lines(workspace, task_id, args, last_report, pending_context, right_width)
1433
- body = body[-body_height:]
1460
+ scroll_offset = max(0, int((state or {}).get("scroll_offset", 0)))
1461
+ body = slice_tui_body(body, body_height, scroll_offset, left_width)
1434
1462
  side = side[:body_height]
1435
1463
  for index in range(body_height):
1436
1464
  left = body[index] if index < len(body) else ""
1437
1465
  right = side[index] if index < len(side) else ""
1438
- lines.append(f"{left:<{left_width}} {chat_color('|', 'dim')} {right:<{right_width}}")
1466
+ lines.append(f"{pad_display(left, left_width)} {chat_color('|', 'dim')} {pad_display(right, right_width)}")
1439
1467
  lines.extend(footer)
1440
1468
  return "\n".join(lines)
1441
1469
 
@@ -1451,10 +1479,10 @@ def tui_header_lines(
1451
1479
  status_label = status.upper()
1452
1480
  status_color = "green" if status == "ready" else "yellow" if status == "running" else "cyan"
1453
1481
  tokens = 0 if not last_report else last_report.get("totals", {}).get("total_tokens", 0)
1454
- title = f" Context Kernel TUI // AKERNEL // {status_label} "
1482
+ title = f" AKERNEL // {status_label} "
1455
1483
  subtitle = (
1456
- f"{compact_path(workspace.root)} | provider={args.provider} | "
1457
- f"primary={primary_model(args)} | aux={auxiliary_model(args)} | last_tokens={tokens}"
1484
+ f"{compact_path(workspace.root)} | provider {args.provider} | "
1485
+ f"primary {primary_model(args)} | aux {auxiliary_model(args)} | tokens {tokens}"
1458
1486
  )
1459
1487
  return [
1460
1488
  chat_color(tui_rule(title, width), status_color, bold=True),
@@ -1466,13 +1494,13 @@ def tui_header_lines(
1466
1494
  def tui_footer_lines(width: int) -> list[str]:
1467
1495
  return [
1468
1496
  tui_rule(" Input ", width),
1469
- truncate_line("Type a task. Use /help for palette, /compact for resume brief, @path for files, !command for checked shell, /exit to quit.", width),
1497
+ truncate_line("Type a task. @ finds files, @1 attaches a match, /up and /down move transcript view, /latest returns to now.", width),
1470
1498
  "",
1471
1499
  ]
1472
1500
 
1473
1501
 
1474
1502
  def tui_command_strip(width: int) -> str:
1475
- commands = " /help /status /model /compact /runs /cost @file !cmd "
1503
+ commands = " /help /status /model /compact /runs /cost /up /down @file !cmd "
1476
1504
  return chat_color(truncate_line(commands.center(width, "-"), width), "dim")
1477
1505
 
1478
1506
 
@@ -1480,25 +1508,24 @@ def tui_body_lines(transcript: list[dict[str, str]], width: int, *, status: str
1480
1508
  if not transcript:
1481
1509
  return ["No messages yet. Start with one concrete task."]
1482
1510
  lines: list[str] = []
1483
- lines.append(f"Transcript [{status}]")
1484
- lines.append("-" * min(width, 22))
1511
+ lines.append(f"Conversation [{status}]")
1512
+ lines.append("-" * min(width, 24))
1485
1513
  for item in transcript:
1486
1514
  title = item.get("title", item.get("role", "message"))
1487
1515
  role = item.get("role", "system")
1488
1516
  label = tui_role_label(role, title)
1489
1517
  lines.append("")
1490
- lines.append(truncate_line(f"+-- {label} " + "-" * max(0, width - len(label) - 5), width))
1491
- prefix = "| " if role != "user" else "> "
1518
+ lines.append(truncate_line(f"{label}", width))
1519
+ prefix = " " if role != "user" else "> "
1492
1520
  for line in wrap_plain(item.get("text", ""), width=max(20, width - len(prefix))).splitlines():
1493
1521
  lines.append(truncate_line(prefix + line, width))
1494
- lines.append("")
1495
1522
  return lines
1496
1523
 
1497
1524
 
1498
1525
  def tui_role_label(role: str, title: str) -> str:
1499
1526
  labels = {
1500
1527
  "user": "YOU",
1501
- "assistant": "AGENT",
1528
+ "assistant": "AKERNEL",
1502
1529
  "system": "SYSTEM",
1503
1530
  }
1504
1531
  base = labels.get(role, role.upper())
@@ -1513,23 +1540,23 @@ def tui_sidebar_lines(
1513
1540
  pending_context: list[str],
1514
1541
  width: int,
1515
1542
  ) -> list[str]:
1516
- rows = tui_section("Cockpit", width)
1543
+ rows = tui_section("Session", width)
1517
1544
  rows.extend(
1518
1545
  [
1519
- f"provider: {args.provider}",
1520
- f"profile: {getattr(args, 'profile', DEFAULT_PROFILE)}",
1521
- f"routing: {getattr(args, 'model_routing', 'auto')}",
1522
- f"steps: {getattr(args, 'max_steps', '?')}",
1523
- f"pending: {len(pending_context)}",
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),
1524
1551
  "",
1525
1552
  ]
1526
1553
  )
1527
- rows.extend(tui_section("Model Stack", width))
1554
+ rows.extend(tui_section("Models", width))
1528
1555
  rows.extend(
1529
1556
  [
1530
- f"primary: {primary_model(args)}",
1531
- f"auxiliary: {auxiliary_model(args)}",
1532
- f"review: {getattr(args, 'aux_review', 'auto')}",
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),
1533
1560
  "",
1534
1561
  ]
1535
1562
  )
@@ -1549,22 +1576,26 @@ def tui_sidebar_lines(
1549
1576
 
1550
1577
 
1551
1578
  def tui_section(title: str, width: int) -> list[str]:
1552
- label = f"[ {title} ]"
1553
- return [label, "-" * min(width, len(label) + 6)]
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)
1554
1585
 
1555
1586
 
1556
1587
  def tui_task_panel(workspace: Workspace, task_id: str, width: int) -> list[str]:
1557
- rows = tui_section("Mission", width)
1588
+ rows = tui_section("Task", width)
1558
1589
  try:
1559
1590
  task = TaskStore(workspace).get(task_id)
1560
1591
  except (KeyError, FileNotFoundError):
1561
- rows.extend([f"task {task_id}", "status unknown", ""])
1592
+ rows.extend([tui_kv("id", task_id, width), tui_kv("status", "unknown", width), ""])
1562
1593
  return rows
1563
1594
  rows.extend(
1564
1595
  [
1565
- f"task: {task.get('id', task_id)}",
1566
- f"status: {task.get('status', 'unknown')}",
1567
- f"title: {truncate_line(str(task.get('title', '')), max(10, width - 10))}",
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),
1568
1599
  ]
1569
1600
  )
1570
1601
  plan = task.get("plan")
@@ -1572,26 +1603,26 @@ def tui_task_panel(workspace: Workspace, task_id: str, width: int) -> list[str]:
1572
1603
  progress = plan.get("milestones", [])
1573
1604
  completed = sum(1 for item in progress if item.get("status") == "completed")
1574
1605
  active = next((item for item in progress if item.get("status") == "active"), None)
1575
- rows.append(f"plan: {completed}/{len(progress)} done")
1606
+ rows.append(tui_kv("plan", f"{completed}/{len(progress)} done", width))
1576
1607
  if active:
1577
- rows.append(f"active: {active.get('id')} {truncate_line(str(active.get('title', '')), max(8, width - 13))}")
1608
+ rows.append(tui_kv("active", f"{active.get('id')} {active.get('title', '')}", width))
1578
1609
  rows.append("")
1579
1610
  return rows
1580
1611
 
1581
1612
 
1582
1613
  def tui_last_run_panel(report: dict[str, Any], width: int) -> list[str]:
1583
- rows = tui_section("Last Run Timeline", width)
1614
+ rows = tui_section("Last Run", width)
1584
1615
  rows.extend(
1585
1616
  [
1586
- f"id: {report.get('id')}",
1587
- f"status: {report.get('status')}",
1588
- f"tokens: {report.get('totals', {}).get('total_tokens', 0)}",
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),
1589
1620
  ]
1590
1621
  )
1591
1622
  steps = report.get("steps", [])
1592
1623
  if steps:
1593
1624
  compact_actions = " -> ".join(str((step.get("action") or {}).get("action") or "none") for step in steps)
1594
- rows.append(f"actions: {truncate_line(compact_actions, max(10, width - 9))}")
1625
+ rows.append(tui_kv("actions", compact_actions, width))
1595
1626
  for step in steps[:4]:
1596
1627
  action = str((step.get("action") or {}).get("action") or "none")
1597
1628
  ok = "ok" if step.get("verifier_ok", True) else "check"
@@ -1601,6 +1632,18 @@ def tui_last_run_panel(report: dict[str, Any], width: int) -> list[str]:
1601
1632
  return rows
1602
1633
 
1603
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
+
1604
1647
  def format_tui_report(report: dict[str, Any]) -> str:
1605
1648
  actions = " -> ".join(str((step.get("action") or {}).get("action") or "none") for step in report.get("steps", []))
1606
1649
  parts = [
@@ -1635,7 +1678,34 @@ def wrap_plain(text: str, *, width: int) -> str:
1635
1678
 
1636
1679
  def truncate_line(text: str, width: int) -> str:
1637
1680
  value = str(text)
1638
- return value if len(value) <= width else value[: max(0, width - 3)] + "..."
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
1639
1709
 
1640
1710
 
1641
1711
  def print_chat_turn_start(request: str, args: argparse.Namespace) -> None:
@@ -1692,11 +1762,14 @@ def print_chat_help() -> None:
1692
1762
  ("/config", "show setup and environment guidance"),
1693
1763
  ("/compact", "show the compact task brief used for resume context"),
1694
1764
  ("/paste", "enter a multi-line task; finish with /end"),
1695
- ("@path", "attach a workspace file to the next task"),
1765
+ ("@query", "search workspace files; use @1, @2... to attach a listed match"),
1696
1766
  ("!command", "run a policy-checked command and attach its summary"),
1697
1767
  ("/task", "print the current task session JSON"),
1698
1768
  ("/runs", "list recent agent runs"),
1699
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"),
1700
1773
  ("/clear", "clear and redraw the session header"),
1701
1774
  ("/exit", "leave the interactive session"),
1702
1775
  ],
@@ -1723,6 +1796,101 @@ def print_recent_agent_runs(workspace: Workspace, *, limit: int) -> None:
1723
1796
  )
1724
1797
 
1725
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
+
1726
1894
  def attach_chat_file(
1727
1895
  workspace: Workspace,
1728
1896
  tasks: TaskStore,
@@ -1323,10 +1323,10 @@ class RuntimeTests(unittest.TestCase):
1323
1323
  status="ready",
1324
1324
  )
1325
1325
 
1326
- self.assertIn("Context Kernel TUI", screen)
1327
- self.assertIn("provider: mock", screen)
1326
+ self.assertIn("AKERNEL // READY", screen)
1327
+ self.assertIn("provider mock", screen)
1328
1328
  self.assertIn("Last Run", screen)
1329
- self.assertIn("actions: respond", screen)
1329
+ self.assertIn("actions respond", screen)
1330
1330
 
1331
1331
  def test_tui_chat_runs_agent_loop_after_user_message(self) -> None:
1332
1332
  with tempfile.TemporaryDirectory() as tmp:
@@ -1353,11 +1353,70 @@ class RuntimeTests(unittest.TestCase):
1353
1353
  reports = list(workspace.agent_runs_dir.glob("*.json"))
1354
1354
 
1355
1355
  self.assertEqual(len(reports), 1)
1356
- self.assertIn("Context Kernel TUI", output)
1356
+ self.assertIn("AKERNEL // READY", output)
1357
1357
  self.assertIn("Assistant", output)
1358
1358
  self.assertIn("Mock agent response", output)
1359
1359
  self.assertIn("bye", output)
1360
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)
1419
+
1361
1420
  def test_tui_screen_surfaces_task_plan_and_command_strip(self) -> None:
1362
1421
  with tempfile.TemporaryDirectory() as tmp:
1363
1422
  workspace = Workspace(Path(tmp))
@@ -1393,9 +1452,9 @@ class RuntimeTests(unittest.TestCase):
1393
1452
 
1394
1453
  self.assertIn("AKERNEL // READY", screen)
1395
1454
  self.assertIn("/compact", screen)
1396
- self.assertIn("[ Mission ]", screen)
1397
- self.assertIn("plan:", screen)
1398
- self.assertIn("active:", screen)
1455
+ self.assertIn("Task", screen)
1456
+ self.assertIn("plan", screen)
1457
+ self.assertIn("active", screen)
1399
1458
 
1400
1459
  def test_bare_akernel_starts_chat_and_initializes_default_workspace(self) -> None:
1401
1460
  with tempfile.TemporaryDirectory() as tmp:
File without changes
File without changes