klaude-code 1.2.6__py3-none-any.whl → 1.8.0__py3-none-any.whl
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.
- klaude_code/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
klaude_code/ui/rich/markdown.py
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
# copy from https://github.com/Aider-AI/aider/blob/main/aider/mdstream.py
|
|
2
1
|
from __future__ import annotations
|
|
3
2
|
|
|
3
|
+
import contextlib
|
|
4
4
|
import io
|
|
5
5
|
import time
|
|
6
|
+
from collections.abc import Callable
|
|
6
7
|
from typing import Any, ClassVar
|
|
7
8
|
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from rich
|
|
11
|
-
from rich.
|
|
9
|
+
from markdown_it import MarkdownIt
|
|
10
|
+
from markdown_it.token import Token
|
|
11
|
+
from rich import box
|
|
12
|
+
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
|
13
|
+
from rich.markdown import CodeBlock, Heading, Markdown, MarkdownElement, TableElement
|
|
12
14
|
from rich.rule import Rule
|
|
13
|
-
from rich.
|
|
14
|
-
from rich.style import Style
|
|
15
|
+
from rich.style import Style, StyleType
|
|
15
16
|
from rich.syntax import Syntax
|
|
17
|
+
from rich.table import Table
|
|
16
18
|
from rich.text import Text
|
|
17
19
|
from rich.theme import Theme
|
|
18
20
|
|
|
19
21
|
from klaude_code import const
|
|
22
|
+
from klaude_code.ui.rich.code_panel import CodePanel
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
class NoInsetCodeBlock(CodeBlock):
|
|
@@ -29,9 +32,44 @@ class NoInsetCodeBlock(CodeBlock):
|
|
|
29
32
|
self.lexer_name,
|
|
30
33
|
theme=self.theme,
|
|
31
34
|
word_wrap=True,
|
|
32
|
-
padding=(0,
|
|
35
|
+
padding=(0, 0),
|
|
33
36
|
)
|
|
34
|
-
yield
|
|
37
|
+
yield CodePanel(syntax, border_style="markdown.code.border")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ThinkingCodeBlock(CodeBlock):
|
|
41
|
+
"""A code block for thinking content that uses grey styling instead of syntax highlighting."""
|
|
42
|
+
|
|
43
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
44
|
+
code = str(self.text).rstrip()
|
|
45
|
+
text = Text(code, "markdown.code.block")
|
|
46
|
+
yield CodePanel(text, border_style="markdown.code.border")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Divider(MarkdownElement):
|
|
50
|
+
"""A horizontal rule with an extra blank line below."""
|
|
51
|
+
|
|
52
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
53
|
+
style = console.get_style("markdown.hr", default="none")
|
|
54
|
+
yield Rule(style=style, characters="-")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MarkdownTable(TableElement):
|
|
58
|
+
"""A table element with MINIMAL_HEAVY_HEAD box style."""
|
|
59
|
+
|
|
60
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
61
|
+
table = Table(box=box.MARKDOWN, border_style=console.get_style("markdown.table.border"))
|
|
62
|
+
|
|
63
|
+
if self.header is not None and self.header.row is not None:
|
|
64
|
+
for column in self.header.row.cells:
|
|
65
|
+
table.add_column(column.content)
|
|
66
|
+
|
|
67
|
+
if self.body is not None:
|
|
68
|
+
for row in self.body.rows:
|
|
69
|
+
row_content = [element.content for element in row.cells]
|
|
70
|
+
table.add_row(*row_content)
|
|
71
|
+
|
|
72
|
+
yield table
|
|
35
73
|
|
|
36
74
|
|
|
37
75
|
class LeftHeading(Heading):
|
|
@@ -40,18 +78,12 @@ class LeftHeading(Heading):
|
|
|
40
78
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
41
79
|
text = self.text
|
|
42
80
|
text.justify = "left" # Override justification
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# yield Panel(
|
|
48
|
-
# text,
|
|
49
|
-
# box=box.SQUARE,
|
|
50
|
-
# style="markdown.h1.border",
|
|
51
|
-
# )
|
|
52
|
-
if self.tag == "h2":
|
|
81
|
+
if self.tag == "h1":
|
|
82
|
+
h1_text = text.assemble((" ", "markdown.h1"), text, (" ", "markdown.h1"))
|
|
83
|
+
yield h1_text
|
|
84
|
+
elif self.tag == "h2":
|
|
53
85
|
text.stylize(Style(bold=True, underline=False))
|
|
54
|
-
yield
|
|
86
|
+
yield text
|
|
55
87
|
else:
|
|
56
88
|
yield text
|
|
57
89
|
|
|
@@ -64,25 +96,47 @@ class NoInsetMarkdown(Markdown):
|
|
|
64
96
|
"fence": NoInsetCodeBlock,
|
|
65
97
|
"code_block": NoInsetCodeBlock,
|
|
66
98
|
"heading_open": LeftHeading,
|
|
99
|
+
"hr": Divider,
|
|
100
|
+
"table_open": MarkdownTable,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ThinkingMarkdown(Markdown):
|
|
105
|
+
"""Markdown for thinking content with grey-styled code blocks and left-justified headings."""
|
|
106
|
+
|
|
107
|
+
elements: ClassVar[dict[str, type[Any]]] = {
|
|
108
|
+
**Markdown.elements,
|
|
109
|
+
"fence": ThinkingCodeBlock,
|
|
110
|
+
"code_block": ThinkingCodeBlock,
|
|
111
|
+
"heading_open": LeftHeading,
|
|
112
|
+
"hr": Divider,
|
|
113
|
+
"table_open": MarkdownTable,
|
|
67
114
|
}
|
|
68
115
|
|
|
69
116
|
|
|
70
117
|
class MarkdownStream:
|
|
71
|
-
"""
|
|
118
|
+
"""Block-based streaming Markdown renderer.
|
|
119
|
+
|
|
120
|
+
This renderer is optimized for terminal UX:
|
|
72
121
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
122
|
+
- Stable area: only prints *completed* Markdown blocks to scrollback (append-only).
|
|
123
|
+
- Live area: continuously repaints only the final *possibly incomplete* block.
|
|
124
|
+
|
|
125
|
+
Block boundaries are computed with `MarkdownIt("commonmark")` (token maps / top-level tokens).
|
|
126
|
+
Rendering is done with Rich Markdown (customizable via `markdown_class`).
|
|
76
127
|
"""
|
|
77
128
|
|
|
78
129
|
def __init__(
|
|
79
130
|
self,
|
|
131
|
+
console: Console,
|
|
80
132
|
mdargs: dict[str, Any] | None = None,
|
|
81
133
|
theme: Theme | None = None,
|
|
82
|
-
|
|
83
|
-
spinner: Spinner | None = None,
|
|
134
|
+
live_sink: Callable[[RenderableType | None], None] | None = None,
|
|
84
135
|
mark: str | None = None,
|
|
85
|
-
|
|
136
|
+
mark_style: StyleType | None = None,
|
|
137
|
+
left_margin: int = 0,
|
|
138
|
+
right_margin: int = const.MARKDOWN_RIGHT_MARGIN,
|
|
139
|
+
markdown_class: Callable[..., Markdown] | None = None,
|
|
86
140
|
) -> None:
|
|
87
141
|
"""Initialize the markdown stream.
|
|
88
142
|
|
|
@@ -90,40 +144,169 @@ class MarkdownStream:
|
|
|
90
144
|
mdargs (dict, optional): Additional arguments to pass to rich Markdown renderer
|
|
91
145
|
theme (Theme, optional): Theme for rendering markdown
|
|
92
146
|
console (Console, optional): External console to use for rendering
|
|
93
|
-
mark (str | None, optional): Marker shown before the first non-empty line when
|
|
94
|
-
|
|
147
|
+
mark (str | None, optional): Marker shown before the first non-empty line when left_margin >= 2
|
|
148
|
+
mark_style (StyleType | None, optional): Style to apply to the mark
|
|
149
|
+
left_margin (int, optional): Number of columns to reserve on the left side
|
|
150
|
+
right_margin (int, optional): Number of columns to reserve on the right side
|
|
151
|
+
markdown_class: Markdown class to use for rendering (defaults to NoInsetMarkdown)
|
|
95
152
|
"""
|
|
96
|
-
self.
|
|
153
|
+
self._stable_rendered_lines: list[str] = []
|
|
154
|
+
self._stable_source_line_count: int = 0
|
|
97
155
|
|
|
98
156
|
if mdargs:
|
|
99
157
|
self.mdargs: dict[str, Any] = mdargs
|
|
100
158
|
else:
|
|
101
159
|
self.mdargs = {}
|
|
102
160
|
|
|
103
|
-
|
|
104
|
-
self.live: Live | None = None
|
|
105
|
-
self._live_started: bool = False
|
|
161
|
+
self._live_sink = live_sink
|
|
106
162
|
|
|
107
163
|
# Streaming control
|
|
108
164
|
self.when: float = 0.0 # Timestamp of last update
|
|
109
165
|
self.min_delay: float = 1.0 / 20 # Minimum time between updates (20fps)
|
|
110
|
-
self.
|
|
111
|
-
# Track the maximum height the live window has ever reached
|
|
112
|
-
# so we only pad when it shrinks from a previous height,
|
|
113
|
-
# instead of always padding to live_window from the start.
|
|
114
|
-
self._live_window_seen_height: int = 0
|
|
166
|
+
self._parser: MarkdownIt = MarkdownIt("commonmark")
|
|
115
167
|
|
|
116
168
|
self.theme = theme
|
|
117
169
|
self.console = console
|
|
118
|
-
self.spinner: Spinner | None = spinner
|
|
119
170
|
self.mark: str | None = mark
|
|
120
|
-
self.
|
|
171
|
+
self.mark_style: StyleType | None = mark_style
|
|
172
|
+
|
|
173
|
+
self.left_margin: int = max(left_margin, 0)
|
|
174
|
+
|
|
175
|
+
self.right_margin: int = max(right_margin, 0)
|
|
176
|
+
self.markdown_class: Callable[..., Markdown] = markdown_class or NoInsetMarkdown
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def _live_started(self) -> bool:
|
|
180
|
+
"""Check if Live display has been started (derived from self.live)."""
|
|
181
|
+
return self._live_sink is not None
|
|
182
|
+
|
|
183
|
+
def _get_base_width(self) -> int:
|
|
184
|
+
return self.console.options.max_width
|
|
185
|
+
|
|
186
|
+
def compute_candidate_stable_line(self, text: str) -> int:
|
|
187
|
+
"""Return the start line of the last top-level block, or 0.
|
|
188
|
+
|
|
189
|
+
This value is not monotonic; callers should clamp it (e.g. with the
|
|
190
|
+
previous stable line) before using it to advance state.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
tokens = self._parser.parse(text)
|
|
195
|
+
except Exception: # markdown-it-py may raise various internal errors during parsing
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
top_level: list[Token] = [token for token in tokens if token.level == 0 and token.map is not None]
|
|
199
|
+
if len(top_level) < 2:
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
last = top_level[-1]
|
|
203
|
+
assert last.map is not None
|
|
204
|
+
start_line = last.map[0]
|
|
205
|
+
return max(start_line, 0)
|
|
206
|
+
|
|
207
|
+
def split_blocks(self, text: str, *, min_stable_line: int = 0, final: bool = False) -> tuple[str, str, int]:
|
|
208
|
+
"""Split full markdown into stable and live sources.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
stable_source: Completed blocks (append-only)
|
|
212
|
+
live_source: Last (possibly incomplete) block
|
|
213
|
+
stable_line: Line index where live starts
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
lines = text.splitlines(keepends=True)
|
|
217
|
+
line_count = len(lines)
|
|
218
|
+
|
|
219
|
+
stable_line = line_count if final else self.compute_candidate_stable_line(text)
|
|
220
|
+
|
|
221
|
+
stable_line = min(stable_line, line_count)
|
|
222
|
+
stable_line = max(stable_line, min_stable_line)
|
|
223
|
+
|
|
224
|
+
stable_source = "".join(lines[:stable_line])
|
|
225
|
+
live_source = "".join(lines[stable_line:])
|
|
226
|
+
return stable_source, live_source, stable_line
|
|
227
|
+
|
|
228
|
+
def render_ansi(self, text: str, *, apply_mark: bool) -> str:
|
|
229
|
+
"""Render markdown source to an ANSI string.
|
|
230
|
+
|
|
231
|
+
This is primarily intended for internal debugging and tests.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
return "".join(self._render_markdown_to_lines(text, apply_mark=apply_mark))
|
|
235
|
+
|
|
236
|
+
def render_stable_ansi(self, stable_source: str, *, has_live_suffix: bool, final: bool) -> str:
|
|
237
|
+
"""Render stable prefix to ANSI, preserving inter-block spacing."""
|
|
238
|
+
|
|
239
|
+
if not stable_source:
|
|
240
|
+
return ""
|
|
241
|
+
|
|
242
|
+
render_source = stable_source
|
|
243
|
+
if not final and has_live_suffix:
|
|
244
|
+
render_source = self._append_nonfinal_sentinel(stable_source)
|
|
245
|
+
|
|
246
|
+
return self.render_ansi(render_source, apply_mark=True)
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def normalize_live_ansi_for_boundary(*, stable_ansi: str, live_ansi: str) -> str:
|
|
250
|
+
"""Normalize whitespace at the stable/live boundary.
|
|
251
|
+
|
|
252
|
+
Some Rich Markdown blocks (e.g. lists) render with a leading blank line.
|
|
253
|
+
If the stable prefix already renders a trailing blank line, rendering the
|
|
254
|
+
live suffix separately may introduce an extra blank line that wouldn't
|
|
255
|
+
appear when rendering the full document.
|
|
256
|
+
|
|
257
|
+
This function removes *overlapping* blank lines from the live ANSI when
|
|
258
|
+
the stable ANSI already ends with one or more blank lines.
|
|
259
|
+
|
|
260
|
+
Important: don't remove *all* leading blank lines from the live suffix.
|
|
261
|
+
In some incomplete-block cases, the live render may begin with multiple
|
|
262
|
+
blank lines while the full-document render would keep one of them.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
stable_lines = stable_ansi.splitlines(keepends=True)
|
|
266
|
+
if not stable_lines:
|
|
267
|
+
return live_ansi
|
|
268
|
+
|
|
269
|
+
stable_trailing_blank = 0
|
|
270
|
+
for line in reversed(stable_lines):
|
|
271
|
+
if line.strip():
|
|
272
|
+
break
|
|
273
|
+
stable_trailing_blank += 1
|
|
274
|
+
if stable_trailing_blank <= 0:
|
|
275
|
+
return live_ansi
|
|
276
|
+
|
|
277
|
+
live_lines = live_ansi.splitlines(keepends=True)
|
|
278
|
+
live_leading_blank = 0
|
|
279
|
+
for line in live_lines:
|
|
280
|
+
if line.strip():
|
|
281
|
+
break
|
|
282
|
+
live_leading_blank += 1
|
|
283
|
+
|
|
284
|
+
drop = min(stable_trailing_blank, live_leading_blank)
|
|
285
|
+
if drop > 0:
|
|
286
|
+
live_lines = live_lines[drop:]
|
|
287
|
+
return "".join(live_lines)
|
|
288
|
+
|
|
289
|
+
def _append_nonfinal_sentinel(self, stable_source: str) -> str:
|
|
290
|
+
"""Make Rich render stable content as if it isn't the last block.
|
|
291
|
+
|
|
292
|
+
Rich Markdown may omit trailing spacing for the last block in a document.
|
|
293
|
+
When we render only the stable prefix (without the live suffix), we still
|
|
294
|
+
need the *inter-block* spacing to match the full document.
|
|
295
|
+
|
|
296
|
+
A harmless HTML comment block causes Rich Markdown to emit the expected
|
|
297
|
+
spacing while rendering no visible content.
|
|
298
|
+
"""
|
|
121
299
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
self._live_started: bool = False
|
|
300
|
+
if not stable_source:
|
|
301
|
+
return stable_source
|
|
125
302
|
|
|
126
|
-
|
|
303
|
+
if stable_source.endswith("\n\n"):
|
|
304
|
+
return stable_source + "<!-- -->"
|
|
305
|
+
if stable_source.endswith("\n"):
|
|
306
|
+
return stable_source + "\n<!-- -->"
|
|
307
|
+
return stable_source + "\n\n<!-- -->"
|
|
308
|
+
|
|
309
|
+
def _render_markdown_to_lines(self, text: str, *, apply_mark: bool) -> list[str]:
|
|
127
310
|
"""Render markdown text to a list of lines.
|
|
128
311
|
|
|
129
312
|
Args:
|
|
@@ -135,15 +318,10 @@ class MarkdownStream:
|
|
|
135
318
|
# Render the markdown to a string buffer
|
|
136
319
|
string_io = io.StringIO()
|
|
137
320
|
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
if self.console is not None:
|
|
141
|
-
base_width = self.console.options.max_width
|
|
142
|
-
else:
|
|
143
|
-
probe_console = Console(theme=self.theme)
|
|
144
|
-
base_width = probe_console.options.max_width
|
|
321
|
+
# Keep width stable across frames to prevent reflow/jitter.
|
|
322
|
+
base_width = self._get_base_width()
|
|
145
323
|
|
|
146
|
-
effective_width = max(base_width - self.
|
|
324
|
+
effective_width = max(base_width - self.left_margin - self.right_margin, 1)
|
|
147
325
|
|
|
148
326
|
# Use external console for consistent theming, or create temporary one
|
|
149
327
|
temp_console = Console(
|
|
@@ -153,23 +331,35 @@ class MarkdownStream:
|
|
|
153
331
|
width=effective_width,
|
|
154
332
|
)
|
|
155
333
|
|
|
156
|
-
markdown =
|
|
334
|
+
markdown = self.markdown_class(text, **self.mdargs)
|
|
157
335
|
temp_console.print(markdown)
|
|
158
336
|
output = string_io.getvalue()
|
|
159
337
|
|
|
160
|
-
# Split rendered output into lines, strip trailing spaces, and apply left
|
|
338
|
+
# Split rendered output into lines, strip trailing spaces, and apply left margin.
|
|
161
339
|
lines = output.splitlines(keepends=True)
|
|
162
|
-
indent_prefix = " " * self.
|
|
340
|
+
indent_prefix = " " * self.left_margin if self.left_margin > 0 else ""
|
|
163
341
|
processed_lines: list[str] = []
|
|
164
342
|
mark_applied = False
|
|
165
|
-
use_mark = bool(self.mark) and self.
|
|
343
|
+
use_mark = apply_mark and bool(self.mark) and self.left_margin >= 2
|
|
344
|
+
|
|
345
|
+
# Pre-render styled mark if needed
|
|
346
|
+
styled_mark: str | None = None
|
|
347
|
+
if use_mark and self.mark:
|
|
348
|
+
if self.mark_style:
|
|
349
|
+
mark_text = Text(self.mark, style=self.mark_style)
|
|
350
|
+
mark_buffer = io.StringIO()
|
|
351
|
+
mark_console = Console(file=mark_buffer, force_terminal=True, theme=self.theme)
|
|
352
|
+
mark_console.print(mark_text, end="")
|
|
353
|
+
styled_mark = mark_buffer.getvalue()
|
|
354
|
+
else:
|
|
355
|
+
styled_mark = self.mark
|
|
166
356
|
|
|
167
357
|
for line in lines:
|
|
168
358
|
stripped = line.rstrip()
|
|
169
359
|
|
|
170
|
-
# Apply mark to the first non-empty line only when
|
|
360
|
+
# Apply mark to the first non-empty line only when left_margin is at least 2.
|
|
171
361
|
if use_mark and not mark_applied and stripped:
|
|
172
|
-
stripped = f"{
|
|
362
|
+
stripped = f"{styled_mark} {stripped}"
|
|
173
363
|
mark_applied = True
|
|
174
364
|
elif indent_prefix:
|
|
175
365
|
stripped = indent_prefix + stripped
|
|
@@ -182,127 +372,72 @@ class MarkdownStream:
|
|
|
182
372
|
|
|
183
373
|
def __del__(self) -> None:
|
|
184
374
|
"""Destructor to ensure Live display is properly cleaned up."""
|
|
185
|
-
if self.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
pass # Ignore any errors during cleanup
|
|
375
|
+
if self._live_sink is None:
|
|
376
|
+
return
|
|
377
|
+
with contextlib.suppress(Exception):
|
|
378
|
+
self._live_sink(None)
|
|
190
379
|
|
|
191
380
|
def update(self, text: str, final: bool = False) -> None:
|
|
192
|
-
"""Update the
|
|
193
|
-
|
|
194
|
-
Args:
|
|
195
|
-
text (str): The markdown text received so far
|
|
196
|
-
final (bool): If True, this is the final update and we should clean up
|
|
197
|
-
|
|
198
|
-
Splits the output into "stable" older lines and the "last few" lines
|
|
199
|
-
which aren't considered stable. They may shift around as new chunks
|
|
200
|
-
are appended to the markdown text.
|
|
201
|
-
|
|
202
|
-
The stable lines emit to the console above the Live window.
|
|
203
|
-
The unstable lines emit into the Live window so they can be repainted.
|
|
204
|
-
|
|
205
|
-
Markdown going to the console works better in terminal scrollback buffers.
|
|
206
|
-
The live window doesn't play nice with terminal scrollback.
|
|
207
|
-
"""
|
|
208
|
-
# On the first call, start the Live renderer
|
|
209
|
-
if not self._live_started:
|
|
210
|
-
initial_content = self._live_renderable(Text(""), final=False)
|
|
211
|
-
self.live = Live(
|
|
212
|
-
initial_content,
|
|
213
|
-
refresh_per_second=1.0 / self.min_delay,
|
|
214
|
-
console=self.console,
|
|
215
|
-
)
|
|
216
|
-
self.live.start()
|
|
217
|
-
self._live_started = True
|
|
218
|
-
|
|
219
|
-
# If live rendering isn't available (e.g., after a final update), stop.
|
|
220
|
-
if self.live is None:
|
|
221
|
-
return
|
|
381
|
+
"""Update the display with the latest full markdown buffer."""
|
|
222
382
|
|
|
223
383
|
now = time.time()
|
|
224
|
-
# Throttle updates to maintain smooth rendering
|
|
225
384
|
if not final and now - self.when < self.min_delay:
|
|
226
385
|
return
|
|
227
386
|
self.when = now
|
|
228
387
|
|
|
229
|
-
|
|
388
|
+
previous_stable_line = self._stable_source_line_count
|
|
389
|
+
|
|
390
|
+
stable_source, live_source, stable_line = self.split_blocks(
|
|
391
|
+
text,
|
|
392
|
+
min_stable_line=previous_stable_line,
|
|
393
|
+
final=final,
|
|
394
|
+
)
|
|
395
|
+
|
|
230
396
|
start = time.time()
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if final or num_lines > 0:
|
|
247
|
-
# Lines to append to stable area
|
|
248
|
-
num_printed = len(self.printed)
|
|
249
|
-
to_append_count = num_lines - num_printed
|
|
250
|
-
|
|
251
|
-
if to_append_count > 0:
|
|
252
|
-
# Print new stable lines above Live window
|
|
253
|
-
append_chunk = lines[num_printed:num_lines]
|
|
254
|
-
append_chunk_text = Text.from_ansi("".join(append_chunk))
|
|
255
|
-
live = self.live
|
|
256
|
-
assert live is not None
|
|
257
|
-
live.console.print(append_chunk_text) # Print above Live area
|
|
258
|
-
|
|
259
|
-
# Track printed stable lines
|
|
260
|
-
self.printed = lines[:num_lines]
|
|
261
|
-
|
|
262
|
-
# Handle final update cleanup
|
|
397
|
+
|
|
398
|
+
stable_changed = final or stable_line > self._stable_source_line_count
|
|
399
|
+
if stable_changed and stable_source:
|
|
400
|
+
stable_ansi = self.render_stable_ansi(stable_source, has_live_suffix=bool(live_source), final=final)
|
|
401
|
+
stable_lines = stable_ansi.splitlines(keepends=True)
|
|
402
|
+
new_lines = stable_lines[len(self._stable_rendered_lines) :]
|
|
403
|
+
if new_lines:
|
|
404
|
+
stable_chunk = "".join(new_lines)
|
|
405
|
+
self.console.print(Text.from_ansi(stable_chunk), end="\n")
|
|
406
|
+
self._stable_rendered_lines = stable_lines
|
|
407
|
+
self._stable_source_line_count = stable_line
|
|
408
|
+
elif final and not stable_source:
|
|
409
|
+
self._stable_rendered_lines = []
|
|
410
|
+
self._stable_source_line_count = stable_line
|
|
411
|
+
|
|
263
412
|
if final:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
live.update(Text(""))
|
|
267
|
-
live.stop()
|
|
268
|
-
self.live = None
|
|
413
|
+
if self._live_sink is not None:
|
|
414
|
+
self._live_sink(None)
|
|
269
415
|
return
|
|
270
416
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
def _live_renderable(self, rest: Text, final: bool) -> RenderableType:
|
|
299
|
-
if final or not self.spinner:
|
|
300
|
-
return rest
|
|
301
|
-
else:
|
|
302
|
-
return Group(rest, Text(), self.spinner)
|
|
303
|
-
|
|
304
|
-
def find_minimal_suffix(self, text: str, match_lines: int = 50) -> None:
|
|
305
|
-
"""
|
|
306
|
-
Splits text into chunks on blank lines "\n\n".
|
|
307
|
-
"""
|
|
308
|
-
return None
|
|
417
|
+
if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._live_sink is not None:
|
|
418
|
+
apply_mark_live = self._stable_source_line_count == 0
|
|
419
|
+
live_lines = self._render_markdown_to_lines(live_source, apply_mark=apply_mark_live)
|
|
420
|
+
|
|
421
|
+
if self._stable_rendered_lines:
|
|
422
|
+
stable_trailing_blank = 0
|
|
423
|
+
for line in reversed(self._stable_rendered_lines):
|
|
424
|
+
if line.strip():
|
|
425
|
+
break
|
|
426
|
+
stable_trailing_blank += 1
|
|
427
|
+
|
|
428
|
+
if stable_trailing_blank > 0:
|
|
429
|
+
live_leading_blank = 0
|
|
430
|
+
for line in live_lines:
|
|
431
|
+
if line.strip():
|
|
432
|
+
break
|
|
433
|
+
live_leading_blank += 1
|
|
434
|
+
|
|
435
|
+
drop = min(stable_trailing_blank, live_leading_blank)
|
|
436
|
+
if drop > 0:
|
|
437
|
+
live_lines = live_lines[drop:]
|
|
438
|
+
|
|
439
|
+
live_text = Text.from_ansi("".join(live_lines))
|
|
440
|
+
self._live_sink(live_text)
|
|
441
|
+
|
|
442
|
+
elapsed = time.time() - start
|
|
443
|
+
self.min_delay = min(max(elapsed * 6, 1.0 / 30), 0.5)
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Iterable, Sequence
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class SearchableFormattedText:
|
|
7
7
|
"""
|
|
8
8
|
Wrapper for prompt_toolkit formatted text that also supports string-like
|
|
9
|
-
methods
|
|
10
|
-
|
|
11
|
-
This allows using ``use_search_filter=True`` with a formatted ``Choice.title``.
|
|
9
|
+
methods commonly expected by search filters (e.g., ``.lower()``).
|
|
12
10
|
|
|
13
11
|
- ``fragments``: A sequence of (style, text) tuples accepted by
|
|
14
12
|
prompt_toolkit's ``to_formatted_text``.
|
|
@@ -16,8 +14,8 @@ class SearchableFormattedText:
|
|
|
16
14
|
concatenating the text parts of the fragments.
|
|
17
15
|
"""
|
|
18
16
|
|
|
19
|
-
def __init__(self, fragments: Sequence[
|
|
20
|
-
self._fragments:
|
|
17
|
+
def __init__(self, fragments: Sequence[tuple[str, str]], plain: str | None = None):
|
|
18
|
+
self._fragments: list[tuple[str, str]] = list(fragments)
|
|
21
19
|
if plain is None:
|
|
22
20
|
plain = "".join(text for _, text in self._fragments)
|
|
23
21
|
self._plain = plain
|
|
@@ -25,14 +23,14 @@ class SearchableFormattedText:
|
|
|
25
23
|
# Recognized by prompt_toolkit's to_formatted_text(value)
|
|
26
24
|
def __pt_formatted_text__(
|
|
27
25
|
self,
|
|
28
|
-
) -> Iterable[
|
|
26
|
+
) -> Iterable[tuple[str, str]]: # pragma: no cover - passthrough
|
|
29
27
|
return self._fragments
|
|
30
28
|
|
|
31
29
|
# Provide a human-readable representation.
|
|
32
30
|
def __str__(self) -> str: # pragma: no cover - utility
|
|
33
31
|
return self._plain
|
|
34
32
|
|
|
35
|
-
# Minimal string API
|
|
33
|
+
# Minimal string API for search filtering.
|
|
36
34
|
def lower(self) -> str:
|
|
37
35
|
return self._plain.lower()
|
|
38
36
|
|
|
@@ -45,16 +43,15 @@ class SearchableFormattedText:
|
|
|
45
43
|
return self._plain
|
|
46
44
|
|
|
47
45
|
|
|
48
|
-
class SearchableFormattedList(list[
|
|
46
|
+
class SearchableFormattedList(list[tuple[str, str]]):
|
|
49
47
|
"""
|
|
50
|
-
List variant compatible with
|
|
48
|
+
List variant compatible with prompt_toolkit formatted-text usage.
|
|
51
49
|
|
|
52
|
-
- Behaves like ``List[Tuple[str, str]]`` for rendering (so ``isinstance(..., list)`` works)
|
|
53
|
-
preserving existing styling behavior in questionary.
|
|
50
|
+
- Behaves like ``List[Tuple[str, str]]`` for rendering (so ``isinstance(..., list)`` works).
|
|
54
51
|
- Provides ``.lower()``/``.upper()`` returning the plain text for search filtering.
|
|
55
52
|
"""
|
|
56
53
|
|
|
57
|
-
def __init__(self, fragments: Sequence[
|
|
54
|
+
def __init__(self, fragments: Sequence[tuple[str, str]], plain: str | None = None):
|
|
58
55
|
super().__init__(fragments)
|
|
59
56
|
if plain is None:
|
|
60
57
|
plain = "".join(text for _, text in fragments)
|