rlmness-cli 0.2.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.
Files changed (203) hide show
  1. rlmagent/__init__.py +1 -0
  2. rlmagent/__main__.py +8 -0
  3. rlmagent/acp/__init__.py +1 -0
  4. rlmagent/acp/runtime.py +71 -0
  5. rlmagent/acp/server.py +598 -0
  6. rlmagent/acp/updates.py +128 -0
  7. rlmagent/agentnet.py +237 -0
  8. rlmagent/agents/__init__.py +1 -0
  9. rlmagent/agents/approval.py +90 -0
  10. rlmagent/agents/bridge.py +109 -0
  11. rlmagent/agents/budget.py +212 -0
  12. rlmagent/agents/child_handles.py +46 -0
  13. rlmagent/agents/child_mailbox.py +30 -0
  14. rlmagent/agents/kernel_api.py +156 -0
  15. rlmagent/agents/output.py +34 -0
  16. rlmagent/agents/patch.py +107 -0
  17. rlmagent/agents/prompts.py +156 -0
  18. rlmagent/agents/trace.py +290 -0
  19. rlmagent/agents/tree.py +682 -0
  20. rlmagent/agents/wasm_api.py +105 -0
  21. rlmagent/agents/workspace.py +357 -0
  22. rlmagent/autopilot.py +447 -0
  23. rlmagent/backend.py +946 -0
  24. rlmagent/backend_context.py +74 -0
  25. rlmagent/cli/__init__.py +5 -0
  26. rlmagent/cli/config_cmd.py +177 -0
  27. rlmagent/cli/health.py +156 -0
  28. rlmagent/cli/main.py +1194 -0
  29. rlmagent/cli/update.py +188 -0
  30. rlmagent/clipboard.py +63 -0
  31. rlmagent/config/__init__.py +37 -0
  32. rlmagent/config/config.py +297 -0
  33. rlmagent/config/loader.py +119 -0
  34. rlmagent/config/prompts.py +124 -0
  35. rlmagent/config/secure_input.py +213 -0
  36. rlmagent/config/store.py +275 -0
  37. rlmagent/context/__init__.py +47 -0
  38. rlmagent/context/budget.py +557 -0
  39. rlmagent/contract.py +311 -0
  40. rlmagent/contract_run.py +232 -0
  41. rlmagent/conversation.py +2547 -0
  42. rlmagent/cron.py +77 -0
  43. rlmagent/directives.py +893 -0
  44. rlmagent/discovery.py +356 -0
  45. rlmagent/execution_log.py +66 -0
  46. rlmagent/factory.py +189 -0
  47. rlmagent/factory_graph.py +413 -0
  48. rlmagent/harness/__init__.py +63 -0
  49. rlmagent/harness/contracts/__init__.py +1 -0
  50. rlmagent/harness/contracts/stream.py +129 -0
  51. rlmagent/harness/contracts/tooling.py +127 -0
  52. rlmagent/harness/contracts/transcript/__init__.py +68 -0
  53. rlmagent/harness/contracts/transcript/_base.py +29 -0
  54. rlmagent/harness/contracts/transcript/diagnostics.py +52 -0
  55. rlmagent/harness/contracts/transcript/entries.py +180 -0
  56. rlmagent/harness/contracts/transcript/segments.py +64 -0
  57. rlmagent/harness/contracts/values.py +6 -0
  58. rlmagent/harness/driver.py +397 -0
  59. rlmagent/harness/engine.py +489 -0
  60. rlmagent/harness/mending.py +153 -0
  61. rlmagent/harness/provider/__init__.py +1 -0
  62. rlmagent/harness/provider/base.py +27 -0
  63. rlmagent/harness/provider/wire.py +189 -0
  64. rlmagent/harness/py.typed +0 -0
  65. rlmagent/harness/session/__init__.py +5 -0
  66. rlmagent/harness/session/index.py +218 -0
  67. rlmagent/harness/session/lease.py +66 -0
  68. rlmagent/harness/session/records.py +118 -0
  69. rlmagent/harness/session/replay.py +165 -0
  70. rlmagent/harness/session/store.py +155 -0
  71. rlmagent/harness/session/summary.py +46 -0
  72. rlmagent/hints.py +47 -0
  73. rlmagent/history.py +92 -0
  74. rlmagent/hooks/__init__.py +1 -0
  75. rlmagent/hooks/hooks.py +1 -0
  76. rlmagent/instructions.py +157 -0
  77. rlmagent/job_runner.py +117 -0
  78. rlmagent/jobs.py +365 -0
  79. rlmagent/kept_agents.py +397 -0
  80. rlmagent/kernel/__init__.py +5 -0
  81. rlmagent/kernel/session.py +205 -0
  82. rlmagent/kernel/wasm.py +169 -0
  83. rlmagent/logs.py +208 -0
  84. rlmagent/managed_release.py +368 -0
  85. rlmagent/mcp.py +411 -0
  86. rlmagent/mcp_oauth.py +213 -0
  87. rlmagent/model/__init__.py +29 -0
  88. rlmagent/model/_claude/__init__.py +1 -0
  89. rlmagent/model/_claude/courier.py +117 -0
  90. rlmagent/model/_claude/emitter.py +378 -0
  91. rlmagent/model/_oai/__init__.py +1 -0
  92. rlmagent/model/_oai/helpers.py +256 -0
  93. rlmagent/model/_oai/normalize.py +263 -0
  94. rlmagent/model/_oai/parsers.py +317 -0
  95. rlmagent/model/_oai/payloads.py +226 -0
  96. rlmagent/model/_oai/transport.py +127 -0
  97. rlmagent/model/claude.py +552 -0
  98. rlmagent/model/copilot.py +142 -0
  99. rlmagent/model/correlation.py +21 -0
  100. rlmagent/model/deadline.py +19 -0
  101. rlmagent/model/discovery.py +181 -0
  102. rlmagent/model/external/__init__.py +11 -0
  103. rlmagent/model/external/acp.py +202 -0
  104. rlmagent/model/external/acp_install.py +109 -0
  105. rlmagent/model/external/cli.py +248 -0
  106. rlmagent/model/external/textcalls.py +57 -0
  107. rlmagent/model/failover.py +129 -0
  108. rlmagent/model/google.py +199 -0
  109. rlmagent/model/limits.py +46 -0
  110. rlmagent/model/oai_compatible.py +414 -0
  111. rlmagent/model/preferences.py +56 -0
  112. rlmagent/model/py.typed +0 -0
  113. rlmagent/model/registered_oauth.py +195 -0
  114. rlmagent/model/scripted.py +55 -0
  115. rlmagent/model/settings.py +271 -0
  116. rlmagent/model/subscription.py +398 -0
  117. rlmagent/model/token_bounds.py +15 -0
  118. rlmagent/model/transport/__init__.py +1 -0
  119. rlmagent/model/transport/backoff.py +87 -0
  120. rlmagent/model/transport/client.py +87 -0
  121. rlmagent/model/transport/faults.py +65 -0
  122. rlmagent/model/transport/responses_socket.py +223 -0
  123. rlmagent/model/vertex.py +93 -0
  124. rlmagent/network.py +123 -0
  125. rlmagent/packages.py +246 -0
  126. rlmagent/planning.py +84 -0
  127. rlmagent/playbook/__init__.py +1 -0
  128. rlmagent/playbook/apply.py +106 -0
  129. rlmagent/playbook/digest.py +63 -0
  130. rlmagent/playbook/improver.py +272 -0
  131. rlmagent/playbook/plan.py +99 -0
  132. rlmagent/playbook/store.py +67 -0
  133. rlmagent/plugins.py +327 -0
  134. rlmagent/policy.py +105 -0
  135. rlmagent/prompt_refs.py +57 -0
  136. rlmagent/prompts.py +331 -0
  137. rlmagent/prune.py +60 -0
  138. rlmagent/py.typed +0 -0
  139. rlmagent/reasoning.py +135 -0
  140. rlmagent/refinement/__init__.py +1 -0
  141. rlmagent/refinement/controller.py +448 -0
  142. rlmagent/refinement/engine.py +220 -0
  143. rlmagent/refinement/evidence.py +131 -0
  144. rlmagent/refinement/scoped.py +90 -0
  145. rlmagent/refinement/store.py +193 -0
  146. rlmagent/refresh.py +202 -0
  147. rlmagent/routing.py +52 -0
  148. rlmagent/runtime.py +575 -0
  149. rlmagent/safety.py +267 -0
  150. rlmagent/session_html.py +57 -0
  151. rlmagent/session_import.py +52 -0
  152. rlmagent/session_share.py +55 -0
  153. rlmagent/sessions.py +249 -0
  154. rlmagent/shell_sandbox.py +88 -0
  155. rlmagent/skillset.py +320 -0
  156. rlmagent/steering.py +148 -0
  157. rlmagent/stops.py +62 -0
  158. rlmagent/subscriptions.py +219 -0
  159. rlmagent/tasks.py +98 -0
  160. rlmagent/toolhost.py +310 -0
  161. rlmagent/tools/__init__.py +56 -0
  162. rlmagent/tools/_shared.py +210 -0
  163. rlmagent/tools/files.py +462 -0
  164. rlmagent/tools/kernel.py +93 -0
  165. rlmagent/tools/patch_tool.py +68 -0
  166. rlmagent/tools/search.py +188 -0
  167. rlmagent/trust.py +383 -0
  168. rlmagent/tui/__init__.py +1 -0
  169. rlmagent/tui/adapter.py +506 -0
  170. rlmagent/tui/app.py +711 -0
  171. rlmagent/tui/approval_screen.py +57 -0
  172. rlmagent/tui/choice_screen.py +47 -0
  173. rlmagent/tui/completions.py +116 -0
  174. rlmagent/tui/components.py +25 -0
  175. rlmagent/tui/composer.py +40 -0
  176. rlmagent/tui/factory_view.py +86 -0
  177. rlmagent/tui/inspect.py +193 -0
  178. rlmagent/tui/messages.py +215 -0
  179. rlmagent/tui/model_screen.py +307 -0
  180. rlmagent/tui/prompt.py +98 -0
  181. rlmagent/tui/question_screen.py +52 -0
  182. rlmagent/tui/replay.py +141 -0
  183. rlmagent/tui/rlm_agent.tcss +405 -0
  184. rlmagent/tui/search_box.py +91 -0
  185. rlmagent/tui/sidebar.py +182 -0
  186. rlmagent/tui/status.py +49 -0
  187. rlmagent/tui/steps.py +283 -0
  188. rlmagent/tui/suggest.py +778 -0
  189. rlmagent/tui/transcript.py +172 -0
  190. rlmagent/tui/trust_dialog.py +58 -0
  191. rlmagent/tui/worker_attach.py +141 -0
  192. rlmagent/ui/__init__.py +1 -0
  193. rlmagent/ui/render.py +211 -0
  194. rlmagent/undo.py +109 -0
  195. rlmagent/utils/__init__.py +1 -0
  196. rlmagent/websearch.py +75 -0
  197. rlmagent/worker.py +1275 -0
  198. rlmagent/worker_service.py +203 -0
  199. rlmness_cli-0.2.0.dist-info/METADATA +882 -0
  200. rlmness_cli-0.2.0.dist-info/RECORD +203 -0
  201. rlmness_cli-0.2.0.dist-info/WHEEL +4 -0
  202. rlmness_cli-0.2.0.dist-info/entry_points.txt +2 -0
  203. rlmness_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
rlmagent/tui/replay.py ADDED
@@ -0,0 +1,141 @@
1
+ """Saved runs read back: step cards for a past session, and per-agent totals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from textual.app import App, ComposeResult
9
+ from textual.binding import Binding
10
+ from textual.widgets import Footer, Static
11
+
12
+ from rlmagent.tui.steps import StepBook, short_id
13
+
14
+
15
+ def events_path(sessions: Path, session_id: str) -> Path:
16
+ return sessions / f"{session_id}.backend.events.jsonl"
17
+
18
+
19
+ def load_events(path: Path) -> list[dict]:
20
+ """The run events a session saved, skipping lines that do not parse."""
21
+ events = []
22
+ for line in path.read_text(encoding="utf-8").splitlines():
23
+ try:
24
+ event = json.loads(line)
25
+ except json.JSONDecodeError:
26
+ continue
27
+ if isinstance(event, dict):
28
+ events.append(event)
29
+ return events
30
+
31
+
32
+ def runs_of(events: list[dict]) -> list[StepBook]:
33
+ """One book per run: a root run_started begins the next one."""
34
+ books: list[StepBook] = []
35
+ for event in events:
36
+ if event.get("kind") == "run_started" and not event.get("parent_run_id"):
37
+ books.append(StepBook())
38
+ if not books:
39
+ books.append(StepBook())
40
+ books[-1].feed(event)
41
+ return books
42
+
43
+
44
+ def agent_rows(books: list[StepBook]) -> list[dict]:
45
+ """Per-agent steps, tokens, cost and seconds over every run of a session."""
46
+ rows = []
47
+ for run, book in enumerate(books, 1):
48
+ for agent in book.agents.values():
49
+ steps = [book.steps[(agent.run_id, n)] for n in agent.steps]
50
+ costs = [s.cost for s in steps if s.cost is not None]
51
+ seconds = [s.seconds for s in steps if s.seconds is not None]
52
+ rows.append({
53
+ "run": run,
54
+ "agent": agent.run_id,
55
+ "parent": agent.parent,
56
+ "depth": agent.depth,
57
+ "status": agent.status,
58
+ "steps": len(steps),
59
+ "failed_steps": sum(1 for s in steps if s.state == "failed"),
60
+ "tokens": sum(s.tokens for s in steps),
61
+ "cost": round(sum(costs), 6) if costs else None,
62
+ "seconds": round(sum(seconds), 2) if seconds else None,
63
+ })
64
+ return rows
65
+
66
+
67
+ def format_rows(rows: list[dict]) -> str:
68
+ if not rows:
69
+ return "No agent steps were saved for this session."
70
+ lines = [f"{'run':>3} {'agent':<8} {'depth':>5} {'status':<9} {'steps':>5} "
71
+ f"{'tokens':>8} {'cost':>9} {'seconds':>8}"]
72
+ for row in rows:
73
+ cost = "?" if row["cost"] is None else f"${row['cost']:.4f}"
74
+ seconds = "?" if row["seconds"] is None else f"{row['seconds']:.1f}"
75
+ lines.append(f"{row['run']:>3} {short_id(row['agent']):<8} {row['depth']:>5} "
76
+ f"{row['status']:<9} {row['steps']:>5} {row['tokens']:>8} "
77
+ f"{cost:>9} {seconds:>8}")
78
+ tokens = sum(r["tokens"] for r in rows)
79
+ costs = [r["cost"] for r in rows if r["cost"] is not None]
80
+ total = f"${sum(costs):.4f}" if costs else "unknown"
81
+ lines.append(f"total: {len(rows)} agents, {sum(r['steps'] for r in rows)} steps, "
82
+ f"{tokens} tokens, cost {total}")
83
+ return "\n".join(lines)
84
+
85
+
86
+ class ReplayApp(App[None]):
87
+ """A saved session's steps as cards, read only."""
88
+
89
+ CSS_PATH = Path(__file__).with_name("rlm_agent.tcss")
90
+ TITLE = "rlm-agent replay"
91
+ BINDINGS = [Binding("q", "quit", "Quit"), Binding("f2", "agent_tree", "Agents"),
92
+ Binding("f3", "timeline", "Timeline")]
93
+
94
+ def __init__(self, session_id: str, books: list[StepBook]) -> None:
95
+ super().__init__()
96
+ self.session_id = session_id
97
+ self.books = books
98
+
99
+ def compose(self) -> ComposeResult:
100
+ from rlmagent.tui.transcript import TranscriptView
101
+
102
+ yield Static(f"session {self.session_id} · {len(self.books)} runs", classes="step-foot")
103
+ yield TranscriptView()
104
+ yield Footer()
105
+
106
+ def on_mount(self) -> None:
107
+ from rlmagent.tui.transcript import TranscriptView
108
+
109
+ view = self.query_one(TranscriptView)
110
+ for run, book in enumerate(self.books, 1):
111
+ view.add_notice(f"run {run}")
112
+ view.new_run()
113
+ for key in book.order:
114
+ view.show_step(book.steps[key])
115
+
116
+ def on_step_card_inspect(self, event) -> None:
117
+ from rlmagent.tui.inspect import StepScreen
118
+
119
+ self.push_screen(StepScreen(event.step))
120
+
121
+ def _merged(self) -> StepBook:
122
+ merged = StepBook()
123
+ for book in self.books:
124
+ merged.agents.update(book.agents)
125
+ merged.steps.update(book.steps)
126
+ merged.child_names.update(book.child_names)
127
+ merged.child_notes.update(book.child_notes)
128
+ return merged
129
+
130
+ def action_agent_tree(self) -> None:
131
+ from rlmagent.tui.inspect import AgentTreeScreen
132
+
133
+ self.push_screen(AgentTreeScreen(self._merged()))
134
+
135
+ def action_timeline(self) -> None:
136
+ from rlmagent.tui.inspect import TimelineScreen
137
+
138
+ self.push_screen(TimelineScreen(self.books[-1] if self.books else StepBook()))
139
+
140
+
141
+ __all__ = ["ReplayApp", "agent_rows", "events_path", "format_rows", "load_events", "runs_of"]
@@ -0,0 +1,405 @@
1
+ /* rlm-agent TUI — black, minimal, no chrome colour.
2
+ Geometry and palette live here so widget modules stay presentation-free. */
3
+
4
+ Screen {
5
+ layout: vertical;
6
+ background: #000000;
7
+ color: #c8c8c8;
8
+ }
9
+
10
+ Header {
11
+ background: #000000;
12
+ color: #8a8a8a;
13
+ text-style: none;
14
+ }
15
+
16
+ Footer {
17
+ background: #000000;
18
+ color: #6a6a6a;
19
+ }
20
+
21
+ FooterKey {
22
+ background: #000000;
23
+ color: #6a6a6a;
24
+ }
25
+
26
+ FooterKey > .footer-key--key {
27
+ background: #000000;
28
+ color: #9a9a9a;
29
+ }
30
+
31
+ /* Row between header and footer: sidebar | main column. */
32
+ #body {
33
+ height: 1fr;
34
+ width: 100%;
35
+ background: #000000;
36
+ }
37
+
38
+ Sidebar {
39
+ width: 28;
40
+ min-width: 18;
41
+ padding: 1 1;
42
+ background: #000000;
43
+ border-right: solid #1e1e1e;
44
+ }
45
+
46
+ #sidebar-title {
47
+ padding: 0 1 1 1;
48
+ color: #5a5a5a;
49
+ text-style: none;
50
+ }
51
+
52
+ #session-list {
53
+ height: 1fr;
54
+ background: #000000;
55
+ border: none;
56
+ scrollbar-size: 0 0;
57
+ }
58
+
59
+ #session-list > ListItem {
60
+ padding: 0 1;
61
+ background: #000000;
62
+ color: #7a7a7a;
63
+ }
64
+
65
+ #session-list > ListItem.active-session {
66
+ color: #e8e8e8;
67
+ }
68
+
69
+ #session-list > ListItem.-highlight {
70
+ background: #000000;
71
+ color: #c9a4ff;
72
+ text-style: none;
73
+ }
74
+
75
+ #main {
76
+ width: 1fr;
77
+ height: 100%;
78
+ background: #000000;
79
+ }
80
+
81
+ /* Transcript flexes; the composer below it is fixed height. */
82
+ TranscriptView {
83
+ height: 1fr;
84
+ padding: 1 2;
85
+ background: #000000;
86
+ scrollbar-size: 0 0;
87
+ }
88
+
89
+ /* ── composer ─────────────────────────────────────────────────────── */
90
+
91
+ Composer {
92
+ height: 3;
93
+ background: #000000;
94
+ border: solid #1e1e1e;
95
+ }
96
+
97
+ Composer:focus-within {
98
+ border: solid #333333;
99
+ }
100
+
101
+ #prompt-mark {
102
+ width: 3;
103
+ padding: 0 1;
104
+ color: #c9a4ff;
105
+ background: #000000;
106
+ }
107
+
108
+ PromptInput {
109
+ width: 1fr;
110
+ height: 1fr;
111
+ background: #000000;
112
+ border: none;
113
+ padding: 0;
114
+ scrollbar-size: 0 0;
115
+ }
116
+
117
+ #model-badge {
118
+ width: auto;
119
+ padding: 0 1;
120
+ color: #5a5a5a;
121
+ background: #000000;
122
+ content-align: right middle;
123
+ }
124
+
125
+ /* ── autocomplete ─────────────────────────────────────────────────── */
126
+
127
+ #completion-bar {
128
+ height: auto;
129
+ max-height: 8;
130
+ background: #0a0a0a;
131
+ border: solid #1e1e1e;
132
+ padding: 0 1;
133
+ }
134
+
135
+ #completion-body {
136
+ color: #7a7a7a;
137
+ background: #0a0a0a;
138
+ }
139
+
140
+ /* ── model picker modal ───────────────────────────────────────────── */
141
+
142
+ ModelScreen {
143
+ align: center middle;
144
+ background: #000000 70%;
145
+ }
146
+
147
+ #model-dialog {
148
+ width: 52;
149
+ height: auto;
150
+ max-height: 16;
151
+ padding: 1 2;
152
+ background: #0a0a0a;
153
+ border: solid #333333;
154
+ }
155
+
156
+ #model-title {
157
+ padding: 0 0 1 0;
158
+ color: #e8e8e8;
159
+ }
160
+
161
+ #model-list {
162
+ height: auto;
163
+ max-height: 8;
164
+ background: #0a0a0a;
165
+ border: none;
166
+ scrollbar-size: 0 0;
167
+ }
168
+
169
+ #model-list > ListItem {
170
+ padding: 0 1;
171
+ background: #0a0a0a;
172
+ color: #9a9a9a;
173
+ }
174
+
175
+ #model-list > ListItem.-highlight {
176
+ background: #0a0a0a;
177
+ color: #c9a4ff;
178
+ text-style: none;
179
+ }
180
+
181
+ #model-key {
182
+ background: #0a0a0a;
183
+ color: #e8e8e8;
184
+ border: solid #1e1e1e;
185
+ }
186
+
187
+ #model-key:focus {
188
+ border: solid #333333;
189
+ }
190
+
191
+ #model-hint {
192
+ padding: 1 0 0 0;
193
+ color: #5a5a5a;
194
+ }
195
+
196
+ /* ── status ───────────────────────────────────────────────────────── */
197
+
198
+ #status-bar {
199
+ height: 1;
200
+ padding: 0 1;
201
+ background: #000000;
202
+ color: #5a5a5a;
203
+ }
204
+
205
+ /* Visible while a generation is running and can be stopped. */
206
+ #status-bar.busy {
207
+ color: #c9a4ff;
208
+ }
209
+
210
+ /* ── transcript blocks ────────────────────────────────────────────── */
211
+
212
+ .user-block {
213
+ padding: 1 0 0 0;
214
+ color: #e8e8e8;
215
+ }
216
+
217
+ .assistant-block {
218
+ padding: 0 0 1 0;
219
+ color: #b4b4b4;
220
+ }
221
+
222
+ .tool-code {
223
+ padding: 0 0 0 4;
224
+ height: auto;
225
+ }
226
+
227
+ .tool-line {
228
+ padding: 0 0 0 2;
229
+ color: #6a6a6a;
230
+ }
231
+
232
+ .tool-error {
233
+ color: #a05050;
234
+ }
235
+
236
+ .tool-output {
237
+ padding: 0 0 0 2;
238
+ height: auto;
239
+ color: #6a6a6a;
240
+ }
241
+
242
+ .notice-block {
243
+ padding: 0 0 1 0;
244
+ color: #5a5a5a;
245
+ }
246
+
247
+ #approval-dialog {
248
+ width: 80;
249
+ height: auto;
250
+ max-height: 30;
251
+ padding: 1 2;
252
+ background: #0a0a0a;
253
+ border: solid #333333;
254
+ }
255
+
256
+ #approval-body {
257
+ max-height: 18;
258
+ overflow-y: auto;
259
+ margin: 1 0;
260
+ }
261
+
262
+ /* Step cards: one per agent step, indented by depth in code. */
263
+ .step-card {
264
+ height: auto;
265
+ margin-bottom: 1;
266
+ padding: 0 1;
267
+ border-left: solid #2a2a2a;
268
+ }
269
+
270
+ .step-card.step-live {
271
+ border-left: solid #c9a4ff;
272
+ }
273
+
274
+ .step-card.step-failed {
275
+ border-left: solid #d36a6a;
276
+ }
277
+
278
+ .step-head {
279
+ color: #8a8a8a;
280
+ }
281
+
282
+ .step-card.step-live > .step-head {
283
+ color: #c9a4ff;
284
+ }
285
+
286
+ .step-output {
287
+ color: #9a9a9a;
288
+ }
289
+
290
+ .step-foot {
291
+ color: #5a5a5a;
292
+ }
293
+
294
+ .step-card:focus {
295
+ background: #0d0d0d;
296
+ }
297
+
298
+ .inspect-screen {
299
+ align: center middle;
300
+ background: #000000 80%;
301
+ }
302
+
303
+ .inspect-body {
304
+ width: 90%;
305
+ height: 90%;
306
+ padding: 1 2;
307
+ background: #000000;
308
+ border: solid #2a2a2a;
309
+ }
310
+
311
+ #task-panel {
312
+ height: auto;
313
+ max-height: 50%;
314
+ padding: 1 1 0 1;
315
+ color: #8a8a8a;
316
+ border-top: solid #1e1e1e;
317
+ }
318
+
319
+ /* ── folded errors, shell box, echoes, side answers, queue ───────── */
320
+
321
+ .error-block {
322
+ height: auto;
323
+ padding: 0 0 1 0;
324
+ }
325
+
326
+ .shell-block {
327
+ height: auto;
328
+ margin: 0 0 1 2;
329
+ padding: 0 1;
330
+ color: #9a9a9a;
331
+ border: round #333333;
332
+ border-title-color: #c9a4ff;
333
+ border-subtitle-color: #5a5a5a;
334
+ }
335
+
336
+ .shell-block.shell-failed {
337
+ border: round #a05050;
338
+ }
339
+
340
+ .command-echo {
341
+ padding: 1 0 0 0;
342
+ color: #9a9a9a;
343
+ }
344
+
345
+ .side-answer {
346
+ height: auto;
347
+ margin: 0 0 1 2;
348
+ padding: 0 1;
349
+ color: #9a9a9a;
350
+ border-left: thick #c9a4ff;
351
+ }
352
+
353
+ .kind-notice {
354
+ padding: 0 0 1 0;
355
+ color: #6a6a6a;
356
+ }
357
+
358
+ #queued-strip {
359
+ height: auto;
360
+ max-height: 6;
361
+ padding: 0 1;
362
+ color: #8a8a8a;
363
+ background: #0a0a0a;
364
+ border-top: solid #1e1e1e;
365
+ }
366
+
367
+ /* ── session search and sections ─────────────────────────────────── */
368
+
369
+ #session-search {
370
+ height: 3;
371
+ margin: 0 0 1 0;
372
+ background: #0a0a0a;
373
+ border: solid #1e1e1e;
374
+ color: #c8c8c8;
375
+ }
376
+
377
+ #session-search:focus {
378
+ border: solid #333333;
379
+ }
380
+
381
+ #session-list > ListItem.session-heading {
382
+ padding: 1 1 0 1;
383
+ color: #5a5a5a;
384
+ background: #000000;
385
+ }
386
+
387
+ /* ── session search and sections ─────────────────────────────────── */
388
+
389
+ #session-search {
390
+ height: 3;
391
+ margin: 0 0 1 0;
392
+ background: #0a0a0a;
393
+ border: solid #1e1e1e;
394
+ color: #c8c8c8;
395
+ }
396
+
397
+ #session-search:focus {
398
+ border: solid #333333;
399
+ }
400
+
401
+ #session-list > ListItem.session-heading {
402
+ padding: 1 1 0 1;
403
+ color: #5a5a5a;
404
+ background: #000000;
405
+ }
@@ -0,0 +1,91 @@
1
+ """A one-line search field with undo and Emacs-style kill and yank.
2
+
3
+ Ctrl+Z undoes the last change. Ctrl+K cuts to the end, Ctrl+U to the start,
4
+ Ctrl+W the word before the cursor; Ctrl+Y puts the last cut back at the
5
+ cursor. Cuts in a row join into one, as a terminal user would expect.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from textual.binding import Binding
11
+ from textual.widgets import Input
12
+
13
+ #: How many earlier values Ctrl+Z can step back through.
14
+ UNDO_DEPTH = 50
15
+
16
+
17
+ class SearchBox(Input):
18
+ """``Input`` plus undo history and a single-slot kill ring."""
19
+
20
+ BINDINGS = [
21
+ Binding("ctrl+z", "undo", "Undo", show=False),
22
+ Binding("ctrl+k", "kill_to_end", "Cut to end", show=False),
23
+ Binding("ctrl+u", "kill_to_start", "Cut to start", show=False),
24
+ Binding("ctrl+w", "kill_word", "Cut word", show=False),
25
+ Binding("ctrl+y", "yank", "Paste cut", show=False),
26
+ ]
27
+
28
+ def __init__(self, *args, **kwargs) -> None:
29
+ super().__init__(*args, **kwargs)
30
+ self._history: list[str] = []
31
+ self._last = ""
32
+ self._undoing = False
33
+ self.killed = ""
34
+ self._killing = False
35
+
36
+ def on_input_changed(self, event: Input.Changed) -> None:
37
+ # Record the value before each change so Ctrl+Z can return to it.
38
+ if event.input is self and not self._undoing and event.value != self._last:
39
+ self._history.append(self._last)
40
+ del self._history[:-UNDO_DEPTH]
41
+ self._last = event.value
42
+ self._undoing = False
43
+
44
+ def action_undo(self) -> None:
45
+ if self._history:
46
+ self._undoing = True
47
+ self.value = self._history.pop()
48
+ self.cursor_position = len(self.value)
49
+
50
+ def _cut(self, start: int, end: int, *, before: bool) -> None:
51
+ piece = self.value[start:end]
52
+ if not piece:
53
+ return
54
+ # Consecutive cuts build one piece, in reading order.
55
+ if self._killing:
56
+ self.killed = piece + self.killed if before else self.killed + piece
57
+ else:
58
+ self.killed = piece
59
+ self.value = self.value[:start] + self.value[end:]
60
+ self.cursor_position = start
61
+ self._killing = True
62
+
63
+ def action_kill_to_end(self) -> None:
64
+ self._cut(self.cursor_position, len(self.value), before=False)
65
+
66
+ def action_kill_to_start(self) -> None:
67
+ self._cut(0, self.cursor_position, before=True)
68
+
69
+ def action_kill_word(self) -> None:
70
+ end = self.cursor_position
71
+ start = end
72
+ while start and self.value[start - 1] == " ":
73
+ start -= 1
74
+ while start and self.value[start - 1] != " ":
75
+ start -= 1
76
+ self._cut(start, end, before=True)
77
+
78
+ def action_yank(self) -> None:
79
+ if self.killed:
80
+ at = self.cursor_position
81
+ self.value = self.value[:at] + self.killed + self.value[at:]
82
+ self.cursor_position = at + len(self.killed)
83
+ self._killing = False
84
+
85
+ def on_key(self, event) -> None:
86
+ # Any other key ends a run of cuts.
87
+ if event.key not in ("ctrl+k", "ctrl+u", "ctrl+w"):
88
+ self._killing = False
89
+
90
+
91
+ __all__ = ["SearchBox", "UNDO_DEPTH"]