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.
Files changed (96) hide show
  1. akernel_runtime-0.1.6/.github/release-notes/v0.1.5.md +31 -0
  2. akernel_runtime-0.1.6/.github/release-notes/v0.1.6.md +33 -0
  3. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/CHANGELOG.md +20 -0
  4. {akernel_runtime-0.1.4/src/akernel_runtime.egg-info → akernel_runtime-0.1.6}/PKG-INFO +1 -1
  5. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/packages/npm/akernel/package.json +1 -1
  6. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/pyproject.toml +1 -1
  7. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6/src/akernel_runtime.egg-info}/PKG-INFO +1 -1
  8. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/SOURCES.txt +2 -0
  9. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/__init__.py +1 -1
  10. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/cli.py +222 -53
  11. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/providers.py +7 -4
  12. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/tests/test_runtime.py +95 -6
  13. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.env.example +0 -0
  14. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  15. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  16. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/pull_request_template.md +0 -0
  17. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.0.md +0 -0
  18. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.1.md +0 -0
  19. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.2.md +0 -0
  20. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.3.md +0 -0
  21. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/release-notes/v0.1.4.md +0 -0
  22. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/workflows/ci.yml +0 -0
  23. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/.github/workflows/release.yml +0 -0
  24. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/CODE_OF_CONDUCT.md +0 -0
  25. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/CONTRIBUTING.md +0 -0
  26. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/LICENSE +0 -0
  27. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/MANIFEST.in +0 -0
  28. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/NOTICE +0 -0
  29. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/README.md +0 -0
  30. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/SECURITY.md +0 -0
  31. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/00-vision.md +0 -0
  32. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/01-architecture.md +0 -0
  33. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/02-execution-plan.md +0 -0
  34. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/03-cli-mvp.md +0 -0
  35. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/04-evaluation.md +0 -0
  36. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/05-local-wake.md +0 -0
  37. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/06-skill-compiler.md +0 -0
  38. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/07-release-and-ci.md +0 -0
  39. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/08-open-source-plan.md +0 -0
  40. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/09-product-roadmap.md +0 -0
  41. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/10-benchmark-evidence.md +0 -0
  42. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/docs/11-publishing-setup.md +0 -0
  43. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/phase2/01-routing.json +0 -0
  44. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/phase2/02-memory.json +0 -0
  45. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/phase2/03-budget-profiles.json +0 -0
  46. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/scale/01-context-pressure.json +0 -0
  47. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/scale/02-agent-editing.json +0 -0
  48. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/benchmarks/scale/03-global-memory-marketplace.json +0 -0
  49. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/evals/phase2.json +0 -0
  50. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/marketplace/skills/index.json +0 -0
  51. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/skills/context_budget.json +0 -0
  52. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/skills/edit_file.json +0 -0
  53. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/examples/skills/markdown/context_budget.md +0 -0
  54. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/packages/npm/akernel/README.md +0 -0
  55. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/packages/npm/akernel/bin/akernel.js +0 -0
  56. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/scripts/install_remote.ps1 +0 -0
  57. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/scripts/release_check.ps1 +0 -0
  58. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/setup.cfg +0 -0
  59. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/setup.cmd +0 -0
  60. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/setup.ps1 +0 -0
  61. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/dependency_links.txt +0 -0
  62. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/entry_points.txt +0 -0
  63. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/requires.txt +0 -0
  64. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/akernel_runtime.egg-info/top_level.txt +0 -0
  65. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/__main__.py +0 -0
  66. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/agent_reports.py +0 -0
  67. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/benchmarks.py +0 -0
  68. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/budget.py +0 -0
  69. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/context.py +0 -0
  70. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/evals.py +0 -0
  71. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/global_memory.py +0 -0
  72. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/loop.py +0 -0
  73. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace.py +0 -0
  74. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/context_budget.json +0 -0
  75. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/context_compaction.json +0 -0
  76. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/edit_file.json +0 -0
  77. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/index.json +0 -0
  78. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/long_task_planning.json +0 -0
  79. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/marketplace_data/skills/multi_file_bugfix.json +0 -0
  80. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/memory.py +0 -0
  81. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/models.py +0 -0
  82. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/planner.py +0 -0
  83. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/policy.py +0 -0
  84. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/project.py +0 -0
  85. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/report_costs.py +0 -0
  86. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/runner.py +0 -0
  87. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/skills.py +0 -0
  88. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/state_writer.py +0 -0
  89. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/storage.py +0 -0
  90. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/tasks.py +0 -0
  91. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/text.py +0 -0
  92. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/tokenizer.py +0 -0
  93. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/tools.py +0 -0
  94. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/src/context_kernel/verifier.py +0 -0
  95. {akernel_runtime-0.1.4 → akernel_runtime-0.1.6}/wake.cmd +0 -0
  96. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akernel-runtime
3
- Version: 0.1.4
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.4",
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.4"
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.4
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  """Context Kernel CLI-first agent runtime prototype."""
2
2
 
3
- __version__ = "0.1.4"
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,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, 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.",
1223
1225
  }
1224
1226
  ]
1225
- 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
+ )
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
- 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)
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: 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))
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
- 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="")
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
- 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)
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:<{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)}")
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" Context Kernel TUI // AKERNEL // {status_label} "
1482
+ title = f" AKERNEL // {status_label} "
1454
1483
  subtitle = (
1455
- f"{compact_path(workspace.root)} | provider={args.provider} | "
1456
- 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}"
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. 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),
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"Transcript [{status}]")
1483
- lines.append("-" * min(width, 22))
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"+-- {label} " + "-" * max(0, width - len(label) - 5), width))
1490
- prefix = "| " if role != "user" else "> "
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": "AGENT",
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("Cockpit", width)
1543
+ rows = tui_section("Session", width)
1516
1544
  rows.extend(
1517
1545
  [
1518
- f"provider: {args.provider}",
1519
- f"profile: {getattr(args, 'profile', DEFAULT_PROFILE)}",
1520
- f"routing: {getattr(args, 'model_routing', 'auto')}",
1521
- f"steps: {getattr(args, 'max_steps', '?')}",
1522
- 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),
1523
1551
  "",
1524
1552
  ]
1525
1553
  )
1526
- rows.extend(tui_section("Model Stack", width))
1554
+ rows.extend(tui_section("Models", width))
1527
1555
  rows.extend(
1528
1556
  [
1529
- f"primary: {primary_model(args)}",
1530
- f"auxiliary: {auxiliary_model(args)}",
1531
- 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),
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"[ {title} ]"
1552
- 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)
1553
1585
 
1554
1586
 
1555
1587
  def tui_task_panel(workspace: Workspace, task_id: str, width: int) -> list[str]:
1556
- rows = tui_section("Mission", width)
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([f"task {task_id}", "status unknown", ""])
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
- f"task: {task.get('id', task_id)}",
1565
- f"status: {task.get('status', 'unknown')}",
1566
- 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),
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"plan: {completed}/{len(progress)} done")
1606
+ rows.append(tui_kv("plan", f"{completed}/{len(progress)} done", width))
1575
1607
  if active:
1576
- 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))
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 Timeline", width)
1614
+ rows = tui_section("Last Run", width)
1583
1615
  rows.extend(
1584
1616
  [
1585
- f"id: {report.get('id')}",
1586
- f"status: {report.get('status')}",
1587
- 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),
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(f"actions: {truncate_line(compact_actions, max(10, width - 9))}")
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
- 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
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
- ("@path", "attach a workspace file to the next task"),
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
- for directory in [Path.cwd(), *Path.cwd().parents]:
852
- path = directory / ".env"
853
- if path.exists():
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("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
+
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("[ Mission ]", screen)
1367
- self.assertIn("plan:", screen)
1368
- self.assertIn("active:", screen)
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