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.
Files changed (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. 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 = ""