zjcode 0.0.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.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- zjcode-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Goal acceptance-criteria review widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, Literal, TypedDict
|
|
6
|
+
|
|
7
|
+
from textual.binding import Binding, BindingType
|
|
8
|
+
from textual.containers import Container, Vertical, VerticalScroll
|
|
9
|
+
from textual.content import Content
|
|
10
|
+
from textual.message import Message
|
|
11
|
+
from textual.widgets import Markdown, Static
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
import asyncio
|
|
15
|
+
|
|
16
|
+
from textual import events
|
|
17
|
+
from textual.app import ComposeResult
|
|
18
|
+
|
|
19
|
+
from deepagents_code import theme
|
|
20
|
+
from deepagents_code.config import get_glyphs, is_ascii_mode
|
|
21
|
+
from deepagents_code.tui.widgets.ask_user import AskUserTextArea
|
|
22
|
+
|
|
23
|
+
# Menu options in display order: (label, `action_*` suffix). The list index is
|
|
24
|
+
# the cursor position, so labels and dispatch stay aligned from one source.
|
|
25
|
+
_OPTIONS: tuple[tuple[str, str], ...] = (
|
|
26
|
+
("1. Accept proposed criteria (y)", "accept"),
|
|
27
|
+
("2. Edit criteria (e)", "edit"),
|
|
28
|
+
("3. Reject with message (r)", "reject_with_message"),
|
|
29
|
+
("4. Cancel (n)", "cancel"),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GoalReviewAccepted(TypedDict):
|
|
34
|
+
"""Widget result when the generated criteria are accepted unchanged."""
|
|
35
|
+
|
|
36
|
+
type: Literal["accepted"]
|
|
37
|
+
"""Discriminator tag for accepting generated criteria unchanged."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GoalReviewEdited(TypedDict):
|
|
41
|
+
"""Widget result when the user submits revised criteria."""
|
|
42
|
+
|
|
43
|
+
type: Literal["edited"]
|
|
44
|
+
"""Discriminator tag for submitting revised criteria."""
|
|
45
|
+
|
|
46
|
+
criteria: str
|
|
47
|
+
"""User-edited acceptance criteria to activate for the goal."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GoalReviewRejected(TypedDict):
|
|
51
|
+
"""Widget result when the user rejects criteria with feedback."""
|
|
52
|
+
|
|
53
|
+
type: Literal["rejected"]
|
|
54
|
+
"""Discriminator tag for regenerating criteria from user feedback."""
|
|
55
|
+
|
|
56
|
+
message: str
|
|
57
|
+
"""User feedback explaining how the criteria should be regenerated."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class GoalReviewCancelled(TypedDict):
|
|
61
|
+
"""Widget result when the user cancels the proposal."""
|
|
62
|
+
|
|
63
|
+
type: Literal["cancelled"]
|
|
64
|
+
"""Discriminator tag for cancelling the pending goal proposal."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
GoalReviewResult = (
|
|
68
|
+
GoalReviewAccepted | GoalReviewEdited | GoalReviewRejected | GoalReviewCancelled
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class GoalReviewTextArea(AskUserTextArea):
|
|
73
|
+
"""Text input that keeps goal-review edit keystrokes inside the editor."""
|
|
74
|
+
|
|
75
|
+
class CancelEdit(Message):
|
|
76
|
+
"""Posted when Escape should leave goal criteria edit mode."""
|
|
77
|
+
|
|
78
|
+
async def _on_key(self, event: events.Key) -> None:
|
|
79
|
+
if event.key == "escape":
|
|
80
|
+
event.prevent_default()
|
|
81
|
+
event.stop()
|
|
82
|
+
self.post_message(self.CancelEdit())
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
await super()._on_key(event)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class GoalReviewMenu(Container):
|
|
89
|
+
"""Inline review widget for generated goal acceptance criteria."""
|
|
90
|
+
|
|
91
|
+
can_focus = True
|
|
92
|
+
"""Allow the menu itself to receive navigation and quick-key focus."""
|
|
93
|
+
|
|
94
|
+
can_focus_children = True
|
|
95
|
+
"""Allow the inline criteria editor to receive text input focus."""
|
|
96
|
+
|
|
97
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
98
|
+
Binding("up", "move_up", "Up", show=False),
|
|
99
|
+
Binding("k", "move_up", "Up", show=False),
|
|
100
|
+
Binding("down", "move_down", "Down", show=False),
|
|
101
|
+
Binding("j", "move_down", "Down", show=False),
|
|
102
|
+
Binding("enter", "select", "Select", show=False),
|
|
103
|
+
Binding("1", "accept", "Accept", show=False),
|
|
104
|
+
Binding("y", "accept", "Accept", show=False),
|
|
105
|
+
Binding("2", "edit", "Edit", show=False),
|
|
106
|
+
Binding("e", "edit", "Edit", show=False),
|
|
107
|
+
Binding("3", "reject_with_message", "Reject with message", show=False),
|
|
108
|
+
Binding("r", "reject_with_message", "Reject with message", show=False),
|
|
109
|
+
Binding("4", "cancel", "Cancel", show=False),
|
|
110
|
+
Binding("n", "cancel", "Cancel", show=False),
|
|
111
|
+
Binding("escape", "cancel", "Cancel", show=False),
|
|
112
|
+
]
|
|
113
|
+
"""Keyboard bindings for navigation, accepting, editing, and cancelling."""
|
|
114
|
+
|
|
115
|
+
class Decided(Message):
|
|
116
|
+
"""Message sent when the user accepts, edits, or cancels."""
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
result: GoalReviewResult,
|
|
121
|
+
widget: GoalReviewMenu | None = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Initialize a decision message."""
|
|
124
|
+
super().__init__()
|
|
125
|
+
self.result = result
|
|
126
|
+
"""Decision payload emitted by the review widget."""
|
|
127
|
+
|
|
128
|
+
self.widget = widget
|
|
129
|
+
"""Review widget that emitted the decision."""
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
objective: str,
|
|
134
|
+
criteria: str,
|
|
135
|
+
*,
|
|
136
|
+
amendment: bool = False,
|
|
137
|
+
id: str | None = None, # noqa: A002
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Initialize the goal review menu."""
|
|
140
|
+
super().__init__(id=id or "goal-review-menu", classes="goal-review-menu")
|
|
141
|
+
self._objective = objective
|
|
142
|
+
"""Goal objective whose generated criteria are being reviewed."""
|
|
143
|
+
|
|
144
|
+
self._criteria = criteria
|
|
145
|
+
"""Generated acceptance criteria proposed for the goal."""
|
|
146
|
+
|
|
147
|
+
self._amendment = amendment
|
|
148
|
+
"""Whether this review updates an existing goal."""
|
|
149
|
+
|
|
150
|
+
self._selected = 0
|
|
151
|
+
"""Index of the currently highlighted action option."""
|
|
152
|
+
|
|
153
|
+
self._option_widgets: list[Static] = []
|
|
154
|
+
"""Mounted option widgets updated when selection changes."""
|
|
155
|
+
|
|
156
|
+
self._help_widget: Static | None = None
|
|
157
|
+
"""Mounted keyboard-help widget, populated during composition."""
|
|
158
|
+
|
|
159
|
+
self._edit_input: GoalReviewTextArea | None = None
|
|
160
|
+
"""Inline editor used for revised criteria or rejection feedback."""
|
|
161
|
+
|
|
162
|
+
self._input_mode: Literal["edit", "reject"] | None = None
|
|
163
|
+
"""Whether an inline text input is currently active."""
|
|
164
|
+
|
|
165
|
+
self._future: asyncio.Future[GoalReviewResult] | None = None
|
|
166
|
+
"""Future resolved when the user accepts, edits, or cancels."""
|
|
167
|
+
|
|
168
|
+
self._submitted = False
|
|
169
|
+
"""Whether a terminal decision has already been emitted."""
|
|
170
|
+
|
|
171
|
+
def set_future(self, future: asyncio.Future[GoalReviewResult]) -> None:
|
|
172
|
+
"""Set the future to resolve when the user decides."""
|
|
173
|
+
self._future = future
|
|
174
|
+
|
|
175
|
+
def compose(self) -> ComposeResult:
|
|
176
|
+
"""Compose the review widget.
|
|
177
|
+
|
|
178
|
+
Yields:
|
|
179
|
+
Widgets for the title, criteria preview, actions, editor, and help text.
|
|
180
|
+
"""
|
|
181
|
+
glyphs = get_glyphs()
|
|
182
|
+
title = "Review goal amendment" if self._amendment else "Review goal criteria"
|
|
183
|
+
yield Static(
|
|
184
|
+
Content.from_markup("$cursor $title", cursor=glyphs.cursor, title=title),
|
|
185
|
+
classes="goal-review-title",
|
|
186
|
+
)
|
|
187
|
+
with (
|
|
188
|
+
VerticalScroll(classes="goal-review-content"),
|
|
189
|
+
Vertical(classes="goal-review-body"),
|
|
190
|
+
):
|
|
191
|
+
source = f"**Proposed criteria**\n\n{self._criteria}"
|
|
192
|
+
if self._amendment:
|
|
193
|
+
source = (
|
|
194
|
+
f"**Proposed objective**\n\n{self._objective}\n\n"
|
|
195
|
+
f"**Proposed criteria**\n\n{self._criteria}"
|
|
196
|
+
)
|
|
197
|
+
yield Markdown(source, classes="goal-review-markdown")
|
|
198
|
+
with Container(classes="goal-review-options-container"):
|
|
199
|
+
for _ in _OPTIONS:
|
|
200
|
+
widget = Static("", classes="goal-review-option")
|
|
201
|
+
self._option_widgets.append(widget)
|
|
202
|
+
yield widget
|
|
203
|
+
self._edit_input = GoalReviewTextArea(classes="goal-review-edit-input")
|
|
204
|
+
self._edit_input.text = self._criteria
|
|
205
|
+
self._edit_input.display = False
|
|
206
|
+
yield self._edit_input
|
|
207
|
+
self._help_widget = Static("", classes="goal-review-help")
|
|
208
|
+
yield self._help_widget
|
|
209
|
+
|
|
210
|
+
async def on_mount(self) -> None:
|
|
211
|
+
"""Focus the menu and render options after mount."""
|
|
212
|
+
if is_ascii_mode():
|
|
213
|
+
colors = theme.get_theme_colors(self)
|
|
214
|
+
self.styles.border = ("ascii", colors.success)
|
|
215
|
+
self._update_options()
|
|
216
|
+
self.focus()
|
|
217
|
+
|
|
218
|
+
def focus_active(self) -> None:
|
|
219
|
+
"""Focus the active control."""
|
|
220
|
+
if self._input_mode is not None and self._edit_input is not None:
|
|
221
|
+
self._edit_input.focus()
|
|
222
|
+
return
|
|
223
|
+
self.focus()
|
|
224
|
+
|
|
225
|
+
def action_move_up(self) -> None:
|
|
226
|
+
"""Move selection up."""
|
|
227
|
+
if self._input_mode is not None:
|
|
228
|
+
return
|
|
229
|
+
self._selected = (self._selected - 1) % len(_OPTIONS)
|
|
230
|
+
self._update_options()
|
|
231
|
+
|
|
232
|
+
def action_move_down(self) -> None:
|
|
233
|
+
"""Move selection down."""
|
|
234
|
+
if self._input_mode is not None:
|
|
235
|
+
return
|
|
236
|
+
self._selected = (self._selected + 1) % len(_OPTIONS)
|
|
237
|
+
self._update_options()
|
|
238
|
+
|
|
239
|
+
def action_select(self) -> None:
|
|
240
|
+
"""Select the highlighted option."""
|
|
241
|
+
if self._input_mode is not None:
|
|
242
|
+
return
|
|
243
|
+
action_name = _OPTIONS[self._selected][1]
|
|
244
|
+
getattr(self, f"action_{action_name}")()
|
|
245
|
+
|
|
246
|
+
def action_accept(self) -> None:
|
|
247
|
+
"""Accept the proposed criteria unchanged."""
|
|
248
|
+
if self._input_mode is not None:
|
|
249
|
+
return
|
|
250
|
+
self._submit({"type": "accepted"})
|
|
251
|
+
|
|
252
|
+
def action_edit(self) -> None:
|
|
253
|
+
"""Open the inline editor for revised criteria."""
|
|
254
|
+
if self._submitted or self._input_mode is not None:
|
|
255
|
+
return
|
|
256
|
+
self._input_mode = "edit"
|
|
257
|
+
if self._edit_input is not None:
|
|
258
|
+
self._edit_input.text = self._criteria
|
|
259
|
+
self._edit_input.display = True
|
|
260
|
+
self._edit_input.focus()
|
|
261
|
+
self._update_options()
|
|
262
|
+
|
|
263
|
+
def action_reject_with_message(self) -> None:
|
|
264
|
+
"""Open the inline feedback input for regenerating criteria."""
|
|
265
|
+
if self._submitted or self._input_mode is not None:
|
|
266
|
+
return
|
|
267
|
+
self._input_mode = "reject"
|
|
268
|
+
if self._edit_input is not None:
|
|
269
|
+
self._edit_input.text = ""
|
|
270
|
+
self._edit_input.display = True
|
|
271
|
+
self._edit_input.focus()
|
|
272
|
+
self._update_options()
|
|
273
|
+
|
|
274
|
+
def action_cancel(self) -> None:
|
|
275
|
+
"""Cancel editing or cancel the whole proposal."""
|
|
276
|
+
if self._submitted:
|
|
277
|
+
return
|
|
278
|
+
if self._input_mode is not None:
|
|
279
|
+
self._input_mode = None
|
|
280
|
+
if self._edit_input is not None:
|
|
281
|
+
self._edit_input.display = False
|
|
282
|
+
self._update_options()
|
|
283
|
+
self.focus()
|
|
284
|
+
return
|
|
285
|
+
self._submit({"type": "cancelled"})
|
|
286
|
+
|
|
287
|
+
def on_ask_user_text_area_submitted(
|
|
288
|
+
self,
|
|
289
|
+
event: AskUserTextArea.Submitted,
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Submit edited criteria when Enter is pressed in the editor."""
|
|
292
|
+
if event.text_area is not self._edit_input:
|
|
293
|
+
return
|
|
294
|
+
event.stop()
|
|
295
|
+
if self._input_mode == "edit":
|
|
296
|
+
self._submit_edit()
|
|
297
|
+
return
|
|
298
|
+
if self._input_mode == "reject":
|
|
299
|
+
self._submit_rejection()
|
|
300
|
+
|
|
301
|
+
def on_goal_review_text_area_cancel_edit(
|
|
302
|
+
self,
|
|
303
|
+
event: GoalReviewTextArea.CancelEdit,
|
|
304
|
+
) -> None:
|
|
305
|
+
"""Return from edit mode when Escape is pressed in the editor."""
|
|
306
|
+
event.stop()
|
|
307
|
+
self.action_cancel()
|
|
308
|
+
|
|
309
|
+
def on_blur(self, event: events.Blur) -> None: # noqa: PLR6301 # Textual event handler
|
|
310
|
+
"""Prevent blur from dismissing the review prompt."""
|
|
311
|
+
event.stop()
|
|
312
|
+
|
|
313
|
+
def _submit_edit(self) -> None:
|
|
314
|
+
"""Submit the current editor text as revised criteria."""
|
|
315
|
+
if self._edit_input is None:
|
|
316
|
+
return
|
|
317
|
+
criteria = self._edit_input.text.strip()
|
|
318
|
+
if not criteria:
|
|
319
|
+
self._hint_empty_submission("criteria")
|
|
320
|
+
return
|
|
321
|
+
self._submit({"type": "edited", "criteria": criteria})
|
|
322
|
+
|
|
323
|
+
def _submit_rejection(self) -> None:
|
|
324
|
+
"""Submit the current editor text as regeneration feedback."""
|
|
325
|
+
if self._edit_input is None:
|
|
326
|
+
return
|
|
327
|
+
message = self._edit_input.text.strip()
|
|
328
|
+
if not message:
|
|
329
|
+
self._hint_empty_submission("feedback")
|
|
330
|
+
return
|
|
331
|
+
self._submit({"type": "rejected", "message": message})
|
|
332
|
+
|
|
333
|
+
def _hint_empty_submission(self, what: str) -> None:
|
|
334
|
+
"""Explain why an empty editor submission did nothing.
|
|
335
|
+
|
|
336
|
+
Without this the editor silently no-ops on an empty Enter, leaving the
|
|
337
|
+
user unsure whether the keypress registered.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
what: Noun for the missing content (e.g. `criteria`, `feedback`).
|
|
341
|
+
"""
|
|
342
|
+
if self._help_widget is None:
|
|
343
|
+
return
|
|
344
|
+
glyphs = get_glyphs()
|
|
345
|
+
self._help_widget.update(
|
|
346
|
+
f"Enter some {what}, or press Esc to go back {glyphs.bullet} "
|
|
347
|
+
"Shift+Enter newline"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def _submit(self, result: GoalReviewResult) -> None:
|
|
351
|
+
"""Resolve the result future and post the decision message."""
|
|
352
|
+
if self._submitted:
|
|
353
|
+
return
|
|
354
|
+
self._submitted = True
|
|
355
|
+
self.display = False
|
|
356
|
+
if self._future is not None and not self._future.done():
|
|
357
|
+
self._future.set_result(result)
|
|
358
|
+
self.post_message(self.Decided(result, self))
|
|
359
|
+
|
|
360
|
+
def _update_options(self) -> None:
|
|
361
|
+
"""Render option labels and help text."""
|
|
362
|
+
for i, ((text, _), widget) in enumerate(
|
|
363
|
+
zip(_OPTIONS, self._option_widgets, strict=True)
|
|
364
|
+
):
|
|
365
|
+
cursor = f"{get_glyphs().cursor} " if i == self._selected else " "
|
|
366
|
+
widget.update(f"{cursor}{text}")
|
|
367
|
+
widget.remove_class("goal-review-option-selected")
|
|
368
|
+
if i == self._selected and self._input_mode is None:
|
|
369
|
+
widget.add_class("goal-review-option-selected")
|
|
370
|
+
|
|
371
|
+
if self._help_widget is None:
|
|
372
|
+
return
|
|
373
|
+
glyphs = get_glyphs()
|
|
374
|
+
if self._input_mode == "edit":
|
|
375
|
+
self._help_widget.update(
|
|
376
|
+
f"Enter save edits {glyphs.bullet} "
|
|
377
|
+
f"Shift+Enter newline {glyphs.bullet} Esc back"
|
|
378
|
+
)
|
|
379
|
+
return
|
|
380
|
+
if self._input_mode == "reject":
|
|
381
|
+
self._help_widget.update(
|
|
382
|
+
f"Enter regenerate {glyphs.bullet} "
|
|
383
|
+
f"Shift+Enter newline {glyphs.bullet} Esc back"
|
|
384
|
+
)
|
|
385
|
+
return
|
|
386
|
+
self._help_widget.update(
|
|
387
|
+
f"{glyphs.arrow_up}/{glyphs.arrow_down} navigate {glyphs.bullet} "
|
|
388
|
+
f"Enter select {glyphs.bullet} y/e/r/n quick keys {glyphs.bullet} "
|
|
389
|
+
"Esc cancel"
|
|
390
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Persistent inline display for the current goal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from textual.content import Content
|
|
8
|
+
from textual.widgets import Static
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from deepagents_code.resume_state import GoalStatus
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GoalStatusPanel(Static):
|
|
15
|
+
"""Keep the current goal and lifecycle state visible above the input."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, *, id: str | None = None) -> None: # noqa: A002
|
|
18
|
+
"""Initialize an empty hidden goal panel."""
|
|
19
|
+
super().__init__("", id=id, classes="goal-status-panel")
|
|
20
|
+
self.display = False
|
|
21
|
+
|
|
22
|
+
def set_goal(
|
|
23
|
+
self,
|
|
24
|
+
objective: str | None,
|
|
25
|
+
status: GoalStatus | None,
|
|
26
|
+
note: str | None,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Render the current goal or hide the panel when no goal exists.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
objective: Persisted goal objective, if set.
|
|
32
|
+
status: Current lifecycle state.
|
|
33
|
+
note: Blocker or completion note associated with the state.
|
|
34
|
+
"""
|
|
35
|
+
if not objective:
|
|
36
|
+
self.update("")
|
|
37
|
+
self.display = False
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
current = status or "active"
|
|
41
|
+
label = "completed" if current == "complete" else current
|
|
42
|
+
content = Content.from_markup(
|
|
43
|
+
"[bold]Goal · $status[/bold]\n$objective",
|
|
44
|
+
status=label,
|
|
45
|
+
objective=objective,
|
|
46
|
+
)
|
|
47
|
+
if note and current in {"blocked", "complete"}:
|
|
48
|
+
content += Content.from_markup("\n[dim]$note[/dim]", note=note)
|
|
49
|
+
self.update(content)
|
|
50
|
+
self.display = True
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Command history manager for input persistence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HistoryManager:
|
|
16
|
+
"""Manages command history with file persistence.
|
|
17
|
+
|
|
18
|
+
Uses append-only writes for concurrent safety. Multiple agents can
|
|
19
|
+
safely write to the same history file without corruption.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, history_file: Path, max_entries: int = 100) -> None:
|
|
23
|
+
"""Initialize the history manager.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
history_file: Path to the JSON-lines history file
|
|
27
|
+
max_entries: Maximum number of entries to keep
|
|
28
|
+
"""
|
|
29
|
+
self.history_file = history_file
|
|
30
|
+
self.max_entries = max_entries
|
|
31
|
+
self._entries: list[str] = []
|
|
32
|
+
self._current_index: int = -1
|
|
33
|
+
self._temp_input: str = ""
|
|
34
|
+
self._query: str = ""
|
|
35
|
+
self._load_history()
|
|
36
|
+
|
|
37
|
+
def _load_history(self) -> None:
|
|
38
|
+
"""Load history from file."""
|
|
39
|
+
if not self.history_file.exists():
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
with self.history_file.open("r", encoding="utf-8") as f:
|
|
44
|
+
entries = []
|
|
45
|
+
for raw_line in f:
|
|
46
|
+
line = raw_line.rstrip("\n\r")
|
|
47
|
+
if not line:
|
|
48
|
+
continue
|
|
49
|
+
try:
|
|
50
|
+
entry = json.loads(line)
|
|
51
|
+
except json.JSONDecodeError:
|
|
52
|
+
entry = line
|
|
53
|
+
entries.append(entry if isinstance(entry, str) else str(entry))
|
|
54
|
+
self._entries = entries[-self.max_entries :]
|
|
55
|
+
except (OSError, UnicodeDecodeError):
|
|
56
|
+
logger.warning(
|
|
57
|
+
"Failed to load history from %s; starting with empty history",
|
|
58
|
+
self.history_file,
|
|
59
|
+
exc_info=True,
|
|
60
|
+
)
|
|
61
|
+
self._entries = []
|
|
62
|
+
|
|
63
|
+
def _append_to_file(self, text: str) -> None:
|
|
64
|
+
"""Append a single entry to history file (concurrent-safe)."""
|
|
65
|
+
try:
|
|
66
|
+
self.history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
with self.history_file.open("a", encoding="utf-8") as f:
|
|
68
|
+
f.write(json.dumps(text) + "\n")
|
|
69
|
+
except OSError:
|
|
70
|
+
logger.warning(
|
|
71
|
+
"Failed to append history entry to %s",
|
|
72
|
+
self.history_file,
|
|
73
|
+
exc_info=True,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def _compact_history(self) -> None:
|
|
77
|
+
"""Rewrite history file to remove old entries.
|
|
78
|
+
|
|
79
|
+
Only called when entries exceed 2x max_entries to minimize rewrites.
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
self.history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
with self.history_file.open("w", encoding="utf-8") as f:
|
|
84
|
+
for entry in self._entries:
|
|
85
|
+
f.write(json.dumps(entry) + "\n")
|
|
86
|
+
except OSError:
|
|
87
|
+
logger.warning(
|
|
88
|
+
"Failed to compact history file %s",
|
|
89
|
+
self.history_file,
|
|
90
|
+
exc_info=True,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def add(self, text: str) -> None:
|
|
94
|
+
"""Add a command to history.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
text: The command text to add
|
|
98
|
+
"""
|
|
99
|
+
text = text.strip()
|
|
100
|
+
# Skip empty input and slash commands, except the explicit
|
|
101
|
+
# `/skill:<name>` form (case-insensitive), which is kept so users can
|
|
102
|
+
# recall it with up-arrow. Note: history stores the raw submitted text
|
|
103
|
+
# *before* app-layer alias rewriting, so convenience aliases such as
|
|
104
|
+
# `/remember` (later rewritten to `/skill:remember`) are dropped here
|
|
105
|
+
# despite being skill invocations.
|
|
106
|
+
lower_text = text.lower()
|
|
107
|
+
if not text or (text.startswith("/") and not lower_text.startswith("/skill:")):
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Skip duplicates of the last entry
|
|
111
|
+
if self._entries and self._entries[-1] == text:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
self._entries.append(text)
|
|
115
|
+
|
|
116
|
+
# Append to file (fast, concurrent-safe)
|
|
117
|
+
self._append_to_file(text)
|
|
118
|
+
|
|
119
|
+
# Compact only when we have 2x max entries (rare operation)
|
|
120
|
+
if len(self._entries) > self.max_entries * 2:
|
|
121
|
+
self._entries = self._entries[-self.max_entries :]
|
|
122
|
+
self._compact_history()
|
|
123
|
+
|
|
124
|
+
self.reset_navigation()
|
|
125
|
+
|
|
126
|
+
def get_previous(self, current_input: str, *, query: str = "") -> str | None:
|
|
127
|
+
"""Get the previous history entry matching a substring query.
|
|
128
|
+
|
|
129
|
+
The query is captured on the first call of a navigation session
|
|
130
|
+
(when `_current_index == -1`) and reused for all subsequent calls until
|
|
131
|
+
`reset_navigation`. Passing a different value on later calls has
|
|
132
|
+
no effect.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
current_input: Current input text. Saved only on the first call of a
|
|
136
|
+
navigation session; ignored on subsequent calls.
|
|
137
|
+
query: Substring to match against history entries.
|
|
138
|
+
Captured once on the first call of a navigation session.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Previous matching entry or `None`.
|
|
142
|
+
"""
|
|
143
|
+
if not self._entries:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
# Save current input and capture query on first navigation
|
|
147
|
+
if self._current_index == -1:
|
|
148
|
+
self._temp_input = current_input
|
|
149
|
+
self._current_index = len(self._entries)
|
|
150
|
+
self._query = query.strip().lower()
|
|
151
|
+
|
|
152
|
+
# Search backwards for matching entry
|
|
153
|
+
for i in range(self._current_index - 1, -1, -1):
|
|
154
|
+
if not self._query or self._query in self._entries[i].lower():
|
|
155
|
+
self._current_index = i
|
|
156
|
+
return self._entries[i]
|
|
157
|
+
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def get_next(self) -> str | None:
|
|
161
|
+
"""Get the next history entry matching the stored query.
|
|
162
|
+
|
|
163
|
+
Uses the query captured by the most recent `get_previous` call.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The next matching entry, or the original input when past the newest
|
|
167
|
+
match.
|
|
168
|
+
|
|
169
|
+
`None` if not currently navigating history.
|
|
170
|
+
"""
|
|
171
|
+
if self._current_index == -1:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
# Search forwards for matching entry
|
|
175
|
+
for i in range(self._current_index + 1, len(self._entries)):
|
|
176
|
+
if not self._query or self._query in self._entries[i].lower():
|
|
177
|
+
self._current_index = i
|
|
178
|
+
return self._entries[i]
|
|
179
|
+
|
|
180
|
+
# Return to original input at the end
|
|
181
|
+
result = self._temp_input
|
|
182
|
+
self.reset_navigation()
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def in_history(self) -> bool:
|
|
187
|
+
"""Whether currently navigating history entries."""
|
|
188
|
+
return self._current_index >= 0
|
|
189
|
+
|
|
190
|
+
def reset_navigation(self) -> None:
|
|
191
|
+
"""Reset navigation state."""
|
|
192
|
+
self._current_index = -1
|
|
193
|
+
self._temp_input = ""
|
|
194
|
+
self._query = ""
|