deepy-cli 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepy/__init__.py +9 -0
- deepy/__main__.py +7 -0
- deepy/cli.py +413 -0
- deepy/config/__init__.py +21 -0
- deepy/config/settings.py +237 -0
- deepy/data/__init__.py +1 -0
- deepy/data/tools/AskUserQuestion.md +10 -0
- deepy/data/tools/WebFetch.md +9 -0
- deepy/data/tools/WebSearch.md +9 -0
- deepy/data/tools/__init__.py +1 -0
- deepy/data/tools/bash.md +7 -0
- deepy/data/tools/edit.md +13 -0
- deepy/data/tools/modify.md +17 -0
- deepy/data/tools/read.md +8 -0
- deepy/data/tools/write.md +12 -0
- deepy/errors.py +63 -0
- deepy/llm/__init__.py +13 -0
- deepy/llm/agent.py +31 -0
- deepy/llm/context.py +109 -0
- deepy/llm/events.py +187 -0
- deepy/llm/model_capabilities.py +7 -0
- deepy/llm/provider.py +81 -0
- deepy/llm/replay.py +120 -0
- deepy/llm/runner.py +412 -0
- deepy/llm/thinking.py +30 -0
- deepy/prompts/__init__.py +6 -0
- deepy/prompts/compact.py +100 -0
- deepy/prompts/rules.py +24 -0
- deepy/prompts/runtime_context.py +98 -0
- deepy/prompts/system.py +72 -0
- deepy/prompts/tool_docs.py +21 -0
- deepy/sessions/__init__.py +17 -0
- deepy/sessions/jsonl.py +306 -0
- deepy/sessions/manager.py +202 -0
- deepy/skills.py +202 -0
- deepy/status.py +65 -0
- deepy/tools/__init__.py +6 -0
- deepy/tools/agents.py +343 -0
- deepy/tools/builtin.py +2113 -0
- deepy/tools/file_state.py +85 -0
- deepy/tools/result.py +54 -0
- deepy/tools/shell_utils.py +83 -0
- deepy/ui/__init__.py +5 -0
- deepy/ui/app.py +118 -0
- deepy/ui/ask_user_question.py +182 -0
- deepy/ui/exit_summary.py +142 -0
- deepy/ui/loading_text.py +87 -0
- deepy/ui/markdown.py +152 -0
- deepy/ui/message_view.py +546 -0
- deepy/ui/prompt_buffer.py +176 -0
- deepy/ui/prompt_input.py +286 -0
- deepy/ui/session_list.py +140 -0
- deepy/ui/session_picker.py +179 -0
- deepy/ui/slash_commands.py +67 -0
- deepy/ui/styles.py +21 -0
- deepy/ui/terminal.py +959 -0
- deepy/ui/thinking_state.py +29 -0
- deepy/ui/welcome.py +195 -0
- deepy/update_check.py +195 -0
- deepy/usage.py +192 -0
- deepy/utils/__init__.py +15 -0
- deepy/utils/debug_logger.py +62 -0
- deepy/utils/error_logger.py +107 -0
- deepy/utils/json.py +29 -0
- deepy/utils/notify.py +66 -0
- deepy_cli-0.1.1.dist-info/METADATA +205 -0
- deepy_cli-0.1.1.dist-info/RECORD +69 -0
- deepy_cli-0.1.1.dist-info/WHEEL +4 -0
- deepy_cli-0.1.1.dist-info/entry_points.txt +3 -0
deepy/ui/loading_text.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
from deepy.usage import format_usage_line, normalize_usage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
STALL_THRESHOLD_MS = 3_000
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_loading_text(
|
|
13
|
+
*,
|
|
14
|
+
progress: Any | None,
|
|
15
|
+
now_ms: int | float,
|
|
16
|
+
processes: Mapping[str, Any] | None = None,
|
|
17
|
+
usage: Any | None = None,
|
|
18
|
+
) -> str:
|
|
19
|
+
usage_suffix = _usage_suffix(usage)
|
|
20
|
+
process_text = build_process_loading_text(processes, now_ms=now_ms)
|
|
21
|
+
if process_text:
|
|
22
|
+
return f"{process_text}{usage_suffix}"
|
|
23
|
+
|
|
24
|
+
if progress is None:
|
|
25
|
+
return f"Thinking...{usage_suffix}"
|
|
26
|
+
|
|
27
|
+
started_at = parse_timestamp_ms(_field(progress, "startedAt"))
|
|
28
|
+
if started_at is None:
|
|
29
|
+
return f"Thinking...{usage_suffix}"
|
|
30
|
+
|
|
31
|
+
elapsed_ms = max(0, int(now_ms - started_at))
|
|
32
|
+
if elapsed_ms < STALL_THRESHOLD_MS:
|
|
33
|
+
return f"Thinking...{usage_suffix}"
|
|
34
|
+
|
|
35
|
+
elapsed_seconds = elapsed_ms // 1_000
|
|
36
|
+
tokens = _field(progress, "formattedTokens") or "0"
|
|
37
|
+
return f"Thinking... ({elapsed_seconds}s) · ↓ {tokens} tokens{usage_suffix}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_process_loading_text(
|
|
41
|
+
processes: Mapping[str, Any] | None,
|
|
42
|
+
*,
|
|
43
|
+
now_ms: int | float,
|
|
44
|
+
) -> str | None:
|
|
45
|
+
if not processes:
|
|
46
|
+
return None
|
|
47
|
+
first = next(iter(processes.values()), None)
|
|
48
|
+
if first is None:
|
|
49
|
+
return None
|
|
50
|
+
start_time = _field(first, "startTime") or ""
|
|
51
|
+
command = _field(first, "command") or "Running process..."
|
|
52
|
+
return f"({format_elapsed_time(start_time, now_ms=now_ms)}) {command}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def format_elapsed_time(start_time_iso: str, *, now_ms: int | float) -> str:
|
|
56
|
+
start_time = parse_timestamp_ms(start_time_iso)
|
|
57
|
+
elapsed_ms = 0 if start_time is None else max(0, int(now_ms - start_time))
|
|
58
|
+
elapsed_seconds = elapsed_ms // 1_000
|
|
59
|
+
minutes = elapsed_seconds // 60
|
|
60
|
+
seconds = elapsed_seconds % 60
|
|
61
|
+
if minutes > 0:
|
|
62
|
+
return f"{minutes}m{seconds}s"
|
|
63
|
+
return f"{seconds}s"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_timestamp_ms(value: Any) -> int | None:
|
|
67
|
+
if not isinstance(value, str):
|
|
68
|
+
return None
|
|
69
|
+
try:
|
|
70
|
+
normalized = value.replace("Z", "+00:00")
|
|
71
|
+
parsed = datetime.fromisoformat(normalized)
|
|
72
|
+
except ValueError:
|
|
73
|
+
return None
|
|
74
|
+
return int(parsed.timestamp() * 1_000)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _field(value: Any, name: str) -> Any:
|
|
78
|
+
if isinstance(value, Mapping):
|
|
79
|
+
return value.get(name)
|
|
80
|
+
return getattr(value, name, None)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _usage_suffix(usage: Any | None) -> str:
|
|
84
|
+
normalized = normalize_usage(usage)
|
|
85
|
+
if not normalized.known:
|
|
86
|
+
return ""
|
|
87
|
+
return f" · {format_usage_line(normalized)}"
|
deepy/ui/markdown.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
SegmentKind = Literal["text", "code"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class MarkdownSegment:
|
|
15
|
+
kind: SegmentKind
|
|
16
|
+
body: str
|
|
17
|
+
lang: str = ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def render_markdown(text: str) -> Text:
|
|
21
|
+
output = Text()
|
|
22
|
+
if not text:
|
|
23
|
+
return output
|
|
24
|
+
|
|
25
|
+
for segment in split_by_fences(text):
|
|
26
|
+
if segment.kind == "code":
|
|
27
|
+
if segment.lang:
|
|
28
|
+
output.append(f"code {segment.lang}\n", style="dim")
|
|
29
|
+
for index, line in enumerate(segment.body.splitlines() or [""]):
|
|
30
|
+
if index:
|
|
31
|
+
output.append("\n")
|
|
32
|
+
output.append(f" {line}", style="bright_white on #1f2430")
|
|
33
|
+
else:
|
|
34
|
+
output.append(render_inline_block(segment.body))
|
|
35
|
+
return output
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def split_by_fences(text: str) -> list[MarkdownSegment]:
|
|
39
|
+
segments: list[MarkdownSegment] = []
|
|
40
|
+
lines = text.splitlines()
|
|
41
|
+
buffer: list[str] = []
|
|
42
|
+
in_fence = False
|
|
43
|
+
fence_lang = ""
|
|
44
|
+
fence_body: list[str] = []
|
|
45
|
+
|
|
46
|
+
def flush_text() -> None:
|
|
47
|
+
nonlocal buffer
|
|
48
|
+
if buffer:
|
|
49
|
+
segments.append(MarkdownSegment(kind="text", body="\n".join(buffer)))
|
|
50
|
+
buffer = []
|
|
51
|
+
|
|
52
|
+
for line in lines:
|
|
53
|
+
fence_match = re.match(r"^\s*```(\w*)\s*$", line)
|
|
54
|
+
if fence_match:
|
|
55
|
+
if not in_fence:
|
|
56
|
+
flush_text()
|
|
57
|
+
in_fence = True
|
|
58
|
+
fence_lang = fence_match.group(1) or ""
|
|
59
|
+
fence_body = []
|
|
60
|
+
else:
|
|
61
|
+
segments.append(
|
|
62
|
+
MarkdownSegment(kind="code", lang=fence_lang, body="\n".join(fence_body))
|
|
63
|
+
)
|
|
64
|
+
in_fence = False
|
|
65
|
+
fence_lang = ""
|
|
66
|
+
fence_body = []
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
if in_fence:
|
|
70
|
+
fence_body.append(line)
|
|
71
|
+
else:
|
|
72
|
+
buffer.append(line)
|
|
73
|
+
|
|
74
|
+
if in_fence:
|
|
75
|
+
segments.append(MarkdownSegment(kind="code", lang=fence_lang, body="\n".join(fence_body)))
|
|
76
|
+
else:
|
|
77
|
+
flush_text()
|
|
78
|
+
return segments
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def render_inline_block(text: str) -> Text:
|
|
82
|
+
rendered = Text()
|
|
83
|
+
lines = text.split("\n")
|
|
84
|
+
for index, line in enumerate(lines):
|
|
85
|
+
if index:
|
|
86
|
+
rendered.append("\n")
|
|
87
|
+
rendered.append(render_inline_line(line))
|
|
88
|
+
return rendered
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def render_inline_line(line: str) -> Text:
|
|
92
|
+
heading = re.match(r"^(\s*)(#{1,6})\s+(.*)$", line)
|
|
93
|
+
if heading:
|
|
94
|
+
lead, hashes, content = heading.groups()
|
|
95
|
+
rendered = Text(lead)
|
|
96
|
+
rendered.append(content, style="bold cyan" if len(hashes) > 2 else "bold bright_cyan")
|
|
97
|
+
return rendered
|
|
98
|
+
|
|
99
|
+
list_match = re.match(r"^(\s*)([-*+])\s+(.*)$", line)
|
|
100
|
+
if list_match:
|
|
101
|
+
lead, _bullet, content = list_match.groups()
|
|
102
|
+
rendered = Text(lead)
|
|
103
|
+
rendered.append("•", style="bright_blue")
|
|
104
|
+
rendered.append(" ")
|
|
105
|
+
rendered.append(render_inline_spans(content))
|
|
106
|
+
return rendered
|
|
107
|
+
|
|
108
|
+
number_match = re.match(r"^(\s*)(\d+\.)\s+(.*)$", line)
|
|
109
|
+
if number_match:
|
|
110
|
+
lead, marker, content = number_match.groups()
|
|
111
|
+
rendered = Text(lead)
|
|
112
|
+
rendered.append(marker, style="yellow")
|
|
113
|
+
rendered.append(" ")
|
|
114
|
+
rendered.append(render_inline_spans(content))
|
|
115
|
+
return rendered
|
|
116
|
+
|
|
117
|
+
quote = re.match(r"^(\s*)>\s?(.*)$", line)
|
|
118
|
+
if quote:
|
|
119
|
+
lead, content = quote.groups()
|
|
120
|
+
rendered = Text(lead)
|
|
121
|
+
rendered.append("| ", style="dim")
|
|
122
|
+
start = len(rendered)
|
|
123
|
+
rendered.append(render_inline_spans(content))
|
|
124
|
+
rendered.stylize("italic", start, len(rendered))
|
|
125
|
+
return rendered
|
|
126
|
+
|
|
127
|
+
return render_inline_spans(line)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def render_inline_spans(text: str) -> Text:
|
|
131
|
+
rendered = Text()
|
|
132
|
+
pattern = re.compile(
|
|
133
|
+
r"`([^`]+)`"
|
|
134
|
+
r"|\*\*([^*]+)\*\*"
|
|
135
|
+
r"|(?<!\*)\*([^*]+)\*(?!\*)"
|
|
136
|
+
r"|_([^_\n]+)_"
|
|
137
|
+
)
|
|
138
|
+
position = 0
|
|
139
|
+
for match in pattern.finditer(text):
|
|
140
|
+
if match.start() > position:
|
|
141
|
+
rendered.append(text[position : match.start()])
|
|
142
|
+
code, bold, star_italic, underscore_italic = match.groups()
|
|
143
|
+
if code is not None:
|
|
144
|
+
rendered.append(code, style="bold bright_yellow")
|
|
145
|
+
elif bold is not None:
|
|
146
|
+
rendered.append(bold, style="bold bright_white")
|
|
147
|
+
else:
|
|
148
|
+
rendered.append(star_italic or underscore_italic or "", style="italic")
|
|
149
|
+
position = match.end()
|
|
150
|
+
if position < len(text):
|
|
151
|
+
rendered.append(text[position:])
|
|
152
|
+
return rendered
|