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.
Files changed (84) hide show
  1. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/PKG-INFO +1 -1
  2. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/pyproject.toml +1 -1
  3. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/__init__.py +1 -1
  4. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/prompt_input.py +17 -13
  5. deepy_cli-0.2.3/src/deepy/ui/status_footer.py +117 -0
  6. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/styles.py +18 -6
  7. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/terminal.py +337 -62
  8. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/README.md +0 -0
  9. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/__main__.py +0 -0
  10. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/cli.py +0 -0
  11. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/config/__init__.py +0 -0
  12. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/config/settings.py +0 -0
  13. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/__init__.py +0 -0
  14. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
  15. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
  16. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/AskUserQuestion.md +0 -0
  17. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/WebFetch.md +0 -0
  18. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/WebSearch.md +0 -0
  19. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/__init__.py +0 -0
  20. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/edit.md +0 -0
  21. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/modify.md +0 -0
  22. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/read.md +0 -0
  23. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/shell.md +0 -0
  24. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/data/tools/write.md +0 -0
  25. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/errors.py +0 -0
  26. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/__init__.py +0 -0
  27. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/agent.py +0 -0
  28. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/compaction.py +0 -0
  29. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/context.py +0 -0
  30. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/events.py +0 -0
  31. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/model_capabilities.py +0 -0
  32. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/provider.py +0 -0
  33. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/replay.py +0 -0
  34. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/runner.py +0 -0
  35. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/llm/thinking.py +0 -0
  36. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/mcp.py +0 -0
  37. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/__init__.py +0 -0
  38. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/compact.py +0 -0
  39. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/init_agents.py +0 -0
  40. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/rules.py +0 -0
  41. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/runtime_context.py +0 -0
  42. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/system.py +0 -0
  43. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/prompts/tool_docs.py +0 -0
  44. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/sessions/__init__.py +0 -0
  45. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/sessions/jsonl.py +0 -0
  46. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/sessions/manager.py +0 -0
  47. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/skill_market.py +0 -0
  48. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/skills.py +0 -0
  49. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/status.py +0 -0
  50. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/__init__.py +0 -0
  51. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/agents.py +0 -0
  52. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/builtin.py +0 -0
  53. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/file_state.py +0 -0
  54. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/result.py +0 -0
  55. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/shell_output.py +0 -0
  56. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/tools/shell_utils.py +0 -0
  57. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/types/__init__.py +0 -0
  58. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/types/sdk.py +0 -0
  59. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/types/tool_payloads.py +0 -0
  60. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/__init__.py +0 -0
  61. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/app.py +0 -0
  62. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/ask_user_question.py +0 -0
  63. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/exit_summary.py +0 -0
  64. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/file_mentions.py +0 -0
  65. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/loading_text.py +0 -0
  66. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/local_command.py +0 -0
  67. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/markdown.py +0 -0
  68. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/message_view.py +0 -0
  69. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/model_picker.py +0 -0
  70. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/prompt_buffer.py +0 -0
  71. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/session_list.py +0 -0
  72. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/session_picker.py +0 -0
  73. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/skill_picker.py +0 -0
  74. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/slash_commands.py +0 -0
  75. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/theme_picker.py +0 -0
  76. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/thinking_state.py +0 -0
  77. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/ui/welcome.py +0 -0
  78. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/update_check.py +0 -0
  79. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/usage.py +0 -0
  80. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/utils/__init__.py +0 -0
  81. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/utils/debug_logger.py +0 -0
  82. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/utils/error_logger.py +0 -0
  83. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/utils/json.py +0 -0
  84. {deepy_cli-0.2.2 → deepy_cli-0.2.3}/src/deepy/utils/notify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: deepy-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Deepy - Vibe coding for DeepSeek models in your terminal
5
5
  Keywords: deepseek,coding-agent,terminal,cli,agents
6
6
  Author: kirineko
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepy-cli"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Deepy - Vibe coding for DeepSeek models in your terminal"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.3"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -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 = "Ctrl+J newline · Ctrl+D twice exit"
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: StyleAndTextTuples = [
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"bg:{palette.toolbar_background} {palette.toolbar_foreground}",
147
- "toolbar.context": f"bg:{palette.toolbar_background} {palette.toolbar_context}",
148
- "toolbar.separator": f"bg:{palette.toolbar_background} {palette.toolbar_separator}",
149
- "toolbar.help": f"bg:{palette.toolbar_background} {palette.toolbar_foreground}",
150
- "bottom-toolbar": f"bg:{palette.toolbar_background} {palette.toolbar_foreground}",
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="#a6adc8",
82
- toolbar_context="#a6adc8",
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="#e2e8f0",
119
- toolbar_foreground="#0f172a",
120
- toolbar_context="#047857 bold",
121
- toolbar_separator="#64748b",
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
- context_status = _format_context_footer(
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
- context_status = _format_context_footer(
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(context_status),
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
- context_status = _format_context_footer(
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
- context_status = _format_context_footer(
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
- context_status = _format_context_footer(
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
- context_status = _format_context_footer(
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
- context_status = _format_context_footer(
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 console.status(_working_status_text(started_at, palette=active_palette), spinner="dots") as status:
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 console.status(
536
- _local_command_status_text(command_input.command, started_at, palette=palette),
537
- spinner="dots",
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 += text
704
- self._print_stable_reasoning()
705
- summary = build_thinking_summary(self.reasoning_buffer or text)
706
- if self.status is not None and summary:
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"Running {summary}")
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._print_stable_reasoning(force=True)
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
- parts = [
1863
- f"model {settings.model.name}",
1864
- f"thinking {settings.model.reasoning_mode}",
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
- parts.append(f"cwd {format_home_relative_path(project_root)}")
1912
+ segments.append(StatusFooterSegment(f"cwd {format_home_relative_path(project_root)}", "metadata"))
1868
1913
  if has_agents_instructions(project_root):
1869
- parts.append("AGENTS.md loaded")
1914
+ segments.append(StatusFooterSegment("[AGENTS.md]", "loaded"))
1870
1915
  if mcp_runtime is not None and mcp_runtime.active_servers:
1871
- parts.append(f"MCP {len(mcp_runtime.active_servers)} server(s)")
1916
+ segments.append(StatusFooterSegment(f"mcp {len(mcp_runtime.active_servers)}", "loaded"))
1872
1917
  else:
1873
- parts.append("cwd unknown")
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
- parts.append("context unknown")
1879
- return " · ".join(parts)
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
- parts.append(_format_context_window_status(session_entry, window_tokens, compact_threshold))
1927
+ segments.append(
1928
+ StatusFooterSegment(
1929
+ _format_context_window_status(session_entry, window_tokens, compact_threshold),
1930
+ "context",
1931
+ )
1932
+ )
1883
1933
 
1884
- return " · ".join(parts)
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 win unknown/{window_text}"
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 win {_format_token_count_short(used_tokens)}/{window_text} "
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(1):
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
- return Text.assemble(
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