Thinking improvements.
This commit is contained in:
+2
-2
@@ -149,7 +149,7 @@ agents:
|
|||||||
prompt_file: miranda_chief_of_staff.md
|
prompt_file: miranda_chief_of_staff.md
|
||||||
provider: vastblueai_thinking # Routing and synthesis benefit from deep reasoning
|
provider: vastblueai_thinking # Routing and synthesis benefit from deep reasoning
|
||||||
model: ""
|
model: ""
|
||||||
temperature: 0.6 # Qwen3 thinking-mode recommendation
|
temperature: 1.0 # Qwen3 thinking-mode recommendation
|
||||||
max_tokens: 32768 # Synthesizes the longest outputs; needs headroom
|
max_tokens: 32768 # Synthesizes the longest outputs; needs headroom
|
||||||
stateful: true
|
stateful: true
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ agents:
|
|||||||
prompt_file: vera_auditor.md
|
prompt_file: vera_auditor.md
|
||||||
provider: vastblueai_thinking # Careful auditing benefits from thinking
|
provider: vastblueai_thinking # Careful auditing benefits from thinking
|
||||||
model: ""
|
model: ""
|
||||||
temperature: 0.6 # Qwen3 thinking-mode recommendation
|
temperature: 1.0 # Qwen3 thinking-mode recommendation
|
||||||
max_tokens: 16384
|
max_tokens: 16384
|
||||||
stateful: false
|
stateful: false
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"session_id": "20260403_101101",
|
||||||
|
"session_start": "2026-04-03T10:11:26",
|
||||||
|
"tasks": {
|
||||||
|
"T-20260403-001-A": {
|
||||||
|
"task_id": "T-20260403-001-A",
|
||||||
|
"directive": "Analyze Evelyn's agent prompt.",
|
||||||
|
"assigned_to": "clio",
|
||||||
|
"brief": "TASK BRIEF\nIssued by: Miranda\nTo: Clio — Director of Analysis\nTask ID: T-20260403-001\nDirective: Analyze Evelyn's agent prompt specifications for the 5 EHS Specialist Agents.\nScope:\n - Review the system prompts designed for Monitor, Tracker, Triage, Validator, and Generator agents\n - Validate alignment with security architecture (T-20260402-003)\n - Identify any gaps in regulatory compliance controls, especially for SCAQMD rule ingestion\n - Confirm the Validator Agent's role as security gate is adequately enforced\nConstraints:\n - Deadline: 2026-04-05 14:00 (2 business days)\n - Depth: Technical review sufficient to confirm or reject prompt readiness for instantiation\n - Assumptions: Evelyn's prompt documents are available in the secure repository\nDependencies: T-20260402-003 (Clio's Architecture Design); Evelyn's Agent Generation System (Evelyn)\nReturn format: Analysis memo with Go/No-Go recommendation for agent instantiation\n```\n\n---\n\n**IMPLICATIONS**\n\nWhile Clio performs this analysis:\n- Cole must hold all EHS construction tasks pending T-20260402-004 approval\n- Evelyn cannot finalize agent instantiation until prompts are validated\n\nI will update the Standing Brief when Clio's analysis returns. Please confirm this routing is acceptable, or specify if you require a different focus for this review.\n\n---\n\n**STANDING BRIEF NOTE**\n\nThis will be recorded at session close. No changes to active tasks yet — T-20260402-004 remains pending Principal Authorization, but now has an additional validation gate before Evelyn can proceed.",
|
||||||
|
"status": "complete",
|
||||||
|
"output": "",
|
||||||
|
"error": "",
|
||||||
|
"created_at": "2026-04-03T10:11:26",
|
||||||
|
"updated_at": "2026-04-03T10:13:18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
-11
@@ -526,17 +526,47 @@ All agent calls default to `stream=True`. The flow:
|
|||||||
2. `call_async_streaming()` on `OpenAIClient` connects with `stream=True` and
|
2. `call_async_streaming()` on `OpenAIClient` connects with `stream=True` and
|
||||||
yields `('thinking', text)` and `('content', text)` tuples as chunks arrive.
|
yields `('thinking', text)` and `('content', text)` tuples as chunks arrive.
|
||||||
Thinking tokens come from `delta.reasoning_content` (Qwen3 / DeepSeek R1).
|
Thinking tokens come from `delta.reasoning_content` (Qwen3 / DeepSeek R1).
|
||||||
3. `stream_agent_output()` in `ui.py` consumes the async generator and renders
|
3. `stream_agent_output()` in `ui.py` consumes the async generator using a
|
||||||
output live using Rich's `Live` display.
|
two-phase rendering strategy (see below).
|
||||||
|
|
||||||
**Stream modes** (passed as `stream_mode` to `call_agent_async`):
|
**Stream modes** (passed as `stream_mode` to `call_agent_async`):
|
||||||
|
|
||||||
| Mode | Used for | Behaviour |
|
| Mode | Used for | Behaviour |
|
||||||
|------|----------|-----------|
|
|------|----------|-----------|
|
||||||
| `"deliverable"` | Miranda's routing/synthesis responses | Full panel with thinking + content |
|
| `"deliverable"` | Miranda's routing/synthesis responses | Two-phase thinking + content render |
|
||||||
| `"audit"` | Vera's audit memos | Yellow thinking identity; verdict-aware border colour |
|
| `"audit"` | Vera's audit memos | Yellow thinking identity; verdict-aware border colour |
|
||||||
| `"background"` | Standing brief update | Compact transient spinner; no panel |
|
| `"background"` | Standing brief update | Compact transient spinner; no panel |
|
||||||
|
|
||||||
|
**Two-phase rendering** (`"deliverable"` and `"audit"` modes):
|
||||||
|
|
||||||
|
The generator is consumed in two distinct, non-competing display regions to
|
||||||
|
prevent thinking text from scrolling off the top of the screen as content grows.
|
||||||
|
|
||||||
|
*Phase 1 — Thinking* (`transient=True`, `vertical_overflow="crop"`):
|
||||||
|
- A compact Live display shows the last 8 lines of thinking text with a
|
||||||
|
`+N lines earlier` overflow indicator.
|
||||||
|
- `transient=True`: the display clears from the screen when Phase 1 ends —
|
||||||
|
it does not permanently occupy lines in the scroll buffer.
|
||||||
|
- `vertical_overflow="crop"`: the panel is hard-capped at terminal height and
|
||||||
|
can never grow to push content off screen.
|
||||||
|
- The generator is iterated with manual `__anext__()` calls (not `async for`)
|
||||||
|
so that breaking out of the loop does **not** call `.aclose()` on the
|
||||||
|
generator — the same iterator is resumed in Phase 2.
|
||||||
|
|
||||||
|
*Thinking summary* (printed between phases):
|
||||||
|
After Phase 1 a single persistent line is printed to the scroll buffer:
|
||||||
|
```
|
||||||
|
▸ Miranda — thinking complete [4,821 chars · 47 lines] /thinking to expand
|
||||||
|
```
|
||||||
|
This line is always visible in the terminal's scrollback history.
|
||||||
|
|
||||||
|
*Phase 2 — Content* (`transient=False`, `vertical_overflow="visible"`):
|
||||||
|
- A separate Live display picks up the same iterator and streams content.
|
||||||
|
- The final Markdown panel (with verdict border in audit mode) is rendered
|
||||||
|
inside the Live block before it exits, avoiding any erase/reprint flash.
|
||||||
|
- Content is the only thing in this display — thinking can never compete
|
||||||
|
for vertical space here.
|
||||||
|
|
||||||
`_last_stream_rendered` is set to `True` after `stream_agent_output` completes,
|
`_last_stream_rendered` is set to `True` after `stream_agent_output` completes,
|
||||||
causing the subsequent `print_deliverable()` / `print_audit_memo()` to become
|
causing the subsequent `print_deliverable()` / `print_audit_memo()` to become
|
||||||
no-ops (avoiding double rendering).
|
no-ops (avoiding double rendering).
|
||||||
@@ -573,7 +603,10 @@ that adding or recoloring an agent in config is reflected automatically.
|
|||||||
|
|
||||||
| Function | Description |
|
| Function | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| `stream_agent_output(agent_name, gen, mode)` | Live streaming render with thinking panel |
|
| `get_directive()` | Interactive `Principal ›` prompt with inline slash-command completion |
|
||||||
|
| `set_completions(entries)` | Register `(name, description)` pairs for the completion dropdown |
|
||||||
|
| `stream_agent_output(agent_name, gen, mode)` | Two-phase live streaming render |
|
||||||
|
| `_build_thinking_panel(text, agent, mode)` | Thinking panel renderable (last 8 lines, height-capped) |
|
||||||
| `print_agent_panel(agent_name, content)` | Static Markdown panel for an agent |
|
| `print_agent_panel(agent_name, content)` | Static Markdown panel for an agent |
|
||||||
| `print_deliverable(content, task_id)` | Orchestrator's synthesized output |
|
| `print_deliverable(content, task_id)` | Orchestrator's synthesized output |
|
||||||
| `print_audit_memo(content, task_id)` | Vera's memo with verdict-based border |
|
| `print_audit_memo(content, task_id)` | Vera's memo with verdict-based border |
|
||||||
@@ -584,15 +617,46 @@ that adding or recoloring an agent in config is reflected automatically.
|
|||||||
| `print_session_footer(count)` | Closing rule |
|
| `print_session_footer(count)` | Closing rule |
|
||||||
| `print_status(...)` | `/status` command output |
|
| `print_status(...)` | `/status` command output |
|
||||||
| `print_agents(configs, provider)` | `/agents` roster table |
|
| `print_agents(configs, provider)` | `/agents` roster table |
|
||||||
| `get_directive()` | Styled `Principal ›` input prompt |
|
|
||||||
| `get_last_thinking()` | Returns stored thinking from last response |
|
| `get_last_thinking()` | Returns stored thinking from last response |
|
||||||
|
|
||||||
|
### Slash-command completion
|
||||||
|
|
||||||
|
`get_directive()` uses a `prompt_toolkit` `PromptSession` to provide an
|
||||||
|
inline vertical dropdown as the user types at the `Principal ›` prompt.
|
||||||
|
|
||||||
|
**Behaviour:**
|
||||||
|
- Typing `/` immediately opens a dropdown showing the top 5 matching commands
|
||||||
|
with their descriptions alongside each name.
|
||||||
|
- Each additional character narrows the list in real time (`complete_while_typing=True`).
|
||||||
|
- Arrow keys navigate the list; Enter or Tab accepts a selection; Escape dismisses.
|
||||||
|
- Up/Down arrows outside the dropdown navigate the session's input history
|
||||||
|
(backed by `InMemoryHistory` — persists for the lifetime of the process).
|
||||||
|
- No dropdown appears for non-slash input — the completer only activates when
|
||||||
|
the text starts with `/`.
|
||||||
|
|
||||||
|
**Wiring:**
|
||||||
|
`orchestrator.py` calls `ui.set_completions([(name, desc), ...])` once at
|
||||||
|
module import time, after all `@command` decorators have executed. The list is
|
||||||
|
built from the `COMMANDS` registry so any new `@command` is automatically
|
||||||
|
included with no extra wiring.
|
||||||
|
|
||||||
|
`_completions` is a module-level `list[tuple[str, str]]` sorted alphabetically.
|
||||||
|
The `_SlashCompleter` class reads it at query time, so `set_completions` can be
|
||||||
|
called at any point before the user types.
|
||||||
|
|
||||||
|
**Fallback:** if `prompt_toolkit` is not installed, `get_directive()` falls
|
||||||
|
back to a plain styled `input()` with no completion.
|
||||||
|
|
||||||
### Thinking panel behaviour
|
### Thinking panel behaviour
|
||||||
|
|
||||||
During the thinking phase a dim panel shows a rolling 300-character window
|
The thinking display is Phase 1 of the two-phase render (see Section 9). It
|
||||||
of the thinking text with an overflow indicator (`+N chars earlier`) when
|
shows the **last 8 lines** of thinking text with a `+N lines earlier` overflow
|
||||||
it exceeds the window. When the first content token arrives the panel
|
indicator when earlier lines have been clipped. The panel is `transient` and
|
||||||
collapses to a single summary line: `▸ Thinking [N chars · L lines]`.
|
`crop`-overflow so it never grows beyond terminal height and clears cleanly
|
||||||
|
when content begins.
|
||||||
|
|
||||||
|
After Phase 1, a persistent one-line summary is printed to the scroll buffer:
|
||||||
|
`▸ Agent — thinking complete [N chars · L lines] /thinking to expand`.
|
||||||
|
|
||||||
### Audit verdict border
|
### Audit verdict border
|
||||||
|
|
||||||
@@ -857,5 +921,7 @@ async def cmd_mycommand(session: Session, args: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
```
|
```
|
||||||
|
|
||||||
Commands self-register at import time via `COMMANDS` list; no other
|
Commands self-register at import time via the `COMMANDS` list. The
|
||||||
wiring needed.
|
`ui.set_completions(...)` call that follows `_dispatch_command` rebuilds the
|
||||||
|
completion list from `COMMANDS`, so the new command and all its aliases
|
||||||
|
automatically appear in the `Principal ›` dropdown — no extra wiring needed.
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
Principal, the Standing Brief has been updated to Version 3 to reflect the closure of Session 1 and the completion of Task T-20260402-001. Below is the finalized document ready for save.
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# Standing Brief
|
# Standing Brief
|
||||||
**Maintained by:** Miranda — Chief of Staff
|
**Maintained by:** Miranda — Chief of Staff
|
||||||
**For:** Principal
|
**For:** Principal
|
||||||
**Version:** 3 — 2026-04-02
|
**Version:** 1 — [YYYY-MM-DD]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -47,7 +44,7 @@ Miranda will surface these at the start of each session if unresolved.
|
|||||||
Completed tasks from recent sessions that may be relevant to ongoing work.
|
Completed tasks from recent sessions that may be relevant to ongoing work.
|
||||||
Items older than 30 days or with no ongoing relevance are moved to the Archive.
|
Items older than 30 days or with no ongoing relevance are moved to the Archive.
|
||||||
|
|
||||||
* **T-20260402-001** — Identity inquiry (Direct response by Miranda).
|
*No recent completions.*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,7 +61,7 @@ Questions identified but not yet formally tasked, or ongoing background inquirie
|
|||||||
Status of recent Vera reviews. Full audit memos go directly to the Principal —
|
Status of recent Vera reviews. Full audit memos go directly to the Principal —
|
||||||
this section tracks coverage only.
|
this section tracks coverage only.
|
||||||
|
|
||||||
* **T-20260402-001** — Cleared by Vera.
|
*No audit items.*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -99,7 +96,7 @@ Not a comprehensive archive — only what is currently load-bearing for decision
|
|||||||
A brief record of each session. Most recent at the top.
|
A brief record of each session. Most recent at the top.
|
||||||
Keep only the last 10 sessions; archive older entries.
|
Keep only the last 10 sessions; archive older entries.
|
||||||
|
|
||||||
* **2026-04-02** — Session 1. Directive: Identity inquiry. Outcome: Completed. Task ID: T-20260402-001.
|
*No sessions recorded yet.*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -115,4 +112,3 @@ The archive is not loaded into session context by default — request it explici
|
|||||||
|
|
||||||
*End of Standing Brief*
|
*End of Standing Brief*
|
||||||
*Next update due: close of next session*
|
*Next update due: close of next session*
|
||||||
```
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -832,13 +832,9 @@ async def cmd_reload(session: Session, args: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@command(["/thinking"], "Show the thinking output from the last response")
|
@command(["/thinking"], "Browse thinking output history for this session")
|
||||||
async def cmd_thinking(session: Session, args: str) -> bool:
|
async def cmd_thinking(session: Session, args: str) -> bool:
|
||||||
last = ui.get_last_thinking()
|
await ui.show_thinking_picker()
|
||||||
if not last:
|
|
||||||
ui.print_system("No thinking output recorded — thinking mode may not be active.")
|
|
||||||
else:
|
|
||||||
ui.print_thinking_expansion(last)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -897,6 +893,11 @@ def _dispatch_command(name: str):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Register all slash command names for tab completion.
|
||||||
|
# This runs once at import time, after all @command decorators have executed.
|
||||||
|
ui.set_completions([(name, desc) for names, desc, _ in COMMANDS for name in names])
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Direct Agent Session
|
# Direct Agent Session
|
||||||
# Opens an interactive REPL with one named agent.
|
# Opens an interactive REPL with one named agent.
|
||||||
@@ -920,7 +921,7 @@ async def direct_agent_session(agent_name: str, dry_run: bool = False):
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
user_input = ui.get_directive()
|
user_input = await ui.get_directive()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -928,11 +929,7 @@ async def direct_agent_session(agent_name: str, dry_run: bool = False):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if user_input.lower() == "/thinking":
|
if user_input.lower() == "/thinking":
|
||||||
last = ui.get_last_thinking()
|
await ui.show_thinking_picker()
|
||||||
if not last:
|
|
||||||
ui.print_system("No thinking output recorded — thinking mode may not be active.")
|
|
||||||
else:
|
|
||||||
ui.print_thinking_expansion(last)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
response = await call_agent_async(agent, user_input, dry_run=dry_run, stream=True)
|
response = await call_agent_async(agent, user_input, dry_run=dry_run, stream=True)
|
||||||
@@ -955,7 +952,7 @@ async def principal_session(dry_run: bool = False):
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
raw = ui.get_directive()
|
raw = await ui.get_directive()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
ui.console.print()
|
ui.console.print()
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pyyaml>=6.0.0 # reads config/agents.yaml
|
|||||||
ruamel.yaml>=0.18 # comment-preserving YAML round-trips (tools.py)
|
ruamel.yaml>=0.18 # comment-preserving YAML round-trips (tools.py)
|
||||||
openai>=1.50.0 # covers openai, openai_compatible, lmstudio, llamacpp
|
openai>=1.50.0 # covers openai, openai_compatible, lmstudio, llamacpp
|
||||||
rich>=13.0.0 # terminal UI (Iris)
|
rich>=13.0.0 # terminal UI (Iris)
|
||||||
|
prompt_toolkit>=3.0.0 # interactive input with inline slash-command completion
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Provider SDKs — install only if using that provider
|
# Provider SDKs — install only if using that provider
|
||||||
|
|||||||
+339
-161
@@ -22,6 +22,17 @@ from contextlib import contextmanager
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json as _json
|
import json as _json
|
||||||
|
|
||||||
|
try:
|
||||||
|
from prompt_toolkit import PromptSession as _PromptSession
|
||||||
|
from prompt_toolkit.completion import Completer as _PTCompleter, Completion as _PTCompletion
|
||||||
|
from prompt_toolkit.formatted_text import FormattedText as _FormattedText
|
||||||
|
from prompt_toolkit.history import InMemoryHistory as _InMemoryHistory
|
||||||
|
from prompt_toolkit.shortcuts import CompleteStyle as _CompleteStyle, radiolist_dialog as _radiolist_dialog
|
||||||
|
from prompt_toolkit.styles import Style as _PTStyle
|
||||||
|
_HAS_PROMPT_TOOLKIT = True
|
||||||
|
except ImportError:
|
||||||
|
_HAS_PROMPT_TOOLKIT = False
|
||||||
|
|
||||||
import config as _config
|
import config as _config
|
||||||
|
|
||||||
from rich.console import Console, Group
|
from rich.console import Console, Group
|
||||||
@@ -77,9 +88,96 @@ def _build_theme() -> Theme:
|
|||||||
|
|
||||||
console = Console(theme=_build_theme(), highlight=False)
|
console = Console(theme=_build_theme(), highlight=False)
|
||||||
|
|
||||||
_last_thinking: str = "" # Stored from most recent streamed response
|
|
||||||
_last_stream_rendered: bool = False # True when stream_agent_output rendered the final panel
|
_last_stream_rendered: bool = False # True when stream_agent_output rendered the final panel
|
||||||
|
|
||||||
|
# Thinking history — most recent appended last, capped at _THINKING_MAX entries.
|
||||||
|
# Each entry: {agent, content, n_chars, n_lines, timestamp, index}
|
||||||
|
_thinking_history: list[dict] = []
|
||||||
|
_THINKING_MAX = 20
|
||||||
|
|
||||||
|
|
||||||
|
def _record_thinking(agent_name: str, content: str) -> None:
|
||||||
|
"""Append a thinking record to the history, trimming to _THINKING_MAX."""
|
||||||
|
if not content:
|
||||||
|
return
|
||||||
|
_thinking_history.append({
|
||||||
|
"agent": agent_name,
|
||||||
|
"content": content,
|
||||||
|
"n_chars": len(content),
|
||||||
|
"n_lines": content.count("\n") + 1,
|
||||||
|
"timestamp": datetime.now().strftime("%H:%M"),
|
||||||
|
"index": len(_thinking_history) + 1,
|
||||||
|
})
|
||||||
|
if len(_thinking_history) > _THINKING_MAX:
|
||||||
|
del _thinking_history[:-_THINKING_MAX]
|
||||||
|
|
||||||
|
# Slash-command completions: list of (command_name, description) pairs.
|
||||||
|
_completions: list[tuple[str, str]] = []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Inline slash-command completion (prompt_toolkit)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if _HAS_PROMPT_TOOLKIT:
|
||||||
|
class _SlashCompleter(_PTCompleter):
|
||||||
|
"""
|
||||||
|
Yields up to 5 matching slash commands as the user types.
|
||||||
|
Only activates when the current input starts with '/'.
|
||||||
|
Each completion shows the command name and its description.
|
||||||
|
"""
|
||||||
|
_MAX = 5
|
||||||
|
|
||||||
|
def get_completions(self, document, complete_event):
|
||||||
|
text = document.text_before_cursor
|
||||||
|
if not text.startswith("/"):
|
||||||
|
return
|
||||||
|
count = 0
|
||||||
|
for word, desc in _completions:
|
||||||
|
if word.startswith(text):
|
||||||
|
yield _PTCompletion(
|
||||||
|
word,
|
||||||
|
start_position=-len(text),
|
||||||
|
display=word,
|
||||||
|
display_meta=desc,
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
if count >= self._MAX:
|
||||||
|
break
|
||||||
|
|
||||||
|
_PT_STYLE = _PTStyle.from_dict({
|
||||||
|
# Prompt text
|
||||||
|
"prompt": "bold #ffffff",
|
||||||
|
# Completion dropdown — unselected
|
||||||
|
"completion-menu.completion": "bg:#1e1e1e #aaaaaa",
|
||||||
|
"completion-menu.meta.completion": "bg:#1e1e1e #555555",
|
||||||
|
# Completion dropdown — selected row
|
||||||
|
"completion-menu.completion.current": "bg:#005f87 bold #ffffff",
|
||||||
|
"completion-menu.meta.completion.current": "bg:#005f87 #bbbbbb",
|
||||||
|
# Scrollbar
|
||||||
|
"scrollbar.background": "bg:#1e1e1e",
|
||||||
|
"scrollbar.button": "bg:#4e4e4e",
|
||||||
|
})
|
||||||
|
|
||||||
|
_pt_session: _PromptSession | None = _PromptSession(
|
||||||
|
history=_InMemoryHistory(),
|
||||||
|
completer=_SlashCompleter(),
|
||||||
|
complete_while_typing=True, # popup appears as the user types '/'
|
||||||
|
complete_style=_CompleteStyle.COLUMN,
|
||||||
|
style=_PT_STYLE,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_pt_session = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_completions(entries: list[tuple[str, str]]) -> None:
|
||||||
|
"""
|
||||||
|
Register (command_name, description) pairs for inline completion.
|
||||||
|
Call once at startup after all slash commands are defined.
|
||||||
|
"""
|
||||||
|
global _completions
|
||||||
|
_completions = sorted(entries, key=lambda e: e[0])
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Agent visual config
|
# Agent visual config
|
||||||
@@ -133,10 +231,31 @@ def print_session_footer(task_count: int):
|
|||||||
# Input prompt
|
# Input prompt
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def get_directive() -> str:
|
async def get_directive() -> str:
|
||||||
"""Styled input prompt for the Principal."""
|
"""
|
||||||
|
Styled input prompt for the Principal.
|
||||||
|
|
||||||
|
When prompt_toolkit is available (the default):
|
||||||
|
- An inline vertical dropdown appears as soon as the user types '/'.
|
||||||
|
- Up to 5 matching commands are shown with their descriptions.
|
||||||
|
- Arrow keys navigate the list; Enter or Tab accepts a selection.
|
||||||
|
- Up/Down arrow keys also navigate input history within the session.
|
||||||
|
|
||||||
|
Uses prompt_async() so it cooperates with the running asyncio event loop
|
||||||
|
instead of trying to nest a second one (which raises RuntimeError).
|
||||||
|
|
||||||
|
Falls back to plain input() if prompt_toolkit is not installed.
|
||||||
|
"""
|
||||||
console.print()
|
console.print()
|
||||||
return console.input("[bold white]Principal ›[/bold white] ").strip()
|
if _pt_session is not None:
|
||||||
|
return (await _pt_session.prompt_async(
|
||||||
|
_FormattedText([("class:prompt", "Principal › ")]),
|
||||||
|
)).strip()
|
||||||
|
# Fallback: plain styled prompt with no completion.
|
||||||
|
console.print("[bold white]Principal ›[/bold white] ", end="")
|
||||||
|
if hasattr(console.file, "flush"):
|
||||||
|
console.file.flush()
|
||||||
|
return input().strip()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# System / status messages
|
# System / status messages
|
||||||
@@ -451,47 +570,41 @@ def _thinking_panel_title(agent_name: str, mode: str) -> str:
|
|||||||
return f"[{color} dim]Thinking…[/{color} dim]"
|
return f"[{color} dim]Thinking…[/{color} dim]"
|
||||||
|
|
||||||
|
|
||||||
def _build_thinking_renderable(
|
def _build_thinking_panel(thinking_text: str, agent_name: str, mode: str,
|
||||||
thinking_text: str,
|
visible_lines: int = 8) -> object:
|
||||||
active: bool,
|
|
||||||
agent_name: str,
|
|
||||||
mode: str,
|
|
||||||
window: int = 300,
|
|
||||||
) -> object:
|
|
||||||
"""
|
"""
|
||||||
Build the thinking panel (active) or collapsed summary line (done).
|
Build the live thinking panel for Phase 1 streaming.
|
||||||
|
|
||||||
Active: dim panel with a rolling window of the last `window` chars,
|
Shows the last `visible_lines` lines of thinking so the panel never
|
||||||
plus an overflow indicator when earlier content has scrolled off.
|
grows beyond a predictable height (safe with vertical_overflow="crop").
|
||||||
Collapsed: single dim line — ' ▸ Thinking (N chars, L lines)'.
|
An overflow indicator shows how many earlier lines have scrolled off.
|
||||||
"""
|
"""
|
||||||
if not thinking_text:
|
if not thinking_text:
|
||||||
return None
|
|
||||||
|
|
||||||
if active:
|
|
||||||
overflow = len(thinking_text) - window
|
|
||||||
if overflow > 0:
|
|
||||||
preview = thinking_text[-window:]
|
|
||||||
header = Text(f" +{overflow} chars earlier\n", style="dim italic")
|
|
||||||
body = Text(preview, style="dim")
|
|
||||||
content = Group(header, body)
|
|
||||||
else:
|
|
||||||
content = Text(thinking_text, style="dim")
|
|
||||||
|
|
||||||
return Panel(
|
|
||||||
content,
|
|
||||||
title=_thinking_panel_title(agent_name, mode),
|
|
||||||
border_style="dim",
|
|
||||||
padding=(0, 1),
|
|
||||||
box=box.SIMPLE,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
lines = thinking_text.count("\n") + 1
|
|
||||||
return Text(
|
return Text(
|
||||||
f" ▸ Thinking [{len(thinking_text):,} chars · {lines} lines]",
|
f" [{_agent_color(agent_name)} dim]{_agent_display_title(agent_name)} thinking…"
|
||||||
|
f"[/{_agent_color(agent_name)} dim]",
|
||||||
style="dim",
|
style="dim",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
lines = thinking_text.splitlines()
|
||||||
|
hidden = max(0, len(lines) - visible_lines)
|
||||||
|
visible = lines[-visible_lines:] if hidden else lines
|
||||||
|
|
||||||
|
items = []
|
||||||
|
if hidden:
|
||||||
|
items.append(Text(f" +{hidden} line{'s' if hidden != 1 else ''} earlier\n",
|
||||||
|
style="dim italic"))
|
||||||
|
items.append(Text("\n".join(visible), style="dim"))
|
||||||
|
content = Group(*items) if len(items) > 1 else items[0]
|
||||||
|
|
||||||
|
return Panel(
|
||||||
|
content,
|
||||||
|
title=_thinking_panel_title(agent_name, mode),
|
||||||
|
border_style="dim",
|
||||||
|
padding=(0, 1),
|
||||||
|
box=box.SIMPLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def stream_agent_output(
|
async def stream_agent_output(
|
||||||
agent_name: str,
|
agent_name: str,
|
||||||
@@ -502,47 +615,41 @@ async def stream_agent_output(
|
|||||||
Consume an async generator of ('thinking'|'content', text) chunks and
|
Consume an async generator of ('thinking'|'content', text) chunks and
|
||||||
render them live in the terminal.
|
render them live in the terminal.
|
||||||
|
|
||||||
|
Two-phase rendering
|
||||||
|
-------------------
|
||||||
|
Phase 1 — Thinking (transient, cropped to terminal height):
|
||||||
|
A compact Live panel streams thinking text showing the last 8 lines.
|
||||||
|
It is transient — it clears from the screen when Phase 1 ends.
|
||||||
|
A persistent one-line summary is then printed to the scroll buffer:
|
||||||
|
"▸ Agent — thinking complete [N chars · L lines] /thinking to expand"
|
||||||
|
|
||||||
|
Phase 2 — Content (non-transient, persists in scroll buffer):
|
||||||
|
A separate Live display streams the content and renders the final
|
||||||
|
Markdown panel inside the Live block before exiting (no flash).
|
||||||
|
|
||||||
|
This keeps thinking and content in separate, non-competing display
|
||||||
|
regions. Thinking never pushes content off-screen, and the summary
|
||||||
|
line is always visible in the scroll buffer.
|
||||||
|
|
||||||
mode values
|
mode values
|
||||||
-----------
|
-----------
|
||||||
"deliverable" Normal agent response — thinking panel + full styled panel.
|
"deliverable" Normal agent response.
|
||||||
The Live display is NOT transient: the final Markdown panel
|
"audit" Like deliverable but with Vera's identity and verdict border.
|
||||||
is rendered inside the Live block, so there is no erase/
|
"background" Housekeeping — compact transient spinner only, no panel.
|
||||||
reprint flash. Sets _last_stream_rendered = True so that
|
|
||||||
the caller's print_deliverable / print_agent_panel becomes
|
|
||||||
a no-op.
|
|
||||||
|
|
||||||
"audit" Like "deliverable" but uses Vera's yellow identity cues
|
|
||||||
during the thinking phase. Sets _last_stream_rendered so
|
|
||||||
print_audit_memo is a no-op (the verdict-border panel is
|
|
||||||
rendered here instead).
|
|
||||||
|
|
||||||
"background" Housekeeping call (e.g. standing-brief update). A compact
|
|
||||||
transient progress line is shown; no full panel is printed.
|
|
||||||
_last_stream_rendered is set so the caller's print_* is a
|
|
||||||
no-op.
|
|
||||||
|
|
||||||
In all modes the full content string is returned and _last_thinking is
|
|
||||||
updated so /thinking works.
|
|
||||||
|
|
||||||
Thinking tokens stream inside a dim panel. When the first content token
|
|
||||||
arrives the panel collapses to a single dim summary line and content begins
|
|
||||||
streaming in the agent's normal styled panel.
|
|
||||||
|
|
||||||
The full thinking text is stored in _last_thinking and can be retrieved
|
|
||||||
with get_last_thinking() (exposed to the /thinking command).
|
|
||||||
|
|
||||||
|
In all modes the thinking content is recorded via _record_thinking() so
|
||||||
|
the /thinking picker has a full history.
|
||||||
Returns the full content string.
|
Returns the full content string.
|
||||||
"""
|
"""
|
||||||
global _last_thinking, _last_stream_rendered
|
global _last_stream_rendered
|
||||||
|
|
||||||
color = _agent_color(agent_name)
|
color = _agent_color(agent_name)
|
||||||
title_text = _agent_display_title(agent_name)
|
title_text = _agent_display_title(agent_name)
|
||||||
|
|
||||||
# ── Background mode: compact transient progress line ────────────────────
|
# ── Background mode ──────────────────────────────────────────────────────
|
||||||
if mode == "background":
|
if mode == "background":
|
||||||
thinking_parts: list[str] = []
|
thinking_parts: list[str] = []
|
||||||
content_parts: list[str] = []
|
content_parts: list[str] = []
|
||||||
|
|
||||||
with Progress(
|
with Progress(
|
||||||
SpinnerColumn(style="dim"),
|
SpinnerColumn(style="dim"),
|
||||||
TextColumn("[dim]Updating standing brief…[/dim]"),
|
TextColumn("[dim]Updating standing brief…[/dim]"),
|
||||||
@@ -556,20 +663,16 @@ async def stream_agent_output(
|
|||||||
thinking_parts.append(text)
|
thinking_parts.append(text)
|
||||||
elif chunk_type == "content":
|
elif chunk_type == "content":
|
||||||
content_parts.append(text)
|
content_parts.append(text)
|
||||||
|
_record_thinking(agent_name, "".join(thinking_parts))
|
||||||
_last_thinking = "".join(thinking_parts)
|
|
||||||
_last_stream_rendered = True
|
_last_stream_rendered = True
|
||||||
return "".join(content_parts)
|
return "".join(content_parts)
|
||||||
|
|
||||||
# ── Deliverable / audit modes ────────────────────────────────────────────
|
# ── Deliverable / audit modes ─────────────────────────────────────────────
|
||||||
thinking_parts: list[str] = []
|
thinking_parts: list[str] = []
|
||||||
content_parts: list[str] = []
|
content_parts: list[str] = []
|
||||||
phase = "start" # "start" | "thinking" | "content"
|
|
||||||
|
|
||||||
def _build_final_panel(content_text: str) -> Panel:
|
def _build_final_panel(content_text: str) -> Panel:
|
||||||
"""Render the agent's finished output as a polished Markdown panel."""
|
|
||||||
if mode == "audit":
|
if mode == "audit":
|
||||||
# Re-derive verdict border from completed content
|
|
||||||
aud_name = _config.AUDITOR_AGENT
|
aud_name = _config.AUDITOR_AGENT
|
||||||
aud_title = _config.agent_title(aud_name)
|
aud_title = _config.agent_title(aud_name)
|
||||||
aud_color = _agent_color(aud_name)
|
aud_color = _agent_color(aud_name)
|
||||||
@@ -577,120 +680,127 @@ async def stream_agent_output(
|
|||||||
for line in content_text.splitlines():
|
for line in content_text.splitlines():
|
||||||
upper = line.upper()
|
upper = line.upper()
|
||||||
if "VERDICT:" in upper:
|
if "VERDICT:" in upper:
|
||||||
if "REJECT" in upper:
|
if "REJECT" in upper: verdict_style = "reject"
|
||||||
verdict_style = "reject"
|
elif "FLAG" in upper: verdict_style = "flag"
|
||||||
elif "FLAG" in upper:
|
elif "PASS WITH NOTES" in upper: verdict_style = "warning"
|
||||||
verdict_style = "flag"
|
elif "PASS" in upper: verdict_style = "pass"
|
||||||
elif "PASS WITH NOTES" in upper:
|
|
||||||
verdict_style = "warning"
|
|
||||||
elif "PASS" in upper:
|
|
||||||
verdict_style = "pass"
|
|
||||||
break
|
break
|
||||||
return Panel(
|
return Panel(
|
||||||
Markdown(content_text),
|
Markdown(content_text),
|
||||||
title=f"[{aud_color}] {aud_name.capitalize()} — {aud_title} [/{aud_color}]",
|
title=f"[{aud_color}] {aud_name.capitalize()} — {aud_title} [/{aud_color}]",
|
||||||
border_style=verdict_style if verdict_style != "dim" else aud_color,
|
border_style=verdict_style if verdict_style != "dim" else aud_color,
|
||||||
padding=(1, 2),
|
padding=(1, 2), box=box.ROUNDED,
|
||||||
box=box.ROUNDED,
|
|
||||||
)
|
)
|
||||||
else:
|
return Panel(
|
||||||
return Panel(
|
Markdown(content_text),
|
||||||
Markdown(content_text),
|
title=f"[{color}] {title_text} [/{color}]",
|
||||||
title=f"[{color}] {title_text} [/{color}]",
|
border_style=color,
|
||||||
border_style=color,
|
padding=(1, 2), box=box.ROUNDED,
|
||||||
padding=(1, 2),
|
|
||||||
box=box.ROUNDED,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _renderable(final: bool = False):
|
|
||||||
"""
|
|
||||||
Build the live renderable for the current stream state.
|
|
||||||
|
|
||||||
final=True: replace plain Text content with Markdown and apply the
|
|
||||||
verdict-aware border so the Live display ends in its final polished
|
|
||||||
form. This avoids a transient erase + reprint flash.
|
|
||||||
"""
|
|
||||||
thinking_text = "".join(thinking_parts)
|
|
||||||
content_text = "".join(content_parts)
|
|
||||||
items: list = []
|
|
||||||
|
|
||||||
# ── Agent identity line (shown while only thinking, not yet content) ──
|
|
||||||
if phase in ("start", "thinking") and not content_text:
|
|
||||||
items.append(Text(
|
|
||||||
f" {title_text}",
|
|
||||||
style=f"dim {color}",
|
|
||||||
))
|
|
||||||
|
|
||||||
# ── Thinking panel / collapsed summary ──────────────────────────────
|
|
||||||
thinking_active = (phase == "thinking")
|
|
||||||
think_renderable = _build_thinking_renderable(
|
|
||||||
thinking_text, thinking_active, agent_name, mode
|
|
||||||
)
|
)
|
||||||
if think_renderable is not None:
|
|
||||||
items.append(think_renderable)
|
|
||||||
|
|
||||||
# ── Content panel ────────────────────────────────────────────────────
|
|
||||||
if content_text or phase == "content":
|
|
||||||
if final and content_text:
|
|
||||||
items.append(_build_final_panel(content_text))
|
|
||||||
else:
|
|
||||||
items.append(Panel(
|
|
||||||
Text(content_text),
|
|
||||||
title=f"[{color}] {title_text} [/{color}]",
|
|
||||||
border_style=color,
|
|
||||||
padding=(1, 2),
|
|
||||||
box=box.ROUNDED,
|
|
||||||
))
|
|
||||||
|
|
||||||
if not items:
|
|
||||||
# Nothing yet — show a faint connecting indicator
|
|
||||||
return Text(
|
|
||||||
f" [{_agent_color(agent_name)} dim]{title_text} …[/{_agent_color(agent_name)} dim]",
|
|
||||||
style="dim",
|
|
||||||
)
|
|
||||||
return Group(*items) if len(items) > 1 else items[0]
|
|
||||||
|
|
||||||
console.print()
|
console.print()
|
||||||
# transient=False: the Live display persists — we render the final panel
|
|
||||||
# inside it before stopping, so there is no erase/reprint cycle.
|
# ── Phase 1: Thinking — transient, height-capped Live display ────────────
|
||||||
|
# We use manual __anext__() instead of `async for` so that breaking out of
|
||||||
|
# the loop does NOT call aclose() on the generator. The same iterator is
|
||||||
|
# then reused in Phase 2 to continue from where we left off.
|
||||||
|
aiter = stream_gen.__aiter__()
|
||||||
|
exhausted = False
|
||||||
|
|
||||||
with Live(
|
with Live(
|
||||||
_renderable(),
|
_build_thinking_panel("", agent_name, mode),
|
||||||
|
console=console,
|
||||||
|
refresh_per_second=15,
|
||||||
|
transient=True, # clears from screen when the block exits
|
||||||
|
vertical_overflow="crop", # never grows beyond terminal height
|
||||||
|
) as live:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk_type, text = await aiter.__anext__()
|
||||||
|
except StopAsyncIteration:
|
||||||
|
exhausted = True
|
||||||
|
break
|
||||||
|
if chunk_type == "thinking":
|
||||||
|
thinking_parts.append(text)
|
||||||
|
live.update(_build_thinking_panel(
|
||||||
|
"".join(thinking_parts), agent_name, mode
|
||||||
|
))
|
||||||
|
elif chunk_type == "content":
|
||||||
|
content_parts.append(text)
|
||||||
|
break # hand off to Phase 2; generator is NOT closed
|
||||||
|
|
||||||
|
# Print a persistent collapsed summary when thinking was present.
|
||||||
|
# This line lives in the terminal's scroll buffer — always scrollable.
|
||||||
|
if thinking_parts:
|
||||||
|
full_thinking = "".join(thinking_parts)
|
||||||
|
n_chars = len(full_thinking)
|
||||||
|
n_lines = full_thinking.count("\n") + 1
|
||||||
|
id_color = _agent_color(_config.AUDITOR_AGENT) if mode == "audit" else color
|
||||||
|
console.print(Text.assemble(
|
||||||
|
(" ▸ ", "dim"),
|
||||||
|
(title_text, f"dim {id_color}"),
|
||||||
|
(" thinking complete ", "dim"),
|
||||||
|
(f"[{n_chars:,} chars · {n_lines} lines]", "dim"),
|
||||||
|
(" /thinking to expand", "dim italic"),
|
||||||
|
))
|
||||||
|
|
||||||
|
# Edge case: generator exhausted with no content (thinking-only response)
|
||||||
|
if exhausted:
|
||||||
|
_record_thinking(agent_name, "".join(thinking_parts))
|
||||||
|
_last_stream_rendered = True
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# ── Phase 2: Content — non-transient, persists in scroll buffer ──────────
|
||||||
|
def _content_renderable(final: bool = False) -> object:
|
||||||
|
content_text = "".join(content_parts)
|
||||||
|
if not content_text:
|
||||||
|
return Text(
|
||||||
|
f" [{color} dim]{title_text} …[/{color} dim]",
|
||||||
|
style="dim",
|
||||||
|
)
|
||||||
|
if final:
|
||||||
|
return _build_final_panel(content_text)
|
||||||
|
return Panel(
|
||||||
|
Text(content_text),
|
||||||
|
title=f"[{color}] {title_text} [/{color}]",
|
||||||
|
border_style=color,
|
||||||
|
padding=(1, 2), box=box.ROUNDED,
|
||||||
|
)
|
||||||
|
|
||||||
|
with Live(
|
||||||
|
_content_renderable(),
|
||||||
console=console,
|
console=console,
|
||||||
refresh_per_second=15,
|
refresh_per_second=15,
|
||||||
transient=False,
|
transient=False,
|
||||||
vertical_overflow="visible",
|
vertical_overflow="visible",
|
||||||
) as live:
|
) as live:
|
||||||
async for chunk_type, text in stream_gen:
|
async for chunk_type, text in aiter:
|
||||||
if chunk_type == "thinking":
|
if chunk_type == "content":
|
||||||
if phase == "start":
|
|
||||||
phase = "thinking"
|
|
||||||
thinking_parts.append(text)
|
|
||||||
elif chunk_type == "content":
|
|
||||||
if phase in ("start", "thinking"):
|
|
||||||
phase = "content"
|
|
||||||
content_parts.append(text)
|
content_parts.append(text)
|
||||||
live.update(_renderable())
|
live.update(_content_renderable())
|
||||||
|
live.update(_content_renderable(final=True))
|
||||||
|
|
||||||
# Final update: replace plain Text with formatted Markdown panel
|
_record_thinking(agent_name, "".join(thinking_parts))
|
||||||
# (and apply verdict border for audit mode) before the Live context
|
|
||||||
# exits and the display freezes in place.
|
|
||||||
live.update(_renderable(final=True))
|
|
||||||
|
|
||||||
_last_thinking = "".join(thinking_parts)
|
|
||||||
_last_stream_rendered = True
|
_last_stream_rendered = True
|
||||||
return "".join(content_parts)
|
return "".join(content_parts)
|
||||||
|
|
||||||
|
|
||||||
def get_last_thinking() -> str:
|
def get_last_thinking() -> str:
|
||||||
"""Return the thinking text captured from the most recent streamed response."""
|
"""Return the thinking text from the most recent response (empty string if none)."""
|
||||||
return _last_thinking
|
return _thinking_history[-1]["content"] if _thinking_history else ""
|
||||||
|
|
||||||
|
|
||||||
def print_thinking_expansion(thinking_text: str):
|
def _display_thinking_entry(entry: dict) -> None:
|
||||||
"""Render the stored thinking output in a dim panel."""
|
"""Render one thinking history entry in a dim panel."""
|
||||||
|
title_text = _agent_display_title(entry["agent"])
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
Text(thinking_text, style="dim"),
|
Text(entry["content"], style="dim"),
|
||||||
title="[dim] Thinking — last response [/dim]",
|
title=(
|
||||||
|
f"[dim] {title_text} — thinking "
|
||||||
|
f"#{entry['index']} "
|
||||||
|
f"[{entry['n_chars']:,} chars · {entry['n_lines']} lines] [/dim]"
|
||||||
|
),
|
||||||
|
subtitle=f"[dim]{entry['timestamp']}[/dim]",
|
||||||
border_style="dim",
|
border_style="dim",
|
||||||
padding=(1, 2),
|
padding=(1, 2),
|
||||||
box=box.ROUNDED,
|
box=box.ROUNDED,
|
||||||
@@ -699,6 +809,74 @@ def print_thinking_expansion(thinking_text: str):
|
|||||||
console.print(panel)
|
console.print(panel)
|
||||||
|
|
||||||
|
|
||||||
|
async def show_thinking_picker() -> None:
|
||||||
|
"""
|
||||||
|
Browse the thinking history for this session.
|
||||||
|
|
||||||
|
- 0 entries : prints a system notice.
|
||||||
|
- 1 entry : displays it directly.
|
||||||
|
- 2+ entries: opens an interactive radiolist dialog (prompt_toolkit) so the
|
||||||
|
user can navigate with arrow keys and press Enter to view an
|
||||||
|
entry. Falls back to a numbered text list + input() prompt
|
||||||
|
when prompt_toolkit is unavailable.
|
||||||
|
"""
|
||||||
|
if not _thinking_history:
|
||||||
|
print_system("No thinking output recorded — thinking mode may not be active.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(_thinking_history) == 1:
|
||||||
|
_display_thinking_entry(_thinking_history[0])
|
||||||
|
return
|
||||||
|
|
||||||
|
if _HAS_PROMPT_TOOLKIT:
|
||||||
|
# Show most-recent-first in the dialog.
|
||||||
|
values = [
|
||||||
|
(
|
||||||
|
entry,
|
||||||
|
(
|
||||||
|
f"#{entry['index']} "
|
||||||
|
f"{entry['agent'].capitalize()} "
|
||||||
|
f"[{entry['n_chars']:,} chars · {entry['n_lines']} lines]"
|
||||||
|
f" {entry['timestamp']}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for entry in reversed(_thinking_history)
|
||||||
|
]
|
||||||
|
selected = await _radiolist_dialog(
|
||||||
|
title="Thinking History",
|
||||||
|
text=f"Select a thinking output to view ({len(_thinking_history)} stored):",
|
||||||
|
values=values,
|
||||||
|
style=_PT_STYLE,
|
||||||
|
).run_async()
|
||||||
|
if selected is not None:
|
||||||
|
_display_thinking_entry(selected)
|
||||||
|
else:
|
||||||
|
# Fallback: numbered list + input prompt.
|
||||||
|
console.print()
|
||||||
|
console.rule("[dim]Thinking History[/dim]", style="dim")
|
||||||
|
console.print()
|
||||||
|
for entry in reversed(_thinking_history):
|
||||||
|
console.print(
|
||||||
|
f" [dim]{entry['index']:>2}.[/dim] "
|
||||||
|
f"[white]{entry['agent'].capitalize()}[/white] "
|
||||||
|
f"[dim]{entry['n_chars']:,} chars · {entry['n_lines']} lines"
|
||||||
|
f" {entry['timestamp']}[/dim]"
|
||||||
|
)
|
||||||
|
console.print()
|
||||||
|
try:
|
||||||
|
choice = input(" Enter number to view (or Enter to cancel): ").strip()
|
||||||
|
if choice.isdigit():
|
||||||
|
match = next(
|
||||||
|
(e for e in _thinking_history if e["index"] == int(choice)), None
|
||||||
|
)
|
||||||
|
if match:
|
||||||
|
_display_thinking_entry(match)
|
||||||
|
else:
|
||||||
|
print_system(f"No thinking output #{choice}.")
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Dry-run callout
|
# Dry-run callout
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user