feat(help): add invocation-aware path rendering for nested CLI help

- introduce InvocationContext and InvocationSegment for styled invocation paths
- thread invocation_context through command arg resolution and help/tldr rendering
- render CLI and namespace help from routed context instead of static program formatting
- support per-segment styling for nested namespaces and command paths
- rebase help target context for `help -k` so usage matches the target command path
- clean up context module docs and remove old invocation path formatting helper
This commit is contained in:
2026-04-11 20:00:01 -04:00
parent 30cb8b97b5
commit 8ece2a5de6
5 changed files with 344 additions and 132 deletions

View File

@@ -1,18 +1,24 @@
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
"""Context management for Falyx CLI.
"""Context models for Falyx execution and invocation state.
This module defines `ExecutionContext` and `SharedContext`, which are responsible for
capturing per-action and cross-action metadata during CLI workflow execution. These
context objects provide structured introspection, result tracking, error recording,
and time-based performance metrics.
This module defines the core context objects used throughout Falyx to track both
runtime execution metadata and routed invocation-path state.
- `ExecutionContext`: Captures runtime information for a single action execution,
including arguments, results, exceptions, timing, and logging.
- `SharedContext`: Maintains shared state and result propagation across
`ChainedAction` or `ActionGroup` executions.
It provides:
- `ExecutionContext` for per-action execution details such as arguments,
results, exceptions, timing, and summary logging.
- `SharedContext` for transient shared state across grouped or chained
actions, including propagated results, indexed errors, and arbitrary
shared data.
- `InvocationSegment` for representing a single styled token within a
rendered invocation path.
- `InvocationContext` for capturing the current routed command path as an
immutable value object that supports both plain-text and Rich-markup
rendering.
These contexts enable rich introspection, traceability, and workflow coordination,
supporting hook lifecycles, retries, and structured output generation.
Together, these models support Falyx lifecycle hooks, execution tracing,
history/introspection, and context-aware help and usage rendering across CLI
and menu modes.
"""
from __future__ import annotations
@@ -23,6 +29,7 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from rich.console import Console
from rich.markup import escape
from falyx.console import console
from falyx.mode import FalyxMode
@@ -285,28 +292,161 @@ class SharedContext(BaseModel):
)
class InvocationSegment(BaseModel):
"""Styled path segment used to build an invocation display path.
`InvocationSegment` represents a single token within an `InvocationContext`,
such as a namespace key, command key, or alias. It stores the raw display
text and an optional Rich style so invocation paths can be rendered either
as plain text or styled markup.
Attributes:
text (str): Display text for this path segment.
style (str | None): Optional Rich style applied when rendering this
segment in markup output.
"""
text: str
style: str | None = None
class InvocationContext(BaseModel):
"""Immutable invocation-path context for routed Falyx help and execution.
`InvocationContext` captures the current displayable command path as the router
descends through namespaces and commands. It stores both the raw typed path
(`typed_path`) and a styled segment representation (`segments`) so the same
context can be rendered as plain text or Rich markup.
This model is intended to be treated as an immutable value object. Methods such
as `with_path_segment()` and `without_last_path_segment()` return new context
instances rather than mutating the existing one.
Attributes:
program (str): Root program name used in CLI-mode help and usage output.
program_style (str): Rich style applied to the program name when rendering
`markup_path`.
typed_path (list[str]): Raw invocation tokens collected during routing,
excluding the root program name.
segments (list[InvocationSegment]): Styled path segments used to render the
invocation path with Rich markup.
mode (FalyxMode): Active Falyx mode for this invocation context. This is
used to determine whether the path should include the program name.
is_preview (bool): Whether the current invocation is a preview flow rather
than a normal execution flow.
"""
program: str = ""
program_style: str = ""
typed_path: list[str] = Field(default_factory=list)
segments: list[InvocationSegment] = Field(default_factory=list)
mode: FalyxMode = FalyxMode.MENU
is_preview: bool = False
@property
def is_cli_mode(self) -> bool:
"""Whether this context should render using CLI path semantics.
Returns:
bool: `True` when the invocation is not in menu mode, meaning rendered
paths should include the program name. `False` when in menu mode.
"""
return self.mode != FalyxMode.MENU
def child(self, token: str) -> InvocationContext:
def with_path_segment(
self,
token: str,
*,
style: str | None = None,
) -> InvocationContext:
"""Return a new context with one additional path segment appended.
This method preserves the current context and creates a new
`InvocationContext` with the provided token added to both `typed_path` and
`segments`.
Args:
token (str): Raw path token to append, such as a namespace key,
command key, or alias.
style (str | None): Optional Rich style for the appended segment.
Returns:
InvocationContext: A new context containing the appended path segment.
"""
return InvocationContext(
program=self.program,
program_style=self.program_style,
typed_path=[*self.typed_path, token],
segments=[*self.segments, InvocationSegment(text=token, style=style)],
mode=self.mode,
is_preview=self.is_preview,
)
def display_path(self) -> str:
def without_last_path_segment(self) -> InvocationContext:
"""Return a new context with the last path segment removed.
This method preserves the current context and creates a new
`InvocationContext` with the last token removed from both `typed_path` and
`segments`.
Returns:
InvocationContext: A new context with the last path segment removed, or the
current context if no path segments are present.
"""
if not self.typed_path:
return self
return InvocationContext(
program=self.program,
program_style=self.program_style,
typed_path=self.typed_path[:-1],
segments=self.segments[:-1],
mode=self.mode,
is_preview=self.is_preview,
)
@property
def plain_path(self) -> str:
"""Render the invocation path as plain text.
In CLI mode, the rendered path includes the root program name followed by
all collected path segments. In menu mode, only the collected path segments
are rendered.
Returns:
str: Plain-text invocation path suitable for logs, comparisons, or
non-styled help output.
"""
parts = [seg.text for seg in self.segments]
if self.is_cli_mode:
return " ".join([self.program, *self.typed_path]).strip()
return " ".join(self.typed_path).strip()
return " ".join([self.program, *parts]).strip()
return " ".join(parts).strip()
@property
def markup_path(self) -> str:
"""Render the invocation path as escaped Rich markup.
In CLI mode, the root program name is included and styled with
`program_style` when provided. Each path segment is escaped and styled
using its associated `InvocationSegment.style` value when present.
Returns:
str: Rich-markup invocation path suitable for help and usage rendering.
"""
parts: list[str] = []
if self.is_cli_mode and self.program:
if self.program_style:
parts.append(
f"[{self.program_style}]{escape(self.program)}[/{self.program_style}]"
)
else:
parts.append(escape(self.program))
for seg in self.segments:
if seg.style:
parts.append(f"[{seg.style}]{escape(seg.text)}[/{seg.style}]")
else:
parts.append(escape(seg.text))
return " ".join(parts).strip()
if __name__ == "__main__":