noah-code 0.4.0__tar.gz → 0.4.1__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.
- {noah_code-0.4.0 → noah_code-0.4.1}/PKG-INFO +1 -1
- noah_code-0.4.1/docs/releases/v0.4.1.md +54 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/pyproject.toml +1 -1
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/__init__.py +1 -1
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/agent.py +13 -3
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/cli.py +10 -5
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/commands.py +76 -22
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/composer.py +4 -1
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/config.py +57 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/host.py +153 -1
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/providers.py +7 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/steer.py +45 -1
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/process_tools.py +10 -2
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/workspace_tools.py +86 -10
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/ui/textual.css +199 -2
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/ui/textual_app.py +1421 -120
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_agent_security.py +7 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_cache_context.py +2 -1
- noah_code-0.4.1/tests/test_commands.py +65 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_composer.py +2 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_config.py +16 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_host.py +51 -1
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_process_tools.py +23 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_providers.py +13 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_steer.py +27 -1
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_textual_tui.py +483 -17
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_workspace_tools.py +41 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/uv.lock +1 -1
- {noah_code-0.4.0 → noah_code-0.4.1}/.github/workflows/ci.yml +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/.github/workflows/release.yml +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/.gitignore +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/.pre-commit-config.yaml +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/README.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/assets/noah-in-action.svg +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/assets/noah-logo.svg +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/configuration.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/development.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/extensions.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/interactive-reference.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/releases/v0.1.0.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/releases/v0.1.1.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/releases/v0.2.0.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/releases/v0.2.1.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/releases/v0.2.2.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/releases/v0.2.3.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/releases/v0.2.4.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/releases/v0.3.0.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/releases/v0.4.0.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/reliability.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/docs/security.md +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/install.sh +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/__main__.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/agents.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/approvals.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/budget.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/checkpoints.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/credentials.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/custom_commands.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/event_bridge.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/events.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/github.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/hooks.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/llm.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/llm_replies.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/macos_sandbox.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/mcp_setup.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/nooa_compat.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/permissions.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/predict.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/project_notes.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/redaction.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/runtime_state.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/secure_files.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/sessions.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/skills_setup.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/snapshots.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/summarization.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/telemetry.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/themes.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tool_output.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/__init__.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/diff_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/git_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/github_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/lsp_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/media_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/memory_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/plan_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/question_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/task_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/tools/web_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/ui/__init__.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/ui/console.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/ui/protocol.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/updates.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/usage.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/workspace.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/src/noah_code/worktree.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/conftest.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_agents.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_approvals.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_budget.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_checkpoints.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_cli.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_compaction_eviction.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_console_ui.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_credentials.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_custom_commands.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_diff_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_efficiency.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_event_bridge.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_event_bridge_and_shell.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_git_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_github.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_hooks.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_installer.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_llm.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_llm_replies.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_lsp_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_mcp_setup.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_permissions.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_plan_memory_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_project_notes.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_prompt_scorecard.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_question_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_redaction.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_run_exit.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_runtime_state.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_sessions.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_skills_setup.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_snapshots.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_summarization.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_task_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_telemetry.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_tool_output.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_updates.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_wave1_e2e.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_web_tools.py +0 -0
- {noah_code-0.4.0 → noah_code-0.4.1}/tests/test_worktree.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: noah-code
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Noah Code terminal coding agent, built on NVIDIA OO Agents (NOOA)
|
|
5
5
|
Project-URL: Homepage, https://github.com/skundu42/noah-code
|
|
6
6
|
Project-URL: Documentation, https://github.com/skundu42/noah-code#readme
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Noah Code v0.4.1
|
|
2
|
+
|
|
3
|
+
This release makes Noah's terminal interface easier to learn, calmer during long-running work,
|
|
4
|
+
and more reliable across supported platforms.
|
|
5
|
+
|
|
6
|
+
## Guided and discoverable UX
|
|
7
|
+
|
|
8
|
+
- Added first-run onboarding that detects missing model configuration and guides users through
|
|
9
|
+
provider setup instead of stopping at an error.
|
|
10
|
+
- Added a searchable, context-aware keyboard help overlay with shortcuts for the currently focused
|
|
11
|
+
panel.
|
|
12
|
+
- Streamlined slash-command discovery into one flat list, removed duplicate command entries, and
|
|
13
|
+
kept labels focused on the commands themselves.
|
|
14
|
+
- Added `/continue` session selection for the ten most recent sessions in the current workspace.
|
|
15
|
+
- Added automatic update notices with the appropriate upgrade command.
|
|
16
|
+
|
|
17
|
+
## Clearer long-running work
|
|
18
|
+
|
|
19
|
+
- Added tool-aware progress with elapsed time and concise counts for searches and other operations.
|
|
20
|
+
- Added a collapsible task timeline for thinking, tool calls, retries, approvals, and validation
|
|
21
|
+
without adding noise to the transcript.
|
|
22
|
+
- Added context visibility for active files, instructions, project memory, and attachments.
|
|
23
|
+
- Added queue management for pending prompts and attachments, expandable error details, safer undo
|
|
24
|
+
confirmation, and compact completion receipts.
|
|
25
|
+
|
|
26
|
+
## Faster, steadier TUI
|
|
27
|
+
|
|
28
|
+
- Reworked the working indicator into a continuous left-to-right animation with isolated 80 ms
|
|
29
|
+
loader updates, while elapsed status refreshes remain throttled separately.
|
|
30
|
+
- Added reduced-motion support and semantic states for thinking, running tools, waiting for input or
|
|
31
|
+
approval, cancelling, and errors.
|
|
32
|
+
- Improved live-output following so users can inspect earlier output while new-line counts remain
|
|
33
|
+
visible and can jump back to the latest output on demand.
|
|
34
|
+
- Avoided unnecessary full-screen redraws that caused flashing and visual instability.
|
|
35
|
+
|
|
36
|
+
## Reliability
|
|
37
|
+
|
|
38
|
+
- Made common search and read requests accept familiar paths, regex arguments, and content helper
|
|
39
|
+
forms more consistently.
|
|
40
|
+
- Hardened loader shutdown against timer/widget teardown races found on Intel macOS CI.
|
|
41
|
+
- Added a direct-child termination fallback when macOS denies an owned process-group signal.
|
|
42
|
+
- Stabilized asynchronous TUI validation across Linux, Apple-silicon macOS, and Intel macOS.
|
|
43
|
+
|
|
44
|
+
## Upgrade
|
|
45
|
+
|
|
46
|
+
Existing managed installations can run:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
noah update
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
New installations can use the one-command installer from the README.
|
|
53
|
+
|
|
54
|
+
**Full changelog:** https://github.com/skundu42/noah-code/compare/v0.4.0...v0.4.1
|
|
@@ -119,6 +119,7 @@ class _PermissionSandboxedExecutor(SandboxedExecutor):
|
|
|
119
119
|
("ws", "edit"),
|
|
120
120
|
("ws", "inspect"),
|
|
121
121
|
("ws", "apply_patch"),
|
|
122
|
+
("ws", "apply_unified_diff"),
|
|
122
123
|
("ws", "list"),
|
|
123
124
|
("ws", "list_files"),
|
|
124
125
|
("ws", "read"),
|
|
@@ -146,7 +147,13 @@ class _PermissionSandboxedExecutor(SandboxedExecutor):
|
|
|
146
147
|
("web", "search"),
|
|
147
148
|
("task", "list"),
|
|
148
149
|
("task", "run"),
|
|
150
|
+
("task", "run_many"),
|
|
151
|
+
("task", "collaborate"),
|
|
149
152
|
("processes", "input"),
|
|
153
|
+
("processes", "open_terminal"),
|
|
154
|
+
("processes", "terminal_run"),
|
|
155
|
+
("processes", "terminal_status"),
|
|
156
|
+
("processes", "close_terminal"),
|
|
150
157
|
("processes", "logs"),
|
|
151
158
|
("processes", "start"),
|
|
152
159
|
("processes", "status"),
|
|
@@ -389,6 +396,9 @@ tools with `await`. `self.message(text)` is synchronous—never await it.
|
|
|
389
396
|
three arguments; two-argument calls are invalid. Prefer one atomic
|
|
390
397
|
`self.ws.apply_patch(changes)` for a
|
|
391
398
|
coherent batch; `self.ws.apply_unified_diff` accepts git-style hunks.
|
|
399
|
+
- Common shapes: `ws.search(pattern, path=".", paths=None, regex=True)` returns
|
|
400
|
+
iterable Matches plus `.stdout`; `ws.read(path, lines=None)` returns a Match
|
|
401
|
+
with `.text`/`.content`; `ws.list(pattern="**/*", path=".")` returns paths.
|
|
392
402
|
- Run commands with `await self.ws.run(command)` and inspect
|
|
393
403
|
returncode/stdout/stderr. `read_only=True` skips approval only for commands
|
|
394
404
|
the engine recognizes as read-only (Git inspection, search, listing, text
|
|
@@ -418,7 +428,7 @@ when the host was explicitly launched for a throwaway workspace.
|
|
|
418
428
|
|
|
419
429
|
Sandboxed cells forbid host/system imports (including os, sys, subprocess,
|
|
420
430
|
shutil, nooa, and noah_code), dynamic execution, input, and attaching callables
|
|
421
|
-
to `self`. Use dedicated tools instead; use `doc(
|
|
431
|
+
to `self`. Use dedicated tools instead; use `doc(self)` for API help.
|
|
422
432
|
|
|
423
433
|
End with exactly one RespondResult: DONE, NEED_INPUT, or WAIT (WAIT requires a
|
|
424
434
|
registered running job). In code call
|
|
@@ -432,8 +442,8 @@ _NOAH_EXECUTION_CONTEXT = """## Execution Context
|
|
|
432
442
|
Inside `execute_python`, method parameters and `self` are already in scope and
|
|
433
443
|
state persists across cells. Always available: `print`, `pprint`, `doc`,
|
|
434
444
|
`return_result`, `RespondReason`, `asyncio`, and `typing`. Inspect only the API
|
|
435
|
-
you need with `doc(self
|
|
436
|
-
module imports are intentionally not part of the agent-facing contract.
|
|
445
|
+
you need with `doc(self)`; nested sandbox proxies are not introspectable. Noah's
|
|
446
|
+
internal module imports are intentionally not part of the agent-facing contract.
|
|
437
447
|
"""
|
|
438
448
|
|
|
439
449
|
_OBSERVABILITY_EVENT_TYPES = (
|
|
@@ -948,13 +948,18 @@ async def _interactive(
|
|
|
948
948
|
return code
|
|
949
949
|
workspace, config, store, meta = prepared
|
|
950
950
|
if use_tui:
|
|
951
|
+
from noah_code.providers import model_setup_required
|
|
952
|
+
|
|
951
953
|
host = AgentHost(workspace, config, session_meta=meta, store=store)
|
|
952
954
|
onboarding_required = (
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
955
|
+
(
|
|
956
|
+
first_run
|
|
957
|
+
and model is None
|
|
958
|
+
and meta is None
|
|
959
|
+
and not os.environ.get("NOAH_CODE_MODEL")
|
|
960
|
+
and config.model == NoahCodeConfig().model
|
|
961
|
+
)
|
|
962
|
+
or model_setup_required(meta.model if meta is not None else config.model)
|
|
958
963
|
)
|
|
959
964
|
try:
|
|
960
965
|
try:
|
|
@@ -25,18 +25,24 @@ class CommandSpec:
|
|
|
25
25
|
class CommandSuggestion:
|
|
26
26
|
invocation: str
|
|
27
27
|
description: str
|
|
28
|
+
category: str = "Other"
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
BUILTIN_COMMANDS: list[CommandSpec] = [
|
|
31
32
|
CommandSpec("help", "Show available commands"),
|
|
32
33
|
CommandSpec("config", "Show every resolved setting or one path", "config [PATH]"),
|
|
33
34
|
CommandSpec("theme", "Show or switch the interface theme", "theme [NAME]"),
|
|
35
|
+
CommandSpec("animations", "Enable or disable interface motion", "animations [on|off]"),
|
|
34
36
|
CommandSpec("mode", "Show or switch the active mode", "mode [build|plan]"),
|
|
35
|
-
CommandSpec(
|
|
37
|
+
CommandSpec(
|
|
38
|
+
"model",
|
|
39
|
+
"Configure a provider or switch this session's model",
|
|
40
|
+
"model [MODEL | --global MODEL]",
|
|
41
|
+
),
|
|
36
42
|
CommandSpec(
|
|
37
43
|
"reasoning",
|
|
38
44
|
"Show or set reasoning effort for compatible models",
|
|
39
|
-
"reasoning [
|
|
45
|
+
"reasoning [EFFORT | --global EFFORT]",
|
|
40
46
|
),
|
|
41
47
|
CommandSpec(
|
|
42
48
|
"providers",
|
|
@@ -62,10 +68,11 @@ BUILTIN_COMMANDS: list[CommandSpec] = [
|
|
|
62
68
|
"Show, save, forget, or clear project conventions",
|
|
63
69
|
"memory [save|forget|clear]",
|
|
64
70
|
),
|
|
65
|
-
CommandSpec("continue", "
|
|
71
|
+
CommandSpec("continue", "Choose from the latest ten workspace sessions"),
|
|
66
72
|
CommandSpec("compact", "Trigger history summarization"),
|
|
67
73
|
CommandSpec("todos", "Show todo list"),
|
|
68
74
|
CommandSpec("status", "Show mode/model/session/context"),
|
|
75
|
+
CommandSpec("context", "Inspect files, instructions, memory, and attachments in context"),
|
|
69
76
|
CommandSpec("health", "Show durable runtime health"),
|
|
70
77
|
CommandSpec("checkpoints", "List rolling Git worktree checkpoints"),
|
|
71
78
|
CommandSpec("tokens", "Show token, cache, cost, and latency usage"),
|
|
@@ -79,6 +86,8 @@ BUILTIN_COMMANDS: list[CommandSpec] = [
|
|
|
79
86
|
CommandSpec("redo", "Redo last undone turn"),
|
|
80
87
|
CommandSpec("agents", "List built-in and markdown subagents"),
|
|
81
88
|
CommandSpec("work", "Show live agent, terminal, and background-job work"),
|
|
89
|
+
CommandSpec("timeline", "Inspect the current task timeline"),
|
|
90
|
+
CommandSpec("queue", "Inspect and manage queued prompts and attachments"),
|
|
82
91
|
CommandSpec("terminals", "List persistent terminal sessions"),
|
|
83
92
|
CommandSpec("attach", "Attach a workspace file or image to the next turn", "attach [PATH]"),
|
|
84
93
|
CommandSpec("skills", "Search skills or add a compatible skill folder", "skills [add PATH]"),
|
|
@@ -88,6 +97,47 @@ BUILTIN_COMMANDS: list[CommandSpec] = [
|
|
|
88
97
|
]
|
|
89
98
|
|
|
90
99
|
|
|
100
|
+
_COMMAND_CATEGORIES = {
|
|
101
|
+
"help": "General",
|
|
102
|
+
"config": "Settings",
|
|
103
|
+
"theme": "Settings",
|
|
104
|
+
"animations": "Settings",
|
|
105
|
+
"mode": "Agent",
|
|
106
|
+
"model": "Model",
|
|
107
|
+
"reasoning": "Model",
|
|
108
|
+
"providers": "Model",
|
|
109
|
+
"efficiency": "Model",
|
|
110
|
+
"session": "Session",
|
|
111
|
+
"sessions": "Session",
|
|
112
|
+
"new": "Session",
|
|
113
|
+
"continue": "Session",
|
|
114
|
+
"compact": "Session",
|
|
115
|
+
"worktree": "Git",
|
|
116
|
+
"pr": "Git",
|
|
117
|
+
"diff": "Git",
|
|
118
|
+
"undo": "Git",
|
|
119
|
+
"redo": "Git",
|
|
120
|
+
"checkpoints": "Git",
|
|
121
|
+
"plan": "Project",
|
|
122
|
+
"memory": "Project",
|
|
123
|
+
"todos": "Project",
|
|
124
|
+
"attach": "Project",
|
|
125
|
+
"status": "Runtime",
|
|
126
|
+
"context": "Runtime",
|
|
127
|
+
"health": "Runtime",
|
|
128
|
+
"tokens": "Runtime",
|
|
129
|
+
"trace": "Runtime",
|
|
130
|
+
"agents": "Work",
|
|
131
|
+
"work": "Work",
|
|
132
|
+
"timeline": "Work",
|
|
133
|
+
"queue": "Work",
|
|
134
|
+
"terminals": "Work",
|
|
135
|
+
"skills": "Extensions",
|
|
136
|
+
"mcp": "Extensions",
|
|
137
|
+
"exit": "General",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
91
141
|
_SECRET_CONFIG_KEYS = frozenset(
|
|
92
142
|
{
|
|
93
143
|
"access_key",
|
|
@@ -218,21 +268,19 @@ def all_command_suggestions(
|
|
|
218
268
|
custom: dict[str, CustomCommand] | None = None,
|
|
219
269
|
) -> list[CommandSuggestion]:
|
|
220
270
|
suggestions = [
|
|
221
|
-
CommandSuggestion(command.invocation, command.description) for command in BUILTIN_COMMANDS
|
|
222
|
-
]
|
|
223
|
-
suggestions.append(
|
|
224
|
-
CommandSuggestion("/model --global MODEL", "Set the default model for every repository")
|
|
225
|
-
)
|
|
226
|
-
suggestions.append(
|
|
227
271
|
CommandSuggestion(
|
|
228
|
-
|
|
229
|
-
|
|
272
|
+
command.invocation,
|
|
273
|
+
command.description,
|
|
274
|
+
_COMMAND_CATEGORIES.get(command.name, "Other"),
|
|
230
275
|
)
|
|
231
|
-
|
|
276
|
+
for command in BUILTIN_COMMANDS
|
|
277
|
+
]
|
|
232
278
|
if custom:
|
|
279
|
+
reserved = {command.name for command in BUILTIN_COMMANDS}
|
|
233
280
|
suggestions.extend(
|
|
234
|
-
CommandSuggestion(f"/{name} [ARGS]", command.description)
|
|
281
|
+
CommandSuggestion(f"/{name} [ARGS]", command.description, "Custom")
|
|
235
282
|
for name, command in sorted(custom.items())
|
|
283
|
+
if name not in reserved
|
|
236
284
|
)
|
|
237
285
|
return suggestions
|
|
238
286
|
|
|
@@ -241,21 +289,27 @@ def config_command_suggestions(config: Any) -> list[CommandSuggestion]:
|
|
|
241
289
|
suggestions = []
|
|
242
290
|
for path, value in config_entries(config):
|
|
243
291
|
display_value = value if len(value) <= 80 else f"{value[:77]}…"
|
|
244
|
-
suggestions.append(
|
|
292
|
+
suggestions.append(
|
|
293
|
+
CommandSuggestion(f"/config {path}", f"current: {display_value}", "Settings")
|
|
294
|
+
)
|
|
245
295
|
return suggestions
|
|
246
296
|
|
|
247
297
|
|
|
248
298
|
def help_text(custom: dict[str, CustomCommand] | None = None) -> str:
|
|
249
299
|
lines = ["Noah Code commands:", ""]
|
|
250
|
-
for cmd in BUILTIN_COMMANDS
|
|
251
|
-
lines.append(f" {cmd.invocation:<28} {cmd.description}")
|
|
252
|
-
lines.append(f" {'/model --global MODEL':<28} Set the default model for every repository")
|
|
253
|
-
lines.append(f" {'/reasoning --global EFFORT':<28} Set the global reasoning effort default")
|
|
300
|
+
lines.extend(f" {cmd.invocation:<38} {cmd.description}" for cmd in BUILTIN_COMMANDS)
|
|
254
301
|
if custom:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
302
|
+
reserved = {command.name for command in BUILTIN_COMMANDS}
|
|
303
|
+
visible_custom = [
|
|
304
|
+
(name, command)
|
|
305
|
+
for name, command in sorted(custom.items())
|
|
306
|
+
if name not in reserved
|
|
307
|
+
]
|
|
308
|
+
else:
|
|
309
|
+
visible_custom = []
|
|
310
|
+
if visible_custom:
|
|
311
|
+
for name, c in visible_custom:
|
|
312
|
+
lines.append(f" /{name + ' [ARGS]':<37} {c.description}")
|
|
259
313
|
lines.append("")
|
|
260
314
|
lines.append(
|
|
261
315
|
"File-journal undo only covers WorkspaceTools edits, not arbitrary shell mutations."
|
|
@@ -38,6 +38,7 @@ class ExpandedTurn:
|
|
|
38
38
|
|
|
39
39
|
text: str
|
|
40
40
|
images: list[Any] = field(default_factory=list)
|
|
41
|
+
paths: list[Path] = field(default_factory=list)
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
def _workspace_files(root: Path) -> list[Path]:
|
|
@@ -100,6 +101,7 @@ def expand_turn(
|
|
|
100
101
|
extras = [Path(path) for path in attach_paths or []]
|
|
101
102
|
sections: list[str] = []
|
|
102
103
|
images: list[Any] = []
|
|
104
|
+
paths: list[Path] = []
|
|
103
105
|
seen: set[str] = set()
|
|
104
106
|
|
|
105
107
|
for raw in [*mentioned, *[str(path) for path in extras]]:
|
|
@@ -110,6 +112,7 @@ def expand_turn(
|
|
|
110
112
|
if key in seen:
|
|
111
113
|
continue
|
|
112
114
|
seen.add(key)
|
|
115
|
+
paths.append(resolved)
|
|
113
116
|
relative = _display(root, resolved)
|
|
114
117
|
suffix = resolved.suffix.lower()
|
|
115
118
|
if suffix in IMAGE_TYPES:
|
|
@@ -128,7 +131,7 @@ def expand_turn(
|
|
|
128
131
|
if not sections:
|
|
129
132
|
return ExpandedTurn(text=text)
|
|
130
133
|
notice = "Attached files from @mentions:\n\n" + "\n\n".join(sections)
|
|
131
|
-
return ExpandedTurn(text=f"{text.rstrip()}\n\n{notice}", images=images)
|
|
134
|
+
return ExpandedTurn(text=f"{text.rstrip()}\n\n{notice}", images=images, paths=paths)
|
|
132
135
|
|
|
133
136
|
|
|
134
137
|
def _resolve(root: Path, raw: str) -> Path | None:
|
|
@@ -134,6 +134,7 @@ class UIConfig(BaseModel):
|
|
|
134
134
|
model_config = ConfigDict(extra="forbid")
|
|
135
135
|
|
|
136
136
|
theme: ThemeName = "atom-one-dark"
|
|
137
|
+
animations: bool = True
|
|
137
138
|
show_reasoning: bool = False
|
|
138
139
|
markdown: bool = True
|
|
139
140
|
frontend: Literal["tui", "console"] = "tui"
|
|
@@ -563,6 +564,62 @@ def save_user_theme(theme: str) -> Path:
|
|
|
563
564
|
return path
|
|
564
565
|
|
|
565
566
|
|
|
567
|
+
def save_user_animations(enabled: bool) -> Path:
|
|
568
|
+
"""Persist the reduced-motion preference inside the user ``[ui]`` table."""
|
|
569
|
+
|
|
570
|
+
path = _user_config_path()
|
|
571
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
572
|
+
existing = path.read_text() if path.is_file() else ""
|
|
573
|
+
lines = existing.splitlines(keepends=True)
|
|
574
|
+
replacement = f"animations = {'true' if enabled else 'false'}\n"
|
|
575
|
+
|
|
576
|
+
table_start = next(
|
|
577
|
+
(index for index, line in enumerate(lines) if line.strip() == "[ui]"),
|
|
578
|
+
None,
|
|
579
|
+
)
|
|
580
|
+
if table_start is None:
|
|
581
|
+
if lines and not lines[-1].endswith(("\n", "\r")):
|
|
582
|
+
lines[-1] += "\n"
|
|
583
|
+
if lines and lines[-1].strip():
|
|
584
|
+
lines.append("\n")
|
|
585
|
+
lines.extend(["[ui]\n", replacement])
|
|
586
|
+
else:
|
|
587
|
+
table_end = next(
|
|
588
|
+
(
|
|
589
|
+
index
|
|
590
|
+
for index, line in enumerate(lines[table_start + 1 :], start=table_start + 1)
|
|
591
|
+
if line.lstrip().startswith("[")
|
|
592
|
+
),
|
|
593
|
+
len(lines),
|
|
594
|
+
)
|
|
595
|
+
animation_line = next(
|
|
596
|
+
(
|
|
597
|
+
index
|
|
598
|
+
for index, line in enumerate(
|
|
599
|
+
lines[table_start + 1 : table_end], start=table_start + 1
|
|
600
|
+
)
|
|
601
|
+
if re.match(r"^\s*animations\s*=", line)
|
|
602
|
+
),
|
|
603
|
+
None,
|
|
604
|
+
)
|
|
605
|
+
if animation_line is None:
|
|
606
|
+
lines.insert(table_end, replacement)
|
|
607
|
+
else:
|
|
608
|
+
lines[animation_line] = replacement
|
|
609
|
+
|
|
610
|
+
descriptor, temporary_name = tempfile.mkstemp(prefix=".config-", dir=path.parent)
|
|
611
|
+
temporary_path = Path(temporary_name)
|
|
612
|
+
try:
|
|
613
|
+
with os.fdopen(descriptor, "w") as stream:
|
|
614
|
+
stream.write("".join(lines))
|
|
615
|
+
os.chmod(temporary_path, 0o600)
|
|
616
|
+
os.replace(temporary_path, path)
|
|
617
|
+
finally:
|
|
618
|
+
if temporary_path.exists():
|
|
619
|
+
temporary_path.unlink()
|
|
620
|
+
return path
|
|
621
|
+
|
|
622
|
+
|
|
566
623
|
def _project_config_path(workspace: Path) -> Path:
|
|
567
624
|
return workspace / ".noah-code" / "config.toml"
|
|
568
625
|
|
|
@@ -19,6 +19,7 @@ from noah_code.commands import config_text, help_text, parse_slash
|
|
|
19
19
|
from noah_code.config import (
|
|
20
20
|
REASONING_EFFORTS,
|
|
21
21
|
NoahCodeConfig,
|
|
22
|
+
save_user_animations,
|
|
22
23
|
save_user_default_model,
|
|
23
24
|
save_user_reasoning_effort,
|
|
24
25
|
save_user_theme,
|
|
@@ -248,6 +249,7 @@ class AgentHost:
|
|
|
248
249
|
self._current_run_id: str | None = None
|
|
249
250
|
self._interrupted_run: RunRecord | None = None
|
|
250
251
|
self._pending_attach_paths: list[Path] = []
|
|
252
|
+
self._active_context_paths: set[Path] = set()
|
|
251
253
|
self._pending_worktree_name = ""
|
|
252
254
|
self.on_session_changed: Any = None # optional UI callback
|
|
253
255
|
|
|
@@ -956,6 +958,7 @@ class AgentHost:
|
|
|
956
958
|
"""Persist current session and open a fresh one in-process."""
|
|
957
959
|
self._require_idle_turn()
|
|
958
960
|
self._clear_steer_state()
|
|
961
|
+
self._active_context_paths.clear()
|
|
959
962
|
await self._cancel_background_tasks()
|
|
960
963
|
await self._persist_async()
|
|
961
964
|
if workspace is not None:
|
|
@@ -984,6 +987,7 @@ class AgentHost:
|
|
|
984
987
|
meta = self.store.load_meta(session_id)
|
|
985
988
|
rebound = self.store.workspace_for_resume(meta, self.workspace)
|
|
986
989
|
self._clear_steer_state()
|
|
990
|
+
self._active_context_paths.clear()
|
|
987
991
|
await self._cancel_background_tasks()
|
|
988
992
|
await self._persist_async()
|
|
989
993
|
self._teardown_event_bridge()
|
|
@@ -1439,6 +1443,110 @@ class AgentHost:
|
|
|
1439
1443
|
paths, self._pending_attach_paths = self._pending_attach_paths, []
|
|
1440
1444
|
return paths
|
|
1441
1445
|
|
|
1446
|
+
def pending_attach_paths(self) -> tuple[Path, ...]:
|
|
1447
|
+
"""Return files waiting to be attached to the next queued prompt."""
|
|
1448
|
+
|
|
1449
|
+
return tuple(self._pending_attach_paths)
|
|
1450
|
+
|
|
1451
|
+
def active_context_paths(self) -> tuple[Path, ...]:
|
|
1452
|
+
"""Return attachment paths already delivered in the active session."""
|
|
1453
|
+
|
|
1454
|
+
return tuple(sorted(self._active_context_paths))
|
|
1455
|
+
|
|
1456
|
+
def context_snapshot(self) -> list[dict[str, str]]:
|
|
1457
|
+
"""Return a display-safe inventory of context sources for the UI."""
|
|
1458
|
+
|
|
1459
|
+
from noah_code.project_notes import MEMORY_RELATIVE, PLAN_RELATIVE, MemoryStore, PlanStore
|
|
1460
|
+
|
|
1461
|
+
rows: list[dict[str, str]] = []
|
|
1462
|
+
root = self.workspace.root
|
|
1463
|
+
for relative in ("AGENTS.md", "CLAUDE.md", ".noah-code/instructions.md"):
|
|
1464
|
+
path = root / relative
|
|
1465
|
+
if path.is_file():
|
|
1466
|
+
rows.append(
|
|
1467
|
+
{
|
|
1468
|
+
"kind": "instruction",
|
|
1469
|
+
"label": relative,
|
|
1470
|
+
"detail": f"Repository instructions · {path}",
|
|
1471
|
+
}
|
|
1472
|
+
)
|
|
1473
|
+
if PlanStore(root).exists():
|
|
1474
|
+
rows.append(
|
|
1475
|
+
{
|
|
1476
|
+
"kind": "plan",
|
|
1477
|
+
"label": PLAN_RELATIVE,
|
|
1478
|
+
"detail": "Pinned project plan included at each turn boundary",
|
|
1479
|
+
}
|
|
1480
|
+
)
|
|
1481
|
+
if MemoryStore(root).exists():
|
|
1482
|
+
rows.append(
|
|
1483
|
+
{
|
|
1484
|
+
"kind": "memory",
|
|
1485
|
+
"label": MEMORY_RELATIVE,
|
|
1486
|
+
"detail": "Durable project conventions included in model context",
|
|
1487
|
+
}
|
|
1488
|
+
)
|
|
1489
|
+
for path in self.active_context_paths():
|
|
1490
|
+
rows.append(
|
|
1491
|
+
{
|
|
1492
|
+
"kind": "attachment",
|
|
1493
|
+
"label": path.name,
|
|
1494
|
+
"detail": f"Attached in this session · {path}",
|
|
1495
|
+
}
|
|
1496
|
+
)
|
|
1497
|
+
for path in self.pending_attach_paths():
|
|
1498
|
+
rows.append(
|
|
1499
|
+
{
|
|
1500
|
+
"kind": "pending",
|
|
1501
|
+
"label": path.name,
|
|
1502
|
+
"detail": f"Waiting for the next prompt · {path}",
|
|
1503
|
+
}
|
|
1504
|
+
)
|
|
1505
|
+
if self._agent is not None:
|
|
1506
|
+
with contextlib.suppress(Exception):
|
|
1507
|
+
for info in self.list_skill_infos():
|
|
1508
|
+
if info.active:
|
|
1509
|
+
rows.append(
|
|
1510
|
+
{
|
|
1511
|
+
"kind": "skill",
|
|
1512
|
+
"label": info.name,
|
|
1513
|
+
"detail": f"Active skill · {info.source}",
|
|
1514
|
+
}
|
|
1515
|
+
)
|
|
1516
|
+
for name in sorted(self._mcp_attached):
|
|
1517
|
+
rows.append(
|
|
1518
|
+
{
|
|
1519
|
+
"kind": "mcp",
|
|
1520
|
+
"label": name,
|
|
1521
|
+
"detail": "Connected MCP server tools available to Noah",
|
|
1522
|
+
}
|
|
1523
|
+
)
|
|
1524
|
+
return rows
|
|
1525
|
+
|
|
1526
|
+
def context_status_text(self) -> str:
|
|
1527
|
+
rows = self.context_snapshot()
|
|
1528
|
+
if not rows:
|
|
1529
|
+
return "Context sources\n Workspace defaults only"
|
|
1530
|
+
lines = ["Context sources"]
|
|
1531
|
+
lines.extend(f" {row['kind']:<11} {row['label']}\n {row['detail']}" for row in rows)
|
|
1532
|
+
return "\n".join(lines)
|
|
1533
|
+
|
|
1534
|
+
def remove_pending_attach(self, index: int) -> Path | None:
|
|
1535
|
+
if index < 0 or index >= len(self._pending_attach_paths):
|
|
1536
|
+
return None
|
|
1537
|
+
return self._pending_attach_paths.pop(index)
|
|
1538
|
+
|
|
1539
|
+
def remove_queued_steer(self, index: int) -> bool:
|
|
1540
|
+
item = self.steer_queue.remove(index)
|
|
1541
|
+
if item is None:
|
|
1542
|
+
return False
|
|
1543
|
+
if self._runtime is not None and item.sequence is not None:
|
|
1544
|
+
self._runtime.acknowledge_inbox(item.sequence, dropped=True)
|
|
1545
|
+
return True
|
|
1546
|
+
|
|
1547
|
+
def move_queued_steer(self, index: int, delta: int) -> bool:
|
|
1548
|
+
return self.steer_queue.move(index, delta)
|
|
1549
|
+
|
|
1442
1550
|
def enqueue_steer(self, text: str, attach_paths: list[Path] | None = None) -> bool:
|
|
1443
1551
|
"""Queue a follow-up for the current turn. Returns True if oldest dropped."""
|
|
1444
1552
|
|
|
@@ -1591,6 +1699,26 @@ class AgentHost:
|
|
|
1591
1699
|
)
|
|
1592
1700
|
)
|
|
1593
1701
|
return "handled"
|
|
1702
|
+
if name == "animations":
|
|
1703
|
+
requested = args.strip().lower()
|
|
1704
|
+
if not requested:
|
|
1705
|
+
state = "on" if self.config.ui.animations else "off"
|
|
1706
|
+
self.ui.render(HostEvent(HostEventKind.STATUS, f"animations={state}"))
|
|
1707
|
+
return "handled"
|
|
1708
|
+
if requested not in {"on", "off"}:
|
|
1709
|
+
self.ui.render(HostEvent(HostEventKind.ERROR, "usage: /animations [on|off]"))
|
|
1710
|
+
return "handled"
|
|
1711
|
+
enabled = requested == "on"
|
|
1712
|
+
path = save_user_animations(enabled)
|
|
1713
|
+
self.config.ui.animations = enabled
|
|
1714
|
+
self.ui.render(
|
|
1715
|
+
HostEvent(
|
|
1716
|
+
HostEventKind.STATUS,
|
|
1717
|
+
f"animations set to {requested} · saved in {path}",
|
|
1718
|
+
meta={"kind": "animations", "enabled": enabled},
|
|
1719
|
+
)
|
|
1720
|
+
)
|
|
1721
|
+
return "handled"
|
|
1594
1722
|
if name in self._custom_commands:
|
|
1595
1723
|
return await self._run_custom_command(name, args)
|
|
1596
1724
|
if name == "exit" or name == "quit":
|
|
@@ -1803,6 +1931,18 @@ class AgentHost:
|
|
|
1803
1931
|
if name == "work":
|
|
1804
1932
|
self.ui.render(_command_output(self.work_status_text()))
|
|
1805
1933
|
return "handled"
|
|
1934
|
+
if name == "queue":
|
|
1935
|
+
queued = self.steer_queue.items()
|
|
1936
|
+
attached = self.pending_attach_paths()
|
|
1937
|
+
lines = [f"Queued prompts: {len(queued)}", f"Pending attachments: {len(attached)}"]
|
|
1938
|
+
for index, item in enumerate(queued, start=1):
|
|
1939
|
+
preview = " ".join(item.text.split())[:80]
|
|
1940
|
+
suffix = f" · {len(item.attach_paths)} file(s)" if item.attach_paths else ""
|
|
1941
|
+
lines.append(f" {index}. {preview}{suffix}")
|
|
1942
|
+
for index, path in enumerate(attached, start=1):
|
|
1943
|
+
lines.append(f" A{index}. {path}")
|
|
1944
|
+
self.ui.render(_command_output("\n".join(lines)))
|
|
1945
|
+
return "handled"
|
|
1806
1946
|
if name == "terminals":
|
|
1807
1947
|
self.ui.render(_command_output(await agent.processes.terminal_status()))
|
|
1808
1948
|
return "handled"
|
|
@@ -1842,6 +1982,17 @@ class AgentHost:
|
|
|
1842
1982
|
)
|
|
1843
1983
|
)
|
|
1844
1984
|
return "handled"
|
|
1985
|
+
if name == "context":
|
|
1986
|
+
self.ui.render(_command_output(self.context_status_text()))
|
|
1987
|
+
return "handled"
|
|
1988
|
+
if name == "timeline":
|
|
1989
|
+
self.ui.render(
|
|
1990
|
+
HostEvent(
|
|
1991
|
+
HostEventKind.STATUS,
|
|
1992
|
+
"Open the task timeline with F2 in the TUI",
|
|
1993
|
+
)
|
|
1994
|
+
)
|
|
1995
|
+
return "handled"
|
|
1845
1996
|
if name == "health":
|
|
1846
1997
|
health = self._runtime.health() if self._runtime is not None else {}
|
|
1847
1998
|
rows = ["Durable runtime health"]
|
|
@@ -1944,7 +2095,7 @@ class AgentHost:
|
|
|
1944
2095
|
status = "history compacted" if compacted else "nothing eligible to compact"
|
|
1945
2096
|
self.ui.render(HostEvent(HostEventKind.STATUS, status))
|
|
1946
2097
|
return "handled"
|
|
1947
|
-
if name
|
|
2098
|
+
if name == "continue":
|
|
1948
2099
|
latest = self.store.latest_for_workspace(self.workspace)
|
|
1949
2100
|
if latest is None:
|
|
1950
2101
|
self.ui.render(
|
|
@@ -2101,6 +2252,7 @@ class AgentHost:
|
|
|
2101
2252
|
return expand_turn(text, self.workspace.root, attach_paths=attach_paths)
|
|
2102
2253
|
|
|
2103
2254
|
def _deliver_expanded(self, agent: Any, expanded: Any) -> None:
|
|
2255
|
+
self._active_context_paths.update(Path(path).resolve() for path in expanded.paths)
|
|
2104
2256
|
if expanded.images:
|
|
2105
2257
|
agent.media.queue(expanded.images)
|
|
2106
2258
|
self.ui.render(
|
|
@@ -218,6 +218,13 @@ def list_providers(active_model: str = "") -> list[ProviderInfo]:
|
|
|
218
218
|
]
|
|
219
219
|
|
|
220
220
|
|
|
221
|
+
def model_setup_required(model: str) -> bool:
|
|
222
|
+
"""Return whether a recognized model route is missing its credentials."""
|
|
223
|
+
|
|
224
|
+
active = [info for info in list_providers(model) if info.active]
|
|
225
|
+
return bool(active and not active[0].configured)
|
|
226
|
+
|
|
227
|
+
|
|
221
228
|
def format_providers(active_model: str = "") -> str:
|
|
222
229
|
lines = [
|
|
223
230
|
"Model providers",
|