feat(interface): show resume hint on the existing exit panel
When a scan ends without calling ``finish_scan`` (Ctrl+C, TUI quit,
crash), ``display_completion_message`` now appends one extra line
inside the existing completion panel:
Resume strix --run-name <run_name>
Same ``dim``-label / coloured-value styling as the panel's ``Target``
and ``Output`` rows. Only rendered when ``scan_completed`` is False —
a finished scan doesn't need a resume nudge.
Triggers ``orchestration/scan.py``'s implicit-resume path on the next
invocation (presence of ``{run_dir}/bus.json`` is the trigger), so
the user gets back exactly where they left off — root + every
non-terminal subagent's full LLM history, bus topology, prior
findings.
Covers both ``run_cli`` and ``run_tui`` paths since
``display_completion_message`` is called from ``main()`` regardless
of which front-end ran.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,6 @@ from strix.telemetry.tracer import Tracer, set_global_tracer
|
|||||||
|
|
||||||
from .utils import (
|
from .utils import (
|
||||||
build_live_stats_text,
|
build_live_stats_text,
|
||||||
format_resume_hint,
|
|
||||||
format_vulnerability_report,
|
format_vulnerability_report,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,10 +139,6 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
|||||||
|
|
||||||
def signal_handler(_signum: int, _frame: Any) -> None:
|
def signal_handler(_signum: int, _frame: Any) -> None:
|
||||||
tracer.cleanup()
|
tracer.cleanup()
|
||||||
hint = format_resume_hint(args.run_name)
|
|
||||||
if hint is not None:
|
|
||||||
console.print()
|
|
||||||
console.print(hint)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
atexit.register(cleanup_on_exit)
|
atexit.register(cleanup_on_exit)
|
||||||
@@ -217,10 +212,6 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"[bold red]Error during penetration test:[/] {e}")
|
console.print(f"[bold red]Error during penetration test:[/] {e}")
|
||||||
hint = format_resume_hint(args.run_name)
|
|
||||||
if hint is not None:
|
|
||||||
console.print()
|
|
||||||
console.print(hint)
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if tracer.final_scan_result:
|
if tracer.final_scan_result:
|
||||||
|
|||||||
@@ -464,6 +464,14 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) ->
|
|||||||
results_text.append(str(results_path), style="#60a5fa")
|
results_text.append(str(results_path), style="#60a5fa")
|
||||||
panel_parts.extend(["\n", results_text])
|
panel_parts.extend(["\n", results_text])
|
||||||
|
|
||||||
|
if not scan_completed:
|
||||||
|
resume_text = Text()
|
||||||
|
resume_text.append("\n")
|
||||||
|
resume_text.append("Resume", style="dim")
|
||||||
|
resume_text.append(" ")
|
||||||
|
resume_text.append(f"strix --run-name {args.run_name}", style="#22c55e")
|
||||||
|
panel_parts.extend(["\n", resume_text])
|
||||||
|
|
||||||
panel_content = Text.assemble(*panel_parts)
|
panel_content = Text.assemble(*panel_parts)
|
||||||
|
|
||||||
border_style = "#22c55e" if scan_completed else "#eab308"
|
border_style = "#22c55e" if scan_completed else "#eab308"
|
||||||
|
|||||||
+2
-14
@@ -35,7 +35,7 @@ from strix.config import load_settings
|
|||||||
from strix.interface.tool_components.agent_message_renderer import AgentMessageRenderer
|
from strix.interface.tool_components.agent_message_renderer import AgentMessageRenderer
|
||||||
from strix.interface.tool_components.registry import get_tool_renderer
|
from strix.interface.tool_components.registry import get_tool_renderer
|
||||||
from strix.interface.tool_components.user_message_renderer import UserMessageRenderer
|
from strix.interface.tool_components.user_message_renderer import UserMessageRenderer
|
||||||
from strix.interface.utils import build_tui_stats_text, format_resume_hint
|
from strix.interface.utils import build_tui_stats_text
|
||||||
from strix.orchestration.scan import run_strix_scan
|
from strix.orchestration.scan import run_strix_scan
|
||||||
from strix.runtime import session_manager
|
from strix.runtime import session_manager
|
||||||
from strix.telemetry.tracer import Tracer, set_global_tracer
|
from strix.telemetry.tracer import Tracer, set_global_tracer
|
||||||
@@ -1991,16 +1991,4 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
async def run_tui(args: argparse.Namespace) -> None:
|
async def run_tui(args: argparse.Namespace) -> None:
|
||||||
"""Run strix in interactive TUI mode with textual."""
|
"""Run strix in interactive TUI mode with textual."""
|
||||||
app = StrixTUIApp(args)
|
app = StrixTUIApp(args)
|
||||||
try:
|
await app.run_async()
|
||||||
await app.run_async()
|
|
||||||
finally:
|
|
||||||
# After Textual restores the real terminal, print the resume
|
|
||||||
# hint so the user knows the exact ``strix --run-name X``
|
|
||||||
# invocation that picks up where they left off.
|
|
||||||
from rich.console import Console
|
|
||||||
|
|
||||||
hint = format_resume_hint(getattr(args, "run_name", None))
|
|
||||||
if hint is not None:
|
|
||||||
console = Console()
|
|
||||||
console.print()
|
|
||||||
console.print(hint)
|
|
||||||
|
|||||||
@@ -1449,37 +1449,3 @@ def validate_config_file(config_path: str) -> Path:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def format_resume_hint(run_name: str | None) -> Panel | None:
|
|
||||||
"""Tell the user how to resume the current run, if it persisted state.
|
|
||||||
|
|
||||||
Returns a Rich :class:`Panel` ready to ``console.print`` when
|
|
||||||
``strix_runs/<run_name>/bus.json`` exists (i.e. the scan registered
|
|
||||||
at least the root agent and has snapshot state to resume from).
|
|
||||||
Returns ``None`` when there's nothing to resume — e.g. the user hit
|
|
||||||
Ctrl+C before sandbox bring-up, or no run-name was assigned yet.
|
|
||||||
|
|
||||||
Mirrors the auto-resume contract in ``orchestration/scan.py``:
|
|
||||||
re-running with the same ``--run-name`` picks up where this scan
|
|
||||||
left off; deleting ``strix_runs/<run_name>/`` forces a fresh start.
|
|
||||||
"""
|
|
||||||
if not run_name:
|
|
||||||
return None
|
|
||||||
run_dir = Path("strix_runs") / run_name
|
|
||||||
if not (run_dir / "bus.json").exists():
|
|
||||||
return None
|
|
||||||
|
|
||||||
text = Text()
|
|
||||||
text.append("Run paused. Resume any time with:\n\n", style="white")
|
|
||||||
text.append(f" strix --run-name {run_name}\n", style="bold #60a5fa")
|
|
||||||
text.append("\nOr delete ", style="dim")
|
|
||||||
text.append(f"strix_runs/{run_name}/", style="bold dim")
|
|
||||||
text.append(" to start fresh.", style="dim")
|
|
||||||
return Panel(
|
|
||||||
text,
|
|
||||||
title="[bold white]STRIX",
|
|
||||||
title_align="left",
|
|
||||||
border_style="#60a5fa",
|
|
||||||
padding=(1, 2),
|
|
||||||
)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user