coderio 0.4.0__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.
- coderio/__init__.py +14 -0
- coderio/agent/__init__.py +5 -0
- coderio/agent/_deepagents_compat.py +139 -0
- coderio/agent/command_review.py +140 -0
- coderio/agent/deep_loop.py +758 -0
- coderio/agent/harness.py +768 -0
- coderio/agent/harness_middleware.py +293 -0
- coderio/agent/permission_middleware.py +52 -0
- coderio/agent/prompts.py +304 -0
- coderio/agent/skill_tool.py +70 -0
- coderio/agent/state.py +144 -0
- coderio/agent/stream.py +93 -0
- coderio/cli/__init__.py +0 -0
- coderio/cli/app.py +257 -0
- coderio/cli/commands.py +339 -0
- coderio/cli/credentials.py +70 -0
- coderio/cli/mcp_cmd.py +227 -0
- coderio/cli/multimodal.py +94 -0
- coderio/cli/onboarding.py +270 -0
- coderio/cli/providers.py +113 -0
- coderio/cli/render.py +28 -0
- coderio/cli/repl.py +189 -0
- coderio/cli/run_cmd.py +171 -0
- coderio/cli/skills_cmd.py +77 -0
- coderio/cli/stream.py +224 -0
- coderio/cli/tui.py +1584 -0
- coderio/cli/tui_onboarding.py +465 -0
- coderio/cli/tui_screens.py +334 -0
- coderio/cli/tui_widgets.py +453 -0
- coderio/config/__init__.py +21 -0
- coderio/config/bootstrap.py +38 -0
- coderio/config/loader.py +273 -0
- coderio/config/models.py +193 -0
- coderio/config/trust.py +136 -0
- coderio/llm/__init__.py +3 -0
- coderio/llm/factory.py +164 -0
- coderio/llm/probe.py +153 -0
- coderio/mcp_loader.py +274 -0
- coderio/session/__init__.py +3 -0
- coderio/session/message.py +79 -0
- coderio/session/store.py +244 -0
- coderio/skills/__init__.py +4 -0
- coderio/skills/bundled/executing-plans/SKILL.md +15 -0
- coderio/skills/lion-skills/LICENSE +21 -0
- coderio/skills/lion-skills/README.md +114 -0
- coderio/skills/lion-skills/skills/clarifying-questions/SKILL.md +119 -0
- coderio/skills/lion-skills/skills/clarifying-questions/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/code-review/SKILL.md +102 -0
- coderio/skills/lion-skills/skills/code-review/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/commit-message/SKILL.md +97 -0
- coderio/skills/lion-skills/skills/commit-message/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/debugging/SKILL.md +116 -0
- coderio/skills/lion-skills/skills/debugging/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/error-handling/SKILL.md +74 -0
- coderio/skills/lion-skills/skills/error-handling/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/lion-writing-skills/SKILL.md +85 -0
- coderio/skills/lion-skills/skills/lion-writing-skills/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/naming/SKILL.md +85 -0
- coderio/skills/lion-skills/skills/naming/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/onboarding-unknown-codebase/SKILL.md +95 -0
- coderio/skills/lion-skills/skills/onboarding-unknown-codebase/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/spec-writing/SKILL.md +154 -0
- coderio/skills/lion-skills/skills/spec-writing/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/task-breakdown/SKILL.md +134 -0
- coderio/skills/lion-skills/skills/task-breakdown/evals/evals.json +46 -0
- coderio/skills/lion-skills/skills/testing/SKILL.md +134 -0
- coderio/skills/lion-skills/skills/testing/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/verify-and-fix/SKILL.md +132 -0
- coderio/skills/lion-skills/skills/verify-and-fix/evals/evals.json +40 -0
- coderio/skills/models.py +66 -0
- coderio/skills/parser.py +50 -0
- coderio/skills/store.py +102 -0
- coderio/tools/__init__.py +94 -0
- coderio/tools/base.py +55 -0
- coderio/tools/bash.py +168 -0
- coderio/tools/command_policy.py +364 -0
- coderio/tools/edit_file.py +55 -0
- coderio/tools/glob_tool.py +21 -0
- coderio/tools/grep_tool.py +90 -0
- coderio/tools/linux_sandbox.py +196 -0
- coderio/tools/list_dir.py +58 -0
- coderio/tools/multi_edit.py +62 -0
- coderio/tools/note.py +72 -0
- coderio/tools/permission.py +214 -0
- coderio/tools/read_file.py +51 -0
- coderio/tools/sandbox_runner.py +113 -0
- coderio/tools/todo.py +71 -0
- coderio/tools/web_fetch.py +188 -0
- coderio/tools/web_search.py +49 -0
- coderio/tools/win_job.py +214 -0
- coderio/tools/win_sandbox.py +639 -0
- coderio/tools/write_file.py +25 -0
- coderio-0.4.0.dist-info/METADATA +490 -0
- coderio-0.4.0.dist-info/RECORD +97 -0
- coderio-0.4.0.dist-info/WHEEL +4 -0
- coderio-0.4.0.dist-info/entry_points.txt +2 -0
- coderio-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""TUI picker screens — extracted from tui.py for modularity.
|
|
2
|
+
|
|
3
|
+
Contains the three modal picker screens used by the TUI:
|
|
4
|
+
- ProfilePickerScreen: /profile (switch saved [[profiles]])
|
|
5
|
+
- ModePickerScreen: /mode with no argument (switch permission mode)
|
|
6
|
+
- SessionPickerScreen: /resume (resume or delete recent sessions)
|
|
7
|
+
|
|
8
|
+
All three are ModalScreen subclasses — pure popups with no top-level coupling
|
|
9
|
+
to the rest of coderio, so this module can be imported standalone.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from textual.app import ComposeResult
|
|
17
|
+
from textual.binding import Binding
|
|
18
|
+
from textual.containers import Vertical
|
|
19
|
+
from textual.screen import ModalScreen
|
|
20
|
+
from textual.widgets import Input, ListItem, ListView, Static
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ProfilePickerScreen(ModalScreen[str | None]):
|
|
24
|
+
"""Interactive profile picker (/profile).
|
|
25
|
+
|
|
26
|
+
Lists all saved [[profiles]] from config.toml as a ListView — each row shows
|
|
27
|
+
the profile name with its provider/model as a dim subtitle, and the active
|
|
28
|
+
profile is marked ★. ↑↓ navigates, Enter switches (dismisses with the chosen
|
|
29
|
+
profile name), Esc cancels (dismisses None). Mirrors the SessionPickerScreen
|
|
30
|
+
UX so both pickers feel the same.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
CSS = """
|
|
34
|
+
ProfilePickerScreen { align: center middle; }
|
|
35
|
+
#profile-box {
|
|
36
|
+
width: 70%; height: auto; max-height: 70%; border: round $accent;
|
|
37
|
+
background: $surface; padding: 1 2;
|
|
38
|
+
}
|
|
39
|
+
#profile-title { text-align: center; margin-bottom: 1; }
|
|
40
|
+
#profile-list { height: auto; max-height: 16; }
|
|
41
|
+
ProfilePickerScreen ListItem { padding: 0 1; }
|
|
42
|
+
ProfilePickerScreen ListItem > Widget :hover { background: $boost; }
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
BINDINGS = [
|
|
46
|
+
Binding("escape", "cancel", "取消", show=True),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
def __init__(self, profiles: list, active_name: str = "") -> None:
|
|
50
|
+
super().__init__()
|
|
51
|
+
self._profiles = profiles
|
|
52
|
+
self._active_name = active_name
|
|
53
|
+
|
|
54
|
+
def compose(self) -> ComposeResult:
|
|
55
|
+
with Vertical(id="profile-box"):
|
|
56
|
+
yield Static(
|
|
57
|
+
"[bold magenta]切换配置[/bold magenta] ↑↓ 选择 · Enter 切换 · Esc 取消",
|
|
58
|
+
id="profile-title",
|
|
59
|
+
)
|
|
60
|
+
yield ListView(id="profile-list")
|
|
61
|
+
|
|
62
|
+
def on_mount(self) -> None:
|
|
63
|
+
lv = self.query_one("#profile-list", ListView)
|
|
64
|
+
for p in self._profiles:
|
|
65
|
+
star = "★ " if p.name == self._active_name else " "
|
|
66
|
+
lv.append(
|
|
67
|
+
ListItem(
|
|
68
|
+
Static(f"{star}{p.name} [dim]{p.provider_id} · {p.model}[/dim]"),
|
|
69
|
+
name=p.name,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
try:
|
|
73
|
+
lv.index = 0
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
lv.focus()
|
|
77
|
+
|
|
78
|
+
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
|
79
|
+
"""Enter on a row → switch to that profile."""
|
|
80
|
+
self.dismiss(event.item.name)
|
|
81
|
+
|
|
82
|
+
def action_cancel(self) -> None:
|
|
83
|
+
self.dismiss(None)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ModePickerScreen(ModalScreen[str | None]):
|
|
87
|
+
"""Visual permission-mode picker (/mode with no argument).
|
|
88
|
+
|
|
89
|
+
Lists the three modes (confirm / plan / auto) as a ListView with the
|
|
90
|
+
current mode marked ★. ↑↓ navigates, Enter selects (dismisses with the
|
|
91
|
+
mode name), Esc cancels. Same UX pattern as ProfilePickerScreen.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
CSS = """
|
|
95
|
+
ModePickerScreen { align: center middle; }
|
|
96
|
+
#mode-box {
|
|
97
|
+
width: 60%; height: auto; max-height: 60%; border: round $accent;
|
|
98
|
+
background: $surface; padding: 1 2;
|
|
99
|
+
}
|
|
100
|
+
#mode-title { text-align: center; margin-bottom: 1; }
|
|
101
|
+
#mode-list { height: auto; max-height: 8; }
|
|
102
|
+
ModePickerScreen ListItem { padding: 0 1; }
|
|
103
|
+
ModePickerScreen ListItem > Widget :hover { background: $boost; }
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
BINDINGS = [
|
|
107
|
+
Binding("escape", "cancel", "取消", show=True),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
# mode -> human-readable description shown as a dim subtitle.
|
|
111
|
+
_MODE_INFO = {
|
|
112
|
+
"plan": "只读模式,阻止所有写操作(最安全)",
|
|
113
|
+
"confirm": "每次写操作前确认",
|
|
114
|
+
"auto_edit": "自动编辑文件,bash/网络仍需确认",
|
|
115
|
+
"full": "全自动,不确认任何操作(需信任)",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def __init__(self, active_mode: str = "") -> None:
|
|
119
|
+
super().__init__()
|
|
120
|
+
self._active_mode = active_mode
|
|
121
|
+
|
|
122
|
+
def compose(self) -> ComposeResult:
|
|
123
|
+
with Vertical(id="mode-box"):
|
|
124
|
+
yield Static(
|
|
125
|
+
"[bold magenta]切换权限模式[/bold magenta] ↑↓ 选择 · Enter 切换 · Esc 取消",
|
|
126
|
+
id="mode-title",
|
|
127
|
+
)
|
|
128
|
+
yield ListView(id="mode-list")
|
|
129
|
+
|
|
130
|
+
def on_mount(self) -> None:
|
|
131
|
+
lv = self.query_one("#mode-list", ListView)
|
|
132
|
+
for mode in ("plan", "confirm", "auto_edit", "full"):
|
|
133
|
+
star = "★ " if mode == self._active_mode else " "
|
|
134
|
+
desc = self._MODE_INFO.get(mode, "")
|
|
135
|
+
lv.append(
|
|
136
|
+
ListItem(
|
|
137
|
+
Static(f"{star}{mode} [dim]{desc}[/dim]"),
|
|
138
|
+
name=mode,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
try:
|
|
142
|
+
lv.index = 0
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
lv.focus()
|
|
146
|
+
|
|
147
|
+
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
|
148
|
+
"""Enter on a row → switch to that mode."""
|
|
149
|
+
self.dismiss(event.item.name)
|
|
150
|
+
|
|
151
|
+
def action_cancel(self) -> None:
|
|
152
|
+
self.dismiss(None)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class SessionPickerScreen(ModalScreen[str | None]):
|
|
156
|
+
"""Interactive session picker (Claude-Code-style /resume).
|
|
157
|
+
|
|
158
|
+
Shows recent sessions as a scrollable list — each row has the first user
|
|
159
|
+
message (so the user recognizes the conversation by what they asked, not by
|
|
160
|
+
an opaque id), the message count, and the time. ↑↓ navigates, Enter resumes,
|
|
161
|
+
Esc cancels. Typing filters the list by the summary text. Dismisses with the
|
|
162
|
+
chosen session id (string) or None (cancelled).
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
CSS = """
|
|
166
|
+
SessionPickerScreen {
|
|
167
|
+
align: center middle;
|
|
168
|
+
}
|
|
169
|
+
#picker-box {
|
|
170
|
+
width: 80%; height: 70%; border: thick $accent; background: $surface;
|
|
171
|
+
padding: 1 2;
|
|
172
|
+
}
|
|
173
|
+
#picker-title { text-align: center; color: $accent; margin-bottom: 1; }
|
|
174
|
+
#picker-filter {
|
|
175
|
+
dock: bottom; margin-top: 1; border: round $accent 50%;
|
|
176
|
+
}
|
|
177
|
+
#picker-filter:focus { border: round $accent; }
|
|
178
|
+
#picker-list { height: 1fr; border: none; }
|
|
179
|
+
.picker-row { padding: 0 1; }
|
|
180
|
+
.picker-row:first-child { margin-top: 0; }
|
|
181
|
+
.picker-summary { color: $text; }
|
|
182
|
+
.picker-meta { color: $text-muted; }
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
BINDINGS = [
|
|
186
|
+
Binding("escape", "cancel", "取消", show=True),
|
|
187
|
+
Binding("delete", "delete_selected", "删除", show=True),
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
def __init__(self, summaries: list[dict], save_dir: str = "", active_session_id: str = "") -> None:
|
|
191
|
+
super().__init__()
|
|
192
|
+
self._all = summaries # full list; filtered view derived on typing
|
|
193
|
+
self._filter = ""
|
|
194
|
+
self._delete_confirm_sid: str | None = None # pending delete confirmation
|
|
195
|
+
# The directory where session files actually live. Must match the
|
|
196
|
+
# source that populated `summaries` — hardcoding ~/.coderio/sessions
|
|
197
|
+
# would delete the wrong files when a custom save_dir is configured.
|
|
198
|
+
self._save_dir = save_dir
|
|
199
|
+
self._active_session_id = active_session_id # cannot be deleted
|
|
200
|
+
|
|
201
|
+
def compose(self) -> ComposeResult:
|
|
202
|
+
with Vertical(id="picker-box"):
|
|
203
|
+
yield Static(
|
|
204
|
+
"[bold]恢复会话[/bold] ↑↓ 选择 · Enter 恢复 · Del 删除(按2次确认) · Esc 取消 · 输入过滤",
|
|
205
|
+
id="picker-title",
|
|
206
|
+
)
|
|
207
|
+
yield ListView(id="picker-list")
|
|
208
|
+
yield Input(placeholder="输入关键字过滤(首条消息 / 时间)", id="picker-filter")
|
|
209
|
+
|
|
210
|
+
def on_mount(self) -> None:
|
|
211
|
+
self._populate()
|
|
212
|
+
# Focus the ListView (not the filter input) so ↑↓ navigation works
|
|
213
|
+
# immediately. The filter input still receives keystrokes via on_key
|
|
214
|
+
# forwarding when the user starts typing.
|
|
215
|
+
lv = self.query_one("#picker-list", ListView)
|
|
216
|
+
try:
|
|
217
|
+
lv.index = 0
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
lv.focus()
|
|
221
|
+
|
|
222
|
+
def on_key(self, event) -> None:
|
|
223
|
+
"""Forward printable keys to the filter input; let ListView handle nav."""
|
|
224
|
+
# Cancel pending delete on any key other than Del.
|
|
225
|
+
if event.key != "delete" and self._delete_confirm_sid is not None:
|
|
226
|
+
self._delete_confirm_sid = None
|
|
227
|
+
inp = self.query_one("#picker-filter", Input)
|
|
228
|
+
if event.key in ("up", "down", "enter", "escape", "delete", "pageup", "pagedown"):
|
|
229
|
+
return # ListView / bindings handle these natively when it has focus
|
|
230
|
+
# Printable character → route to filter input for live search
|
|
231
|
+
if event.character and len(event.character) == 1 and event.character.isprintable():
|
|
232
|
+
inp.focus()
|
|
233
|
+
inp.value += event.character
|
|
234
|
+
event.prevent_default()
|
|
235
|
+
elif event.key == "backspace":
|
|
236
|
+
inp.focus()
|
|
237
|
+
if inp.value:
|
|
238
|
+
inp.value = inp.value[:-1]
|
|
239
|
+
event.prevent_default()
|
|
240
|
+
|
|
241
|
+
def _populate(self) -> None:
|
|
242
|
+
"""Rebuild the list from the (filtered) summaries."""
|
|
243
|
+
lv = self.query_one("#picker-list", ListView)
|
|
244
|
+
lv.clear()
|
|
245
|
+
f = self._filter.lower()
|
|
246
|
+
for s in self._all:
|
|
247
|
+
label = s["first_user"] or "(空会话)"
|
|
248
|
+
if f and f not in label.lower() and f not in s["mtime"].lower() and f not in s["id"].lower():
|
|
249
|
+
continue
|
|
250
|
+
row = Static(
|
|
251
|
+
f"[{s['mtime']}] {label}\n"
|
|
252
|
+
f" [dim]{s['message_count']} 条消息 · {s.get('model') or '?'} · {s['id']}[/dim]",
|
|
253
|
+
classes="picker-row",
|
|
254
|
+
)
|
|
255
|
+
lv.append(ListItem(row, name=s["id"]))
|
|
256
|
+
|
|
257
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
258
|
+
if event.input.id == "picker-filter":
|
|
259
|
+
self._filter = event.value
|
|
260
|
+
self._populate()
|
|
261
|
+
|
|
262
|
+
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
|
263
|
+
"""Enter on a row → resume that session."""
|
|
264
|
+
self.dismiss(event.item.name)
|
|
265
|
+
|
|
266
|
+
def action_cancel(self) -> None:
|
|
267
|
+
self.dismiss(None)
|
|
268
|
+
|
|
269
|
+
def action_delete_selected(self) -> None:
|
|
270
|
+
"""Delete the currently highlighted session (jsonl + sqlite).
|
|
271
|
+
|
|
272
|
+
Two-step confirmation: first Del selects the item for deletion (shows
|
|
273
|
+
a confirmation prompt), second Del on the SAME item executes. Moving
|
|
274
|
+
the cursor or pressing any other key cancels the pending delete.
|
|
275
|
+
"""
|
|
276
|
+
lv = self.query_one("#picker-list", ListView)
|
|
277
|
+
if not lv.children or lv.index is None:
|
|
278
|
+
return
|
|
279
|
+
item = lv.children[lv.index]
|
|
280
|
+
sid = item.name or ""
|
|
281
|
+
if not sid:
|
|
282
|
+
return
|
|
283
|
+
# Protect the currently active session — deleting it would corrupt
|
|
284
|
+
# the running conversation (the Session object still holds a file
|
|
285
|
+
# handle, and the next append would recreate a file without history).
|
|
286
|
+
if sid == self._active_session_id:
|
|
287
|
+
self.app.bell()
|
|
288
|
+
return
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
# Two-step confirmation: if there's a pending delete for THIS sid,
|
|
292
|
+
# execute it. Otherwise, mark it pending and wait for second Del.
|
|
293
|
+
if self._delete_confirm_sid == sid:
|
|
294
|
+
self._delete_confirm_sid = None # clear before executing
|
|
295
|
+
self._do_delete(lv, sid)
|
|
296
|
+
else:
|
|
297
|
+
# First Del — mark pending. User must press Del again to confirm.
|
|
298
|
+
self._delete_confirm_sid = sid
|
|
299
|
+
self.app.bell()
|
|
300
|
+
|
|
301
|
+
def _do_delete(self, lv: ListView, sid: str) -> None:
|
|
302
|
+
"""Actually delete the session files and update the list."""
|
|
303
|
+
# Use the actual save_dir (from config), not a hardcoded path.
|
|
304
|
+
|
|
305
|
+
# Use the actual save_dir (from config), not a hardcoded path.
|
|
306
|
+
if self._save_dir:
|
|
307
|
+
sessions_dir = Path(self._save_dir).expanduser()
|
|
308
|
+
else:
|
|
309
|
+
sessions_dir = Path.home() / ".coderio" / "sessions"
|
|
310
|
+
deleted_any = False
|
|
311
|
+
delete_failed = False
|
|
312
|
+
for suffix in (".jsonl", ".sqlite", ".sqlite-wal", ".sqlite-shm"):
|
|
313
|
+
p = sessions_dir / f"{sid}{suffix}"
|
|
314
|
+
if p.exists():
|
|
315
|
+
try:
|
|
316
|
+
p.unlink()
|
|
317
|
+
deleted_any = True
|
|
318
|
+
except Exception:
|
|
319
|
+
delete_failed = True
|
|
320
|
+
# Only remove from UI if deletion succeeded. If it failed, keep the
|
|
321
|
+
# entry so the user knows it's still on disk (no false success).
|
|
322
|
+
if deleted_any and not delete_failed:
|
|
323
|
+
self._all = [s for s in self._all if s["id"] != sid]
|
|
324
|
+
self._populate()
|
|
325
|
+
if not self._all:
|
|
326
|
+
self.dismiss(None) # no sessions left → close picker
|
|
327
|
+
else:
|
|
328
|
+
try:
|
|
329
|
+
lv.index = 0
|
|
330
|
+
except Exception:
|
|
331
|
+
pass
|
|
332
|
+
elif delete_failed:
|
|
333
|
+
# Show a brief error notice without closing the picker.
|
|
334
|
+
self.app.bell()
|