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.
Files changed (73) hide show
  1. omega/__init__.py +0 -0
  2. omega/__main__.py +589 -0
  3. omega/artifacts.py +151 -0
  4. omega/checkpoint.py +246 -0
  5. omega/compact.py +106 -0
  6. omega/config.py +285 -0
  7. omega/eval/__init__.py +3 -0
  8. omega/eval/cli.py +127 -0
  9. omega/eval/examples/plan-version-flag.yaml +11 -0
  10. omega/eval/examples/relative-age-negative-delta.yaml +14 -0
  11. omega/eval/examples/version-flag.yaml +10 -0
  12. omega/eval/manifest.py +129 -0
  13. omega/eval/prices.py +29 -0
  14. omega/eval/report.py +135 -0
  15. omega/eval/runner.py +199 -0
  16. omega/eval/tasks.py +97 -0
  17. omega/events.py +145 -0
  18. omega/export.py +80 -0
  19. omega/gitlog.py +229 -0
  20. omega/hooks.py +63 -0
  21. omega/instructions.py +103 -0
  22. omega/integrations.py +284 -0
  23. omega/keys.py +173 -0
  24. omega/llm.py +442 -0
  25. omega/loop.py +510 -0
  26. omega/mcp.py +490 -0
  27. omega/memory/__init__.py +5 -0
  28. omega/memory/consolidate.py +103 -0
  29. omega/memory/curate.py +69 -0
  30. omega/memory/store.py +321 -0
  31. omega/memory/tools.py +175 -0
  32. omega/migrate.py +40 -0
  33. omega/onboarding.py +242 -0
  34. omega/permissions.py +137 -0
  35. omega/secrets.py +173 -0
  36. omega/server/__init__.py +7 -0
  37. omega/server/__main__.py +18 -0
  38. omega/server/app.py +71 -0
  39. omega/server/auth.py +73 -0
  40. omega/server/manager.py +287 -0
  41. omega/server/models.py +123 -0
  42. omega/server/tasks_api.py +311 -0
  43. omega/server/terminals.py +245 -0
  44. omega/server/worker.py +186 -0
  45. omega/session.py +209 -0
  46. omega/setup.html +281 -0
  47. omega/setup_server.py +452 -0
  48. omega/skills.py +158 -0
  49. omega/subagent.py +98 -0
  50. omega/tasks.py +195 -0
  51. omega/tools.py +590 -0
  52. omega/trace.py +156 -0
  53. omega/trajectory.py +146 -0
  54. omega/ui/__init__.py +0 -0
  55. omega/ui/composer.py +140 -0
  56. omega/ui/format.py +708 -0
  57. omega/ui/plain.py +141 -0
  58. omega/ui/tui/__init__.py +9 -0
  59. omega/ui/tui/app.py +958 -0
  60. omega/ui/tui/history.py +50 -0
  61. omega/ui/tui/modals.py +292 -0
  62. omega/ui/tui/onboarding.py +367 -0
  63. omega/ui/tui/prefs.py +25 -0
  64. omega/ui/tui/sidebar.py +510 -0
  65. omega/ui/tui/status.py +115 -0
  66. omega/ui/tui/theme.py +91 -0
  67. omega/ui/tui/transcript.py +783 -0
  68. omega/verify.py +133 -0
  69. omega_code-0.4.0.dist-info/METADATA +479 -0
  70. omega_code-0.4.0.dist-info/RECORD +73 -0
  71. omega_code-0.4.0.dist-info/WHEEL +4 -0
  72. omega_code-0.4.0.dist-info/entry_points.txt +2 -0
  73. omega_code-0.4.0.dist-info/licenses/LICENSE +21 -0
@@ -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)