omega-code 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.
- omega/__init__.py +0 -0
- omega/__main__.py +589 -0
- omega/artifacts.py +151 -0
- omega/checkpoint.py +246 -0
- omega/compact.py +106 -0
- omega/config.py +285 -0
- omega/eval/__init__.py +3 -0
- omega/eval/cli.py +127 -0
- omega/eval/examples/plan-version-flag.yaml +11 -0
- omega/eval/examples/relative-age-negative-delta.yaml +14 -0
- omega/eval/examples/version-flag.yaml +10 -0
- omega/eval/manifest.py +129 -0
- omega/eval/prices.py +29 -0
- omega/eval/report.py +135 -0
- omega/eval/runner.py +199 -0
- omega/eval/tasks.py +97 -0
- omega/events.py +145 -0
- omega/export.py +80 -0
- omega/gitlog.py +229 -0
- omega/hooks.py +63 -0
- omega/instructions.py +103 -0
- omega/integrations.py +284 -0
- omega/keys.py +173 -0
- omega/llm.py +442 -0
- omega/loop.py +510 -0
- omega/mcp.py +490 -0
- omega/memory/__init__.py +5 -0
- omega/memory/consolidate.py +103 -0
- omega/memory/curate.py +69 -0
- omega/memory/store.py +321 -0
- omega/memory/tools.py +175 -0
- omega/migrate.py +40 -0
- omega/onboarding.py +242 -0
- omega/permissions.py +137 -0
- omega/secrets.py +173 -0
- omega/server/__init__.py +7 -0
- omega/server/__main__.py +18 -0
- omega/server/app.py +71 -0
- omega/server/auth.py +73 -0
- omega/server/manager.py +287 -0
- omega/server/models.py +123 -0
- omega/server/tasks_api.py +311 -0
- omega/server/terminals.py +245 -0
- omega/server/worker.py +186 -0
- omega/session.py +209 -0
- omega/setup.html +281 -0
- omega/setup_server.py +452 -0
- omega/skills.py +158 -0
- omega/subagent.py +98 -0
- omega/tasks.py +195 -0
- omega/tools.py +590 -0
- omega/trace.py +156 -0
- omega/trajectory.py +146 -0
- omega/ui/__init__.py +0 -0
- omega/ui/composer.py +140 -0
- omega/ui/format.py +708 -0
- omega/ui/plain.py +141 -0
- omega/ui/tui/__init__.py +9 -0
- omega/ui/tui/app.py +958 -0
- omega/ui/tui/history.py +50 -0
- omega/ui/tui/modals.py +292 -0
- omega/ui/tui/onboarding.py +367 -0
- omega/ui/tui/prefs.py +25 -0
- omega/ui/tui/sidebar.py +510 -0
- omega/ui/tui/status.py +115 -0
- omega/ui/tui/theme.py +91 -0
- omega/ui/tui/transcript.py +783 -0
- omega/verify.py +133 -0
- omega_code-0.4.0.dist-info/METADATA +479 -0
- omega_code-0.4.0.dist-info/RECORD +73 -0
- omega_code-0.4.0.dist-info/WHEEL +4 -0
- omega_code-0.4.0.dist-info/entry_points.txt +2 -0
- omega_code-0.4.0.dist-info/licenses/LICENSE +21 -0
omega/ui/tui/history.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""~/.omega/history file-backed line history with up/down cycling. Pure Python
|
|
2
|
+
(no Textual dependency) so it is trivially unit-testable and reusable if
|
|
3
|
+
another front end ever wants it."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InputHistory:
|
|
10
|
+
def __init__(self, path: Path) -> None:
|
|
11
|
+
self.path = path
|
|
12
|
+
self.lines: list[str] = self._load()
|
|
13
|
+
self.pos: int = len(self.lines)
|
|
14
|
+
|
|
15
|
+
def _load(self) -> list[str]:
|
|
16
|
+
if not self.path.exists():
|
|
17
|
+
return []
|
|
18
|
+
return [line for line in self.path.read_text().splitlines() if line]
|
|
19
|
+
|
|
20
|
+
def append(self, line: str) -> None:
|
|
21
|
+
self.lines.append(line)
|
|
22
|
+
self.pos = len(self.lines)
|
|
23
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
with self.path.open("a") as f:
|
|
25
|
+
f.write(line + "\n")
|
|
26
|
+
|
|
27
|
+
def prev(self, current: str) -> str | None:
|
|
28
|
+
"""One entry back, or None if already at the oldest entry or the
|
|
29
|
+
input holds unsaved free text (not empty, not a history entry)."""
|
|
30
|
+
if self.pos <= 0 or not self._showing(current):
|
|
31
|
+
return None
|
|
32
|
+
self.pos -= 1
|
|
33
|
+
return self.lines[self.pos]
|
|
34
|
+
|
|
35
|
+
def next(self, current: str) -> str | None:
|
|
36
|
+
"""One entry forward, "" once past the newest entry, or None if the
|
|
37
|
+
input holds unsaved free text."""
|
|
38
|
+
if not self._showing(current):
|
|
39
|
+
return None
|
|
40
|
+
if self.pos >= len(self.lines) - 1:
|
|
41
|
+
self.pos = len(self.lines)
|
|
42
|
+
return ""
|
|
43
|
+
self.pos += 1
|
|
44
|
+
return self.lines[self.pos]
|
|
45
|
+
|
|
46
|
+
def _showing(self, current: str) -> bool:
|
|
47
|
+
return current == "" or (0 <= self.pos < len(self.lines) and self.lines[self.pos] == current)
|
|
48
|
+
|
|
49
|
+
def reset(self) -> None:
|
|
50
|
+
self.pos = len(self.lines)
|
omega/ui/tui/modals.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Modal screens: the permission-confirm prompt, the ask_user dialog, the
|
|
2
|
+
model/session pickers, and diff/checkpoint views."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rich.syntax import Syntax
|
|
9
|
+
from textual.app import ComposeResult
|
|
10
|
+
from textual.binding import Binding
|
|
11
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
12
|
+
from textual.message import Message
|
|
13
|
+
from textual.screen import ModalScreen
|
|
14
|
+
from textual.widgets import Button, Input, OptionList, Static
|
|
15
|
+
from textual.widgets.option_list import Option
|
|
16
|
+
|
|
17
|
+
from ... import events, gitlog, permissions, session
|
|
18
|
+
from ...config import Model
|
|
19
|
+
from .. import format
|
|
20
|
+
from . import theme
|
|
21
|
+
|
|
22
|
+
_DIALOG_CSS = """
|
|
23
|
+
Vertical#dialog {
|
|
24
|
+
width: 70%;
|
|
25
|
+
max-width: 90;
|
|
26
|
+
height: auto;
|
|
27
|
+
max-height: 80%;
|
|
28
|
+
padding: 1 2;
|
|
29
|
+
border: round $accent;
|
|
30
|
+
}
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ConfirmScreen(ModalScreen[bool]):
|
|
35
|
+
"""Mirrors `ui.plain.confirm`: y/n/a, default deny."""
|
|
36
|
+
|
|
37
|
+
DEFAULT_CSS = _DIALOG_CSS + """
|
|
38
|
+
ConfirmScreen { align: center middle; }
|
|
39
|
+
ConfirmScreen #buttons { margin-top: 1; height: 3; align: center middle; }
|
|
40
|
+
ConfirmScreen #buttons Button { margin: 0 1; }
|
|
41
|
+
"""
|
|
42
|
+
BINDINGS = [
|
|
43
|
+
Binding("y", "answer(True)", "Allow", show=True),
|
|
44
|
+
Binding("n", "answer(False)", "Deny", show=True),
|
|
45
|
+
Binding("a", "answer_always", "Always", show=True),
|
|
46
|
+
Binding("escape", "answer(False)", "Deny", show=False),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
def __init__(self, name: str, args: dict[str, Any], why: str) -> None:
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.tool_name = name
|
|
52
|
+
self.tool_args = args
|
|
53
|
+
self.why = why
|
|
54
|
+
self.detail = str(args.get("command") or args.get("path") or args.get("name") or "")[:200]
|
|
55
|
+
|
|
56
|
+
def compose(self) -> ComposeResult:
|
|
57
|
+
with Vertical(id="dialog"):
|
|
58
|
+
yield Static(f"[yellow]⏸ {self.tool_name}[/yellow] [dim]{self.why}[/dim]")
|
|
59
|
+
if self.detail:
|
|
60
|
+
yield Static(f"[bold]{self.detail}[/bold]")
|
|
61
|
+
with Horizontal(id="buttons"):
|
|
62
|
+
yield Button("Allow (y)", id="yes", variant="success")
|
|
63
|
+
yield Button("Deny (n)", id="no", variant="error")
|
|
64
|
+
yield Button("Always (a)", id="always", variant="primary")
|
|
65
|
+
|
|
66
|
+
def action_answer(self, allow: bool) -> None:
|
|
67
|
+
self.dismiss(allow)
|
|
68
|
+
|
|
69
|
+
def action_answer_always(self) -> None:
|
|
70
|
+
permissions.remember(permissions.rule_for(self.tool_name, self.tool_args), permissions.ALLOW)
|
|
71
|
+
self.dismiss(True)
|
|
72
|
+
|
|
73
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
74
|
+
if event.button.id == "yes":
|
|
75
|
+
self.action_answer(True)
|
|
76
|
+
elif event.button.id == "no":
|
|
77
|
+
self.action_answer(False)
|
|
78
|
+
elif event.button.id == "always":
|
|
79
|
+
self.action_answer_always()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class OptionToggled(Message):
|
|
83
|
+
"""A row in an `_AskOptionList` was toggled with space (multi_select only)."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, index: int) -> None:
|
|
86
|
+
self.index = index
|
|
87
|
+
super().__init__()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _AskOptionList(OptionList):
|
|
91
|
+
BINDINGS = [*OptionList.BINDINGS, Binding("space", "toggle_option", show=False)]
|
|
92
|
+
|
|
93
|
+
def action_toggle_option(self) -> None:
|
|
94
|
+
if self.highlighted is not None:
|
|
95
|
+
self.post_message(OptionToggled(self.highlighted))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class AskUserScreen(ModalScreen[str]):
|
|
99
|
+
"""Mirrors `ui.plain.ask_user`'s contract: returns the selected label(s)
|
|
100
|
+
joined by ", ", free text, or "(no answer)"."""
|
|
101
|
+
|
|
102
|
+
DEFAULT_CSS = _DIALOG_CSS + """
|
|
103
|
+
AskUserScreen { align: center middle; }
|
|
104
|
+
AskUserScreen #options { height: auto; max-height: 12; margin-top: 1; }
|
|
105
|
+
AskUserScreen #freetext { margin-top: 1; }
|
|
106
|
+
"""
|
|
107
|
+
BINDINGS = [Binding("escape", "cancel", show=False)]
|
|
108
|
+
|
|
109
|
+
def __init__(self, question: str, options: list[events.Option], multi_select: bool) -> None:
|
|
110
|
+
super().__init__()
|
|
111
|
+
self.question = question
|
|
112
|
+
self.options = options
|
|
113
|
+
self.multi_select = multi_select
|
|
114
|
+
self.selected: set[int] = set()
|
|
115
|
+
|
|
116
|
+
def compose(self) -> ComposeResult:
|
|
117
|
+
with Vertical(id="dialog"):
|
|
118
|
+
yield Static(f"[yellow]?[/yellow] {self.question}")
|
|
119
|
+
yield _AskOptionList(*self._render_options(), id="options")
|
|
120
|
+
yield Input(placeholder="or type a free-text answer…", id="freetext")
|
|
121
|
+
|
|
122
|
+
def on_mount(self) -> None:
|
|
123
|
+
if self.options:
|
|
124
|
+
self.query_one("#options", _AskOptionList).focus()
|
|
125
|
+
else:
|
|
126
|
+
self.query_one("#freetext", Input).focus()
|
|
127
|
+
|
|
128
|
+
def _render_options(self) -> list[Option]:
|
|
129
|
+
rendered = []
|
|
130
|
+
for i, opt in enumerate(self.options):
|
|
131
|
+
label = opt.get("label", "")
|
|
132
|
+
desc = opt.get("description", "")
|
|
133
|
+
mark = "✓ " if i in self.selected else " "
|
|
134
|
+
text = f"{mark}{label}" + (f" — {desc}" if desc else "")
|
|
135
|
+
rendered.append(Option(text, id=f"opt-{i}"))
|
|
136
|
+
return rendered
|
|
137
|
+
|
|
138
|
+
def _refresh_marks(self) -> None:
|
|
139
|
+
options = self.query_one("#options", _AskOptionList)
|
|
140
|
+
for i in range(len(self.options)):
|
|
141
|
+
label = self.options[i].get("label", "")
|
|
142
|
+
desc = self.options[i].get("description", "")
|
|
143
|
+
mark = "✓ " if i in self.selected else " "
|
|
144
|
+
text = f"{mark}{label}" + (f" — {desc}" if desc else "")
|
|
145
|
+
options.replace_option_prompt(f"opt-{i}", text)
|
|
146
|
+
|
|
147
|
+
def on_option_toggled(self, message: OptionToggled) -> None:
|
|
148
|
+
if not self.multi_select:
|
|
149
|
+
return
|
|
150
|
+
if message.index in self.selected:
|
|
151
|
+
self.selected.discard(message.index)
|
|
152
|
+
else:
|
|
153
|
+
self.selected.add(message.index)
|
|
154
|
+
self._refresh_marks()
|
|
155
|
+
|
|
156
|
+
def on_option_list_option_selected(self, message: OptionList.OptionSelected) -> None:
|
|
157
|
+
if self.multi_select and self.selected:
|
|
158
|
+
labels = [self.options[i].get("label", "") for i in sorted(self.selected)]
|
|
159
|
+
self.dismiss(", ".join(labels))
|
|
160
|
+
else:
|
|
161
|
+
self.dismiss(self.options[message.option_index].get("label", ""))
|
|
162
|
+
|
|
163
|
+
def on_input_submitted(self, message: Input.Submitted) -> None:
|
|
164
|
+
self.dismiss(message.value or "(no answer)")
|
|
165
|
+
|
|
166
|
+
def action_cancel(self) -> None:
|
|
167
|
+
self.dismiss("(no answer)")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ModelPickerScreen(ModalScreen[str | None]):
|
|
171
|
+
"""`/model` with no argument: pick a catalog alias. Returns the chosen
|
|
172
|
+
alias, or None on cancel."""
|
|
173
|
+
|
|
174
|
+
DEFAULT_CSS = _DIALOG_CSS + """
|
|
175
|
+
ModelPickerScreen { align: center middle; }
|
|
176
|
+
ModelPickerScreen #models { height: auto; max-height: 14; margin-top: 1; }
|
|
177
|
+
"""
|
|
178
|
+
BINDINGS = [Binding("escape", "cancel", show=False)]
|
|
179
|
+
|
|
180
|
+
def __init__(self, models: dict[str, Model], current: str | None) -> None:
|
|
181
|
+
super().__init__()
|
|
182
|
+
self.aliases = sorted(models)
|
|
183
|
+
self.models = models
|
|
184
|
+
self.current = current
|
|
185
|
+
|
|
186
|
+
def compose(self) -> ComposeResult:
|
|
187
|
+
with Vertical(id="dialog"):
|
|
188
|
+
yield Static("[yellow]Choose a model[/yellow]")
|
|
189
|
+
yield OptionList(*self._render_options(), id="models")
|
|
190
|
+
|
|
191
|
+
def on_mount(self) -> None:
|
|
192
|
+
self.query_one("#models", OptionList).focus()
|
|
193
|
+
|
|
194
|
+
def _render_options(self) -> list[Option]:
|
|
195
|
+
rendered = []
|
|
196
|
+
for alias in self.aliases:
|
|
197
|
+
m = self.models[alias]
|
|
198
|
+
mark = "› " if alias == self.current else " "
|
|
199
|
+
text = (f"{mark}{alias:<10}{m.model:<26}{m.provider:<14}"
|
|
200
|
+
f"{m.context:>10,} {m.effort or '-'}")
|
|
201
|
+
rendered.append(Option(text, id=f"model-{alias}"))
|
|
202
|
+
return rendered
|
|
203
|
+
|
|
204
|
+
def on_option_list_option_selected(self, message: OptionList.OptionSelected) -> None:
|
|
205
|
+
self.dismiss(self.aliases[message.option_index])
|
|
206
|
+
|
|
207
|
+
def action_cancel(self) -> None:
|
|
208
|
+
self.dismiss(None)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class DiffScreen(ModalScreen[None]):
|
|
212
|
+
"""`git diff -- <path>`, opened from a Git-tab change row -- or, with
|
|
213
|
+
`diff_text` given directly, `/diff`'s checkpoint diff (no `gitlog` fetch,
|
|
214
|
+
`repo` unused)."""
|
|
215
|
+
|
|
216
|
+
DEFAULT_CSS = _DIALOG_CSS + """
|
|
217
|
+
DiffScreen { align: center middle; }
|
|
218
|
+
DiffScreen #dialog { width: 90%; max-width: 140; }
|
|
219
|
+
DiffScreen #diff-body { height: 30; max-height: 80%; }
|
|
220
|
+
"""
|
|
221
|
+
BINDINGS = [Binding("escape", "close", show=False), Binding("q", "close", show=False)]
|
|
222
|
+
|
|
223
|
+
def __init__(self, repo: gitlog.Repo | None, path: str, diff_text: str | None = None) -> None:
|
|
224
|
+
super().__init__()
|
|
225
|
+
self._repo = repo
|
|
226
|
+
self._path = path
|
|
227
|
+
self._diff_text = diff_text
|
|
228
|
+
|
|
229
|
+
def compose(self) -> ComposeResult:
|
|
230
|
+
with Vertical(id="dialog"):
|
|
231
|
+
yield Static(f"[bold]{self._path}[/bold] [dim]esc to close[/dim]")
|
|
232
|
+
yield VerticalScroll(Static("[dim]loading…[/dim]"), id="diff-body")
|
|
233
|
+
|
|
234
|
+
def on_mount(self) -> None:
|
|
235
|
+
if self._diff_text is not None:
|
|
236
|
+
self.run_worker(self._show(self._diff_text), exclusive=True)
|
|
237
|
+
else:
|
|
238
|
+
self.run_worker(self._load(), exclusive=True)
|
|
239
|
+
|
|
240
|
+
async def _load(self) -> None:
|
|
241
|
+
assert self._repo is not None
|
|
242
|
+
text = await gitlog.diff_async(self._repo, self._path)
|
|
243
|
+
await self._show(text)
|
|
244
|
+
|
|
245
|
+
async def _show(self, text: str) -> None:
|
|
246
|
+
body = self.query_one("#diff-body", VerticalScroll)
|
|
247
|
+
await body.remove_children()
|
|
248
|
+
if not text:
|
|
249
|
+
await body.mount(Static("[dim](no diff)[/dim]"))
|
|
250
|
+
return
|
|
251
|
+
await body.mount(Static(Syntax(text, "diff", theme=theme.current_code_theme(self.app),
|
|
252
|
+
word_wrap=True)))
|
|
253
|
+
|
|
254
|
+
def action_close(self) -> None:
|
|
255
|
+
self.dismiss(None)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class SessionsScreen(ModalScreen[str | None]):
|
|
259
|
+
"""`/sessions`: pick one of this cwd's recent sessions to resume.
|
|
260
|
+
Returns the chosen session id, or None on cancel."""
|
|
261
|
+
|
|
262
|
+
DEFAULT_CSS = _DIALOG_CSS + """
|
|
263
|
+
SessionsScreen { align: center middle; }
|
|
264
|
+
SessionsScreen #sessions-list { height: auto; max-height: 14; margin-top: 1; }
|
|
265
|
+
"""
|
|
266
|
+
BINDINGS = [Binding("escape", "cancel", show=False)]
|
|
267
|
+
|
|
268
|
+
def __init__(self, sessions: list[session.Session]) -> None:
|
|
269
|
+
super().__init__()
|
|
270
|
+
self._sessions = sessions
|
|
271
|
+
|
|
272
|
+
def compose(self) -> ComposeResult:
|
|
273
|
+
with Vertical(id="dialog"):
|
|
274
|
+
yield Static("[yellow]Resume a session[/yellow]")
|
|
275
|
+
yield OptionList(*self._render_options(), id="sessions-list")
|
|
276
|
+
|
|
277
|
+
def on_mount(self) -> None:
|
|
278
|
+
self.query_one("#sessions-list", OptionList).focus()
|
|
279
|
+
|
|
280
|
+
def _render_options(self) -> list[Option]:
|
|
281
|
+
rendered = []
|
|
282
|
+
for s in self._sessions:
|
|
283
|
+
ago = format.relative_age(time.time() - s.updated) if s.updated else "?"
|
|
284
|
+
text = f"{s.id:<22} {ago:>4} ago {s.turns:>3} turns {s.first_prompt(50)}"
|
|
285
|
+
rendered.append(Option(text, id=f"sess-{s.id}"))
|
|
286
|
+
return rendered
|
|
287
|
+
|
|
288
|
+
def on_option_list_option_selected(self, message: OptionList.OptionSelected) -> None:
|
|
289
|
+
self.dismiss(self._sessions[message.option_index].id)
|
|
290
|
+
|
|
291
|
+
def action_cancel(self) -> None:
|
|
292
|
+
self.dismiss(None)
|