deepy-cli 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/PKG-INFO +1 -1
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/pyproject.toml +1 -1
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/__init__.py +1 -1
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/prompt_input.py +17 -13
- deepy_cli-0.2.3/src/deepy/ui/status_footer.py +117 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/styles.py +18 -6
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/terminal.py +337 -62
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/README.md +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/cli.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/config/__init__.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/config/settings.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/AskUserQuestion.md +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/WebFetch.md +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/edit.md +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/modify.md +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/read.md +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/shell.md +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/write.md +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/errors.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/agent.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/context.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/provider.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/replay.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/runner.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/mcp.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/init_agents.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/system.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/tool_docs.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/sessions/jsonl.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/skill_market.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/skills.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/status.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/agents.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/builtin.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/file_state.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/result.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/shell_output.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/types/__init__.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/types/sdk.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/types/tool_payloads.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/ask_user_question.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/exit_summary.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/file_mentions.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/local_command.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/message_view.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/skill_picker.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/slash_commands.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/welcome.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/usage.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/utils/error_logger.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/utils/json.py +0 -0
- {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/utils/notify.py +0 -0
|
@@ -16,6 +16,7 @@ from deepy.skills import SkillInfo
|
|
|
16
16
|
from deepy.ui.file_mentions import FileMentionCompleter
|
|
17
17
|
from deepy.ui.prompt_buffer import PromptBufferState
|
|
18
18
|
from deepy.ui.slash_commands import SlashCommandItem
|
|
19
|
+
from deepy.ui.status_footer import StatusFooter
|
|
19
20
|
from deepy.ui.styles import DARK_PALETTE, UiPalette
|
|
20
21
|
|
|
21
22
|
|
|
@@ -23,7 +24,7 @@ DEFAULT_PROMPT_HISTORY = Path.home() / ".deepy" / "prompt-history.txt"
|
|
|
23
24
|
CTRL_D_EXIT_CONFIRM_SIGNAL = "\0deepy:ctrl-d-exit-confirm\0"
|
|
24
25
|
PROMPT_TOOLBAR_BACKGROUND = "#161821"
|
|
25
26
|
PROMPT_TOOLBAR_FOREGROUND = "#a6adc8"
|
|
26
|
-
PROMPT_TOOLBAR_HELP = "
|
|
27
|
+
PROMPT_TOOLBAR_HELP = "newline: ctrl+j"
|
|
27
28
|
PROMPT_MESSAGE: StyleAndTextTuples = [("class:prompt", "> ")]
|
|
28
29
|
PROMPT_PLACEHOLDER: StyleAndTextTuples = [("class:placeholder", "Type your message...")]
|
|
29
30
|
PROMPT_TOOLBAR: StyleAndTextTuples = [("class:toolbar.help", PROMPT_TOOLBAR_HELP)]
|
|
@@ -123,31 +124,34 @@ def prompt_for_input(
|
|
|
123
124
|
|
|
124
125
|
|
|
125
126
|
def build_prompt_toolbar(
|
|
126
|
-
context_status: str = "",
|
|
127
|
+
context_status: str | StatusFooter = "",
|
|
127
128
|
*,
|
|
128
129
|
platform_name: str | None = None,
|
|
129
130
|
) -> AnyFormattedText:
|
|
131
|
+
if isinstance(context_status, StatusFooter):
|
|
132
|
+
return context_status.to_prompt_toolkit(help_text=PROMPT_TOOLBAR_HELP)
|
|
130
133
|
if not context_status:
|
|
131
134
|
return prompt_toolbar(platform_name)
|
|
132
|
-
toolbar
|
|
133
|
-
("class:toolbar.context", context_status),
|
|
134
|
-
("class:toolbar.separator", " · "),
|
|
135
|
-
*prompt_toolbar(platform_name),
|
|
136
|
-
]
|
|
137
|
-
return toolbar
|
|
135
|
+
return [("class:toolbar.context", context_status)]
|
|
138
136
|
|
|
139
137
|
|
|
140
138
|
def prompt_style(palette: UiPalette | None = None) -> Style:
|
|
141
139
|
palette = palette or DARK_PALETTE
|
|
140
|
+
toolbar_base = f"noreverse bg:{palette.toolbar_background}"
|
|
142
141
|
return Style.from_dict(
|
|
143
142
|
{
|
|
144
143
|
"prompt": palette.prompt,
|
|
145
144
|
"placeholder": palette.placeholder,
|
|
146
|
-
"toolbar": f"
|
|
147
|
-
"toolbar.context": f"
|
|
148
|
-
"toolbar.separator": f"
|
|
149
|
-
"toolbar.help": f"
|
|
150
|
-
"
|
|
145
|
+
"toolbar": f"{toolbar_base} {palette.toolbar_foreground}",
|
|
146
|
+
"toolbar.context": f"{toolbar_base} {palette.toolbar_context}",
|
|
147
|
+
"toolbar.separator": f"{toolbar_base} {palette.toolbar_separator}",
|
|
148
|
+
"toolbar.help": f"{toolbar_base} {palette.toolbar_metadata}",
|
|
149
|
+
"toolbar.title": f"{toolbar_base} {palette.toolbar_identity}",
|
|
150
|
+
"toolbar.identity": f"{toolbar_base} {palette.toolbar_identity}",
|
|
151
|
+
"toolbar.active": f"{toolbar_base} {palette.toolbar_active}",
|
|
152
|
+
"toolbar.loaded": f"{toolbar_base} {palette.toolbar_loaded}",
|
|
153
|
+
"toolbar.metadata": f"{toolbar_base} {palette.toolbar_metadata}",
|
|
154
|
+
"bottom-toolbar": f"{toolbar_base} {palette.toolbar_foreground}",
|
|
151
155
|
}
|
|
152
156
|
)
|
|
153
157
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from prompt_toolkit.formatted_text import StyleAndTextTuples
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
from deepy.ui.styles import DARK_PALETTE, UiPalette
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
FooterSegmentRole = Literal["identity", "active", "loaded", "metadata", "context"]
|
|
13
|
+
FooterPartRole = Literal["title", "loaded", "active", "metadata", "context"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class StatusFooterSegment:
|
|
18
|
+
text: str
|
|
19
|
+
role: FooterSegmentRole = "metadata"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class StatusFooter:
|
|
24
|
+
segments: tuple[StatusFooterSegment, ...]
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def plain(self) -> str:
|
|
28
|
+
return " · ".join(segment.text for segment in self.segments if segment.text)
|
|
29
|
+
|
|
30
|
+
def with_active(self, active_work: str | None) -> "StatusFooter":
|
|
31
|
+
active = (active_work or "").strip()
|
|
32
|
+
if not active:
|
|
33
|
+
return self
|
|
34
|
+
segments = [segment for segment in self.segments if segment.role != "active"]
|
|
35
|
+
insert_at = 1 if segments else 0
|
|
36
|
+
segments.insert(insert_at, StatusFooterSegment(active, "active"))
|
|
37
|
+
return StatusFooter(tuple(segments))
|
|
38
|
+
|
|
39
|
+
def to_prompt_toolkit(self, *, help_text: str = "") -> StyleAndTextTuples:
|
|
40
|
+
toolbar: StyleAndTextTuples = []
|
|
41
|
+
for index, segment in enumerate(segment for segment in self.segments if segment.text):
|
|
42
|
+
if index:
|
|
43
|
+
toolbar.append(("class:toolbar.separator", " · "))
|
|
44
|
+
for role, text in _segment_parts(segment):
|
|
45
|
+
toolbar.append((f"class:toolbar.{role}", text))
|
|
46
|
+
if help_text:
|
|
47
|
+
if toolbar:
|
|
48
|
+
toolbar.append(("class:toolbar.separator", " · "))
|
|
49
|
+
for role, text in _help_parts(help_text):
|
|
50
|
+
toolbar.append((f"class:toolbar.{role}", text))
|
|
51
|
+
return toolbar
|
|
52
|
+
|
|
53
|
+
def to_rich_text(self, palette: UiPalette | None = None) -> Text:
|
|
54
|
+
palette = palette or DARK_PALETTE
|
|
55
|
+
text = Text()
|
|
56
|
+
for index, segment in enumerate(segment for segment in self.segments if segment.text):
|
|
57
|
+
if index:
|
|
58
|
+
text.append(" · ", style=palette.toolbar_separator)
|
|
59
|
+
for role, value in _segment_parts(segment):
|
|
60
|
+
text.append(value, style=_rich_style_for_role(role, palette))
|
|
61
|
+
return text
|
|
62
|
+
|
|
63
|
+
def __str__(self) -> str:
|
|
64
|
+
return self.plain
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _segment_parts(segment: StatusFooterSegment) -> list[tuple[FooterPartRole, str]]:
|
|
68
|
+
if segment.role == "loaded" and segment.text.startswith("[") and segment.text.endswith("]"):
|
|
69
|
+
return [("loaded", segment.text)]
|
|
70
|
+
title = _known_title(segment.text)
|
|
71
|
+
if title is None:
|
|
72
|
+
return [(_part_role_for_segment(segment.role), segment.text)]
|
|
73
|
+
rest = segment.text[len(title) :]
|
|
74
|
+
role = "context" if segment.role == "context" else "metadata"
|
|
75
|
+
return [("title", title), (role, rest)]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _help_parts(help_text: str) -> list[tuple[FooterPartRole, str]]:
|
|
79
|
+
title = _known_title(help_text)
|
|
80
|
+
if title is None:
|
|
81
|
+
return [("metadata", help_text)]
|
|
82
|
+
return [("title", title), ("metadata", help_text[len(title) :])]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _known_title(text: str) -> str | None:
|
|
86
|
+
for title in ("model", "cwd", "mcp", "ctx", "newline"):
|
|
87
|
+
if text == title or text.startswith(f"{title} ") or text.startswith(f"{title}:"):
|
|
88
|
+
return title
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _part_role_for_segment(role: FooterSegmentRole) -> FooterPartRole:
|
|
93
|
+
if role == "active":
|
|
94
|
+
return "active"
|
|
95
|
+
if role == "loaded":
|
|
96
|
+
return "loaded"
|
|
97
|
+
if role == "context":
|
|
98
|
+
return "context"
|
|
99
|
+
return "metadata"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _rich_style_for_role(role: FooterPartRole, palette: UiPalette) -> str:
|
|
103
|
+
if role == "title":
|
|
104
|
+
return _rich_style(palette.toolbar_identity)
|
|
105
|
+
if role == "loaded":
|
|
106
|
+
return _rich_style(palette.toolbar_loaded)
|
|
107
|
+
if role == "active":
|
|
108
|
+
return _rich_style(palette.toolbar_active)
|
|
109
|
+
if role == "context":
|
|
110
|
+
return _rich_style(palette.toolbar_context)
|
|
111
|
+
return _rich_style(palette.toolbar_metadata)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _rich_style(style: str) -> str:
|
|
115
|
+
if style.endswith(" bold"):
|
|
116
|
+
return f"bold {style.removesuffix(' bold')}"
|
|
117
|
+
return style
|
|
@@ -43,6 +43,10 @@ class UiPalette:
|
|
|
43
43
|
toolbar_foreground: str
|
|
44
44
|
toolbar_context: str
|
|
45
45
|
toolbar_separator: str
|
|
46
|
+
toolbar_identity: str
|
|
47
|
+
toolbar_active: str
|
|
48
|
+
toolbar_loaded: str
|
|
49
|
+
toolbar_metadata: str
|
|
46
50
|
markdown_heading: str
|
|
47
51
|
markdown_subheading: str
|
|
48
52
|
markdown_bullet: str
|
|
@@ -78,9 +82,13 @@ DARK_PALETTE = UiPalette(
|
|
|
78
82
|
prompt="ansicyan bold",
|
|
79
83
|
placeholder="#8a90aa",
|
|
80
84
|
toolbar_background="#161821",
|
|
81
|
-
toolbar_foreground="#
|
|
82
|
-
toolbar_context="#
|
|
85
|
+
toolbar_foreground="#b7bdd4",
|
|
86
|
+
toolbar_context="#b7bdd4",
|
|
83
87
|
toolbar_separator="#4b5068",
|
|
88
|
+
toolbar_identity="bold #b7bdd4",
|
|
89
|
+
toolbar_active="bold #b7bdd4",
|
|
90
|
+
toolbar_loaded="bold #b7bdd4",
|
|
91
|
+
toolbar_metadata="#b7bdd4",
|
|
84
92
|
markdown_heading="bold bright_cyan",
|
|
85
93
|
markdown_subheading="bold cyan",
|
|
86
94
|
markdown_bullet="bright_blue",
|
|
@@ -115,10 +123,14 @@ LIGHT_PALETTE = UiPalette(
|
|
|
115
123
|
diff_context="#374151",
|
|
116
124
|
prompt="#0369a1 bold",
|
|
117
125
|
placeholder="#64748b",
|
|
118
|
-
toolbar_background="#
|
|
119
|
-
toolbar_foreground="#
|
|
120
|
-
toolbar_context="#
|
|
121
|
-
toolbar_separator="#
|
|
126
|
+
toolbar_background="#d8d8f2",
|
|
127
|
+
toolbar_foreground="#334155",
|
|
128
|
+
toolbar_context="#334155",
|
|
129
|
+
toolbar_separator="#94a3b8",
|
|
130
|
+
toolbar_identity="bold #334155",
|
|
131
|
+
toolbar_active="bold #334155",
|
|
132
|
+
toolbar_loaded="bold #334155",
|
|
133
|
+
toolbar_metadata="#334155",
|
|
122
134
|
markdown_heading="bold #0f766e",
|
|
123
135
|
markdown_subheading="bold #0369a1",
|
|
124
136
|
markdown_bullet="#2563eb",
|
|
@@ -67,7 +67,6 @@ from deepy.ui.local_command import (
|
|
|
67
67
|
shell_tool_result_json,
|
|
68
68
|
)
|
|
69
69
|
from deepy.ui.message_view import (
|
|
70
|
-
build_thinking_summary,
|
|
71
70
|
format_tool_display_label,
|
|
72
71
|
format_tool_call_summary,
|
|
73
72
|
format_tool_progress_summary,
|
|
@@ -91,6 +90,7 @@ from deepy.ui.skill_picker import (
|
|
|
91
90
|
show_skill_detail_view,
|
|
92
91
|
)
|
|
93
92
|
from deepy.ui.slash_commands import build_slash_commands
|
|
93
|
+
from deepy.ui.status_footer import StatusFooter, StatusFooterSegment
|
|
94
94
|
from deepy.ui.styles import (
|
|
95
95
|
DARK_PALETTE,
|
|
96
96
|
UiPalette,
|
|
@@ -167,7 +167,7 @@ def run_interactive(
|
|
|
167
167
|
|
|
168
168
|
loaded_skill_names: list[str] = []
|
|
169
169
|
ctrl_d_exit_pending = False
|
|
170
|
-
|
|
170
|
+
context_footer = _build_status_footer(
|
|
171
171
|
session_id,
|
|
172
172
|
project_root=root,
|
|
173
173
|
settings=settings,
|
|
@@ -176,7 +176,7 @@ def run_interactive(
|
|
|
176
176
|
async_runner = asyncio.Runner()
|
|
177
177
|
mcp_runtime = DeepyMcpRuntime(settings, project_root=root)
|
|
178
178
|
async_runner.run(mcp_runtime.connect())
|
|
179
|
-
|
|
179
|
+
context_footer = _build_status_footer(
|
|
180
180
|
session_id,
|
|
181
181
|
project_root=root,
|
|
182
182
|
settings=settings,
|
|
@@ -202,7 +202,7 @@ def run_interactive(
|
|
|
202
202
|
try:
|
|
203
203
|
text = prompt_for_input(
|
|
204
204
|
prompt_session,
|
|
205
|
-
bottom_toolbar=build_prompt_toolbar(
|
|
205
|
+
bottom_toolbar=build_prompt_toolbar(context_footer),
|
|
206
206
|
)
|
|
207
207
|
except EOFError:
|
|
208
208
|
if ctrl_d_exit_pending:
|
|
@@ -236,8 +236,9 @@ def run_interactive(
|
|
|
236
236
|
session_id,
|
|
237
237
|
settings=settings,
|
|
238
238
|
palette=palette,
|
|
239
|
+
mcp_runtime=mcp_runtime,
|
|
239
240
|
)
|
|
240
|
-
|
|
241
|
+
context_footer = _build_status_footer(
|
|
241
242
|
session_id,
|
|
242
243
|
project_root=root,
|
|
243
244
|
settings=settings,
|
|
@@ -295,7 +296,7 @@ def run_interactive(
|
|
|
295
296
|
session_id = summary.session_id
|
|
296
297
|
_print_assistant_output(output, summary.output, palette=palette)
|
|
297
298
|
_print_usage_footer(output, summary, settings=settings, project_root=root, palette=palette)
|
|
298
|
-
|
|
299
|
+
context_footer = _build_status_footer(
|
|
299
300
|
summary.session_id,
|
|
300
301
|
project_root=root,
|
|
301
302
|
settings=settings,
|
|
@@ -319,7 +320,7 @@ def run_interactive(
|
|
|
319
320
|
session_id = summary.session_id
|
|
320
321
|
_print_assistant_output(output, summary.output, palette=palette)
|
|
321
322
|
_print_usage_footer(output, summary, settings=settings, project_root=root, palette=palette)
|
|
322
|
-
|
|
323
|
+
context_footer = _build_status_footer(
|
|
323
324
|
summary.session_id,
|
|
324
325
|
project_root=root,
|
|
325
326
|
settings=settings,
|
|
@@ -344,8 +345,8 @@ def run_interactive(
|
|
|
344
345
|
if slash.name in {"skills", "theme", "reset", "model"}:
|
|
345
346
|
prompt_session = _create_interactive_prompt_session(root, palette, loaded_skill_names)
|
|
346
347
|
session_id = next_session
|
|
347
|
-
if slash.name in {"new", "resume", "reset", "model", "compact"}:
|
|
348
|
-
|
|
348
|
+
if slash.name in {"new", "resume", "reset", "model", "compact", "skills", "theme", "mcp"}:
|
|
349
|
+
context_footer = _build_status_footer(
|
|
349
350
|
session_id,
|
|
350
351
|
project_root=root,
|
|
351
352
|
settings=settings,
|
|
@@ -394,7 +395,7 @@ def run_interactive(
|
|
|
394
395
|
session_id = summary.session_id
|
|
395
396
|
_print_assistant_output(output, summary.output, palette=palette)
|
|
396
397
|
_print_usage_footer(output, summary, settings=settings, project_root=root, palette=palette)
|
|
397
|
-
|
|
398
|
+
context_footer = _build_status_footer(
|
|
398
399
|
summary.session_id,
|
|
399
400
|
project_root=root,
|
|
400
401
|
settings=settings,
|
|
@@ -465,6 +466,13 @@ def _run_once_with_status(
|
|
|
465
466
|
original_should_interrupt = kwargs.pop("should_interrupt", None)
|
|
466
467
|
project_root = kwargs.get("project_root")
|
|
467
468
|
project_root_text = str(project_root) if project_root is not None else None
|
|
469
|
+
settings = kwargs.get("settings")
|
|
470
|
+
footer = _build_status_footer(
|
|
471
|
+
kwargs.get("session_id"),
|
|
472
|
+
project_root=project_root if isinstance(project_root, Path) else None,
|
|
473
|
+
settings=settings if isinstance(settings, Settings) else None,
|
|
474
|
+
mcp_runtime=kwargs.get("mcp_runtime"),
|
|
475
|
+
)
|
|
468
476
|
renderer: TerminalStreamRenderer | None = None
|
|
469
477
|
started_at = time.monotonic()
|
|
470
478
|
interrupt_requested = threading.Event()
|
|
@@ -479,13 +487,19 @@ def _run_once_with_status(
|
|
|
479
487
|
kwargs["should_interrupt"] = should_interrupt
|
|
480
488
|
|
|
481
489
|
active_palette = palette if isinstance(palette, UiPalette) else DARK_PALETTE
|
|
482
|
-
with
|
|
490
|
+
with _status_display(
|
|
491
|
+
console,
|
|
492
|
+
_working_status_text(started_at, palette=active_palette, footer=footer),
|
|
493
|
+
footer.to_rich_text(active_palette) if footer.segments else None,
|
|
494
|
+
palette=active_palette,
|
|
495
|
+
) as status:
|
|
483
496
|
renderer = TerminalStreamRenderer(
|
|
484
497
|
console,
|
|
485
498
|
project_root=project_root_text,
|
|
486
499
|
status=status,
|
|
487
500
|
status_started_at=started_at,
|
|
488
501
|
palette=active_palette,
|
|
502
|
+
footer=footer,
|
|
489
503
|
)
|
|
490
504
|
stop_status_refresh = threading.Event()
|
|
491
505
|
status_thread = threading.Thread(
|
|
@@ -523,6 +537,7 @@ def _handle_local_command(
|
|
|
523
537
|
*,
|
|
524
538
|
settings: Settings,
|
|
525
539
|
palette: UiPalette | None = None,
|
|
540
|
+
mcp_runtime: DeepyMcpRuntime | None = None,
|
|
526
541
|
) -> str | None:
|
|
527
542
|
palette = palette or resolve_ui_palette(settings.ui.theme)
|
|
528
543
|
if not command_input.command:
|
|
@@ -532,9 +547,27 @@ def _handle_local_command(
|
|
|
532
547
|
_print_user_input(console, command_input.raw_text, palette=palette)
|
|
533
548
|
started_at = time.monotonic()
|
|
534
549
|
interrupt_requested = threading.Event()
|
|
535
|
-
with
|
|
536
|
-
|
|
537
|
-
|
|
550
|
+
with _status_display(
|
|
551
|
+
console,
|
|
552
|
+
_local_command_status_text(
|
|
553
|
+
command_input.command,
|
|
554
|
+
started_at,
|
|
555
|
+
palette=palette,
|
|
556
|
+
footer=_build_status_footer(
|
|
557
|
+
current_session_id,
|
|
558
|
+
project_root=project_root,
|
|
559
|
+
settings=settings,
|
|
560
|
+
mcp_runtime=mcp_runtime,
|
|
561
|
+
active_work="running local command",
|
|
562
|
+
),
|
|
563
|
+
),
|
|
564
|
+
_build_status_footer(
|
|
565
|
+
current_session_id,
|
|
566
|
+
project_root=project_root,
|
|
567
|
+
settings=settings,
|
|
568
|
+
mcp_runtime=mcp_runtime,
|
|
569
|
+
).to_rich_text(palette),
|
|
570
|
+
palette=palette,
|
|
538
571
|
):
|
|
539
572
|
with _esc_interrupt_watcher(interrupt_requested):
|
|
540
573
|
result = run_local_command(
|
|
@@ -666,11 +699,13 @@ class TerminalStreamRenderer:
|
|
|
666
699
|
status: Any | None = None,
|
|
667
700
|
status_started_at: float | None = None,
|
|
668
701
|
palette: UiPalette | None = None,
|
|
702
|
+
footer: StatusFooter | None = None,
|
|
669
703
|
) -> None:
|
|
670
704
|
self.console = console
|
|
671
705
|
self.project_root = project_root
|
|
672
706
|
self.status = status
|
|
673
707
|
self.palette = palette or DARK_PALETTE
|
|
708
|
+
self.footer = footer
|
|
674
709
|
self.status_started_at = (
|
|
675
710
|
status_started_at if status_started_at is not None else time.monotonic()
|
|
676
711
|
)
|
|
@@ -700,15 +735,14 @@ class TerminalStreamRenderer:
|
|
|
700
735
|
),
|
|
701
736
|
)
|
|
702
737
|
self.reasoning_started = True
|
|
703
|
-
self.reasoning_buffer
|
|
704
|
-
self.
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
self.update_status(f"Thinking {summary}")
|
|
738
|
+
self.reasoning_buffer = "printed"
|
|
739
|
+
self.console.print(Text(text, style=self.palette.muted), end="")
|
|
740
|
+
if self.status is not None and self.status_detail != "thinking":
|
|
741
|
+
self.update_status("thinking")
|
|
708
742
|
|
|
709
743
|
def set_tool_status(self, summary: str) -> None:
|
|
710
744
|
if self.status is not None and summary:
|
|
711
|
-
self.update_status(f"
|
|
745
|
+
self.update_status(f"tool {summary}")
|
|
712
746
|
|
|
713
747
|
def update_status(self, detail: str | None = None) -> None:
|
|
714
748
|
if detail is not None:
|
|
@@ -719,23 +753,16 @@ class TerminalStreamRenderer:
|
|
|
719
753
|
self.status_started_at,
|
|
720
754
|
self.status_detail,
|
|
721
755
|
palette=self.palette,
|
|
756
|
+
footer=self.footer,
|
|
722
757
|
)
|
|
723
758
|
)
|
|
724
759
|
|
|
725
760
|
def flush(self) -> None:
|
|
726
|
-
self.
|
|
761
|
+
if self.reasoning_buffer:
|
|
762
|
+
self.console.print()
|
|
727
763
|
self.reasoning_started = False
|
|
728
764
|
self.reasoning_buffer = ""
|
|
729
765
|
|
|
730
|
-
def _print_stable_reasoning(self, *, force: bool = False) -> None:
|
|
731
|
-
text, self.reasoning_buffer = _split_stable_reasoning_text(
|
|
732
|
-
self.reasoning_buffer,
|
|
733
|
-
force=force,
|
|
734
|
-
)
|
|
735
|
-
if text:
|
|
736
|
-
self.console.print(Text(text.rstrip("\n"), style=self.palette.muted))
|
|
737
|
-
|
|
738
|
-
|
|
739
766
|
def _handle_slash_command(
|
|
740
767
|
command: SlashCommand,
|
|
741
768
|
console: Console,
|
|
@@ -1856,32 +1883,55 @@ def _format_context_footer(
|
|
|
1856
1883
|
settings: Settings | None = None,
|
|
1857
1884
|
mcp_runtime: DeepyMcpRuntime | None = None,
|
|
1858
1885
|
) -> str:
|
|
1886
|
+
return _build_status_footer(
|
|
1887
|
+
session_id,
|
|
1888
|
+
project_root=project_root,
|
|
1889
|
+
settings=settings,
|
|
1890
|
+
mcp_runtime=mcp_runtime,
|
|
1891
|
+
).plain
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
def _build_status_footer(
|
|
1895
|
+
session_id: str | None,
|
|
1896
|
+
*,
|
|
1897
|
+
project_root: Path | None = None,
|
|
1898
|
+
settings: Settings | None = None,
|
|
1899
|
+
mcp_runtime: DeepyMcpRuntime | None = None,
|
|
1900
|
+
active_work: str | None = None,
|
|
1901
|
+
) -> StatusFooter:
|
|
1859
1902
|
if settings is None:
|
|
1860
|
-
return
|
|
1903
|
+
return StatusFooter(())
|
|
1861
1904
|
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1905
|
+
segments = [
|
|
1906
|
+
StatusFooterSegment(
|
|
1907
|
+
f"model {settings.model.name}[{settings.model.reasoning_mode}]",
|
|
1908
|
+
"identity",
|
|
1909
|
+
),
|
|
1865
1910
|
]
|
|
1866
1911
|
if project_root is not None:
|
|
1867
|
-
|
|
1912
|
+
segments.append(StatusFooterSegment(f"cwd {format_home_relative_path(project_root)}", "metadata"))
|
|
1868
1913
|
if has_agents_instructions(project_root):
|
|
1869
|
-
|
|
1914
|
+
segments.append(StatusFooterSegment("[AGENTS.md]", "loaded"))
|
|
1870
1915
|
if mcp_runtime is not None and mcp_runtime.active_servers:
|
|
1871
|
-
|
|
1916
|
+
segments.append(StatusFooterSegment(f"mcp {len(mcp_runtime.active_servers)}", "loaded"))
|
|
1872
1917
|
else:
|
|
1873
|
-
|
|
1918
|
+
segments.append(StatusFooterSegment("cwd unknown", "metadata"))
|
|
1874
1919
|
|
|
1875
1920
|
window_tokens = settings.context.window_tokens
|
|
1876
1921
|
compact_threshold = settings.context.resolved_compact_threshold
|
|
1877
1922
|
if window_tokens <= 0:
|
|
1878
|
-
|
|
1879
|
-
return
|
|
1923
|
+
segments.append(StatusFooterSegment("ctx unknown", "context"))
|
|
1924
|
+
return StatusFooter(tuple(segments)).with_active(active_work)
|
|
1880
1925
|
|
|
1881
1926
|
session_entry = _session_entry(project_root, session_id)
|
|
1882
|
-
|
|
1927
|
+
segments.append(
|
|
1928
|
+
StatusFooterSegment(
|
|
1929
|
+
_format_context_window_status(session_entry, window_tokens, compact_threshold),
|
|
1930
|
+
"context",
|
|
1931
|
+
)
|
|
1932
|
+
)
|
|
1883
1933
|
|
|
1884
|
-
return
|
|
1934
|
+
return StatusFooter(tuple(segments)).with_active(active_work)
|
|
1885
1935
|
|
|
1886
1936
|
|
|
1887
1937
|
def _format_context_window_status(
|
|
@@ -1901,11 +1951,11 @@ def _format_context_window_status(
|
|
|
1901
1951
|
)
|
|
1902
1952
|
used_tokens = usage.used_tokens if usage is not None else None
|
|
1903
1953
|
if used_tokens is None:
|
|
1904
|
-
return f"ctx
|
|
1954
|
+
return f"ctx unknown/{window_text}"
|
|
1905
1955
|
remaining_tokens = max(window_tokens - used_tokens, 0)
|
|
1906
1956
|
percentage = used_tokens / window_tokens * 100
|
|
1907
1957
|
status = (
|
|
1908
|
-
f"ctx
|
|
1958
|
+
f"ctx {_format_token_count_short(used_tokens)}/{window_text} "
|
|
1909
1959
|
f"({percentage:.1f}%, {_format_token_count_short(remaining_tokens)} left)"
|
|
1910
1960
|
)
|
|
1911
1961
|
if compact_threshold > 0 and used_tokens >= compact_threshold:
|
|
@@ -1947,18 +1997,209 @@ def _refresh_working_status(
|
|
|
1947
1997
|
renderer: TerminalStreamRenderer,
|
|
1948
1998
|
stop_event: threading.Event,
|
|
1949
1999
|
) -> None:
|
|
1950
|
-
while not stop_event.wait(
|
|
2000
|
+
while not stop_event.wait(0.2):
|
|
1951
2001
|
renderer.update_status()
|
|
1952
2002
|
|
|
1953
2003
|
|
|
2004
|
+
@contextlib.contextmanager
|
|
2005
|
+
def _status_display(
|
|
2006
|
+
console: Console,
|
|
2007
|
+
initial_status: Text,
|
|
2008
|
+
footer_status: Text | None = None,
|
|
2009
|
+
*,
|
|
2010
|
+
palette: UiPalette,
|
|
2011
|
+
):
|
|
2012
|
+
if _should_use_bottom_status_overlay(console):
|
|
2013
|
+
status = _TerminalBottomStatus(console, footer_status=footer_status, palette=palette)
|
|
2014
|
+
status.start()
|
|
2015
|
+
status.update(initial_status)
|
|
2016
|
+
try:
|
|
2017
|
+
yield status
|
|
2018
|
+
finally:
|
|
2019
|
+
status.clear()
|
|
2020
|
+
return
|
|
2021
|
+
|
|
2022
|
+
yield _SilentStatus()
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
def _should_use_bottom_status_overlay(console: Console) -> bool:
|
|
2026
|
+
isatty = getattr(console.file, "isatty", None)
|
|
2027
|
+
return bool(callable(isatty) and isatty())
|
|
2028
|
+
|
|
2029
|
+
|
|
2030
|
+
class _TerminalBottomStatus:
|
|
2031
|
+
def __init__(
|
|
2032
|
+
self,
|
|
2033
|
+
console: Console,
|
|
2034
|
+
*,
|
|
2035
|
+
footer_status: Text | None,
|
|
2036
|
+
palette: UiPalette,
|
|
2037
|
+
) -> None:
|
|
2038
|
+
self.console = console
|
|
2039
|
+
self.footer_status = footer_status
|
|
2040
|
+
self.palette = palette
|
|
2041
|
+
self.rows = 0
|
|
2042
|
+
self.columns = 0
|
|
2043
|
+
|
|
2044
|
+
@property
|
|
2045
|
+
def _reserved_lines(self) -> int:
|
|
2046
|
+
return 2 if self.footer_status is not None else 1
|
|
2047
|
+
|
|
2048
|
+
def start(self) -> None:
|
|
2049
|
+
self.columns, self.rows = shutil.get_terminal_size((80, 24))
|
|
2050
|
+
if self.rows <= self._reserved_lines:
|
|
2051
|
+
return
|
|
2052
|
+
scroll_bottom = self.rows - self._reserved_lines
|
|
2053
|
+
self.console.file.write(f"\x1b7\x1b[1;{scroll_bottom}r\x1b[{scroll_bottom};1H\x1b8")
|
|
2054
|
+
self._write_footer()
|
|
2055
|
+
self.console.file.flush()
|
|
2056
|
+
|
|
2057
|
+
def update(self, status: Text) -> None:
|
|
2058
|
+
columns, rows = shutil.get_terminal_size((80, 24))
|
|
2059
|
+
self.columns = columns
|
|
2060
|
+
self.rows = rows
|
|
2061
|
+
if rows <= self._reserved_lines:
|
|
2062
|
+
return
|
|
2063
|
+
runtime_row = rows - self._reserved_lines + 1
|
|
2064
|
+
self._write_line(runtime_row, status.plain, _terminal_runtime_status_style(self.palette))
|
|
2065
|
+
self._write_footer()
|
|
2066
|
+
self.console.file.flush()
|
|
2067
|
+
|
|
2068
|
+
def clear(self) -> None:
|
|
2069
|
+
columns, rows = shutil.get_terminal_size((80, 24))
|
|
2070
|
+
self.columns = columns
|
|
2071
|
+
self.rows = rows
|
|
2072
|
+
if rows <= self._reserved_lines:
|
|
2073
|
+
return
|
|
2074
|
+
self.console.file.write("\x1b7\x1b[r")
|
|
2075
|
+
for row in range(rows - self._reserved_lines + 1, rows + 1):
|
|
2076
|
+
self.console.file.write(f"\x1b[{row};1H\x1b[2K")
|
|
2077
|
+
self.console.file.write("\x1b8")
|
|
2078
|
+
self.console.file.flush()
|
|
2079
|
+
|
|
2080
|
+
def _write_footer(self) -> None:
|
|
2081
|
+
if self.footer_status is None or self.rows <= self._reserved_lines:
|
|
2082
|
+
return
|
|
2083
|
+
self._write_text_line(self.rows, self.footer_status)
|
|
2084
|
+
|
|
2085
|
+
def _write_line(self, row: int, text: str, style: str) -> None:
|
|
2086
|
+
width = max(self.columns - 1, 1)
|
|
2087
|
+
line = _truncate_status_line(text, max_width=width)
|
|
2088
|
+
padded = line.ljust(width)
|
|
2089
|
+
self.console.file.write(f"\x1b7\x1b[{row};1H\x1b[2K{style}{padded}\x1b[0m\x1b8")
|
|
2090
|
+
|
|
2091
|
+
def _write_text_line(self, row: int, text: Text) -> None:
|
|
2092
|
+
width = max(self.columns - 1, 1)
|
|
2093
|
+
plain = text.plain
|
|
2094
|
+
truncated = len(plain) > width
|
|
2095
|
+
body_width = max(width - 1, 0) if truncated else min(len(plain), width)
|
|
2096
|
+
background = _hex_color(self.palette.toolbar_background)
|
|
2097
|
+
default_style = _terminal_text_style(self.palette.toolbar_metadata, background=background)
|
|
2098
|
+
|
|
2099
|
+
self.console.file.write(f"\x1b7\x1b[{row};1H\x1b[2K")
|
|
2100
|
+
cursor = 0
|
|
2101
|
+
for span in sorted(text.spans, key=lambda item: item.start):
|
|
2102
|
+
start = max(span.start, 0)
|
|
2103
|
+
end = min(span.end, body_width)
|
|
2104
|
+
if end <= cursor or start >= body_width:
|
|
2105
|
+
continue
|
|
2106
|
+
if start > cursor:
|
|
2107
|
+
self.console.file.write(f"{default_style}{plain[cursor:start]}")
|
|
2108
|
+
self.console.file.write(
|
|
2109
|
+
f"{_terminal_text_style(str(span.style), background=background)}{plain[start:end]}"
|
|
2110
|
+
)
|
|
2111
|
+
cursor = end
|
|
2112
|
+
if cursor < body_width:
|
|
2113
|
+
self.console.file.write(f"{default_style}{plain[cursor:body_width]}")
|
|
2114
|
+
if truncated and width > 0:
|
|
2115
|
+
self.console.file.write(f"{default_style}…")
|
|
2116
|
+
padding = width - len(_truncate_status_line(plain, max_width=width))
|
|
2117
|
+
if padding > 0:
|
|
2118
|
+
self.console.file.write(f"{default_style}{' ' * padding}")
|
|
2119
|
+
self.console.file.write("\x1b[0m\x1b8")
|
|
2120
|
+
|
|
2121
|
+
|
|
2122
|
+
class _SilentStatus:
|
|
2123
|
+
def update(self, status: Text) -> None:
|
|
2124
|
+
return None
|
|
2125
|
+
|
|
2126
|
+
|
|
2127
|
+
def _terminal_status_style(palette: UiPalette) -> str:
|
|
2128
|
+
return _terminal_footer_status_style(palette)
|
|
2129
|
+
|
|
2130
|
+
|
|
2131
|
+
def _terminal_footer_status_style(palette: UiPalette) -> str:
|
|
2132
|
+
foreground = _hex_color(palette.toolbar_metadata)
|
|
2133
|
+
background = _hex_color(palette.toolbar_background)
|
|
2134
|
+
return _terminal_ansi_style(foreground=foreground, background=background)
|
|
2135
|
+
|
|
2136
|
+
|
|
2137
|
+
def _terminal_runtime_status_style(palette: UiPalette) -> str:
|
|
2138
|
+
foreground = _hex_color(palette.toolbar_background) or "#161821"
|
|
2139
|
+
background = _hex_color(palette.warning) or "#facc15"
|
|
2140
|
+
return _terminal_ansi_style(foreground=foreground, background=background, bold=True)
|
|
2141
|
+
|
|
2142
|
+
|
|
2143
|
+
def _terminal_text_style(style: str, *, background: str = "") -> str:
|
|
2144
|
+
parts = style.split()
|
|
2145
|
+
foreground = _hex_color(style)
|
|
2146
|
+
return _terminal_ansi_style(
|
|
2147
|
+
foreground=foreground,
|
|
2148
|
+
background=background,
|
|
2149
|
+
bold="bold" in parts,
|
|
2150
|
+
)
|
|
2151
|
+
|
|
2152
|
+
|
|
2153
|
+
def _terminal_ansi_style(
|
|
2154
|
+
*,
|
|
2155
|
+
foreground: str = "",
|
|
2156
|
+
background: str = "",
|
|
2157
|
+
bold: bool = False,
|
|
2158
|
+
) -> str:
|
|
2159
|
+
codes: list[str] = []
|
|
2160
|
+
codes.append("1" if bold else "22")
|
|
2161
|
+
if foreground:
|
|
2162
|
+
codes.append(_ansi_rgb("38", foreground))
|
|
2163
|
+
if background:
|
|
2164
|
+
codes.append(_ansi_rgb("48", background))
|
|
2165
|
+
return f"\x1b[{';'.join(codes)}m" if codes else ""
|
|
2166
|
+
|
|
2167
|
+
|
|
2168
|
+
def _hex_color(style: str) -> str:
|
|
2169
|
+
return next((part for part in style.split() if part.startswith("#") and len(part) == 7), "")
|
|
2170
|
+
|
|
2171
|
+
|
|
2172
|
+
def _ansi_rgb(prefix: str, color: str) -> str:
|
|
2173
|
+
red = int(color[1:3], 16)
|
|
2174
|
+
green = int(color[3:5], 16)
|
|
2175
|
+
blue = int(color[5:7], 16)
|
|
2176
|
+
return f"{prefix};2;{red};{green};{blue}"
|
|
2177
|
+
|
|
2178
|
+
|
|
2179
|
+
def _truncate_status_line(text: str, *, max_width: int) -> str:
|
|
2180
|
+
if len(text) <= max_width:
|
|
2181
|
+
return text
|
|
2182
|
+
if max_width <= 1:
|
|
2183
|
+
return text[:max_width]
|
|
2184
|
+
return f"{text[: max_width - 1]}…"
|
|
2185
|
+
|
|
2186
|
+
|
|
1954
2187
|
def _working_status_text(
|
|
1955
2188
|
started_at: float,
|
|
1956
2189
|
detail: str = "",
|
|
1957
2190
|
*,
|
|
1958
2191
|
palette: UiPalette | None = None,
|
|
2192
|
+
footer: StatusFooter | None = None,
|
|
1959
2193
|
) -> Text:
|
|
1960
2194
|
palette = palette or DARK_PALETTE
|
|
1961
2195
|
elapsed = _format_duration_ms(int((time.monotonic() - started_at) * 1000)) or "0s"
|
|
2196
|
+
if footer is not None and footer.segments:
|
|
2197
|
+
return _runtime_status_text(
|
|
2198
|
+
elapsed=elapsed,
|
|
2199
|
+
detail=detail or "status working",
|
|
2200
|
+
spinner=_runtime_spinner_frame(started_at),
|
|
2201
|
+
palette=palette,
|
|
2202
|
+
)
|
|
1962
2203
|
text = Text.assemble(
|
|
1963
2204
|
("Working ", f"bold {palette.muted}"),
|
|
1964
2205
|
(f"({elapsed} · esc to interrupt)", palette.muted),
|
|
@@ -1974,15 +2215,63 @@ def _local_command_status_text(
|
|
|
1974
2215
|
started_at: float,
|
|
1975
2216
|
*,
|
|
1976
2217
|
palette: UiPalette | None = None,
|
|
2218
|
+
footer: StatusFooter | None = None,
|
|
1977
2219
|
) -> Text:
|
|
1978
2220
|
palette = palette or DARK_PALETTE
|
|
1979
2221
|
elapsed = _format_duration_ms(int((time.monotonic() - started_at) * 1000)) or "0s"
|
|
1980
|
-
|
|
2222
|
+
if footer is not None and footer.segments:
|
|
2223
|
+
text = _runtime_status_text(
|
|
2224
|
+
elapsed=elapsed,
|
|
2225
|
+
detail="local command",
|
|
2226
|
+
spinner=_runtime_spinner_frame(started_at),
|
|
2227
|
+
palette=palette,
|
|
2228
|
+
)
|
|
2229
|
+
text.append(" · ", style=palette.toolbar_separator)
|
|
2230
|
+
text.append(command, style=palette.toolbar_metadata)
|
|
2231
|
+
return text
|
|
2232
|
+
text = Text.assemble(
|
|
1981
2233
|
("Running local command ", f"bold {palette.muted}"),
|
|
1982
2234
|
(f"({elapsed})", palette.muted),
|
|
1983
|
-
(" · ", palette.muted),
|
|
1984
|
-
(command, palette.muted),
|
|
1985
2235
|
)
|
|
2236
|
+
text.append(" · ", style=palette.muted)
|
|
2237
|
+
text.append(command, style=palette.muted)
|
|
2238
|
+
return text
|
|
2239
|
+
|
|
2240
|
+
|
|
2241
|
+
def _runtime_status_text(
|
|
2242
|
+
*,
|
|
2243
|
+
elapsed: str,
|
|
2244
|
+
detail: str,
|
|
2245
|
+
spinner: str = "",
|
|
2246
|
+
palette: UiPalette,
|
|
2247
|
+
) -> Text:
|
|
2248
|
+
parts: list[tuple[str, str]] = []
|
|
2249
|
+
style = _runtime_text_style(palette)
|
|
2250
|
+
if spinner:
|
|
2251
|
+
parts.extend([(spinner, style), (" ", style)])
|
|
2252
|
+
parts.extend(
|
|
2253
|
+
[
|
|
2254
|
+
("time ", style),
|
|
2255
|
+
(elapsed, style),
|
|
2256
|
+
(" · ", style),
|
|
2257
|
+
("esc to interrupt", style),
|
|
2258
|
+
]
|
|
2259
|
+
)
|
|
2260
|
+
text = Text.assemble(*parts)
|
|
2261
|
+
if detail:
|
|
2262
|
+
text.append(" · ", style=style)
|
|
2263
|
+
text.append(detail, style=style)
|
|
2264
|
+
return text
|
|
2265
|
+
|
|
2266
|
+
|
|
2267
|
+
def _runtime_text_style(palette: UiPalette) -> str:
|
|
2268
|
+
return f"bold {palette.toolbar_background}"
|
|
2269
|
+
|
|
2270
|
+
|
|
2271
|
+
def _runtime_spinner_frame(started_at: float) -> str:
|
|
2272
|
+
frames = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
2273
|
+
index = int(max(0.0, time.monotonic() - started_at) * 10) % len(frames)
|
|
2274
|
+
return frames[index]
|
|
1986
2275
|
|
|
1987
2276
|
|
|
1988
2277
|
def _format_duration_ms(duration_ms: int) -> str:
|
|
@@ -2111,20 +2400,6 @@ def _format_tool_output_debug(output: str) -> str:
|
|
|
2111
2400
|
return json_utils.dumps_pretty(parsed)
|
|
2112
2401
|
|
|
2113
2402
|
|
|
2114
|
-
_REASONING_BUFFER_TARGET_CHARS = 180
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
def _split_stable_reasoning_text(text: str, *, force: bool = False) -> tuple[str, str]:
|
|
2118
|
-
if force:
|
|
2119
|
-
return text, ""
|
|
2120
|
-
newline_index = text.rfind("\n")
|
|
2121
|
-
if newline_index >= 0:
|
|
2122
|
-
return text[: newline_index + 1], text[newline_index + 1 :]
|
|
2123
|
-
if len(text) >= _REASONING_BUFFER_TARGET_CHARS:
|
|
2124
|
-
return text, ""
|
|
2125
|
-
return "", text
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
2403
|
def _status_line(text: str, style: str) -> Text:
|
|
2129
2404
|
label_match = re.match(r"(\[[^\]]+\])(\s?.*)", text, flags=re.DOTALL)
|
|
2130
2405
|
if label_match:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|