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
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""The Textual onboarding wizard: one Screen per step, pushed forward as the
|
|
2
|
+
user completes each one. All provider/catalog/config logic lives in
|
|
3
|
+
`omega/onboarding.py`; this module only renders it and reads the answers back
|
|
4
|
+
into `OnboardingApp`'s state."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
|
|
12
|
+
from textual.app import App, ComposeResult
|
|
13
|
+
from textual.binding import Binding
|
|
14
|
+
from textual.containers import VerticalScroll
|
|
15
|
+
from textual.screen import Screen
|
|
16
|
+
from textual.widgets import Input, OptionList, Static
|
|
17
|
+
from textual.widgets.option_list import Option
|
|
18
|
+
|
|
19
|
+
from ... import config, events, loop, onboarding
|
|
20
|
+
from .. import format
|
|
21
|
+
from . import theme
|
|
22
|
+
from .status import SPINNER_FRAMES
|
|
23
|
+
|
|
24
|
+
Body = dict[str, Any]
|
|
25
|
+
|
|
26
|
+
TOTAL_STEPS = 6
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class StepIndicator(Static):
|
|
30
|
+
def __init__(self, step: int) -> None:
|
|
31
|
+
dots = " ".join("●" if i < step else "○" for i in range(TOTAL_STEPS))
|
|
32
|
+
super().__init__(f"[dim]{dots}[/dim]")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _Spinner(Static):
|
|
36
|
+
"""The part-A spinner (same frames/tempo as the status bar), reused here
|
|
37
|
+
for key validation and the live probe turn."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, label: str) -> None:
|
|
40
|
+
super().__init__("")
|
|
41
|
+
self._label = label
|
|
42
|
+
self._frame = 0
|
|
43
|
+
self._stopped = False
|
|
44
|
+
self._timer: Any = None
|
|
45
|
+
|
|
46
|
+
def on_mount(self) -> None:
|
|
47
|
+
self._tick()
|
|
48
|
+
if not self._stopped:
|
|
49
|
+
self._timer = self.set_interval(0.08, self._tick)
|
|
50
|
+
|
|
51
|
+
def _tick(self) -> None:
|
|
52
|
+
if self._stopped:
|
|
53
|
+
return
|
|
54
|
+
glyph = SPINNER_FRAMES[self._frame % len(SPINNER_FRAMES)]
|
|
55
|
+
self._frame += 1
|
|
56
|
+
self.update(f"[dim]{glyph} {self._label}[/dim]")
|
|
57
|
+
|
|
58
|
+
def stop(self, final: str = "") -> None:
|
|
59
|
+
self._stopped = True
|
|
60
|
+
if self._timer is not None:
|
|
61
|
+
self._timer.stop()
|
|
62
|
+
self.update(final)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _base_screen_css(title: str) -> str:
|
|
66
|
+
return f"""
|
|
67
|
+
{title} {{ align: center middle; }}
|
|
68
|
+
{title} #panel {{ width: 76; height: auto; padding: 1 2; }}
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class WizardScreen(Screen[None]):
|
|
73
|
+
"""Shared escape-to-quit behavior for every step."""
|
|
74
|
+
|
|
75
|
+
BINDINGS = [Binding("escape", "quit_wizard", show=False)]
|
|
76
|
+
|
|
77
|
+
def action_quit_wizard(self) -> None:
|
|
78
|
+
cast(OnboardingApp, self.app).exit(False)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class WelcomeScreen(WizardScreen):
|
|
82
|
+
DEFAULT_CSS = _base_screen_css("WelcomeScreen")
|
|
83
|
+
BINDINGS = [*WizardScreen.BINDINGS, Binding("enter", "continue_step", show=False),
|
|
84
|
+
Binding("q", "quit_wizard", show=False)]
|
|
85
|
+
|
|
86
|
+
def compose(self) -> ComposeResult:
|
|
87
|
+
with VerticalScroll(id="panel"):
|
|
88
|
+
yield StepIndicator(1)
|
|
89
|
+
yield Static("[bold]omega[/bold]")
|
|
90
|
+
yield Static("A fast, small coding agent for your terminal. Bring your own models.")
|
|
91
|
+
yield Static("Three quick steps: provider → key → model. "
|
|
92
|
+
"Then we run one real turn to prove it works.")
|
|
93
|
+
yield Static("[dim]enter to continue · q/esc to quit[/dim]")
|
|
94
|
+
|
|
95
|
+
def action_continue_step(self) -> None:
|
|
96
|
+
self.app.push_screen(ProviderScreen())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ProviderScreen(WizardScreen):
|
|
100
|
+
DEFAULT_CSS = _base_screen_css("ProviderScreen")
|
|
101
|
+
|
|
102
|
+
def compose(self) -> ComposeResult:
|
|
103
|
+
with VerticalScroll(id="panel"):
|
|
104
|
+
yield StepIndicator(2)
|
|
105
|
+
yield Static("[bold]Provider[/bold]")
|
|
106
|
+
yield OptionList(*self._options(), id="providers")
|
|
107
|
+
yield Static("[red]base URL must start with http:// or https://[/red]", id="url-error")
|
|
108
|
+
yield Input(placeholder="https://api.example.com/v1", id="base-url")
|
|
109
|
+
|
|
110
|
+
def on_mount(self) -> None:
|
|
111
|
+
self.query_one("#url-error", Static).display = False
|
|
112
|
+
self.query_one("#base-url", Input).display = False
|
|
113
|
+
options = self.query_one("#providers", OptionList)
|
|
114
|
+
options.focus()
|
|
115
|
+
options.highlighted = self._env_preselect()
|
|
116
|
+
|
|
117
|
+
def _env_preselect(self) -> int:
|
|
118
|
+
for i, (_key, _label, _desc, env) in enumerate(onboarding.PROVIDER_INFO):
|
|
119
|
+
if env and os.environ.get(env):
|
|
120
|
+
return i
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
def _options(self) -> list[Option]:
|
|
124
|
+
rendered = []
|
|
125
|
+
for key, label, desc, env in onboarding.PROVIDER_INFO:
|
|
126
|
+
found = " [green]· key found in environment[/green]" if env and os.environ.get(env) else ""
|
|
127
|
+
rendered.append(Option(f"{label}{found}\n[dim]{desc}[/dim]", id=key))
|
|
128
|
+
return rendered
|
|
129
|
+
|
|
130
|
+
def on_option_list_option_selected(self, message: OptionList.OptionSelected) -> None:
|
|
131
|
+
key = message.option_id
|
|
132
|
+
if key == "other":
|
|
133
|
+
self.query_one("#base-url", Input).display = True
|
|
134
|
+
self.query_one("#base-url", Input).focus()
|
|
135
|
+
return
|
|
136
|
+
app = cast(OnboardingApp, self.app)
|
|
137
|
+
app.choice = onboarding.choice_for(key or "anthropic")
|
|
138
|
+
app.push_screen(KeyScreen())
|
|
139
|
+
|
|
140
|
+
def on_input_submitted(self, message: Input.Submitted) -> None:
|
|
141
|
+
url = message.value.strip()
|
|
142
|
+
if not (url.startswith("http://") or url.startswith("https://")):
|
|
143
|
+
self.query_one("#url-error", Static).display = True
|
|
144
|
+
return
|
|
145
|
+
app = cast(OnboardingApp, self.app)
|
|
146
|
+
app.choice = onboarding.choice_for("other", url)
|
|
147
|
+
app.push_screen(KeyScreen())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class KeyScreen(WizardScreen):
|
|
151
|
+
DEFAULT_CSS = _base_screen_css("KeyScreen")
|
|
152
|
+
|
|
153
|
+
def compose(self) -> ComposeResult:
|
|
154
|
+
with VerticalScroll(id="panel"):
|
|
155
|
+
yield StepIndicator(3)
|
|
156
|
+
yield Static("[bold]API key[/bold]")
|
|
157
|
+
yield Static("", id="key-hint")
|
|
158
|
+
yield Input(password=True, id="key-input")
|
|
159
|
+
yield Static("", id="key-result")
|
|
160
|
+
|
|
161
|
+
def on_mount(self) -> None:
|
|
162
|
+
app = cast(OnboardingApp, self.app)
|
|
163
|
+
choice = app.choice
|
|
164
|
+
assert choice is not None
|
|
165
|
+
env = onboarding.env_var_for(choice.provider_key)
|
|
166
|
+
env_val = os.environ.get(env) if env else ""
|
|
167
|
+
self._env_key = env_val or None
|
|
168
|
+
if self._env_key:
|
|
169
|
+
self.query_one("#key-hint", Static).update(
|
|
170
|
+
f"[dim]•••• from ${env} — enter to use, or paste a different one[/dim]")
|
|
171
|
+
self.query_one("#key-input", Input).focus()
|
|
172
|
+
|
|
173
|
+
def on_input_submitted(self, message: Input.Submitted) -> None:
|
|
174
|
+
key = message.value.strip() or (self._env_key or "")
|
|
175
|
+
if not key:
|
|
176
|
+
self.query_one("#key-result", Static).update("[red]enter a key[/red]")
|
|
177
|
+
return
|
|
178
|
+
self.query_one("#key-result", Static).update("")
|
|
179
|
+
self.run_worker(self._validate(key), exclusive=True, thread=False)
|
|
180
|
+
|
|
181
|
+
async def _validate(self, key: str) -> None:
|
|
182
|
+
app = cast(OnboardingApp, self.app)
|
|
183
|
+
result = self.query_one("#key-result", Static)
|
|
184
|
+
spinner = _Spinner("waiting")
|
|
185
|
+
await result.remove()
|
|
186
|
+
panel = self.query_one("#panel", VerticalScroll)
|
|
187
|
+
panel.mount(spinner)
|
|
188
|
+
assert app.choice is not None
|
|
189
|
+
ok, msg = await onboarding.validate_key(app.choice, key)
|
|
190
|
+
if ok:
|
|
191
|
+
spinner.stop("[green]✓ connected[/green]")
|
|
192
|
+
app.choice.provider["apiKey"] = key
|
|
193
|
+
app.push_screen(ModelScreen())
|
|
194
|
+
else:
|
|
195
|
+
spinner.stop(f"[red]✗ {msg[:120]}[/red]")
|
|
196
|
+
panel.mount(Static("", id="key-result"))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ModelScreen(WizardScreen):
|
|
200
|
+
DEFAULT_CSS = _base_screen_css("ModelScreen")
|
|
201
|
+
|
|
202
|
+
def compose(self) -> ComposeResult:
|
|
203
|
+
app = cast(OnboardingApp, self.app)
|
|
204
|
+
choice = app.choice
|
|
205
|
+
assert choice is not None
|
|
206
|
+
with VerticalScroll(id="panel"):
|
|
207
|
+
yield StepIndicator(4)
|
|
208
|
+
yield Static("[bold]Model[/bold]")
|
|
209
|
+
if choice.catalog:
|
|
210
|
+
yield OptionList(*self._options(choice), id="models")
|
|
211
|
+
yield Static(f"[dim]Background roles (subagents, compaction, memory) will use "
|
|
212
|
+
f"{choice.cheap_alias} automatically — change any of this later "
|
|
213
|
+
f"with /model or `omega setup`.[/dim]")
|
|
214
|
+
else:
|
|
215
|
+
yield Input(placeholder="model id served by this endpoint", id="model-id")
|
|
216
|
+
|
|
217
|
+
def on_mount(self) -> None:
|
|
218
|
+
app = cast(OnboardingApp, self.app)
|
|
219
|
+
choice = app.choice
|
|
220
|
+
assert choice is not None
|
|
221
|
+
if choice.catalog:
|
|
222
|
+
options = self.query_one("#models", OptionList)
|
|
223
|
+
options.focus()
|
|
224
|
+
aliases = sorted(choice.catalog)
|
|
225
|
+
options.highlighted = aliases.index(choice.default_alias)
|
|
226
|
+
else:
|
|
227
|
+
self.query_one("#model-id", Input).focus()
|
|
228
|
+
|
|
229
|
+
def _options(self, choice: onboarding._Choice) -> list[Option]:
|
|
230
|
+
rendered = []
|
|
231
|
+
for alias in sorted(choice.catalog):
|
|
232
|
+
m = choice.catalog[alias]
|
|
233
|
+
price_in, price_out = onboarding.PRICES.get(alias, (0.0, 0.0))
|
|
234
|
+
purpose = onboarding.PURPOSES.get(alias, "")
|
|
235
|
+
text = (f"{alias:<11} {m['model']:<26} {m['context']:>10,} "
|
|
236
|
+
f"${price_in:g}/${price_out:g} per M\n[dim]{purpose}[/dim]")
|
|
237
|
+
rendered.append(Option(text, id=alias))
|
|
238
|
+
return rendered
|
|
239
|
+
|
|
240
|
+
def on_option_list_option_selected(self, message: OptionList.OptionSelected) -> None:
|
|
241
|
+
app = cast(OnboardingApp, self.app)
|
|
242
|
+
assert app.choice is not None
|
|
243
|
+
app.main_alias = message.option_id or app.choice.default_alias
|
|
244
|
+
app.cheap_alias = app.choice.cheap_alias
|
|
245
|
+
app.models = dict(app.choice.catalog)
|
|
246
|
+
app.push_screen(ProveScreen())
|
|
247
|
+
|
|
248
|
+
def on_input_submitted(self, message: Input.Submitted) -> None:
|
|
249
|
+
model_id = message.value.strip()
|
|
250
|
+
if not model_id:
|
|
251
|
+
return
|
|
252
|
+
app = cast(OnboardingApp, self.app)
|
|
253
|
+
assert app.choice is not None
|
|
254
|
+
app.main_alias = app.cheap_alias = "main-model"
|
|
255
|
+
app.models = {"main-model": {"model": model_id, "provider": app.choice.provider_key,
|
|
256
|
+
"context": 128000}}
|
|
257
|
+
app.push_screen(ProveScreen())
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class ProveScreen(WizardScreen):
|
|
261
|
+
DEFAULT_CSS = _base_screen_css("ProveScreen") + """
|
|
262
|
+
ProveScreen #panel { width: 90; }
|
|
263
|
+
ProveScreen #prove-log { height: auto; max-height: 16; }
|
|
264
|
+
"""
|
|
265
|
+
BINDINGS = [*WizardScreen.BINDINGS, Binding("r", "retry", show=False),
|
|
266
|
+
Binding("enter", "continue_anyway", show=False)]
|
|
267
|
+
|
|
268
|
+
def compose(self) -> ComposeResult:
|
|
269
|
+
with VerticalScroll(id="panel"):
|
|
270
|
+
yield StepIndicator(5)
|
|
271
|
+
yield Static("[bold]Prove it[/bold]")
|
|
272
|
+
yield VerticalScroll(id="prove-log")
|
|
273
|
+
yield Static("", id="prove-hint")
|
|
274
|
+
|
|
275
|
+
def on_mount(self) -> None:
|
|
276
|
+
self._start()
|
|
277
|
+
|
|
278
|
+
def _start(self) -> None:
|
|
279
|
+
self.query_one("#prove-hint", Static).update("")
|
|
280
|
+
log = self.query_one("#prove-log", VerticalScroll)
|
|
281
|
+
log.remove_children()
|
|
282
|
+
app = cast(OnboardingApp, self.app)
|
|
283
|
+
assert app.choice is not None
|
|
284
|
+
raw = onboarding.build_config(app.choice, app.main_alias, app.cheap_alias, app.models)
|
|
285
|
+
onboarding.save_config(raw)
|
|
286
|
+
self.run_worker(self._probe(), exclusive=True, thread=False)
|
|
287
|
+
|
|
288
|
+
async def _probe(self) -> None:
|
|
289
|
+
log = self.query_one("#prove-log", VerticalScroll)
|
|
290
|
+
spinner = _Spinner("waiting")
|
|
291
|
+
log.mount(spinner)
|
|
292
|
+
started = time.monotonic()
|
|
293
|
+
tool_calls = 0
|
|
294
|
+
tokens_used = 0
|
|
295
|
+
state: dict[str, Any] = {"text_widget": None, "live_text": ""}
|
|
296
|
+
|
|
297
|
+
def emit(ev: events.Event) -> None:
|
|
298
|
+
nonlocal tool_calls, tokens_used
|
|
299
|
+
if isinstance(ev, events.Phase):
|
|
300
|
+
spinner._label = ev.state
|
|
301
|
+
elif isinstance(ev, events.ToolStart):
|
|
302
|
+
tool_calls += 1
|
|
303
|
+
log.mount(Static(format.tool_start(ev)))
|
|
304
|
+
elif isinstance(ev, events.TextDelta):
|
|
305
|
+
if state["text_widget"] is None:
|
|
306
|
+
widget = Static("")
|
|
307
|
+
log.mount(widget)
|
|
308
|
+
state["text_widget"] = widget
|
|
309
|
+
state["live_text"] += ev.text
|
|
310
|
+
cast(Static, state["text_widget"]).update(cast(str, state["live_text"]))
|
|
311
|
+
elif isinstance(ev, events.Usage):
|
|
312
|
+
tokens_used = ev.used
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
cfg = config.load()
|
|
316
|
+
history: list[dict[str, Any]] = [{"role": "user", "content": onboarding.PROBE_PROMPT}]
|
|
317
|
+
await loop.run_agent(cfg, "main", loop.BUILD_SYSTEM, history, emit=emit)
|
|
318
|
+
elapsed = time.monotonic() - started
|
|
319
|
+
spinner.stop(f"[green]✓ working — {tool_calls} tool calls · "
|
|
320
|
+
f"{tokens_used} tokens · {elapsed:.0f}s[/green]")
|
|
321
|
+
await asyncio.sleep(0.6)
|
|
322
|
+
cast(OnboardingApp, self.app).push_screen(DoneScreen())
|
|
323
|
+
except Exception as e:
|
|
324
|
+
spinner.stop(f"[red]✗ {type(e).__name__}: {e}[/red]"[:200])
|
|
325
|
+
self.query_one("#prove-hint", Static).update(
|
|
326
|
+
"[dim]config is saved; press r to retry or enter to continue anyway[/dim]")
|
|
327
|
+
|
|
328
|
+
def action_retry(self) -> None:
|
|
329
|
+
self._start()
|
|
330
|
+
|
|
331
|
+
def action_continue_anyway(self) -> None:
|
|
332
|
+
cast(OnboardingApp, self.app).push_screen(DoneScreen())
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class DoneScreen(WizardScreen):
|
|
336
|
+
DEFAULT_CSS = _base_screen_css("DoneScreen")
|
|
337
|
+
BINDINGS = [*WizardScreen.BINDINGS, Binding("enter", "finish", show=False)]
|
|
338
|
+
|
|
339
|
+
CHEAT_SHEET = (
|
|
340
|
+
"omega open the TUI\n"
|
|
341
|
+
"omega \"fix the test\" one-shot\n"
|
|
342
|
+
"/model or ctrl+o switch models /plan · /build switch modes\n"
|
|
343
|
+
"omega setup full browser setup: more providers, roles, MCP servers")
|
|
344
|
+
|
|
345
|
+
def compose(self) -> ComposeResult:
|
|
346
|
+
with VerticalScroll(id="panel"):
|
|
347
|
+
yield StepIndicator(6)
|
|
348
|
+
yield Static("[bold]You're set up[/bold]")
|
|
349
|
+
yield Static(self.CHEAT_SHEET)
|
|
350
|
+
yield Static("[dim]enter to start[/dim]")
|
|
351
|
+
|
|
352
|
+
def action_finish(self) -> None:
|
|
353
|
+
cast(OnboardingApp, self.app).exit(True)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class OnboardingApp(App[bool]):
|
|
357
|
+
def __init__(self) -> None:
|
|
358
|
+
super().__init__()
|
|
359
|
+
self.register_theme(theme.SYSTEM_THEME)
|
|
360
|
+
self.theme = theme.SYSTEM_THEME.name
|
|
361
|
+
self.choice: onboarding._Choice | None = None
|
|
362
|
+
self.main_alias = ""
|
|
363
|
+
self.cheap_alias = ""
|
|
364
|
+
self.models: Body = {}
|
|
365
|
+
|
|
366
|
+
def on_mount(self) -> None:
|
|
367
|
+
self.push_screen(WelcomeScreen())
|
omega/ui/tui/prefs.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Per-machine TUI preferences persisted at ~/.omega/ui.json -- separate from
|
|
2
|
+
~/.omega/config.json, which holds provider/model/role configuration, not UI
|
|
3
|
+
state like whether the side panel is open."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
PATH = Path(os.environ.get("OMEGA_UI_PREFS", Path.home() / ".omega" / "ui.json"))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load() -> dict[str, Any]:
|
|
15
|
+
if not PATH.exists():
|
|
16
|
+
return {}
|
|
17
|
+
try:
|
|
18
|
+
return dict(json.loads(PATH.read_text()))
|
|
19
|
+
except (json.JSONDecodeError, OSError):
|
|
20
|
+
return {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def save(data: dict[str, Any]) -> None:
|
|
24
|
+
PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
PATH.write_text(json.dumps(data, indent=2) + "\n")
|