diff --git a/CHANGELOG.md b/CHANGELOG.md index ef32c48..fdc593f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to OpenOrbit are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project uses [Semantic Versioning](https://semver.org/). +## [0.12.7] - 2026-09-25 + +### Changed + +- Applied official OpenOrbit branding across the app, browser metadata, and README, + including installable web-app icons and favicons. + +### Fixed + +- Retried Runs that follow the latest Runner version now resolve the current + Runner revision instead of reusing a previously resolved version. +- Resolved Runner source only for Runner workflows, preventing unrelated + workflows from using Runner source resolution. +- Recorded Orbit Assistant failures in operational logs. + ## [0.12.6] - 2026-09-25 ### Added diff --git a/README.md b/README.md index 92fcf0f..594e9a2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ *Read this in other languages: [한국어](i18n/ko/README.md)* -# OpenOrbit +

+ OpenOrbit +

+ +# What is OpenOrbit? > **A local workspace where AI personas find product work, AI coding agents > prepare changes, and your team reviews the result.** diff --git a/backend/app/main.py b/backend/app/main.py index 083d234..2dbcfa1 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,6 +2,7 @@ import asyncio import json +import logging import os import re import shutil @@ -12,6 +13,7 @@ from fastapi import FastAPI, File, HTTPException, Query, Request, Response, UploadFile, WebSocket from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, StreamingResponse +from opentelemetry.trace import Status, StatusCode from orbit_sdk.visual import visual_nodes from pydantic import BaseModel, Field @@ -82,10 +84,42 @@ async def application_lifespan(_: FastAPI): allow_methods=["*"], allow_headers=["*"], ) + + +@app.middleware("http") +async def record_failed_api_requests(request: Request, call_next): + """Retain failed control-room API calls without recording request bodies.""" + try: + response = await call_next(request) + except Exception as error: + if request.url.path.startswith("/api/"): + with store.tracer.start_as_current_span( + "api.request", + attributes={"http.request.method": request.method, "url.path": request.url.path}, + ) as span: + span.record_exception(error) + span.set_status(Status(StatusCode.ERROR)) + span.add_event("api.request.failed", {"error.type": type(error).__name__}) + raise + if request.url.path.startswith("/api/") and response.status_code >= 400: + with store.tracer.start_as_current_span( + "api.request", + attributes={ + "http.request.method": request.method, + "url.path": request.url.path, + "http.response.status_code": response.status_code, + }, + ) as span: + span.set_status(Status(StatusCode.ERROR)) + span.add_event("api.response.failed", {"http.response.status_code": response.status_code}) + return response + + # The API service owns in-memory scheduler threads, so it alone may mark a # leftover local pipeline as interrupted when it starts. store = ConsoleStore(recover_interrupted_runs=True) assistant_ui_broker = AssistantUiBroker() +logger = logging.getLogger(__name__) def bundled_directory(relative_path: Path, roots: tuple[Path, ...] | None = None) -> Path: @@ -1334,21 +1368,42 @@ def chat(values: ChatMessage, request: Request): settings = ModelSettings( **{key: value for key, value in configured.items() if key in ModelSettings.__dataclass_fields__} ) - try: - provider = AzureOpenAIProvider() if settings.provider == "azure-openai" else BedrockProvider() - application = store.application_settings() - tool_executor = AssistantToolExecutor( - application["assistant_tools"], application["coding_agent_provider"] - ) - prompt = build_assistant_prompt( - values.content, - [(turn.role, turn.content) for turn in values.history], - request_locale(request), - ) - response = OrbitAssistantGraph(provider, settings, tool_executor).invoke(prompt) - return {"response": response, "profile_name": profile_name} - except RuntimeError as error: - raise HTTPException(409, str(error)) + provider = AzureOpenAIProvider() if settings.provider == "azure-openai" else BedrockProvider() + application = store.application_settings() + tool_executor = AssistantToolExecutor( + application["assistant_tools"], application["coding_agent_provider"] + ) + prompt = build_assistant_prompt( + values.content, + [(turn.role, turn.content) for turn in values.history], + request_locale(request), + ) + with store.tracer.start_as_current_span( + "assistant.chat", + attributes={ + "orbit.assistant.stream": False, + "gen_ai.provider.name": settings.provider, + "gen_ai.request.model": settings.model, + "orbit.assistant.profile": profile_name, + }, + ) as span: + try: + span.add_event("assistant.request.received") + response = OrbitAssistantGraph(provider, settings, tool_executor).invoke(prompt) + span.set_attribute("orbit.assistant.response.length", len(response)) + span.add_event("assistant.response.completed") + return {"response": response, "profile_name": profile_name} + except RuntimeError as error: + span.record_exception(error) + span.set_status(Status(StatusCode.ERROR)) + span.add_event("assistant.request.failed", {"error.type": type(error).__name__}) + raise HTTPException(409, str(error)) + except Exception as error: + span.record_exception(error) + span.set_status(Status(StatusCode.ERROR)) + span.add_event("assistant.request.failed", {"error.type": type(error).__name__}) + logger.exception("Orbit Assistant request failed") + raise HTTPException(500, "Chat request failed.") @app.post("/api/chat/stream") @@ -1400,16 +1455,38 @@ def activity(phase: str, tool: str | None = None) -> None: async def run() -> None: try: - response = await asyncio.to_thread( - OrbitAssistantGraph( - provider, settings, tool_executor, ui_tools=ui_tools, on_activity=activity - ).invoke, - prompt, - ) + + def invoke() -> str: + with store.tracer.start_as_current_span( + "assistant.chat", + attributes={ + "orbit.assistant.stream": True, + "orbit.assistant.ui_enabled": bool(ui_tools), + "gen_ai.provider.name": settings.provider, + "gen_ai.request.model": settings.model, + "orbit.assistant.profile": profile_name, + }, + ) as span: + try: + span.add_event("assistant.request.received") + response = OrbitAssistantGraph( + provider, settings, tool_executor, ui_tools=ui_tools, on_activity=activity + ).invoke(prompt) + span.set_attribute("orbit.assistant.response.length", len(response)) + span.add_event("assistant.response.completed") + return response + except Exception as error: + span.record_exception(error) + span.set_status(Status(StatusCode.ERROR)) + span.add_event("assistant.request.failed", {"error.type": type(error).__name__}) + raise + + response = await asyncio.to_thread(invoke) await queue.put({"type": "response", "response": response}) except RuntimeError as error: await queue.put({"type": "error", "message": str(error)}) except Exception: + logger.exception("Orbit Assistant streaming request failed") await queue.put({"type": "error", "message": "Chat request failed."}) task = asyncio.create_task(run()) diff --git a/backend/app/store.py b/backend/app/store.py index fff46a8..2468f5c 100644 --- a/backend/app/store.py +++ b/backend/app/store.py @@ -3223,6 +3223,34 @@ def orbit_logs(self) -> list[dict[str, str]]: ), "", ) + if not message: + process_exit = next( + ( + event.get("attributes", {}).get("process.exit_code") + for event in events + if event.get("name") == "process.completed" + and event.get("attributes", {}).get("process.exit_code") not in (None, 0) + ), + None, + ) + if process_exit is not None: + message = f"process exited with code {process_exit}" + if not message: + failure = next( + ( + event + for event in events + if event.get("name", "").endswith( + (".failed", ".invalid", ".rejected", ".timeout") + ) + ), + None, + ) + if failure: + error_type = failure.get("attributes", {}).get("error.type") + message = f"{failure.get('name')}" + (f" ({error_type})" if error_type else "") + if not message: + message = ", ".join(str(event.get("name")) for event in events[-2:] if event.get("name")) entries.append( { "time": str(record.get("exportedAt", "")), @@ -3645,6 +3673,9 @@ def create_run( if execution_mode not in {"run", "test"}: raise ValueError("execution_mode must be run or test") runner_asset = self._runner(runner_id) + # ``None`` is a persistent "follow latest" selection. Resolve it for + # this queued execution, but do not replace the selection on the Run: + # a later retry must resolve the then-current latest version again. resolved_runner_version = runner_version or int(runner_asset["version"]) runner = self._runner_execution_plan(runner_id, resolved_runner_version) runner_source = next( @@ -3657,7 +3688,7 @@ def create_run( id=uuid.uuid4().hex[:12], workflow_id=runner.id, workflow_name=runner.name, - runner_version=resolved_runner_version, + runner_version=runner_version, runner_source_sha256=hashlib.sha256(str(runner_source).encode("utf-8")).hexdigest(), build_id=build_id, build_name=build_name, @@ -3931,6 +3962,22 @@ def _execute(self, run_id: str) -> None: # Insighta user simulator). Keep each step's runner directory intact; # the build repository is still captured on the Run and in its prompt. if workflow.runner_id: + # A Run with no selected version follows the latest runner on every + # execution, including retries. Keep that selected version nullable; + # the checksum records the source actually resolved now. + runner_asset = self._runner(workflow.runner_id) + resolved_runner_version = run.runner_version or int(runner_asset["version"]) + runner_source = next( + item["source"] + for item in runner_asset["versions"] + if int(item["version"]) == resolved_runner_version + ) + run.runner_source_sha256 = hashlib.sha256(str(runner_source).encode("utf-8")).hexdigest() + run.workflow_graph = self._runner_graph_definition( + workflow.runner_id, run.repository, resolved_runner_version + ) + run.updated_at = now() + self._save(run) runner_path = self._runner_entry_path(workflow.runner_id, run.runner_version) for step in [*workflow.steps, *(workflow.test_steps or [])]: step.command = [ @@ -5068,12 +5115,16 @@ def retry(self, run_id: str, restart_from_first: bool, output_locale: str | None run = self._load(run_id) if run.execution_type != "pipeline" or run.status not in {"succeeded", "failed", "cancelled"}: raise ValueError("Only completed, failed, or cancelled pipeline runs can be retried") + if run.build_id: + build = self.build(run.build_id) + # Older Runs resolved a latest build to a number at creation time. + # Restore the build's latest-selection semantics before retrying. + if build.get("runner_version") is None: + run.runner_version = None if output_locale: resolved_output_locale = output_locale.strip() if run.build_id and resolved_output_locale: - prompt_source, prompt_snapshot = self._assembled_prompt( - self.build(run.build_id), resolved_output_locale - ) + prompt_source, prompt_snapshot = self._assembled_prompt(build, resolved_output_locale) run.output_locale = resolved_output_locale run.prompt_source = prompt_source run.prompt_snapshot = prompt_snapshot diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index d89e96a..55a4835 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -28,6 +28,74 @@ def test_health_is_available(): assert response.json() == {"status": "ok"} +def test_orbit_logs_include_process_exit_and_operational_events(monkeypatch): + store = store_module.ConsoleStore() + monkeypatch.setattr( + store, + "telemetry", + lambda: [ + { + "exportedAt": "2026-09-25T00:00:00+00:00", + "resourceSpans": [ + { + "scopeSpans": [ + { + "spans": [ + { + "name": "workflow.step", + "status": "ERROR", + "events": [ + { + "name": "process.completed", + "attributes": {"process.exit_code": 1}, + } + ], + } + ] + } + ] + } + ], + }, + { + "exportedAt": "2026-09-25T00:01:00+00:00", + "resourceSpans": [ + { + "scopeSpans": [ + { + "spans": [ + { + "name": "assistant.chat", + "status": "UNSET", + "events": [ + {"name": "assistant.response.completed", "attributes": {}} + ], + } + ] + } + ] + } + ], + }, + ], + ) + + assert store.orbit_logs() == [ + { + "time": "2026-09-25T00:00:00+00:00", + "name": "workflow.step", + "status": "ERROR", + "message": "process exited with code 1", + }, + { + "time": "2026-09-25T00:01:00+00:00", + "name": "assistant.chat", + "status": "UNSET", + "message": "assistant.response.completed", + }, + ] + + def test_improvement_analytics_excludes_builds_without_recent_runs_or_feedback(monkeypatch): current = datetime(2026, 9, 25, 12, tzinfo=UTC) monkeypatch.setattr(store_module, "now", lambda: current) @@ -850,6 +918,65 @@ def test_completed_pipeline_run_can_be_retried(tmp_path, monkeypatch): assert retried.runner_output == "" +def test_latest_runner_selection_stays_null_when_a_run_is_retried(tmp_path, monkeypatch): + """A latest-version build must not become pinned to its first resolved version.""" + monkeypatch.setattr(store_module, "RUNS", tmp_path / "runs") + store = store_module.ConsoleStore() + versions = { + "version": 2, + "versions": [ + {"version": 1, "source": "first"}, + {"version": 2, "source": "second"}, + ], + } + monkeypatch.setattr( + store, "_runner", lambda _runner_id: {"id": "workflow", "name": "Workflow", **versions} + ) + monkeypatch.setattr( + store, + "_runner_execution_plan", + lambda runner_id, runner_version=None: SimpleNamespace(id=runner_id, name="Workflow"), + ) + monkeypatch.setattr(store, "_runner_graph_definition", lambda *_: None) + monkeypatch.setattr(store, "_start", lambda _: None) + + run = store.create_run("workflow", runner_version=None) + assert run.runner_version is None + run.status = "succeeded" + store._save(run) + + versions["version"] = 3 + versions["versions"].append({"version": 3, "source": "third"}) + retried = store.retry(run.id, restart_from_first=True) + + assert retried.runner_version is None + + +def test_retry_restores_latest_selection_for_a_legacy_resolved_run(tmp_path, monkeypatch): + monkeypatch.setattr(store_module, "RUNS", tmp_path / "runs") + store = store_module.ConsoleStore() + timestamp = store_module.now() + store._save( + Run( + id="legacy-latest-run", + workflow_id="workflow", + workflow_name="Workflow", + runner_version=2, + build_id="latest-build", + execution_type="pipeline", + status="failed", + created_at=timestamp, + updated_at=timestamp, + ) + ) + monkeypatch.setattr(store, "build", lambda _build_id: {"id": "latest-build", "runner_version": None}) + monkeypatch.setattr(store, "_start", lambda _: None) + + retried = store.retry("legacy-latest-run", restart_from_first=True) + + assert retried.runner_version is None + + def test_retry_uses_the_requested_output_locale_and_refreshes_its_prompt(tmp_path, monkeypatch): monkeypatch.setattr(store_module, "RUNS", tmp_path / "runs") store = store_module.ConsoleStore() diff --git a/frontend/index.html b/frontend/index.html index 01f43af..f465a3c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,11 @@ + + + + + OpenOrbit diff --git a/frontend/package.json b/frontend/package.json index 33977c6..d490113 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "agent-improvement-console-ui", "private": true, - "version": "0.12.6", + "version": "0.12.7", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000..f350d6b Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..c097f48 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/icon-192.png b/frontend/public/icon-192.png new file mode 100644 index 0000000..29cd34d Binary files /dev/null and b/frontend/public/icon-192.png differ diff --git a/frontend/public/icon-512.png b/frontend/public/icon-512.png new file mode 100644 index 0000000..6049a0e Binary files /dev/null and b/frontend/public/icon-512.png differ diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg new file mode 100644 index 0000000..a1ec763 --- /dev/null +++ b/frontend/public/icon.svg @@ -0,0 +1,7 @@ + + OpenOrbit + A broken circular orbit with orbiting task nodes. + + + + diff --git a/frontend/public/site.webmanifest b/frontend/public/site.webmanifest new file mode 100644 index 0000000..79fd0ff --- /dev/null +++ b/frontend/public/site.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "OpenOrbit", + "short_name": "OpenOrbit", + "description": "Local control plane for recurring AI evaluations.", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#1687E8", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/frontend/src/app/app-shell.tsx b/frontend/src/app/app-shell.tsx index 0af5d51..6421141 100644 --- a/frontend/src/app/app-shell.tsx +++ b/frontend/src/app/app-shell.tsx @@ -9,7 +9,6 @@ import { Play, TriangleAlert, Settings, - Sparkles, } from "lucide-react"; import { SiGithub } from "react-icons/si"; import { useEffect, useState, type ReactNode } from "react"; @@ -155,11 +154,11 @@ export function AppShell({ title={navigationLabels.expandNavigation} onClick={() => setCollapsed(false)} > - + ) : (
- +
{appMeta.title} {__OPENORBIT_VERSION__} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 64500b1..5f5254f 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -11,7 +11,7 @@ :root { font-family: Manrope, sans-serif; color: var(--text); background: var(--bg); font-synthesis: none; } * { box-sizing: border-box; } html,body,#root { max-width:100%; overflow-x:hidden; } body { margin: 0; min-width:0; background: var(--bg); } button { font: inherit; cursor: pointer; } button:disabled { cursor: not-allowed; } .app-shell { display: flex; width:100%; min-width:0; min-height:100vh; overflow-x:hidden; } -.app-sidebar { width: 244px; padding: 32px 19px; border-right: 1px solid var(--line); background: var(--surface); display: flex; flex-direction: column; } .brand { display: flex; align-items: center; gap: 9px; margin: 0 12px 48px; font-weight: 800; font-size: 21px; letter-spacing: -.9px; }.brand svg { color: var(--accent); } .eyebrow { color: var(--muted); margin: 0 0 7px; letter-spacing: 1.45px; font: 500 10px 'DM Mono', monospace; } .app-navigation { display: grid; gap: 6px; margin-top: 10px; } .app-navigation button { border:0; background:transparent; display:flex; align-items:center; gap:11px; color:var(--muted); border-radius:9px; padding:11px 12px; font-size:13px; text-align:left; } .app-navigation button.active { color:var(--text); background:var(--surface-raised); }.nav-run-count { display:inline-grid; min-width:20px; height:20px; margin-left:auto; padding:0 5px; place-items:center; border-radius:999px; background:var(--accent); color:var(--bg); font:700 10px/1 'DM Mono', monospace; }.side-note { margin: auto 12px 0; color:var(--muted); font-size:12px; line-height:1.8; }.pulse { width:7px; height:7px; border-radius:50%; display:inline-block; margin-right:6px; background:var(--accent); } +.app-sidebar { width: 244px; padding: 32px 19px; border-right: 1px solid var(--line); background: var(--surface); display: flex; flex-direction: column; } .brand { display: flex; align-items: center; gap: 9px; margin: 0 12px 48px; font-weight: 800; font-size: 21px; letter-spacing: -.9px; }.brand-icon { display:block; width:20px; height:20px; flex:none; } .eyebrow { color: var(--muted); margin: 0 0 7px; letter-spacing: 1.45px; font: 500 10px 'DM Mono', monospace; } .app-navigation { display: grid; gap: 6px; margin-top: 10px; } .app-navigation button { border:0; background:transparent; display:flex; align-items:center; gap:11px; color:var(--muted); border-radius:9px; padding:11px 12px; font-size:13px; text-align:left; } .app-navigation button.active { color:var(--text); background:var(--surface-raised); }.nav-run-count { display:inline-grid; min-width:20px; height:20px; margin-left:auto; padding:0 5px; place-items:center; border-radius:999px; background:var(--accent); color:var(--bg); font:700 10px/1 'DM Mono', monospace; }.side-note { margin: auto 12px 0; color:var(--muted); font-size:12px; line-height:1.8; }.pulse { width:7px; height:7px; border-radius:50%; display:inline-block; margin-right:6px; background:var(--accent); } .app-content { flex:1; min-width:0; width:100%; max-width:none; padding:54px 64px 65px; margin:0; } header { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:38px; } h1 { font-size:38px; letter-spacing:-1.9px; margin:0 0 9px; font-weight:700; } h1 em { color:#c5ef87; font: italic 600 39px 'Playfair Display', serif; letter-spacing:-1.4px; }.sub { margin:0; color:#94a19a; font-size:14px; }.health { border:1px solid #334239; color:#a7b4ac; background:#19231e; padding:9px 13px; border-radius:20px; font-size:12px; }.metrics { display:grid; grid-template-columns:repeat(3, 1fr); gap:15px; margin-bottom:16px; }.metric { min-height:130px; border:1px solid #2b3630; border-radius:12px; padding:20px; background:linear-gradient(125deg,#171f1b,#141a17); }.metric p { margin:0 0 13px; color:#94a19a; font-size:12px; }.metric strong { display:block; font:600 32px 'DM Mono'; letter-spacing:-2px; }.metric small { color:#6e7c74; font-size:11px; }.metric.amber strong { color:#f2d29b; }.grid,.lower-grid { display:grid; gap:16px; }.grid { grid-template-columns: .92fr 1.35fr; }.lower-grid { grid-template-columns:1.16fr .84fr; margin-top:16px; }.panel { min-width:0; background:#151c18; border:1px solid #2b3630; border-radius:12px; padding:23px; }.dashboard-panel + .dashboard-panel,.improvement-panel + .improvement-panel { margin-top:16px; }.panel-head { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:18px; }.panel h2 { font-size:17px; letter-spacing:-.5px; margin:0; }.ghost { border:0; background:none; color:#a9bb9d; display:flex; gap:5px; align-items:center; font-size:11px; }.workflow { width:100%; border:1px solid transparent; background:transparent; border-radius:10px; padding:11px 9px; display:flex; align-items:center; gap:10px; text-align:left; color:#dfe8df; }.workflow:hover,.workflow.selected { background:#1e2922; border-color:#344338; }.workflow-icon { width:34px; height:34px; display:grid; place-items:center; border-radius:8px; }.workflow-icon.simulation { color:#b9e8ef; background:#1c3538; }.workflow-icon.improvement { color:#d0e99b; background:#31432b; }.workflow-copy { flex:1; display:grid; gap:2px; }.workflow-copy strong { font-size:12px; }.workflow-copy small { color:#85928a; font-size:10px; }.enabled,.disabled,.risk,.badge { font:500 9px 'DM Mono'; padding:4px 6px; border-radius:4px; }.enabled { color:#b8e984; background:#294027; }.disabled { color:#959f9b; background:#27302b; }.description { color:#9ca9a1; font-size:12px; line-height:1.65; margin:-4px 0 17px; }.risk { color:#eba9a2; background:#402824;}.risk.medium { color:#ead09a; background:#403523; }.steps { border-top:1px solid #29342d; }.step { display:flex; gap:14px; align-items:center; padding:13px 2px; border-bottom:1px solid #29342d; }.step>span { color:#77867d; font:10px 'DM Mono'; }.step div { flex:1; display:grid; gap:4px; }.step strong { font-size:12px; }.step code { font:9px 'DM Mono'; color:#82998d; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.step svg { color:#d5d392; }.run-button { width:100%; border:0; border-radius:8px; padding:12px; background:#c7ee8e; color:#1a2418; font-size:12px; font-weight:800; display:flex; align-items:center; justify-content:center; gap:8px; margin-top:18px; }.run-button:disabled { cursor:not-allowed; color:#718070; background:#28332c; }.approval { display:flex; justify-content:space-between; align-items:center; padding:12px 0; border-top:1px solid #29342d; }.approval:first-of-type { border-top:0; }.approval div:first-child { display:grid; gap:4px; }.approval strong,.history-row strong { font-size:12px; }.approval small,.history-row small { color:#839088; font-size:10px; }.approval-actions { display:flex; gap:6px; }.approve,.reject { border-radius:6px; border:0; padding:6px 9px; font-size:10px; }.approve { background:#bde982; color:#1b2819; }.reject { background:#2c3630; color:#b7c2ba; }.history-row { display:flex; align-items:center; gap:10px; padding:10px 0; border-top:1px solid #29342d; }.history-row>div { display:grid; gap:3px; flex:1; }.dot { width:7px; height:7px; border-radius:50%; }.dot.good { background:#bce989; }.dot.run { background:#85cfe0; }.dot.wait { background:#f1cf88; }.dot.bad { background:#e99c92; }.dot.muted { background:#7d8881; }.badge.good { color:#bce989; background:#283d29; }.badge.run { color:#99daeb; background:#223c40; }.badge.wait { color:#f1d292; background:#403420; }.badge.bad { color:#eaa89f; background:#402624; }.badge.muted { color:#a8b0ab; background:#29322d; }.empty { min-height:93px; color:#708078; display:grid; place-items:center; gap:8px; align-content:center; font-size:11px; }.empty svg { width:20px; } .system-readiness-alert { display:flex; align-items:flex-start; gap:12px; margin:-20px 0 22px; padding:14px 16px; border:1px solid #8a5547; border-radius:10px; color:#ffd2c8; background:#38221e; }.system-readiness-alert>svg { flex:none; margin-top:2px; color:#f0a88f; }.system-readiness-alert__copy { flex:1; min-width:0; }.system-readiness-alert strong { font-size:13px; }.system-readiness-alert p,.system-readiness-alert ul { margin:4px 0 0; font-size:12px; line-height:1.55; }.system-readiness-alert ul { padding-left:18px; }.system-readiness-alert__action { flex:none; border:1px solid #b47764; border-radius:7px; padding:7px 10px; color:#ffe1d9; background:transparent; font-size:11px; }.system-readiness-alert__action:hover { background:#523027; } .provider { display:grid; grid-template-columns:1.5fr repeat(5, minmax(100px, 1fr)); gap:9px; align-items:end; margin-bottom:16px; }.provider small { color:#738078; font-size:10px; }.provider input,.provider select { border:1px solid #334139; border-radius:7px; background:#101613; color:#d9e4dc; padding:9px; min-width:0; font-size:11px; outline:none; }.provider .save { border:0; border-radius:7px; background:#c7ee8e; color:#1a2418; padding:10px; font-size:11px; font-weight:800; } diff --git a/i18n/ko/README.md b/i18n/ko/README.md index 69c8f1c..4619937 100644 --- a/i18n/ko/README.md +++ b/i18n/ko/README.md @@ -6,7 +6,11 @@ *Read this in other languages: [English](../../README.md)* -# OpenOrbit +

+ OpenOrbit +

+ +# OpenOrbit이란? > **AI 페르소나가 제품의 일을 찾고, AI 코딩 에이전트가 변경안을 준비하며, > 팀이 결과를 검토하는 로컬 워크스페이스입니다.** diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..f82572c --- /dev/null +++ b/icon.svg @@ -0,0 +1,7 @@ + + OpenOrbit + A broken circular orbit with an orbiting task node. + + + + diff --git a/logo-lockup.png b/logo-lockup.png new file mode 100644 index 0000000..cd2578a Binary files /dev/null and b/logo-lockup.png differ diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..f12d7df Binary files /dev/null and b/logo.png differ diff --git a/package.json b/package.json index 4dadd95..94a07b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openorbit", - "version": "0.12.6", + "version": "0.12.7", "description": "Open control plane for recurring AI automations", "license": "MIT", "type": "module", diff --git a/pyproject.toml b/pyproject.toml index 8bbc5b3..8123e16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "openorbit" -version = "0.12.6" +version = "0.12.7" description = "Open control plane for recurring AI automations" readme = "README.md" license = "MIT" diff --git a/uv.lock b/uv.lock index 59d985c..b8b2a4c 100644 --- a/uv.lock +++ b/uv.lock @@ -950,7 +950,7 @@ wheels = [ [[package]] name = "openorbit" -version = "0.12.6" +version = "0.12.7" source = { editable = "." } dependencies = [ { name = "fastapi" },