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.
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/PKG-INFO +1 -1
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/pyproject.toml +1 -1
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/__init__.py +1 -1
- deepy_cli-0.2.3/src/deepy/data/tools/modify.md +26 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/system.py +1 -1
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/agents.py +2 -1
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/builtin.py +19 -1
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/file_state.py +16 -0
- {deepy_cli-0.2.1 → 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.1 → deepy_cli-0.2.3}/src/deepy/ui/styles.py +18 -6
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/terminal.py +337 -62
- deepy_cli-0.2.1/src/deepy/data/tools/modify.md +0 -22
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/README.md +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/cli.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/config/__init__.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/config/settings.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/AskUserQuestion.md +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/WebFetch.md +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/edit.md +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/read.md +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/shell.md +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/data/tools/write.md +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/errors.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/agent.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/context.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/provider.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/replay.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/runner.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/mcp.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/init_agents.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/prompts/tool_docs.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/sessions/jsonl.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/skill_market.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/skills.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/status.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/result.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/shell_output.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/types/__init__.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/types/sdk.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/types/tool_payloads.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/ask_user_question.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/exit_summary.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/file_mentions.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/local_command.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/message_view.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/skill_picker.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/slash_commands.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/ui/welcome.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/usage.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/utils/error_logger.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/utils/json.py +0 -0
- {deepy_cli-0.2.1 → deepy_cli-0.2.3}/src/deepy/utils/notify.py +0 -0
|
@@ -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
|
|
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
|
|
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(
|
|
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 = "
|
|
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",
|