deepy-cli 0.2.1__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 (85) hide show
  1. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/PKG-INFO +1 -1
  2. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/pyproject.toml +1 -1
  3. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/__init__.py +1 -1
  4. deepy_cli-0.2.3/src/deepy/data/tools/modify.md +26 -0
  5. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/system.py +1 -1
  6. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/agents.py +2 -1
  7. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/builtin.py +19 -1
  8. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/file_state.py +16 -0
  9. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/prompt_input.py +17 -13
  10. deepy_cli-0.2.3/src/deepy/ui/status_footer.py +117 -0
  11. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/styles.py +18 -6
  12. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/terminal.py +337 -62
  13. deepy_cli-0.2.1/src/deepy/data/tools/modify.md +0 -22
  14. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/README.md +0 -0
  15. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/__main__.py +0 -0
  16. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/cli.py +0 -0
  17. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/config/__init__.py +0 -0
  18. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/config/settings.py +0 -0
  19. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/__init__.py +0 -0
  20. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
  21. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
  22. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/AskUserQuestion.md +0 -0
  23. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/WebFetch.md +0 -0
  24. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/WebSearch.md +0 -0
  25. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/__init__.py +0 -0
  26. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/edit.md +0 -0
  27. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/read.md +0 -0
  28. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/shell.md +0 -0
  29. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/write.md +0 -0
  30. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/errors.py +0 -0
  31. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/__init__.py +0 -0
  32. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/agent.py +0 -0
  33. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/compaction.py +0 -0
  34. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/context.py +0 -0
  35. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/events.py +0 -0
  36. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/model_capabilities.py +0 -0
  37. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/provider.py +0 -0
  38. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/replay.py +0 -0
  39. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/runner.py +0 -0
  40. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/thinking.py +0 -0
  41. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/mcp.py +0 -0
  42. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/__init__.py +0 -0
  43. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/compact.py +0 -0
  44. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/init_agents.py +0 -0
  45. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/rules.py +0 -0
  46. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/runtime_context.py +0 -0
  47. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/tool_docs.py +0 -0
  48. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/sessions/__init__.py +0 -0
  49. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/sessions/jsonl.py +0 -0
  50. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/sessions/manager.py +0 -0
  51. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/skill_market.py +0 -0
  52. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/skills.py +0 -0
  53. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/status.py +0 -0
  54. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/__init__.py +0 -0
  55. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/result.py +0 -0
  56. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/shell_output.py +0 -0
  57. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/shell_utils.py +0 -0
  58. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/types/__init__.py +0 -0
  59. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/types/sdk.py +0 -0
  60. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/types/tool_payloads.py +0 -0
  61. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/__init__.py +0 -0
  62. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/app.py +0 -0
  63. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/ask_user_question.py +0 -0
  64. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/exit_summary.py +0 -0
  65. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/file_mentions.py +0 -0
  66. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/loading_text.py +0 -0
  67. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/local_command.py +0 -0
  68. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/markdown.py +0 -0
  69. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/message_view.py +0 -0
  70. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/model_picker.py +0 -0
  71. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/prompt_buffer.py +0 -0
  72. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/session_list.py +0 -0
  73. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/session_picker.py +0 -0
  74. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/skill_picker.py +0 -0
  75. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/slash_commands.py +0 -0
  76. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/theme_picker.py +0 -0
  77. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/thinking_state.py +0 -0
  78. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/welcome.py +0 -0
  79. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/update_check.py +0 -0
  80. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/usage.py +0 -0
  81. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/utils/__init__.py +0 -0
  82. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/utils/debug_logger.py +0 -0
  83. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/utils/error_logger.py +0 -0
  84. {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/utils/json.py +0 -0
  85. {deepy_cli-0.2.1 → 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.1
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.1"
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.1"
3
+ __version__ = "0.2.3"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -0,0 +1,26 @@
1
+ ## modify
2
+
3
+ Create new files or edit existing files.
4
+
5
+ Use `content` only when the target file does not exist. For existing files, use
6
+ `old_string` and `new_string` for the smallest reliable replacement. You may
7
+ attempt an exact replacement directly when you already know the current text;
8
+ Deepy records the required file snapshot internally when no prior snapshot
9
+ exists. Read first when you need to inspect context. Do not rewrite an existing
10
+ scaffolded file with full content; replace the specific generated block instead.
11
+
12
+ Args for new files: `file_path`, `content`.
13
+
14
+ Args for existing files: `file_path`, `old_string`, `new_string`, optional
15
+ `replace_all`, optional `snippet_id`.
16
+
17
+ Existing-file edits must have a managed snapshot before Deepy commits changes;
18
+ `modify` can create that snapshot internally for direct exact replacements. Stale
19
+ edits are rejected. Repeated matches are rejected unless `replace_all` is true;
20
+ candidate snippets can be reused with `snippet_id`. Success includes diff
21
+ metadata.
22
+
23
+ If several `old_string` attempts fail and you know the complete desired file content,
24
+ re-read the file and use the managed whole-file replacement path. Do not delete the file
25
+ and recreate it with shell commands or here-strings; that bypasses Deepy's encoding,
26
+ newline, and stale-write protections, especially on Windows.
@@ -45,7 +45,7 @@ def build_system_prompt(
45
45
  Core rules:
46
46
  - Work in the repo with tools: inspect, modify, test, verify.
47
47
  - Preserve user changes. Prefer small, verifiable edits.
48
- - Read before changing existing files.
48
+ - Read existing files when you need context; exact `modify` edits can establish the managed snapshot internally.
49
49
  - Use `modify` for file changes: `content` only creates new files; existing files use `old_string`/`new_string`.
50
50
  - After project generators create scaffold files, read and edit the generated block instead of replacing the file.
51
51
  - Run shell commands using the Runtime context's command dialect and path style: `powershell` -> PowerShell with Windows paths; `cmd` -> cmd; `posix` -> POSIX shell.
@@ -107,7 +107,8 @@ def build_function_tools(
107
107
  name="modify",
108
108
  description=(
109
109
  "Create new files or edit existing files. Use content only for files that do not "
110
- "exist. For existing files, read first and use old_string/new_string."
110
+ "exist. For existing files, use old_string/new_string; read first when you need "
111
+ "to inspect context."
111
112
  ),
112
113
  params_json_schema=MODIFY_SCHEMA,
113
114
  on_invoke_tool=invoke_modify,
@@ -1302,7 +1302,14 @@ class ToolRuntime:
1302
1302
  "modify",
1303
1303
  "Provide content for a new file, or both old_string and new_string for an existing file.",
1304
1304
  ).to_json()
1305
- return self.edit(path, old, new, replace_all=replace_all, snippet_id=snippet_id)
1305
+ return self.edit(
1306
+ path,
1307
+ old,
1308
+ new,
1309
+ replace_all=replace_all,
1310
+ snippet_id=snippet_id,
1311
+ auto_read_if_missing_snapshot=True,
1312
+ )
1306
1313
 
1307
1314
  def write(self, path: str, content: object) -> str:
1308
1315
  name = "write"
@@ -1351,6 +1358,7 @@ class ToolRuntime:
1351
1358
  new: str,
1352
1359
  replace_all: bool = False,
1353
1360
  snippet_id: str | None = None,
1361
+ auto_read_if_missing_snapshot: bool = False,
1354
1362
  ) -> str:
1355
1363
  name = "edit"
1356
1364
  if not old:
@@ -1377,6 +1385,14 @@ class ToolRuntime:
1377
1385
  target = _resolve_in_cwd(self.cwd, path)
1378
1386
  if not target.exists():
1379
1387
  return ToolResult.error_result(name, f"File does not exist: {target}").to_json()
1388
+ auto_read_before_modify = False
1389
+ if (
1390
+ auto_read_if_missing_snapshot
1391
+ and snippet is None
1392
+ and self.file_state.snapshot_status(target) == "missing"
1393
+ ):
1394
+ self.file_state.mark_read(target)
1395
+ auto_read_before_modify = True
1380
1396
  ok, error = self.file_state.check_writable(
1381
1397
  target,
1382
1398
  require_read=True,
@@ -1465,6 +1481,8 @@ class ToolRuntime:
1465
1481
  "diff": diff,
1466
1482
  "diff_preview": diff,
1467
1483
  }
1484
+ if auto_read_before_modify:
1485
+ metadata["autoReadBeforeModify"] = True
1468
1486
  if snippet is not None:
1469
1487
  metadata["scope"] = _format_scope_metadata(target, snippet, scope, text)
1470
1488
  return ToolResult.ok_result(name, f"Edited {target}", metadata=metadata).to_json()
@@ -2,6 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
4
  from pathlib import Path
5
+ from typing import Literal
6
+
7
+
8
+ SnapshotStatus = Literal["missing", "full", "partial", "deleted", "stale"]
5
9
 
6
10
 
7
11
  @dataclass
@@ -58,6 +62,18 @@ class FileState:
58
62
  return False, "File changed since it was read; read it again before editing."
59
63
  return True, None
60
64
 
65
+ def snapshot_status(self, path: Path) -> SnapshotStatus:
66
+ resolved = path.resolve()
67
+ snapshot = self._snapshots.get(resolved)
68
+ if snapshot is None:
69
+ return "missing"
70
+ if not resolved.exists():
71
+ return "deleted"
72
+ stat = resolved.stat()
73
+ if stat.st_mtime_ns != snapshot.mtime_ns or stat.st_size != snapshot.size:
74
+ return "stale"
75
+ return "full" if snapshot.full_read else "partial"
76
+
61
77
  def mark_written(self, path: Path) -> None:
62
78
  if path.exists():
63
79
  self.mark_read(path)
@@ -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",