diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 6657576ea..000000000
--- a/.dockerignore
+++ /dev/null
@@ -1,15 +0,0 @@
-.git
-.github
-.venv
-.pytest_cache
-.ruff_cache
-**/__pycache__
-**/*.pyc
-apps/showcase_ui/.angular
-apps/showcase_ui/dist
-apps/showcase_ui/node_modules
-build
-dist
-traces
-.env
-*.log
diff --git a/.env.example b/.env.example
index 8b23d0a53..3515a13ad 100644
--- a/.env.example
+++ b/.env.example
@@ -12,6 +12,11 @@ ANTHROPIC_API_KEY=
OPEN_ROUTER_API_KEY=
XAI_API_KEY=
+# Custom endpoints: every node in config/artemis.jsonc (default/nodes/presets)
+# accepts "api_base", "api_key" and "api_key_env". Priority:
+# config api_base > OPENAI_BASE_URL below > provider official endpoint
+# OPENAI_BASE_URL=http://127.0.0.1:1234/v1
+
# Google Cloud Vision OCR (Optional - for advanced OCR processing)
OCR_API_KEY=
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..b6fb09732
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,18 @@
+# Line endings are a repository fact, not a per-machine default. Without this file the
+# working tree depends on each clone's core.autocrlf, so byte-based checks disagree
+# across machines (changelogs #45 measured it, #47 closes it).
+* text=auto eol=lf
+
+# cmd.exe has a known failure surface with LF batch files (labels / goto).
+*.bat text eol=crlf
+*.cmd text eol=crlf
+
+# No EOL conversion, no diff.
+*.png binary
+*.jpg binary
+*.ico binary
+*.gif binary
+*.mp4 binary
+*.mp3 binary
+*.apk binary
+*.keystore binary
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8d1ce0c5d..142d4efa4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -55,12 +55,66 @@ jobs:
- name: Lint Python
run: uv run ruff check .
+ # Kept un-gated on purpose: these are the mechanical keepers of the "one derived
+ # fact, one authority" rule (changelogs #31, #45) and must run on every event that
+ # produces a run. They finally did: run 38296414103 (event: push, 2026-10-12) executed
+ # this step on both OSes and it passed - which is what closed the "never observed in a
+ # run" blank left by #46 (changelogs #67).
+ - name: Check documentation gates
+ run: |
+ uv run python -X utf8 scripts/check_doc_registry.py
+ uv run python -X utf8 scripts/check_doc_tables.py
+ uv run python -X utf8 scripts/check_doc_tables.py --selftest
+
+# Both of these are pure-python and cheap, so they sit BEFORE the Node/pnpm block: a step that
+# fails there aborts the job, and that is exactly how the ratchet stayed invisible for six weeks
+# (changelogs #69) - the first CI run that ever reached this job died two steps later.
- name: Enforce quality ratchets
run: uv run python scripts/quality_ratchet.py
- name: Type-check protected core modules
run: uv run pyright --project pyright-core.json
+ # Node is needed only by the fixture generator below; the runner image is not the authority
+ # for which version it gets. Set up pnpm first so setup-node can wire the pnpm store cache
+ # (same order as the plugin-contracts job).
+ - name: Set up pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "22.22.3"
+ cache: pnpm
+ cache-dependency-path: plugins/dsh-autogamer/pnpm-lock.yaml
+
+ # The drift check imports the plugin's OWN MCP client, and that client only exists in
+ # `dist/` - a build product, not repository content. Without these two steps the step below
+ # dies with ERR_MODULE_NOT_FOUND, which is exactly how it first failed on the runner
+ # (run 38296414103) while passing on any dev machine that happens to have a stale dist
+ # lying around (changelogs #68).
+ - name: Build the plugin for the fixture check
+ working-directory: plugins/dsh-autogamer
+ run: |
+ pnpm install --frozen-lockfile
+ pnpm run build
+
+ # G62 (changelogs #52) made the py action surface runnable with no adb and no device, so the
+ # plugin's generated fixture can be re-derived here instead of being hand-carried -- until now
+ # no CI job ran scripts/spike-mcp.mjs at all (changelogs #53). Its default mode is the drift
+ # check: any difference between the live listing and the checked-in fixture exits non-zero.
+ # Reproduce both halves locally before changing this step - the runner's view is the one
+ # without a build product:
+ # rm -rf plugins/dsh-autogamer/dist
+ # SPIKE_COMMAND="uv run artemis mcp --type mock" node plugins/dsh-autogamer/scripts/spike-mcp.mjs # exit 1, ERR_MODULE_NOT_FOUND
+ # npm --prefix plugins/dsh-autogamer run build # then the same command exits 0
+ - name: Check the py action surface against the plugin fixture
+ env:
+ SPIKE_COMMAND: uv run artemis mcp --type mock
+ run: node plugins/dsh-autogamer/scripts/spike-mcp.mjs
+
# Device, cloud, manual, and end-to-end suites require dedicated
# infrastructure and intentionally do not run on GitHub-hosted runners.
- name: Run deterministic Python tests
@@ -77,104 +131,44 @@ jobs:
--cov=third_party
--cov-fail-under=60
- frontend:
- name: Frontend tests and production build
- runs-on: ubuntu-latest
+ plugin-contracts:
+ name: DSH plugin contracts / ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
timeout-minutes: 20
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - windows-latest
defaults:
run:
- working-directory: apps/showcase_ui
-
- steps:
- - name: Check out repository
- uses: actions/checkout@v4
-
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: "22.22.3"
- cache: npm
- cache-dependency-path: apps/showcase_ui/package-lock.json
-
- - name: Install locked frontend dependencies
- run: npm ci
-
- - name: Run frontend tests
- run: npm test -- --watch=false --browsers=ChromeHeadless
-
- - name: Build frontend for production
- run: npm run build
-
- package:
- name: Build and smoke-test Python packages
- runs-on: ubuntu-latest
- timeout-minutes: 30
+ working-directory: plugins/dsh-autogamer
steps:
- name: Check out repository
uses: actions/checkout@v4
- - name: Set up Python 3.12
- uses: actions/setup-python@v5
+ # Before setup-node, so setup-node can wire the pnpm store cache.
+ - name: Set up pnpm
+ uses: pnpm/action-setup@v4
with:
- python-version: "3.12"
-
- - name: Set up uv
- uses: astral-sh/setup-uv@v6
- with:
- enable-cache: true
- cache-dependency-glob: uv.lock
+ version: 10
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22.22.3"
- cache: npm
- cache-dependency-path: apps/showcase_ui/package-lock.json
+ cache: pnpm
+ cache-dependency-path: plugins/dsh-autogamer/pnpm-lock.yaml
- - name: Build frontend assets for the wheel
- run: |
- npm ci --prefix apps/showcase_ui
- npm run build --prefix apps/showcase_ui
+ # The @deepseek-ai/* devDependencies are the real DSH runtime the
+ # conformance suite boots; no desktop install, no model, no device.
+ - name: Install locked plugin dependencies
+ run: pnpm install --frozen-lockfile
- - name: Build client and Artemis distributions
- run: |
- uv build --package artemis-client --wheel --out-dir dist
- uv build --package artemis --wheel --sdist --out-dir dist
-
- - name: Install wheels in a clean environment
- run: |
- uv venv .smoke-venv --python 3.12
- uv pip install --python .smoke-venv/bin/python dist/artemis_client-*.whl dist/artemis-*.whl
+ - name: Build the plugin
+ run: pnpm run build
- - name: Smoke-test installed package and CLI
- shell: bash
- run: |
- smoke_dir="$RUNNER_TEMP/artemis-wheel-smoke"
- mkdir -p "$smoke_dir"
- cd "$smoke_dir"
- source "$GITHUB_WORKSPACE/.smoke-venv/bin/activate"
- python - <<'PY'
- from importlib.metadata import files, version
-
- import artemis
- from artemis.resources import get_bundled_config_path, get_bundled_showcase_dist
-
- bundled_config = get_bundled_config_path("artemis.jsonc")
- bundled_ui = get_bundled_showcase_dist()
- assert artemis.__version__ == version("artemis")
- assert bundled_config is not None and bundled_config.is_file()
- assert bundled_ui is not None and (bundled_ui / "index.html").is_file()
- assert not any("node_modules" in str(path) for path in (files("artemis") or ()))
- print(f"Installed Artemis {artemis.__version__} with bundled runtime resources")
- PY
- artemis --version
- artemis --help
-
- - name: Upload distributions
- uses: actions/upload-artifact@v4
- with:
- name: python-distributions
- path: dist/*
- if-no-files-found: error
- retention-days: 7
+ - name: Run plugin contract tests
+ run: pnpm test
diff --git a/.gitignore b/.gitignore
index 31cf3e936..4819f127f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,13 +22,14 @@ share/python-wheels/
*.egg
MANIFEST
-# Virtual Environments
+# Virtual Environments (all anchored: unanchored names also match subdirectories
+# case-insensitively on Windows, which once hid src/env/preflight.ts from git)
.venv/
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
+/venv/
+/ENV/
+/venv.bak/
+/env/
+/env.bak/
# Testing & Coverage
.pytest_cache/
@@ -76,9 +77,7 @@ dmypy.json
.settings/
# Node & Frontend (Showcase UI)
-apps/showcase_ui/node_modules/
-apps/showcase_ui/.angular/
-apps/showcase_ui/.angular/cache/
+apps/showcase_ui_v2/node_modules/
.npm/
npm-debug.log*
yarn-debug.log*
@@ -105,6 +104,8 @@ live_screenshot*.png
.env
.env.*
!.env.example
+credential_bindings.json
+endpoint_library.json
*.pem
*.key
diff --git a/.quality-baseline.json b/.quality-baseline.json
index 5b53f91a6..c46b7f3c1 100644
--- a/.quality-baseline.json
+++ b/.quality-baseline.json
@@ -1,5 +1,5 @@
{
- "broad_exception_handlers": 754,
+ "broad_exception_handlers": 763,
"silent_broad_exception_handlers": 0,
- "type_ignore_comments": 18
+ "type_ignore_comments": 16
}
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..8215cfb37
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,208 @@
+# AGENTS.md
+
+Instructions for ZCode agents working in this repository.
+
+## What this is
+
+ARTEMIS — an autonomous Android mobile agent. Python backend (uv-managed, py312) that drives
+real/mock Android devices through task workers, plus a web console (FastAPI + Vue 3).
+
+## Layout
+
+- `artemis/` — core package: `data_engine/` (session/step persistence, SQLite), `drivers/`
+ (ADB + `drivers/mock/mock_driver.py`), `runtime/` (daemon, device locks, server lifecycle),
+ `interfaces/cli/`, `sdk/`
+- `apps/admin_console/` — FastAPI server: `routers/tasks.py` (`/api/run`, `/api/status`,
+ `/api/sessions`), `services/task_queue_service.py` (queue worker spawning task subprocesses),
+ `core/state.py` (in-process server state + submission registry)
+- `apps/showcase_ui_v2/` — Vue 3 + Arco Design Vue + Pinia console (Vite 7, TS strict)
+- `mcp_server/`, `packages/artemis-client/`, `tests/unit|integration|e2e/`, `docs/research/`
+ (design docs worth reading before touching sensitive areas)
+
+## DSH plugin track (`plugins/dsh-autogamer`) — current mainline
+
+项目正按 R2' 收缩成一个 DSH 插件。方案、决策和进度都在 `docs/TODO.md`(活文档),**动手前先读它和守则,别从代码倒推约定**。
+
+**文档地图(按"什么时候必须读")**
+
+| 文档 | 什么时候读 |
+|---|---|
+| `docs/TODO.md` | 任何 DSH 侧改动之前。方案本体:决策表 `D*`、对接语义 `S*`、差距清单 `G*`、P0-P4 checklist 与「P0 执行状态」 |
+| `docs/migration/plugin-contract-rules.md` | 改插件、preset 或任一 `*.patch.yml` 之前。守则逐条带出处口径与可执行判据(**现行条款与范围只看 `docs/migration/registry.md`**);尤其 **R6 改 patch 必跑 `--dump-config` 自检**、**R7 复验必须开新会话**、**R9 判「生效」只认官方检查器**、**R20 遇 DSH 行为问题先查官方一手资料(subsystems 服务/事件参考、cordis-primer、cookbook、三个官方 skill、公开 packages 源码)再动手,不许先写猜测实现**、**R21 手镜像的宿主契约必须标出处与版本,并区分「单测证明」与「live 证明」**、**R22 功能实现三步序:查文档 → 写方案 → 才写码(方案提交早于实现提交)**、**R23 不得在 session/event 的 emit 里 append(注入类调用先推出发布边界)**、**R24 判「某事没发生」只认会话日志(wire 看不到终结回合的最后一次工具调用,插件 logger 不进 stdout)**、**R25 定根因先读机制文档、一次只测一个变量(推荐方案前先核它是否已是现状)**、**R26 gh 的写操作必须显式 --repo(默认解析可能打到上游别人的仓库)**、**R27 判据必须经变异校验证明它会红(样本量、执行位置、以及「变异本身没生效」都能把红绿读反),字节类口径写明量 blob 还是工作树,未进 CI 的门禁不许当验收**、**R28 实测/调研结论必须当轮回写台账,被推翻的旧表述不许静默保留(说「已完成 X」要能指到 commit 或命令输出)** |
+| `docs/CHANGELOGS.md` | **只记已发生并结案的变更**(`### #N` 三段:问题 / 修法 / 验收),按分类组织、顶部有索引;**已关闭差距 `G*` 的正文在这里**(「已关闭差距史」节)。编号 `#N` 发布后不得改写——守则与 TODO 都按 `changelogs #N` 引用 |
+| `docs/migration/registry.md` | **要引用或新增任何编号(D/S/G/R)之前**。编号唯一权威,含 `inventory/*` 与 `TODO.md` 的 G 编号撞车冲突表;`scripts/check_doc_registry.py` 机械守着 |
+| `docs/migration/glossary.md` | 读方案时卡住词了:`realm`、`parking`、`PTC`、`roster`,尤其是 **`inject` 有两个意思**(服务依赖 vs 不唤醒的上下文追加) |
+| `docs/migration/README.md` | 第一次接触本迁移时的入口页;也是「哪几处别当真」的清单(旧 py 智能栈 prompt 文档、旧控制台研究、docker 材料等) |
+| `docs/migration/upstream-plugin-forms.md` | 需要知道官方到底怎么说插件与工具形态时。调研正文、逐条口径标注(文档/源码/实测)、出处链接,以及被撤回的错误归因 |
+| `docs/migration/dsh-verification.md` | 查 rc.2 各子系统结论时。注意其中 37 处包内代码行引用属 D15 之前的 asar 口径,沿用前须按声明面重验 |
+| `docs/migration/feature-inventory.md` 与 `inventory/01-05` | 担心丢 fork 现有能力时;设备与智能栈部分是上游 canonical 契约 |
+| `docs/migration/upstream-rethink.md` | 要回到"为什么基线是 google/artemis"时 |
+| `docs/migration/ts-fullstack-spec.md` | 动 py 侧、或问"py 到底要不要全删、怎么删"时。**D19 终态规格**:TS 全栈、py 归零;能力迁移矩阵 + 四期分期;流程与勾选在 `docs/TODO.md` §七 |
+| `plugins/dsh-autogamer/README.md` | 安装流与包内形状说明;**状态与阻塞点只在 `docs/TODO.md` §零**,别处不复制 |
+
+上游一手资料(声明面之外唯一允许的补充来源,公开仓库):`github.com/deepseek-ai/deepseek-harness`
+@ `master` —— 三个官方 skill 在 `packages/preset/agent-preset/skills/`(
+`cordis-plugin-development` / `editing-cordis-compositions` / `cordis-composition-reference`),
+加上 `packages/preset/agent-preset{,-registry}/README.zh.md`、
+`packages/preset/agent-preset-registry/src/mount.ts`、`packages/bundle/web-app/presets/*.patch.yml`、
+`docs/cli-help.zh.md`。**解包 `app.asar` 取证一律禁止(R1)**。
+
+**插件侧命令**(在 `plugins/dsh-autogamer/` 内)
+
+- `npm run build`(tsc,插件走 `dist/`,**改 TS 后必须重新 build 再冷重启 dsh**)/ `npm run test`(vitest)
+- **本机装依赖与跑脚本要带旗标**(CI 用 pnpm 10 不受影响):
+ `pnpm --config.verify-deps-before-run=false install --frozen-lockfile --config.autoInstallPeers=false`,
+ `pnpm --config.verify-deps-before-run=false run build|test`。不带前者时 pnpm 11 会在每次 `run` 前隐式
+ install 并**重写 `pnpm-lock.yaml`**(`autoInstallPeers` 翻转,实测 +507 行)——那是环境产物,别提交
+- 声明面自检:`dsh --profile web --patch --dump-config`(`dsh` 取 PATH,不要写死安装盘符路径——写死曾导致找不到宿主),退出码必须为 0 且输出 grep 不到 `unmatched`(R6);schema 侧用 `--dump-config-schema`
+- `dsh plugin` 只是 **pnpm 转发垫片**,只证明"装了",不证明"生效"(R9)
+- spike 现场与转储在 `plugins/dsh-autogamer/spike/`:`web-live.patch.yml` 是 live overlay,
+ `wire-tools.py` 解析 `scripts/proxy-dump.log` 数请求里的 `tools`;`dsh-dump*` / `dsh-schema*` /
+ `dump-config.*` / `web-server*.log` / token 类文件全部 gitignored,**不要提交含 token 的转储**
+
+## Docs: a derived fact has exactly one authority
+
+Anything that can be **derived** from another file (an id range, an id count, a doc's
+line/KB size, "what we are blocked on right now") must live in **one** place; everywhere
+else is a *pointer*, never a copy. A copy has no mechanism to change when its source does,
+so it is wrong on the next edit while still passing every gate.
+
+- id ranges/counts → `docs/migration/registry.md`; status & blockers → `docs/TODO.md` §零;
+ closed-change prose → `docs/CHANGELOGS.md`; doc sizes → generated, never hand-typed, and measured
+ on **LF-normalized bytes** (blob-equivalent) — never raw worktree bytes, which depend on the
+ checkout (`core.autocrlf`), not on the repository
+- Gates enforce it mechanically: `scripts/check_doc_registry.py` fails on a "from id #1 to the
+ current max" total-range claim outside the authority (pointing ranges like `R2-R8` are fine);
+ `scripts/check_doc_tables.py` verifies `docs/migration/README.md` §三's size table and rewrites
+ it with `--refresh-sizes`
+- Both gates now run in CI as an un-gated step (`Check documentation gates` in the `python-quality`
+ job, changelogs **#46**), and they have actually executed on a push (run `38296414103`, changelogs
+ **#67** — which also falsified `G54`'s "this fork's `on: push` never produces a run"; the cause of
+ the earlier silent window is still unknown, so don't restate either claim). Still tick them with the **re-runnable commands**, never with
+ "CI guarantees it": an attestation without a command and without the checkout environment is how
+ six closed entries (#32–#44) signed off a size table that had never matched on an LF checkout
+- Why this rule exists (don't re-derive it): `docs/CHANGELOGS.md` **#30** — six drifted copies
+ were found at once, and #31 made them un-duplicable; **#45** is the third drift, caused by the
+ measurement itself rather than by a hand copy. See rule **R27**: a check must be proven able to
+ fail (mutation-check it) before it may be ticked
+
+## Commands
+
+- Backend tests (deterministic, no device needed): `make test` or
+ `uv run python -m pytest tests/unit/admin_console tests/unit/data_engine -q`
+- Frontend: run inside `apps/showcase_ui_v2/` — `npm run test` (vitest), `npm run typecheck`
+ (vue-tsc), `npm run dev` (or `make dev` / `scripts/dev.sh`: backend :8000 + Vite :5180 with
+ linked teardown)
+- Run the app: `uv run python -m artemis ui` serves API + built console on
+ `http://localhost:8000` (auto-rebuilds the Vue bundle at startup when sources changed);
+ `ARTEMIS_MOCK_DRIVER=1` forces the offline mock device driver (no real device required);
+ `make mock-ui` is the packaged shortcut for exactly that. The server **never auto-opens a
+ browser** (`--open` is opt-in; end-user launchers pass it explicitly)
+- Dev-environment startup habit: start the server in the background with
+ `ARTEMIS_MOCK_DRIVER=1 uv run python -m artemis ui --no-open`, then open
+ `http://localhost:8000` in the **agent's in-app browser** (browser-use IAB tab) — never
+ wait for or trigger the system browser, and don't pass `--open`
+- Lint: `uv run ruff check` / `uv run ruff format --check` (line length 100). `scripts/quality_ratchet.py`
+ is declared as a pre-commit hook but **the hook is not installed in this checkout** (`.git/hooks/pre-commit`
+ absent — `G69`), so run it explicitly: `uv run python scripts/quality_ratchet.py`. It is also an un-gated
+ CI step, ordered *before* the Node/plugin build so a build failure cannot mask it (changelogs **#69**).
+ Don't regress baselines in `.quality-baseline.json`; when raising one, write the measured number
+ (`--print-current`) and the attribution into `docs/CHANGELOGS.md`
+
+## Task lifecycle & session representations (high-regression area)
+
+A submission flows: `POST /api/run` → `TaskQueueService.enqueue_tasks` (assigns
+`conversation_id` when absent, records submission metadata in `state.submission_meta`
+registry) → worker subprocess (env `ARTEMIS_CONVERSATION_ID`, `ARTEMIS_SUBMITTED_AT`,
+`ARTEMIS_SESSION_ID`) → `DataEngine.start_session` persists the session row (SQLite at
+`traces/data_engine.db`; columns auto-migrate in `storage.py` init).
+
+While a task is queued/running it has no DB row; `/api/status` shows it via the queue payload
+(task items + DeviceExecutionLock ticket views) and `active_tasks`. Rules learned the hard way:
+
+- Registry values (thread id, enqueue wall-clock, **raw goal**) must win over lock data: the
+ ticket view's `created_at` is the lock-file mtime (pick-up time, not submission time) and
+ lock `description` is formatted as `"frontend task: "` — never let either reach round
+ titles or ordering
+- The engine persists `submitted_at`; chat rounds order by it, never `start_time` (which
+ drifts to the device-turn moment and reshuffles the queue as rounds launch)
+
+## Frontend console rules (`apps/showcase_ui_v2/src`)
+
+- `stores/session.ts` + `utils/session-merge.ts` implement a 4-step merge (DB rows → pending
+ queue → active tasks → tracking-map bridge). The non-reactive `tracking` map bridges the
+ queue→running gap; don't create fallback entries that shadow it
+- `conversationThreadKey` falls back to a synthetic `round:` key — it is for
+ timeline/group **filtering only**; submissions must use `submitConversationId` (real
+ `conversation_id` or null), never a synthetic key
+- Order/display anchors use `sessionChronoKey` (`submitted_at ?? start_time`); store
+ conversation grouping and `AgentTimeline` must share these helpers
+- Pending/active session representations must carry `conversation_id` / `submitted_at`
+ (`mapPendingQueue`, merge step 3) or rounds flash as separate "ghost" conversations
+- Specs are co-located `*.spec.ts` (vitest + jsdom); `en-US` locale keys are locked by tests —
+ add new keys to both `zh-CN` and `en-US`
+- The Vite dev proxy strips the `Origin` header on `/api` — required by the backend's
+ `SameOriginBoundaryMiddleware` (otherwise 403). Keep this when editing `vite.config.ts`
+- Built bundle goes to `dist/browser/` and is served by FastAPI (SPA fallback built in);
+ `npm run sync:resources` publishes it into the wheel fallback `artemis/resources/showcase_ui`
+
+## Change planning → todo ledger (habit)
+
+Applies to any non-trivial change. **Fixed order, no skipping (守则 R22): consult the official
+DSH docs first, then write the confirmed spec into `docs/TODO.md`, then code - and the spec commit
+must land before the implementation commit.** The moment the user confirms a plan/spec, **before**
+implementation starts, write the confirmed spec into `docs/TODO.md` as a themed entry. The
+entry must be self-sufficient: reading the entry alone must make clear what to do, how to do
+it, how to verify it, and what counts as done — no re-deriving state from code or chat:
+
+- **What**: scope, deliverables, explicit out-of-scope
+- **How**: approach and key decisions from the confirmed plan
+- **Verify**: exact commands / tests / manual checks
+- **Done means**: acceptance criteria as a checklist
+
+During implementation, after each checkpoint completes, update the entry in the same step —
+tick checklist items and record deviations from the plan — proactively, without being asked.
+Finished entries are deleted (docs no-old-archives rule); git history carries the narrative.
+
+**Incidental findings get recorded where they belong**: any problem noticed while working —
+out of scope, in passing — is written down in the same step it is found. Not yet fixed →
+`docs/TODO.md` as a gap row (numbered per `docs/migration/registry.md`, doc gates re-run);
+already fixed on the spot → `docs/CHANGELOGS.md` as a new `### #N` entry (问题 / 修法 / 验收),
+with `docs/TODO.md` keeping at most a one-line pointer. A chat-only mention or a silent fix
+does not count as recorded.
+
+**Change-history rules (docs/CHANGELOGS.md「写入规则」is the authority; this is the summary)**:
+- **One fact, one place**: TODO holds open work, plans and acceptance checklists; CHANGELOGS holds
+ closed changes only. When a gap closes, its prose moves to CHANGELOGS and its id gets one row in
+ the 已关闭差距史 table — TODO keeps no narrative for it.
+- **Entry ids are permanent**: `### #N` takes max+1, never renumbered or reused; categorising moves
+ blocks, never numbers (other docs cite `changelogs #N`).
+- **Every entry has the three sections**: 问题描述 (with measured evidence and the official source
+ text, per R20), 整改方案与实施 (which files and why this way), 验收标准 (re-runnable commands).
+- **Bloat control**: past ~60 lines per entry, move the research body into the matching
+ `docs/migration/*.md` and keep conclusion + link here; new categories must update the index table.
+- **Gates**: `scripts/check_doc_registry.py` reads closed `G*` ids from CHANGELOGS' table, so a closed
+ gap needs a row there **and** in `registry.md`; re-run both doc gates after any of these edits.
+
+## Change wrap-up
+
+When a unit of work is complete (tests green, behavior verified), finish it with a **local
+commit right away**: `git add ` + a Conventional Commit message. Never push —
+the user asks for pushes explicitly. Prefer one commit per coherent fix/feature over letting
+unrelated changes pile up in the working tree.
+
+**Test-sync review habit**: behavioral changes always ride together with their specs — every
+new behavior gets a locking test, and a test of removed behavior is removed in the same
+commit. After a batch of changes (or before wrapping up a longer session), dispatch review
+subagent(s) over the touched test suites (backend `tests/unit/...`, frontend `*.spec.ts`) to
+verify tests still match the implementation, close coverage gaps (especially regression
+guards for the bugs just fixed), and refactor stale/duplicated fixtures — then land the
+follow-up test commit.
+
+## Environment notes
+
+- Primary dev OS is Windows (Git Bash): dev scripts handle msys-vs-win32 PID quirks; don't
+ assume POSIX process control
+- `start.bat` / `start.sh` / `make start` are end-user launchers; prefer `uv run python -m
+ artemis ui --restart` when a stale server holds port 8000
+- Conventional Commits; recent history scopes frontend work as `showcase-v2`
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 61a8c1b43..000000000
--- a/Dockerfile
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-FROM node:22-bookworm-slim AS frontend-builder
-
-WORKDIR /build/apps/showcase_ui
-COPY apps/showcase_ui/package.json apps/showcase_ui/package-lock.json ./
-RUN npm ci
-COPY apps/showcase_ui/angular.json apps/showcase_ui/tsconfig*.json ./
-COPY apps/showcase_ui/public/ ./public/
-COPY apps/showcase_ui/src/ ./src/
-RUN npm run build
-
-
-FROM python:3.12-slim AS wheel-builder
-
-WORKDIR /build
-COPY artemis/ ./artemis/
-COPY apps/ ./apps/
-COPY mcp_server/ ./mcp_server/
-COPY third_party/ ./third_party/
-COPY packages/artemis-client/ ./packages/artemis-client/
-COPY config/ ./config/
-COPY pyproject.toml setup.py README.md LICENSE ./
-COPY --from=frontend-builder /build/apps/showcase_ui/dist/ ./apps/showcase_ui/dist/
-
-# Build wheels for the runtime image.
-RUN python -m pip wheel --no-cache-dir --no-deps --wheel-dir /wheels ./packages/artemis-client \
- && python -m pip wheel --no-cache-dir --no-deps --wheel-dir /wheels .
-
-
-FROM python:3.12-slim
-
-WORKDIR /app
-
-RUN apt-get update && apt-get install -y --no-install-recommends \
- adb \
- curl \
- git \
- && rm -rf /var/lib/apt/lists/*
-
-COPY --from=wheel-builder /wheels/ /wheels/
-RUN pip install --no-cache-dir /wheels/artemis_client-*.whl \
- && pip install --no-cache-dir /wheels/artemis-*.whl \
- && rm -rf /wheels
-
-ENV PYTHONUNBUFFERED=1
-ENV ARTEMIS_APP_DIR=/app
-ENV PORT=8080
-
-EXPOSE 8080
-
-# The console has no user authentication: publish the port to loopback only,
-# e.g. `docker run -p 127.0.0.1:8080:8080 ...`, and reach it remotely through
-# a Tailscale/SSH tunnel. (0.0.0.0 here is container-internal and required
-# for the port mapping to work.)
-CMD ["uvicorn", "apps.admin_console.server:app", "--host", "0.0.0.0", "--port", "8080"]
diff --git a/Makefile b/Makefile
index c3fca4a9a..5292d1177 100644
--- a/Makefile
+++ b/Makefile
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-.PHONY: help test test-integration test-device test-all install install-deps setup start ui restart stop status build-ui doctor clean precommit-install precommit lint format typecheck quality-ratchet
+.PHONY: help test test-integration test-device test-all install install-deps setup start ui dev restart stop status build-ui release-ui doctor clean precommit-install precommit lint format typecheck quality-ratchet
help: ## Show this help message
@echo 'Usage: make [target]'
@@ -20,12 +20,18 @@ help: ## Show this help message
@echo 'Available targets:'
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
+dev: ## One-command dev stack: FastAPI backend + Vite frontend (hot reload)
+ @bash scripts/dev.sh
+
start: ## One-click start Artemis Showcase UI and auto-open browser
@bash start.sh
ui: ## Launch the unified Showcase UI & Admin Console in browser
@uv run artemis ui --open
+mock-ui: ## Start the UI against the mock device driver (offline, no browser auto-open)
+ @ARTEMIS_MOCK_DRIVER=1 uv run python -m artemis ui --no-open
+
restart: ## Restart running Artemis Web UI & server
@uv run artemis restart
@@ -35,9 +41,14 @@ stop: ## Stop running Artemis Web UI & server
status: ## Display Artemis Web UI & server status
@uv run artemis status
-build-ui: ## Build the Showcase UI Angular frontend
+build-ui: ## Build the Showcase UI (Vue 3 + Vite) frontend
@echo "🎨 Building Showcase UI..."
- @cd apps/showcase_ui && npm install && npm run build
+ @cd apps/showcase_ui_v2 && npm install && npm run build
+
+release-ui: ## Build the Showcase UI and sync it into the wheel fallback resources
+ @echo "🚀 Releasing Showcase UI resources..."
+ @$(MAKE) build-ui
+ @cd apps/showcase_ui_v2 && npm run sync:resources
doctor: ## Run system, device, and toolchain diagnostics
@uv run artemis doctor
@@ -106,7 +117,7 @@ precommit: ## Run pre-commit hooks manually on all files
clean: ## Clean up generated files, caches, and traces
@echo "🧹 Cleaning up caches and temporary files..."
- @rm -rf .pytest_cache .ruff_cache .artemis_paused traces scratch .venv apps/showcase_ui/.angular build dist
+ @rm -rf .pytest_cache .ruff_cache .artemis_paused traces scratch .venv apps/showcase_ui_v2/node_modules/.vite build dist
@find . -type d -name "outputs" -exec rm -rf {} + 2>/dev/null || true
@find . -type f -name ".artemis_paused" -delete 2>/dev/null || true
@find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
diff --git a/README.md b/README.md
index 8683bcce3..103a525dd 100644
--- a/README.md
+++ b/README.md
@@ -104,6 +104,17 @@ cd artemis
> **Tip**: Opens `http://localhost:8000` in your default browser with a device connection wizard, live screen mirroring, prompt sandbox, and execution replays. You can also run directly from CLI: `uv run artemis run "Open Settings, find Battery and tell me current level" --profile flash`.
+### Development (hot reload)
+
+Contributing to the web console? One command starts the FastAPI backend and the Vite dev server together, with linked teardown (Ctrl+C stops both):
+
+```bash
+bash scripts/dev.sh # or: make dev
+```
+
+- **Frontend (Vite, hot reload)**: `http://127.0.0.1:5180`
+- **Backend (FastAPI)**: `http://127.0.0.1:8000` — reused automatically when an instance is already running
+
@@ -246,7 +257,7 @@ if __name__ == "__main__":
Console Overview : ① View Switcher (Home / Workspace) · ② Model & Replay (Flash/Pro status & video replay) · ③ Live Agent Stream (Action perception, target coordinates & structured results) · ④ Prompt Dock (Natural language dispatch) · ⑤ Task Queue & Dashboard (Lifecycle & history)
-* **Web Visual Test Console (`uv run artemis ui`)**: Real-time screen projection and interactive panel, supporting natural language test dispatch, live reasoning telemetry, action trajectories, and execution replay; manage server lifecycle anytime from any terminal using `uv run artemis restart`, `uv run artemis stop`, and `uv run artemis status`;
+* **Web Visual Test Console (`uv run artemis ui`)**: Real-time screen projection and interactive panel, supporting natural language test dispatch, live reasoning telemetry, action trajectories, and execution replay; manage server lifecycle anytime from any terminal using `uv run artemis restart`, `uv run artemis stop`, and `uv run artemis status`. The web frontend source lives in [`apps/showcase_ui_v2`](./apps/showcase_ui_v2) (Vue 3 + Arco Design Vue);
* **MCP Server**: Connects **Antigravity, Claude Code, Windsurf**, and other MCP clients to real devices for bug reproduction and test execution;
* **Developer CLI (`uv run artemis run`)**: Direct terminal execution for automated test cases, exploratory stability inspection, or AndroidWorld benchmarks with high-fidelity structured terminal output;
* **Python SDK**: Integrates as a standard Python library into existing automated testing frameworks (e.g., pytest) or CI/CD pipelines with strongly typed Pydantic structured outputs and assertion support.
diff --git a/README_CN.md b/README_CN.md
index 67f461c58..ca72dafcf 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -106,6 +106,17 @@ cd artemis
> **提示**:启动后将自动在默认浏览器中打开 Web 控制台(`http://localhost:8000`),提供设备连接向导、实时投屏、任务演练与状态回放面板。你也可以通过命令行直接运行:`uv run artemis run "打开系统设置,找到电池选项并告诉我当前电量" --profile flash`。
+### 开发模式(热更新)
+
+参与 Web 控制台开发?一条命令同时启动 FastAPI 后端与 Vite 前端,联动退出(Ctrl+C 一键全停):
+
+```bash
+bash scripts/dev.sh # 或者:make dev
+```
+
+- **前端(Vite,热更新)**:`http://127.0.0.1:5180`
+- **后端(FastAPI)**:`http://127.0.0.1:8000` —— 已有实例在运行时自动复用
+
@@ -247,7 +258,7 @@ if __name__ == "__main__":
控制台功能概览 :① 视图切换 (主页与工作区) · ② 运行模式与回放 (Flash/Pro 状态与视频回放) · ③ 实时感知推理流 (动作感知、目标坐标与结构化总结) · ④ 提示词输入坞 (Prompt Dock) (自然语言下发) · ⑤ 任务队列看板 (生命周期与历史回溯)
-* **Web 可视化测试控制台 (`uv run artemis ui`)**:集成设备实时投屏与交互面板,支持通过自然语言下发测试用例,实时观测推理步骤、操作轨迹、截图留存与异常状态回放;支持在任意终端使用 `uv run artemis restart`、`uv run artemis stop`、`uv run artemis status` 一键重启、关停或查看服务状态;
+* **Web 可视化测试控制台 (`uv run artemis ui`)**:集成设备实时投屏与交互面板,支持通过自然语言下发测试用例,实时观测推理步骤、操作轨迹、截图留存与异常状态回放;支持在任意终端使用 `uv run artemis restart`、`uv run artemis stop`、`uv run artemis status` 一键重启、关停或查看服务状态。Web 前端源码位于 [`apps/showcase_ui_v2`](./apps/showcase_ui_v2)(Vue 3 + Arco Design Vue);
* **原生 MCP 协议 (IDE 协同)**:作为标准 MCP 服务器接入 **Antigravity、Claude Code、Windsurf** 等开发环境,在 IDE 中直接驱动真机完成自动化测试与 Bug 复现验证;
* **开发者命令行 CLI (`uv run artemis run`)**:支持通过终端直接执行自动化测试用例、探索性稳定性巡检或 AndroidWorld 基准评测,提供高保真结构化终端输出;
* **Python SDK**:作为标准 Python 库集成至现有自动化测试框架(如 pytest)或 CI/CD 流水线,提供基于 Pydantic 的强类型结构化结果与断言支持。
diff --git a/apps/__init__.py b/apps/__init__.py
deleted file mode 100644
index 89afab843..000000000
--- a/apps/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Artemis Applications Package."""
diff --git a/apps/admin_console/__init__.py b/apps/admin_console/__init__.py
deleted file mode 100644
index 352859e64..000000000
--- a/apps/admin_console/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Artemis Admin & Trace Console Package."""
-
-from apps.admin_console.server import app
-
-__all__ = ["app"]
diff --git a/apps/admin_console/core/__init__.py b/apps/admin_console/core/__init__.py
deleted file mode 100644
index 58d482ea3..000000000
--- a/apps/admin_console/core/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
diff --git a/apps/admin_console/core/config.py b/apps/admin_console/core/config.py
deleted file mode 100644
index b62b89a09..000000000
--- a/apps/admin_console/core/config.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Admin Console configuration facade.
-
-All configuration and path management is centralized in `artemis.config`.
-This module re-exports common constants for full backward compatibility.
-"""
-
-from artemis.config import (
- DB_PATH,
- IMAGES_DIR,
- PAUSE_FILE,
- REPLAY_BASE_DIR,
- TEST_DATA_DIR,
- TEST_OUTPUTS_DIR,
- TRACES_PATH,
- WORKSPACE_ROOT,
- init_ls_address,
-)
-
-__all__ = [
- "DB_PATH",
- "IMAGES_DIR",
- "PAUSE_FILE",
- "REPLAY_BASE_DIR",
- "TEST_DATA_DIR",
- "TEST_OUTPUTS_DIR",
- "TRACES_PATH",
- "WORKSPACE_ROOT",
- "init_ls_address",
-]
diff --git a/apps/admin_console/core/security.py b/apps/admin_console/core/security.py
deleted file mode 100644
index a5b132cee..000000000
--- a/apps/admin_console/core/security.py
+++ /dev/null
@@ -1,178 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Same-origin security boundary for the Artemis console.
-
-Artemis runs without user accounts: whoever can reach the TCP port is the
-operator. For that model to hold for a server that browsers also talk to,
-two browser-borne attack vectors must be closed even on a loopback bind:
-
-- DNS rebinding: a page at ``attacker.example`` re-points its DNS record at
- 127.0.0.1 so the victim's browser reads API responses under the attacker's
- origin. Blocked by validating the ``Host`` header.
-- Cross-site request forgery: any web page can fire side-effectful requests
- at ``http://127.0.0.1:8000`` from a visitor's browser. Blocked by requiring
- the ``Origin`` header, when a browser sends one, to match the request host.
-
-Non-browser clients (the SDK, curl, the CLI, MCP) send no ``Origin`` header
-and pass through untouched. Remote access is expected to arrive over a
-network-level tunnel (Tailscale, SSH port forward); a tunnel hostname that is
-not an IP literal can be admitted via ``ARTEMIS_ALLOWED_HOSTS``.
-"""
-
-from __future__ import annotations
-
-import ipaddress
-import json
-import os
-from urllib.parse import urlsplit
-
-_SECURITY_HEADERS: tuple[tuple[bytes, bytes], ...] = (
- (b"x-content-type-options", b"nosniff"),
- (b"referrer-policy", b"no-referrer"),
- (b"x-frame-options", b"DENY"),
- (b"cross-origin-opener-policy", b"same-origin"),
-)
-
-# Paths whose responses may hold task data, media, or diagnostics and must not
-# land in shared caches or survive on disk after the session.
-_NO_STORE_PREFIXES = ("/api/",)
-
-
-def _hostname(netloc: str) -> str:
- """Extract the lowercase hostname from a ``host[:port]`` netloc."""
- netloc = netloc.strip().lower()
- if netloc.startswith("["): # IPv6 literal, e.g. [::1]:8000
- return netloc.partition("]")[0].lstrip("[")
- return netloc.rpartition(":")[0] if netloc.count(":") == 1 else netloc
-
-
-def _is_ip_literal(host: str) -> bool:
- """An IP-literal Host cannot be forged through DNS rebinding: a browser
- only sends ``Host: 192.168.1.5`` when it actually connected to that IP."""
- try:
- ipaddress.ip_address(host)
- return True
- except ValueError:
- return False
-
-
-def allowed_hostnames() -> frozenset[str]:
- """Hostnames (not IP literals) accepted in Host and Origin headers."""
- names = {"localhost"}
- extra = os.environ.get("ARTEMIS_ALLOWED_HOSTS", "")
- names.update(part.strip().lower() for part in extra.split(",") if part.strip())
- return frozenset(names)
-
-
-def host_is_allowed(host_header: str) -> bool:
- host = _hostname(host_header)
- if not host:
- return False
- return _is_ip_literal(host) or host in allowed_hostnames()
-
-
-def origin_is_allowed(origin_header: str, host_header: str) -> bool:
- """A browser request is acceptable when its page is same-origin with this
- server, or was served from an explicitly allowed tunnel hostname."""
- origin = origin_header.strip().lower()
- if not origin or origin == "null":
- return False
- origin_netloc = urlsplit(origin).netloc
- if not origin_netloc:
- return False
- if origin_netloc == host_header.strip().lower():
- return True
- return _hostname(origin_netloc) in allowed_hostnames()
-
-
-class SameOriginBoundaryMiddleware:
- """Pure ASGI middleware enforcing the Host/Origin boundary and appending
- baseline security headers. Pure ASGI (not BaseHTTPMiddleware) so future
- WebSocket endpoints cannot slip past the Origin check."""
-
- def __init__(self, app):
- self.app = app
-
- async def __call__(self, scope, receive, send):
- if scope["type"] not in ("http", "websocket"):
- await self.app(scope, receive, send)
- return
-
- if os.environ.get("ARTEMIS_DISABLE_ORIGIN_GUARD", "").lower() in {"1", "true", "yes"}:
- await self._forward(scope, receive, send)
- return
-
- headers = {k.decode("latin-1").lower(): v.decode("latin-1") for k, v in scope["headers"]}
- host_header = headers.get("host", "")
- origin_header = headers.get("origin")
-
- if not host_is_allowed(host_header):
- await self._reject(
- scope,
- send,
- "Unrecognized Host header. Access Artemis via localhost or an IP address, "
- "or add your tunnel hostname to ARTEMIS_ALLOWED_HOSTS.",
- )
- return
-
- if origin_header is not None and not origin_is_allowed(origin_header, host_header):
- await self._reject(
- scope,
- send,
- "Cross-origin browser requests are not accepted by the Artemis console.",
- )
- return
-
- await self._forward(scope, receive, send)
-
- async def _forward(self, scope, receive, send):
- if scope["type"] != "http":
- await self.app(scope, receive, send)
- return
-
- path = scope.get("path", "")
-
- async def send_with_headers(message):
- if message["type"] == "http.response.start":
- raw = list(message.get("headers", []))
- existing = {name.lower() for name, _ in raw}
- for name, value in _SECURITY_HEADERS:
- if name not in existing:
- raw.append((name, value))
- if b"cache-control" not in existing and path.startswith(_NO_STORE_PREFIXES):
- raw.append((b"cache-control", b"no-store"))
- message = {**message, "headers": raw}
- await send(message)
-
- await self.app(scope, receive, send_with_headers)
-
- @staticmethod
- async def _reject(scope, send, detail: str):
- if scope["type"] == "websocket":
- await send({"type": "websocket.close", "code": 1008})
- return
- body = json.dumps({"detail": detail}).encode("utf-8")
- await send(
- {
- "type": "http.response.start",
- "status": 403,
- "headers": [
- (b"content-type", b"application/json"),
- (b"content-length", str(len(body)).encode("latin-1")),
- *_SECURITY_HEADERS,
- ],
- }
- )
- await send({"type": "http.response.body", "body": body})
diff --git a/apps/admin_console/core/state.py b/apps/admin_console/core/state.py
deleted file mode 100644
index cc2c4a33c..000000000
--- a/apps/admin_console/core/state.py
+++ /dev/null
@@ -1,243 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import asyncio
-from collections.abc import Callable
-from typing import Any
-
-from artemis.config import PAUSE_FILE
-from artemis.runtime.process_probe import pid_is_alive
-
-
-class ServerState:
- """Encapsulates all runtime states of the debug server."""
-
- def __init__(self):
- self.ipc_subscribers: list[Callable[[str, Any], None]] = []
- self.ipc_server: asyncio.Server | None = None
- self.ipc_serve_task: asyncio.Task | None = None
- self.ipc_port: int | None = None
- self.port: int = 8000
- self.host: str = "127.0.0.1"
- self.is_shutting_down: bool = False
-
- self.current_process: asyncio.subprocess.Process | None = None
- self.current_goal: str | None = None
- self.current_profile: str | None = None
- self.active_connections: dict[str, dict[str, Any]] = {}
- self.active_session_id: str | None = None
- # Run keys (session id, or the synthetic run key of session-less runs)
- # that were stopped manually. Tracked per run so a manual stop of one
- # device's task never pollutes the terminal-status resolution of a
- # concurrently running task on another device. Entries are discarded by
- # each run's finalizer.
- self.manually_stopped_run_ids: set[str] = set()
- self.cancelled_session_ids: set[str] = set()
- self.startup_progress: dict[str, list[dict[str, Any]]] = {}
-
- # Concurrent task executions keyed by session_id. Each value holds
- # {"process", "device_id", "goal", "profile"}. `current_process` /
- # `active_session_id` mirror the most recently launched run for
- # backward compatibility with single-task consumers.
- self.active_runs: dict[str, dict[str, Any]] = {}
-
- # Unified single source of truth for task queue
- self.queue_items: list[dict[str, Any]] = []
- self._wake_event: asyncio.Event | None = None
- self._shutdown_event: asyncio.Event | None = None
- self.worker_task: asyncio.Task | None = None
-
- @property
- def wake_event(self) -> asyncio.Event:
- try:
- loop = asyncio.get_running_loop()
- except RuntimeError:
- loop = None
-
- if self._wake_event is None or (
- loop
- and getattr(self._wake_event, "_loop", None) is not None
- and self._wake_event._loop != loop
- ):
- self._wake_event = asyncio.Event()
- return self._wake_event
-
- @property
- def shutdown_event(self) -> asyncio.Event:
- """Event set as soon as the HTTP server receives a shutdown signal."""
- try:
- loop = asyncio.get_running_loop()
- except RuntimeError:
- loop = None
-
- if self._shutdown_event is None or (
- loop
- and getattr(self._shutdown_event, "_loop", None) is not None
- and self._shutdown_event._loop != loop
- ):
- self._shutdown_event = asyncio.Event()
- return self._shutdown_event
-
- @property
- def task_queue(self) -> list[dict[str, Any]]:
- """Backward compatibility alias for queue_items."""
- return self.queue_items
-
- @property
- def queue_tasks(self) -> list[dict[str, Any]]:
- """Returns all currently pending tasks in the queue."""
- return [t for t in self.queue_items if isinstance(t, dict) and t.get("status") == "pending"]
-
- @queue_tasks.setter
- def queue_tasks(self, val: list[dict[str, Any]]):
- # Compatibility setter
- self.queue_items = list(val)
-
- @property
- def queue_goals(self) -> list[str]:
- """Backward compatibility helper for queue goals."""
- return [t.get("goal", "") for t in self.queue_tasks]
-
- @queue_goals.setter
- def queue_goals(self, val: list[Any]):
- pass
-
- def prune_finished_runs(self) -> None:
- """Drop finished entries from active_runs.
-
- Besides reaped processes (returncode set), also drops entries whose pid no
- longer exists: if a run coroutine dies without reaping its child, the entry
- must not permanently block the scheduler.
- """
- for sid, run in list(self.active_runs.items()):
- proc = run.get("process")
- if proc is None or proc.returncode is not None:
- self.active_runs.pop(sid, None)
- continue
- pid = getattr(proc, "pid", None)
- if pid and not pid_is_alive(pid):
- self.active_runs.pop(sid, None)
-
- @property
- def busy_device_ids(self) -> set[str]:
- """Device serials currently owned by an in-flight run."""
- self.prune_finished_runs()
- return {
- str(run.get("lock_key") or run["device_id"])
- for run in self.active_runs.values()
- if run.get("device_id")
- }
-
- @property
- def is_running(self) -> bool:
- self.prune_finished_runs()
- if self.active_runs:
- return True
- has_proc = False
- if self.current_process is not None:
- if self.current_process.returncode is not None:
- has_proc = False
- self.current_process = None
- else:
- pid = getattr(self.current_process, "pid", None)
- if pid and pid_is_alive(pid):
- has_proc = True
- else:
- has_proc = False
- self.current_process = None
-
- has_running_item = any(
- isinstance(t, dict) and t.get("status") == "running" for t in self.queue_items
- )
-
- has_live_connection = False
- for sid, conn in list(self.active_connections.items()):
- c_pid = conn.get("pid")
- if c_pid:
- if pid_is_alive(c_pid):
- has_live_connection = True
- else:
- self.active_connections.pop(sid, None)
-
- if not has_proc and not has_running_item and not has_live_connection:
- self.active_session_id = None
- self.current_process = None
- return False
-
- return True
-
- @property
- def is_paused(self) -> bool:
- return PAUSE_FILE.exists()
-
- @property
- def paused_error(self) -> str | None:
- """Return the persisted pause reason for clients that missed the SSE event."""
- if not PAUSE_FILE.exists():
- return None
- try:
- message = PAUSE_FILE.read_text(encoding="utf-8", errors="replace").strip()
- except OSError:
- return "AI model request failed. The task is paused."
- if message.startswith("LLM Error: "):
- message = message[len("LLM Error: ") :]
- return message or "AI model request failed. The task is paused."
-
- def add_subscriber(self, callback: Callable[[str, Any], None]):
- if callback not in self.ipc_subscribers:
- self.ipc_subscribers.append(callback)
-
- def remove_subscriber(self, callback: Callable[[str, Any], None]):
- if callback in self.ipc_subscribers:
- self.ipc_subscribers.remove(callback)
-
- def record_startup_progress(self, data: dict[str, Any]) -> None:
- """Retain the short pre-trace timeline so late SSE clients can catch up."""
- session_id = data.get("session_id")
- stage = data.get("stage")
- if not session_id or not stage:
- return
-
- key = str(session_id)
- events = self.startup_progress.setdefault(key, [])
- replacement_index = next(
- (index for index, item in enumerate(events) if item.get("stage") == stage),
- None,
- )
- snapshot = dict(data)
- if replacement_index is None:
- events.append(snapshot)
- else:
- events[replacement_index] = snapshot
- self.startup_progress[key] = events[-16:]
-
- def get_startup_progress(self, session_id: str) -> list[dict[str, Any]]:
- return [dict(item) for item in self.startup_progress.get(str(session_id), [])]
-
- def clear_queue(self):
- """Clears all pending items from the task queue."""
- self.queue_items = [t for t in self.queue_items if t.get("status") == "running"]
- if self._wake_event:
- self._wake_event.set()
-
-
-# Global shared instance
-state = ServerState()
-
-import sys
-
-if __name__ == "admin_console.core.state":
- sys.modules["apps.admin_console.core.state"] = sys.modules[__name__]
-elif __name__ == "apps.admin_console.core.state":
- sys.modules["admin_console.core.state"] = sys.modules[__name__]
diff --git a/apps/admin_console/database/__init__.py b/apps/admin_console/database/__init__.py
deleted file mode 100644
index 58d482ea3..000000000
--- a/apps/admin_console/database/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
diff --git a/apps/admin_console/database/connection.py b/apps/admin_console/database/connection.py
deleted file mode 100644
index 83eeedd85..000000000
--- a/apps/admin_console/database/connection.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from contextlib import contextmanager
-import logging
-from pathlib import Path
-import sqlite3
-
-from artemis.config import DB_PATH
-
-logger = logging.getLogger(__name__)
-
-_initialized_dbs = set()
-
-
-def get_db(db_path: Path | None = None) -> sqlite3.Connection:
- """Returns a SQLite connection with timeout and Row row_factory."""
- path = db_path or DB_PATH
- path_key = str(path)
- if path_key not in _initialized_dbs:
- path.parent.mkdir(parents=True, exist_ok=True)
- try:
- from artemis.data_engine.storage import StorageManager
-
- StorageManager(db_path=path, base_trace_dir=path.parent)
- _initialized_dbs.add(path_key)
- except (ImportError, OSError, sqlite3.Error) as exc:
- # Schema bootstrap is best-effort here; queries against a missing
- # schema will surface their own errors, but log the root cause.
- logger.warning("Schema bootstrap failed for %s: %s", path, exc)
- conn = sqlite3.connect(path, timeout=10.0)
- conn.row_factory = sqlite3.Row
- try:
- conn.execute("PRAGMA journal_mode=WAL")
- conn.execute("PRAGMA busy_timeout=10000")
- except sqlite3.Error:
- # Optional performance PRAGMAs; the connection works without them.
- pass
- return conn
-
-
-@contextmanager
-def db_session(db_path: Path | None = None):
- """Context manager for SQLite connections ensuring clean closure."""
- conn = get_db(db_path)
- try:
- yield conn
- finally:
- conn.close()
diff --git a/apps/admin_console/database/repositories/__init__.py b/apps/admin_console/database/repositories/__init__.py
deleted file mode 100644
index 58d482ea3..000000000
--- a/apps/admin_console/database/repositories/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
diff --git a/apps/admin_console/database/repositories/session_repository.py b/apps/admin_console/database/repositories/session_repository.py
deleted file mode 100644
index a12e86928..000000000
--- a/apps/admin_console/database/repositories/session_repository.py
+++ /dev/null
@@ -1,564 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import json
-import logging
-import os
-import sqlite3
-import time
-from typing import Any
-
-from artemis.runtime import trace_store
-
-logger = logging.getLogger(__name__)
-
-try:
- from admin_console.database.connection import db_session
-except ImportError:
- from apps.admin_console.database.connection import db_session
-
-
-def _canonicalize_status(row: dict[str, Any]) -> dict[str, Any]:
- """Map the legacy "success" terminal status to the canonical "completed".
-
- Rows written before the vocabulary was unified may still carry "success";
- API consumers must only ever see "completed". get_session_status is left
- raw on purpose so the queue reconcile can detect and persist the rewrite.
- """
- if row.get("status") == "success":
- row["status"] = "completed"
- return row
-
-
-class SessionRepository:
- """Repository handling all database queries and updates for Sessions."""
-
- def __init__(self, db_path=None):
- self.db_path = db_path
-
- @staticmethod
- def process_is_alive(pid: Any) -> bool:
- """Return whether a session's worker PID is still alive.
-
- Delegates to the repository-wide probe: indeterminate liveness
- (e.g. AccessDenied) counts as alive so a running session is never
- reaped by mistake.
- """
- try:
- pid_int = int(pid)
- except (TypeError, ValueError):
- return False
- from artemis.runtime.process_probe import pid_is_alive
-
- return pid_is_alive(pid_int)
-
- def get_all_sessions(self) -> list[dict[str, Any]]:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute("SELECT * FROM sessions ORDER BY start_time DESC")
- return [_canonicalize_status(dict(r)) for r in cursor.fetchall()]
-
- def get_video_recordings_map(self) -> dict[str, str]:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT name FROM sqlite_master WHERE type='table' AND name='video_recordings'"
- )
- if cursor.fetchone() is None:
- return {}
- columns = {str(row[1]) for row in cursor.execute("PRAGMA table_info(video_recordings)")}
- # Failed recordings may still be recoverable from disk.
- # Callers exclude in-progress sessions before resolving paths.
- ready_clause = (
- "status IN ('ready', 'failed')" if "status" in columns else "end_time IS NOT NULL"
- )
- cursor.execute(
- "SELECT session_id, local_video_path FROM video_recordings "
- "WHERE session_id IS NOT NULL AND local_video_path IS NOT NULL "
- f"AND {ready_clause} ORDER BY start_time ASC"
- )
- return {str(r[0]): str(r[1]) for r in cursor.fetchall() if r[0] and r[1]}
-
- def get_video_recording_for_session(self, session_id: str) -> dict[str, Any] | None:
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT name FROM sqlite_master WHERE type='table' AND name='video_recordings'"
- )
- if cursor.fetchone() is None:
- return None
- cursor.execute(
- "SELECT * FROM video_recordings WHERE session_id = ? "
- "ORDER BY start_time DESC LIMIT 1",
- (str(session_id),),
- )
- row = cursor.fetchone()
- if not row:
- return None
- result = dict(row)
- if "status" not in result:
- result["status"] = (
- "ready" if result.get("end_time") is not None else "recording"
- )
- result.setdefault("error", None)
- return result
- except Exception:
- return None
-
- def get_latest_video_recordings_map(self) -> dict[str, dict[str, Any]]:
- """Return the newest recording row for every session in one query.
-
- The task-list endpoint renders every session at once. Calling
- ``get_video_recording_for_session`` in that loop used to open a new
- SQLite connection for every row, which made a refresh progressively
- slower as history grew.
- """
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT name FROM sqlite_master WHERE type='table' AND name='video_recordings'"
- )
- if cursor.fetchone() is None:
- return {}
-
- rows = cursor.execute(
- "SELECT * FROM video_recordings WHERE session_id IS NOT NULL "
- "ORDER BY start_time DESC"
- ).fetchall()
- latest: dict[str, dict[str, Any]] = {}
- for row in rows:
- recording = dict(row)
- session_id = str(recording.get("session_id") or "")
- if not session_id or session_id in latest:
- continue
- if "status" not in recording:
- recording["status"] = (
- "ready" if recording.get("end_time") is not None else "recording"
- )
- recording.setdefault("error", None)
- latest[session_id] = recording
- return latest
- except Exception:
- return {}
-
- def get_unfinalized_video_recordings(self) -> list[dict[str, Any]]:
- """Recordings whose worker never finalized them, oldest first.
-
- Rows of sessions that are still ``running`` are skipped: their worker
- (possibly owned by another daemon) may still be writing the file.
- """
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT name FROM sqlite_master WHERE type='table' AND name='video_recordings'"
- )
- if cursor.fetchone() is None:
- return []
- columns = {
- str(row[1]) for row in cursor.execute("PRAGMA table_info(video_recordings)")
- }
- pending_clause = (
- "v.status IN ('recording', 'finalizing', 'failed')"
- if "status" in columns
- else "v.end_time IS NULL"
- )
- cursor.execute(
- "SELECT v.session_id, v.local_video_path, v.start_time, "
- "s.status AS session_status, s.start_time AS session_start_time "
- "FROM video_recordings v LEFT JOIN sessions s ON s.session_id = v.session_id "
- f"WHERE v.session_id IS NOT NULL AND v.local_video_path IS NOT NULL "
- f"AND {pending_clause} ORDER BY v.start_time ASC"
- )
- rows = [dict(r) for r in cursor.fetchall()]
- except (OSError, sqlite3.Error):
- return []
- return [row for row in rows if str(row.get("session_status") or "").lower() != "running"]
-
- def mark_recording_failed_if_pending(self, session_id: str, error: str) -> bool:
- """Close a recording lifecycle when its worker exits before finalization."""
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "UPDATE video_recordings SET status = 'failed', error = ?, "
- "end_time = COALESCE(end_time, ?) "
- "WHERE session_id = ? AND status IN ('recording', 'finalizing')",
- (error, time.time(), str(session_id)),
- )
- conn.commit()
- return cursor.rowcount > 0
- except Exception:
- return False
-
- def mark_recording_ready(self, session_id: str, local_video_path: str) -> bool:
- """Mark a recording as ready with its finalized video path."""
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "UPDATE video_recordings SET status = 'ready', error = NULL, "
- "local_video_path = ?, end_time = COALESCE(end_time, ?) "
- "WHERE session_id = ?",
- (str(local_video_path), time.time(), str(session_id)),
- )
- updated = cursor.rowcount > 0
- if os.path.exists(str(local_video_path)):
- # Mirror the worker's own finalization so the session row
- # and trace tooling see the recovered file too.
- cursor.execute(
- "UPDATE sessions SET video_filepath = ? WHERE session_id = ?",
- (str(local_video_path), str(session_id)),
- )
- conn.commit()
- return updated
- except Exception:
- return False
-
- def get_llm_traces_for_profile(self, session_id: str, limit: int = 3) -> list[str]:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT payload FROM traces WHERE session_id = ? AND type = 'llm_call' LIMIT ?",
- (session_id, limit),
- )
- rows = cursor.fetchall()
- return [r[0] for r in rows if r and r[0]]
-
- def get_llm_traces_for_profiles_map(
- self, session_ids: list[str] | None = None, limit: int = 3
- ) -> dict[str, list[str]]:
- """Return bounded LLM payloads for only the sessions needing fallback."""
- if session_ids is not None and not session_ids:
- return {}
-
- session_filter = ""
- params: list[Any] = []
- if session_ids is not None:
- placeholders = ",".join("?" for _ in session_ids)
- session_filter = f" AND session_id IN ({placeholders})"
- params.extend(session_ids)
- params.append(limit)
-
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT session_id, payload FROM ("
- " SELECT session_id, payload, ROW_NUMBER() OVER ("
- " PARTITION BY session_id ORDER BY timestamp ASC"
- " ) AS row_number FROM traces WHERE type = 'llm_call'"
- f"{session_filter}"
- ") WHERE row_number <= ?",
- params,
- )
- result: dict[str, list[str]] = {}
- for row in cursor.fetchall():
- session_id = str(row[0] or "")
- payload = row[1]
- if session_id and payload:
- result.setdefault(session_id, []).append(payload)
- return result
-
- def get_agent_trace_names(self, session_id: str) -> list[str]:
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT DISTINCT name FROM traces WHERE session_id = ? AND type IN ('agent', 'llm_call')",
- (session_id,),
- )
- rows = cursor.fetchall()
- return [r[0] for r in rows if r and r[0]]
- except Exception:
- return []
-
- def get_agent_trace_names_map(self) -> dict[str, list[str]]:
- """Return distinct agent/LLM trace names grouped by session."""
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT session_id, name FROM traces "
- "WHERE type IN ('agent', 'llm_call') AND session_id IS NOT NULL "
- "GROUP BY session_id, name"
- )
- result: dict[str, list[str]] = {}
- for row in cursor.fetchall():
- session_id = str(row[0] or "")
- name = row[1]
- if session_id and name:
- result.setdefault(session_id, []).append(name)
- return result
-
- def get_session_usage(self, session_id: str) -> dict[str, Any]:
- """Aggregate measured LLM usage for one session.
-
- Sums every ``llm_usage`` trace the token meter recorded (all agents,
- Flash and Pro alike) and reports the prompt size of the latest
- Operator/FlashRunner call as the live executor context. Also echoes
- the profile and Pro tuning the run was started with, read from the
- session's stored ``device_info``.
- """
- from artemis.services.token_meter import (
- OPERATOR_CONTEXT_WINDOW_TOKENS,
- OPERATOR_NODE_NAMES,
- )
-
- def _int(value: Any) -> int:
- try:
- return int(value or 0)
- except (TypeError, ValueError):
- return 0
-
- llm_calls = prompt = completion = total = cached = 0
- operator_context: int | None = None
- operator_context_at: float | None = None
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT payload, timestamp FROM traces WHERE session_id = ?"
- " AND type = 'llm_call' AND name = 'llm_usage' ORDER BY timestamp ASC",
- (session_id,),
- )
- for row in cursor.fetchall():
- raw = row["payload"]
- try:
- payload = json.loads(raw) if isinstance(raw, str) else raw
- except (ValueError, TypeError):
- continue
- if not isinstance(payload, dict):
- continue
- p = _int(payload.get("prompt_tokens"))
- c = _int(payload.get("completion_tokens"))
- t = _int(payload.get("total_tokens")) or (p + c)
- if t <= 0:
- continue
- llm_calls += 1
- prompt += p
- completion += c
- total += t
- cached += _int(payload.get("cached_tokens"))
- node = str(payload.get("node") or "").strip().lower()
- if node in OPERATOR_NODE_NAMES:
- operator_context = _int(payload.get("context_base_tokens")) or p
- operator_context_at = row["timestamp"]
-
- cursor.execute("SELECT device_info FROM sessions WHERE session_id = ?", (session_id,))
- session_row = cursor.fetchone()
-
- profile: str | None = None
- run_tuning: dict[str, Any] | None = None
- if session_row and session_row["device_info"]:
- try:
- d_info = json.loads(session_row["device_info"])
- except (ValueError, TypeError):
- d_info = None
- if isinstance(d_info, dict):
- profile = d_info.get("profile") or None
- tuning = d_info.get("run_tuning")
- if isinstance(tuning, dict) and tuning:
- run_tuning = dict(tuning)
-
- return {
- "session_id": session_id,
- "llm_calls": llm_calls,
- "prompt_tokens": prompt,
- "completion_tokens": completion,
- "total_tokens": total,
- "cached_tokens": cached,
- "operator_context_tokens": operator_context,
- "operator_context_window_tokens": OPERATOR_CONTEXT_WINDOW_TOKENS,
- "operator_context_updated_at": operator_context_at,
- "profile": profile,
- "run_tuning": run_tuning,
- }
-
- def get_session_by_id(self, session_id: str) -> dict[str, Any] | None:
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT * FROM sessions WHERE session_id = ?",
- (str(session_id),),
- )
- row = cursor.fetchone()
- return _canonicalize_status(dict(row)) if row else None
- except Exception:
- return None
-
- def harvest_orphaned_sessions(self, orphaned_ids: list[str]) -> int:
- if not orphaned_ids:
- return 0
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- now = time.time()
- count = 0
- for s_id in orphaned_ids:
- row = cursor.execute(
- "SELECT pid FROM sessions WHERE session_id = ? AND status = ?",
- (s_id, "running"),
- ).fetchone()
- if row is None or self.process_is_alive(row["pid"]):
- continue
- cursor.execute(
- "UPDATE sessions SET status = ?, end_time = ? "
- "WHERE session_id = ? AND status = ?",
- ("failed", now, s_id, "running"),
- )
- count += cursor.rowcount
- conn.commit()
- return count
-
- def reconcile_orphaned_sessions(self) -> int:
- """Mark running sessions with no live worker process as failed.
-
- This is safe to call both during startup and immediately after a
- forced server stop: live workers, including workers owned by another
- server instance, are left untouched.
- """
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- rows = cursor.execute(
- "SELECT session_id, pid FROM sessions WHERE status = ?", ("running",)
- ).fetchall()
- count = 0
- now = time.time()
- for row in rows:
- if self.process_is_alive(row["pid"]):
- continue
- cursor.execute(
- "UPDATE sessions SET status = ?, end_time = ? "
- "WHERE session_id = ? AND status = ?",
- ("failed", now, row["session_id"], "running"),
- )
- count += cursor.rowcount
- try:
- if trace_store.read_status(str(row["session_id"])):
- trace_store.update_trace_status(
- str(row["session_id"]),
- "failed",
- error="Process terminated prior to server startup.",
- )
- except OSError as exc:
- # read_status itself never raises; this guards the
- # lock/write side of update_trace_status.
- logger.warning(
- "Could not mark trace %s failed during orphan reconciliation: %s",
- row["session_id"],
- exc,
- )
- conn.commit()
- return count
- except Exception:
- # Reconciliation is best-effort at startup, but a silent abort
- # would leave every orphaned "running" row untouched -- log it.
- logger.warning("Orphan-session reconciliation aborted", exc_info=True)
- return 0
-
- def cleanup_orphans_on_startup(self) -> int:
- """Backward-compatible startup entrypoint for orphan reconciliation."""
- return self.reconcile_orphaned_sessions()
-
- def get_latest_session(self) -> dict[str, Any] | None:
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT session_id, status FROM sessions ORDER BY start_time DESC LIMIT 1"
- )
- row = cursor.fetchone()
- return _canonicalize_status(dict(row)) if row else None
- except Exception:
- return None
-
- def get_running_session_id(self) -> str | None:
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT session_id FROM sessions WHERE status = 'running' "
- "ORDER BY start_time DESC LIMIT 1"
- )
- row = cursor.fetchone()
- return row["session_id"] if row else None
- except Exception:
- return None
-
- def get_session_status(self, session_id: str) -> str | None:
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT status FROM sessions WHERE session_id = ?",
- (str(session_id),),
- )
- row = cursor.fetchone()
- return row["status"] if row else None
- except Exception:
- return None
-
- def update_session_status(
- self, session_id: str, status: str, end_time: float | None = None
- ) -> bool:
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- t = end_time or time.time()
- cursor.execute(
- "UPDATE sessions SET status = ?, end_time = ? WHERE session_id = ?",
- (status, t, str(session_id)),
- )
- conn.commit()
- return cursor.rowcount > 0
- except Exception:
- return False
-
- def mark_all_running_cancelled(self) -> int:
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "UPDATE sessions SET status = ?, end_time = ? WHERE status = ?",
- ("cancelled", time.time(), "running"),
- )
- count = cursor.rowcount
- conn.commit()
- return count
- except Exception:
- return 0
-
- def get_background_tasks(self, session_id: str) -> list[dict[str, Any]]:
- try:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT * FROM background_tasks WHERE session_id = ? ORDER BY start_time ASC",
- (session_id,),
- )
- return [dict(r) for r in cursor.fetchall()]
- except Exception:
- return []
-
-
-session_repo = SessionRepository()
-
-# Give the base runtime package access to orphan-session reconciliation without
-# letting it import this application-layer module (inverted dependency): any
-# process that loads the admin console DB layer -- the server itself, or an
-# entry point stopping one -- thereby arms stop_server()'s post-kill cleanup.
-from artemis.runtime.server_lifecycle import register_session_reconciler # noqa: E402
-
-register_session_reconciler(session_repo.reconcile_orphaned_sessions)
diff --git a/apps/admin_console/database/repositories/step_repository.py b/apps/admin_console/database/repositories/step_repository.py
deleted file mode 100644
index 515a98783..000000000
--- a/apps/admin_console/database/repositories/step_repository.py
+++ /dev/null
@@ -1,512 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import json
-import re
-from typing import Any
-
-try:
- from admin_console.database.connection import db_session
-except ImportError:
- from apps.admin_console.database.connection import db_session
-
-
-class StepRepository:
- """Repository handling querying and formatting of steps and associated traces."""
-
- def __init__(self, db_path=None):
- self.db_path = db_path
-
- def get_step_session_id(self, step_id: str) -> str | None:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute("SELECT session_id FROM steps WHERE step_id = ?", (step_id,))
- row = cursor.fetchone()
- return row["session_id"] if row else None
-
- @staticmethod
- def _clean_value(val: Any) -> Any:
- """Recursively cleans values to remove non-serializable and internal
- Python runtime representations.
- """
- if val is None:
- return None
- if isinstance(val, (bool, int, float)):
- return val
- if isinstance(val, str):
- if (
- "object at 0x" in val
- or val.startswith(" tuple[str, int]:
- """Extracts readable text and count of image blocks from LLM content."""
- if not content:
- return "", 0
- if isinstance(content, str):
- if content.startswith("data:image/") or "base64," in content[:50]:
- return "", 1
- return content, 0
- if isinstance(content, dict):
- if content.get("type") == "image_url" or "image_url" in content:
- return "", 1
- if content.get("type") == "text":
- return str(content.get("text", "")), 0
- t = content.get("text") or content.get("thought") or ""
- return str(t), 0
- if isinstance(content, list):
- total_text = ""
- num_images = 0
- for item in content:
- txt, imgs = StepRepository._extract_clean_text_and_images(item)
- total_text += txt
- num_images += imgs
- return total_text, num_images
- return str(content), 0
-
- @staticmethod
- def _extract_token_usage_from_trace(payload_raw: Any) -> tuple[int, int, int]:
- """Extract prompt, completion, and total tokens from an LLM trace payload with multimodal awareness."""
- if not payload_raw:
- return 0, 0, 0
- payload_obj = payload_raw
- if isinstance(payload_raw, str):
- try:
- payload_obj = json.loads(payload_raw)
- except (ValueError, TypeError):
- # Non-JSON payload: the isinstance(dict) guard below bails out.
- pass
- if not isinstance(payload_obj, dict):
- return 0, 0, 0
-
- u = payload_obj.get("token_usage") or payload_obj.get("usage_metadata")
- if isinstance(u, dict):
- pr = u.get("prompt_tokens") or u.get("prompt_token_count") or u.get("input_tokens") or 0
- co = (
- u.get("completion_tokens")
- or u.get("candidates_token_count")
- or u.get("output_tokens")
- or 0
- )
- to = u.get("total_tokens") or u.get("total_token_count") or (pr + co)
- if to > 0:
- return int(pr), int(co), int(to)
-
- # Fallback for historical traces: estimate tokens from text and images
- msgs = payload_obj.get("messages", [])
- resp = payload_obj.get("response", [])
-
- prompt_text = ""
- prompt_images = 0
- if isinstance(msgs, list):
- for m in msgs:
- if isinstance(m, dict):
- t, imgs = StepRepository._extract_clean_text_and_images(m.get("content"))
- prompt_text += t
- prompt_images += imgs
- else:
- t, imgs = StepRepository._extract_clean_text_and_images(m)
- prompt_text += t
- prompt_images += imgs
- else:
- t, imgs = StepRepository._extract_clean_text_and_images(msgs)
- prompt_text += t
- prompt_images += imgs
-
- resp_text = ""
- resp_images = 0
- if isinstance(resp, list):
- for r in resp:
- if isinstance(r, dict):
- t, imgs = StepRepository._extract_clean_text_and_images(r.get("content"))
- resp_text += t
- resp_images += imgs
- else:
- t, imgs = StepRepository._extract_clean_text_and_images(r)
- resp_text += t
- resp_images += imgs
-
- # Standard Gemini multimodal token pricing: 258 tokens per image
- pr = (len(prompt_text) // 4) + (prompt_images * 258)
- co = (len(resp_text) // 4) + (resp_images * 258)
- return int(pr), int(co), int(pr + co)
-
- def _clean_tool_payload(
- self,
- payload_raw: Any,
- trace_type: str | None = None,
- trace_name: str | None = None,
- ) -> Any:
- if not payload_raw:
- return None
- payload_obj = payload_raw
- if isinstance(payload_raw, str):
- try:
- payload_obj = json.loads(payload_raw)
- except (ValueError, TypeError):
- # Non-JSON payload: the isinstance(dict) guard below bails out.
- pass
-
- if not isinstance(payload_obj, dict):
- return None
-
- if trace_type == "llm_call":
- allowed_keys = (
- "error",
- "delay",
- "attempt",
- "max_retries",
- "provider",
- "source",
- "recoverable",
- "pause",
- "request_id",
- "scheduled_at",
- "waited_seconds",
- "retries",
- )
- cleaned = {
- key: self._clean_value(payload_obj.get(key))
- for key in allowed_keys
- if key in payload_obj
- }
- return cleaned or None
-
- args = payload_obj.get("args", payload_obj)
- cleaned_args = (
- self._clean_value(args) if isinstance(args, dict) else self._clean_value(args)
- )
- result_payload: dict[str, Any] = {"args": cleaned_args}
-
- # Extract structured action execution results. Video analysis keeps its
- # result because the task stream needs to distinguish cached, partial,
- # recovering, waiting, completed, and terminal outcomes. Other tools
- # retain the historical compact payload to avoid sending large blobs.
- res = payload_obj.get("result")
- is_video_analysis = str(trace_name or "").lower() in {
- "video_analysis",
- "video_analyzer",
- "video_analyzer_pure",
- "spawn_sub_agent",
- "analyze_audio_only",
- }
- if isinstance(res, dict):
- if res.get("post_image_name"):
- result_payload["post_image_name"] = res["post_image_name"]
- if res.get("pre_image_name"):
- result_payload["pre_image_name"] = res["pre_image_name"]
- if res.get("outcome"):
- result_payload["outcome"] = res["outcome"]
- if is_video_analysis:
- result_payload["result"] = self._clean_value(res)
- elif res:
- res_str = str(res)
- if is_video_analysis:
- result_payload["result"] = res_str
- match = re.search(r"([a-f0-9]{64})", res_str)
- if match:
- result_payload["post_image_name"] = match.group(1)
-
- return result_payload
-
- @staticmethod
- def _extract_legacy_pause_error(payload_raw: Any) -> str | None:
- """Extract an LLM error from the warning trace used by older runners."""
- payload_obj = payload_raw
- if isinstance(payload_raw, str):
- try:
- payload_obj = json.loads(payload_raw)
- except Exception:
- return None
- if not isinstance(payload_obj, dict):
- return None
-
- message = payload_obj.get("message")
- if not isinstance(message, str) or "LLM Error:" not in message:
- return None
- pause_marker = ". Pausing execution"
- if pause_marker not in message:
- return None
-
- error = message.split("LLM Error:", 1)[1].split(pause_marker, 1)[0].strip()
- return error or None
-
- def _normalize_display_trace(self, trace_dict: dict[str, Any]) -> dict[str, Any]:
- """Normalize traces consumed by the task stream's tool/error cards."""
- if trace_dict.get("type") == "log":
- error = self._extract_legacy_pause_error(trace_dict.get("payload"))
- if error:
- trace_dict.update(
- {
- "type": "llm_call",
- "name": "llm_pause",
- "status": "failed",
- "payload": {"error": error, "pause": True},
- }
- )
- return trace_dict
-
- trace_dict["payload"] = self._clean_tool_payload(
- trace_dict.get("payload"), trace_dict.get("type"), trace_dict.get("name")
- )
- return trace_dict
-
- def get_session_steps(self, session_id: str, client: str | None = None) -> list[dict[str, Any]]:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT * FROM steps WHERE session_id = ? ORDER BY step_number ASC",
- (session_id,),
- )
- rows = cursor.fetchall()
-
- steps = []
- for r in rows:
- step_dict = dict(r)
-
- # Strip raw binary bytes for API efficiency (clients fetch images by url/hash)
- step_dict["pre_screenshot_bytes"] = None
- step_dict["post_screenshot_bytes"] = None
-
- # Parse JSON fields safely
- for json_col in [
- "action_taken",
- "last_execution_result",
- "extra_metadata",
- ]:
- if step_dict.get(json_col):
- try:
- step_dict[json_col] = json.loads(step_dict[json_col])
- except (ValueError, TypeError):
- # Non-JSON column value: keep the raw string.
- pass
-
- # If post_image_name is identical to pre_image_name, normalize to None
- pre_img = step_dict.get("pre_image_name")
- if pre_img and step_dict.get("post_image_name") == pre_img:
- step_dict["post_image_name"] = None
-
- # Fetch action and generic tool traces for this step
- step_id = step_dict.get("step_id")
- if step_id:
- cursor.execute(
- "SELECT t1.trace_id, t1.parent_trace_id, t1.step_id,"
- " t1.type, t1.name, t1.timestamp, t1.duration, t1.status,"
- " t1.payload, t2.name as agent_name FROM traces t1 LEFT"
- " JOIN traces t2 ON t1.parent_trace_id = t2.trace_id WHERE"
- " t1.step_id = ? AND (t1.type = 'tool' OR t1.type = 'action' OR (t1.type ="
- " 'llm_call' AND t1.status IN ('failed', 'retrying')) OR (t1.type = 'log' AND"
- " t1.payload LIKE '%LLM Error:%Pausing execution%' AND NOT EXISTS"
- " (SELECT 1 FROM traces t3 WHERE t3.session_id = t1.session_id AND"
- " t3.type = 'llm_call' AND t3.status = 'failed' AND"
- " ABS(t3.timestamp - t1.timestamp) < 2))) ORDER BY"
- " t1.timestamp ASC",
- (step_id,),
- )
- trace_rows = cursor.fetchall()
- generic_tools = []
- for tr in trace_rows:
- trace_dict = self._normalize_display_trace(dict(tr))
- generic_tools.append(trace_dict)
- step_dict["generic_tools"] = generic_tools
-
- # Compute token usage for this step from all LLM call traces
- cursor.execute(
- "SELECT payload FROM traces WHERE step_id = ? AND type = 'llm_call'",
- (step_id,),
- )
- llm_rows = cursor.fetchall()
- step_p, step_c, step_t = 0, 0, 0
- for lr in llm_rows:
- p, c, t = self._extract_token_usage_from_trace(lr["payload"])
- step_p += p
- step_c += c
- step_t += t
-
- # If step_t is 0, also check extra_metadata for token_usage
- if step_t == 0 and step_dict.get("extra_metadata"):
- meta = (
- step_dict["extra_metadata"]
- if isinstance(step_dict["extra_metadata"], dict)
- else {}
- )
- u = meta.get("token_usage")
- if isinstance(u, dict):
- step_p = int(u.get("prompt_tokens") or u.get("input_tokens") or 0)
- step_c = int(u.get("completion_tokens") or u.get("output_tokens") or 0)
- step_t = int(u.get("total_tokens") or (step_p + step_c))
-
- step_dict["token_usage"] = {
- "prompt_tokens": step_p,
- "completion_tokens": step_c,
- "total_tokens": step_t,
- }
- step_dict["total_tokens"] = step_t
- else:
- step_dict["generic_tools"] = []
- step_dict["token_usage"] = {
- "prompt_tokens": 0,
- "completion_tokens": 0,
- "total_tokens": 0,
- }
- step_dict["total_tokens"] = 0
-
- try:
- from artemis.utils.coordinates import normalize_step_actions
-
- step_dict = normalize_step_actions(step_dict)
- except ImportError:
- pass
-
- steps.append(step_dict)
-
- # Fetch step-less tool traces (e.g. planner tools or turns without recorded step)
- cursor.execute(
- "SELECT t1.trace_id, t1.parent_trace_id, t1.step_id, t1.type,"
- " t1.name, t1.timestamp, t1.duration, t1.status, t1.payload,"
- " t2.name as agent_name FROM traces t1 LEFT JOIN traces t2 ON"
- " t1.parent_trace_id = t2.trace_id WHERE t1.session_id = ? AND"
- " (t1.step_id IS NULL OR t1.step_id = '' OR t1.step_id NOT IN (SELECT step_id FROM steps WHERE session_id = ?)) AND (t1.type = 'tool' OR"
- " (t1.type = 'llm_call' AND t1.status IN ('failed', 'retrying')) OR (t1.type = 'log'"
- " AND t1.payload LIKE '%LLM Error:%Pausing execution%' AND NOT EXISTS"
- " (SELECT 1 FROM traces t3 WHERE t3.session_id = t1.session_id AND"
- " t3.type = 'llm_call' AND t3.status = 'failed' AND"
- " ABS(t3.timestamp - t1.timestamp) < 2))) ORDER BY"
- " t1.timestamp ASC",
- (session_id, session_id),
- )
- stepless_rows = cursor.fetchall()
- if stepless_rows:
- unassigned_tools = []
- sorted_steps = sorted(steps, key=lambda s: s.get("timestamp") or 0)
-
- for tr in stepless_rows:
- trace_dict = self._normalize_display_trace(dict(tr))
-
- tr_time = trace_dict.get("timestamp") or 0
- target_step = None
- for s in sorted_steps:
- s_time = s.get("timestamp") or 0
- if s_time <= tr_time:
- target_step = s
- else:
- break
-
- if target_step:
- if "generic_tools" not in target_step or not target_step["generic_tools"]:
- target_step["generic_tools"] = []
- target_step["generic_tools"].append(trace_dict)
- target_step["generic_tools"].sort(key=lambda t: t.get("timestamp") or 0)
- else:
- unassigned_tools.append(trace_dict)
-
- if unassigned_tools:
- unassigned_tools.sort(key=lambda t: t.get("timestamp") or 0)
- first_timestamp = unassigned_tools[0].get("timestamp") or 0
-
- # Calculate token usage ONLY for pre-planning LLM calls (before first step)
- first_recorded_step_time = (
- sorted_steps[0].get("timestamp") if sorted_steps else float("inf")
- )
- cursor.execute(
- "SELECT payload FROM traces WHERE session_id = ? AND (step_id IS NULL OR step_id = '') AND timestamp < ? AND type = 'llm_call'",
- (session_id, first_recorded_step_time),
- )
- plan_llm_rows = cursor.fetchall()
- plan_p, plan_c, plan_t = 0, 0, 0
- for lr in plan_llm_rows:
- p, c, t = self._extract_token_usage_from_trace(lr["payload"])
- plan_p += p
- plan_c += c
- plan_t += t
-
- # Retrieve planner thinking / thoughts
- cursor.execute(
- "SELECT type, payload FROM traces WHERE session_id = ? AND (step_id IS NULL OR step_id = '') AND timestamp < ? AND type IN ('raw_thinking', 'thinking') ORDER BY timestamp ASC",
- (session_id, first_recorded_step_time),
- )
- think_rows = cursor.fetchall()
- p_raw_th = []
- p_nat_th = []
- for thr in think_rows:
- if thr["payload"]:
- try:
- th_obj = (
- json.loads(thr["payload"])
- if isinstance(thr["payload"], str)
- else thr["payload"]
- )
- th_text = th_obj.get("thought") or th_obj.get("text") or ""
- if thr["type"] == "raw_thinking" and th_text:
- p_raw_th.append(str(th_text).strip())
- elif thr["type"] == "thinking" and th_text:
- p_nat_th.append(str(th_text).strip())
- except (ValueError, TypeError, AttributeError):
- # Unparseable or non-dict thinking payload: skip it.
- pass
-
- virtual_step = {
- "step_id": "pre-planning",
- "step_type": "planning",
- "session_id": session_id,
- "step_number": 0,
- "timestamp": first_timestamp,
- "operator_native_thinking": "\n\n".join(p_nat_th) if p_nat_th else "",
- "operator_raw_thinking": "\n\n".join(p_raw_th) if p_raw_th else "",
- "action_taken": None,
- "last_execution_result": None,
- "extra_metadata": None,
- "generic_tools": unassigned_tools,
- "token_usage": {
- "prompt_tokens": plan_p,
- "completion_tokens": plan_c,
- "total_tokens": plan_t,
- },
- "total_tokens": plan_t,
- }
- steps.append(virtual_step)
-
- steps.sort(
- key=lambda x: (
- x.get("timestamp") or 0,
- x.get("step_number") or 0,
- )
- )
-
- return steps
-
-
-step_repo = StepRepository()
diff --git a/apps/admin_console/database/repositories/trace_repository.py b/apps/admin_console/database/repositories/trace_repository.py
deleted file mode 100644
index 7bc0dbc01..000000000
--- a/apps/admin_console/database/repositories/trace_repository.py
+++ /dev/null
@@ -1,133 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import json
-from typing import Any
-
-try:
- from admin_console.database.connection import db_session
-except ImportError:
- from apps.admin_console.database.connection import db_session
-
-
-class TraceRepository:
- """Repository handling querying traces and constructing hierarchical trace trees."""
-
- def __init__(self, db_path=None):
- self.db_path = db_path
-
- def get_trace_by_id(self, trace_id: str, db_path=None) -> dict[str, Any] | None:
- with db_session(db_path or self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute("SELECT * FROM traces WHERE trace_id = ?", (trace_id,))
- row = cursor.fetchone()
- return dict(row) if row else None
-
- def get_trace_tree(self, session_id: str) -> list[dict[str, Any]]:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT trace_id, parent_trace_id, type, name, status, timestamp,"
- " duration, CASE WHEN type IN ('raw_thinking', 'thinking') THEN"
- " payload ELSE NULL END AS payload FROM traces WHERE session_id = ?"
- " ORDER BY timestamp ASC",
- (session_id,),
- )
- rows = cursor.fetchall()
-
- nodes = {}
- for r in rows:
- row_dict = dict(r)
- if row_dict.get("payload"):
- try:
- row_dict["payload"] = json.loads(row_dict["payload"])
- except (ValueError, TypeError):
- # Non-JSON payload: keep the raw string for display.
- pass
- nodes[row_dict["trace_id"]] = {**row_dict, "children": []}
-
- root_nodes = []
- for node_id, node in nodes.items():
- parent_id = node["parent_trace_id"]
- if parent_id and parent_id in nodes:
- nodes[parent_id]["children"].append(node)
- else:
- root_nodes.append(node)
-
- def sanitize_tree(nodes_list):
- sanitized = []
- for node in nodes_list:
- node["children"] = sanitize_tree(node["children"])
- if node["name"] == "node":
- sanitized.extend(node["children"])
- else:
- sanitized.append(node)
- return sanitized
-
- return sanitize_tree(root_nodes)
-
- def get_step_traces_tree(self, session_id: str, step_id: str) -> list[dict[str, Any]]:
- with db_session(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "SELECT trace_id, parent_trace_id, step_id, type, name, status,"
- " timestamp, duration, CASE WHEN type IN ('raw_thinking',"
- " 'thinking') THEN payload ELSE NULL END AS payload FROM traces"
- " WHERE session_id = ? ORDER BY timestamp ASC",
- (session_id,),
- )
- rows = cursor.fetchall()
-
- nodes = {}
- for r in rows:
- row_dict = dict(r)
- if row_dict.get("payload"):
- try:
- row_dict["payload"] = json.loads(row_dict["payload"])
- except (ValueError, TypeError):
- # Non-JSON payload: keep the raw string for display.
- pass
- nodes[row_dict["trace_id"]] = {**row_dict, "children": []}
-
- step_roots = []
- for n_id, node in nodes.items():
- if node.get("step_id") == step_id:
- step_roots.append(node)
-
- for n_id, node in nodes.items():
- parent_id = node["parent_trace_id"]
- if parent_id and parent_id in nodes:
- nodes[parent_id]["children"].append(node)
-
- def sanitize_tree(nodes_list, visited=None):
- if visited is None:
- visited = set()
- sanitized = []
- for node in nodes_list:
- node_id = node.get("trace_id")
- if node_id and node_id in visited:
- continue
- if node_id:
- visited.add(node_id)
- node["children"] = sanitize_tree(node["children"], visited.copy())
- if node["name"] == "node":
- sanitized.extend(node["children"])
- else:
- sanitized.append(node)
- return sanitized
-
- return sanitize_tree(step_roots)
-
-
-trace_repo = TraceRepository()
diff --git a/apps/admin_console/index.html b/apps/admin_console/index.html
deleted file mode 100644
index 0d60c4c06..000000000
--- a/apps/admin_console/index.html
+++ /dev/null
@@ -1,3787 +0,0 @@
-
-
-
-
-
-
-
- Artemis Debug UI
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
👣 Steps Timeline
-
🌳 Traces Tree
-
🔄 Step Replay
-
-
- 📚 View All Notes
- 📋 View Task Plan
-
-
-
-
-
-
Select a session from the sidebar to view steps.
-
-
-
-
-
-
- Show Log Traces
-
-
-
-
-
-
-
-
-
-
-
-
- 🔄
-
Interactive Step Replay Timeline
-
-
-
-
-
-
-
-
-
-
-
Select a session from the sidebar to load the replay timeline.
-
-
-
-
-
-
-
-
-
📋 Active Task Plan
- ×
-
-
-
- Refresh Plan
-
-
-
-
-
-
-
-
-
📚 All Notes
- ×
-
-
- Loading notes...
-
-
- Refresh Notes
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Async Tasks
- Background Tasks Tracker
-
-
-
-
-
-
-
-
-
-
-
-
-
-
📹 Run Video
-
- 🔄
-
- 1x
- 2x
- 4x
- 8x
-
- ➕
-
-
-
-
-
-
-
- ts: 0 s
-
-
-
⚠️
-
No Recording Found
-
- No video recording file was found for this session on disk or database records.
-
-
-
-
-
-
-
diff --git a/apps/admin_console/replay_manager.py b/apps/admin_console/replay_manager.py
deleted file mode 100644
index c708629dd..000000000
--- a/apps/admin_console/replay_manager.py
+++ /dev/null
@@ -1,2585 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import json
-import os
-from pathlib import Path
-import shutil
-import sqlite3
-import sys
-import traceback
-from typing import Any
-from uuid import UUID
-
-WORKSPACE_ROOT = Path(__file__).parent.parent.resolve()
-
-# Try to import settings from artemis
-try:
- from artemis.config import settings
-
- TRACES_PATH = Path(settings.TRACES_PATH)
-except ImportError:
- # Fallback to default path if run outside project environment or imports fail
- TRACES_PATH = WORKSPACE_ROOT / "traces"
-
-DB_PATH = TRACES_PATH / "data_engine.db"
-IMAGES_DIR = TRACES_PATH / "images"
-
-try:
- from artemis.context import ArtemisContext
- from artemis.graph.state import State
-except ImportError:
- ArtemisContext = Any
- State = Any
-
-REPLAY_TOOLS_CONFIG = {
- "ask_explorer": {
- "display_name": "Ask Explorer",
- "description": (
- "Visual parsing agent that executes visual ReAct search loops using multi-modal models."
- ),
- "module": "artemis.tools.explorer_tool",
- "function": "_run_explorer_logic",
- "agent_name": "explorer",
- "denylist_args": ["ctx", "state"],
- "fallback_mappings": {},
- "llm_span_name": "gemini_explorer_call",
- },
- "ask_image_processor": {
- "display_name": "Ask Image Processor",
- "description": (
- "Visual parsing agent that writes Python code using OpenCV and"
- " computer vision to analyze and modify screen images."
- ),
- "module": "artemis.tools.image_processor_tool",
- "function": "_run_image_processor_logic",
- "agent_name": "image_processor",
- "denylist_args": ["ctx", "state"],
- "fallback_mappings": {},
- "llm_span_name": "gemini_image_processor_call",
- },
- # Future tools can easily be registered here using plain dictionaries:
- # "ask_validator": {
- # "display_name": "Ask Validator",
- # "description": "Validation agent that checks if a step or action was completed successfully.",
- # "module": "artemis.tools.validator_tool",
- # ...
- # }
-}
-
-
-class ReplayManager:
- """Manages sandboxed execution and diagnostics of visual agent steps.
-
- The ReplayManager is the core orchestrator for the Artemis step replay system.
- It reconstructs historical execution states (SQLite database records, trace
- histories, and screenshots) and replays a single agent step in an isolated,
- deterministic sandbox environment while interacting live with a physical
- Android device and Gemini.
-
- Key Responsibilities:
- - Reconstructs and preloads historical SQLite states up to a specific
- step.
- - Establishes sandboxed `ArtemisContext` and `DataEngine` instances.
- - Integrates with physical devices via ADB and UIAutomator.
- - Hooks and spies on the Gemini API to capture thoughts and raw outputs.
- - Reconciles live and preloaded traces into unified chronological trees.
-
- Attributes:
- workspace_root (Path): The absolute path to the root of the project
- workspace.
- replay_base_dir (Path): Base directory containing replay data and
- outputs.
- test_data_dir (Path): Directory containing chunked historical step
- traces.
- test_outputs_dir (Path): Sandbox directory where replay outputs are
- written.
- db_path (Path): Path to the master Data Engine SQLite database.
- traces_path (Path): Path to the master traces directory.
- images_dir (Path): Path to the master images directory.
- """
-
- def __init__(
- self,
- workspace_root: Path = WORKSPACE_ROOT,
- device_id: str = None,
- original_db_path: Path = None,
- init_device: bool = False,
- ):
- """Initializes the ReplayManager and autodetects the connected Android device.
-
- Args:
- workspace_root (Path, optional): The absolute path to the workspace
- root. Defaults to WORKSPACE_ROOT.
- device_id (str, optional): The serial ID of the target Android
- device. If not specified, autodetects the first connected device.
- original_db_path (Path, optional): Path to the master SQLite
- database. Defaults to DB_PATH.
- init_device (bool, optional): Whether to initialize the ADB device
- connection on startup.
- """
- self.workspace_root = workspace_root
- self.traces_path = TRACES_PATH
- self.replay_base_dir = self.traces_path / "replay"
- self.test_data_dir = self.replay_base_dir / "data"
- self.test_outputs_dir = self.replay_base_dir / "outputs"
- self.db_path = Path(original_db_path) if original_db_path else DB_PATH
- self.images_dir = IMAGES_DIR
-
- # Device connection and autodetection
- self.device_id = None
- self.w = None
- self.h = None
- self.adb = None
- self.ui_client = None
-
- if init_device:
- try:
- self._init_device(device_id)
- except Exception as e:
- print(
- "Warning: Connected device could not be initialized during"
- f" ReplayManager startup: {e}"
- )
-
- def _init_device(self, device_id: str = None):
- """Initializes connection to the target device and queries screen metrics."""
- from adbutils import AdbClient
-
- try:
- from third_party.mobile_use.clients.ui_automator_client import UIAutomatorClient
- except ImportError:
- raise ImportError(
- "Failed to import UIAutomatorClient. Ensure artemis package is installed in path."
- )
-
- self.adb = AdbClient(host="localhost", port=5037)
- try:
- devices = self.adb.device_list()
- except Exception as adb_err:
- raise ConnectionError(
- f"Failed to query device list from ADB server: {adb_err}"
- ) from adb_err
-
- if not devices:
- raise ConnectionError(
- "No active ADB devices connected. Please connect an Android device via ADB."
- )
-
- if device_id:
- matched_device = next((d.serial for d in devices if d.serial == device_id), None)
- if not matched_device:
- raise ConnectionError(
- f"Requested device '{device_id}' is not connected."
- f" Connected devices: {[d.serial for d in devices]}"
- )
- self.device_id = matched_device
- else:
- self.device_id = devices[0].serial
-
- self.ui_client = UIAutomatorClient(device_id=self.device_id)
- ui_data = self.ui_client.get_screen_data()
- self.w, self.h = ui_data.width, ui_data.height
- print(f"Connected to device: {self.device_id} ({self.w}x{self.h})")
-
- def chunk_session_traces(self, session_id: str, output_dir: Path = None) -> Path:
- """Chunks traces, screenshots, and metadata for a specific execution session.
-
- Queries the master SQLite database and exports step-by-step
- subdirectories
- (e.g., `step_00/`, `step_01/`) inside the output directory.
-
- Args:
- session_id: The UUID string of the session to extract and chunk.
- output_dir: Optional path to the directory where chunked results
- should be saved. Defaults to `self.test_data_dir /
- f"{session_id}_chunked"`.
- """
- import sqlite3
- import json
- import shutil
-
- db_path = self.db_path
- traces_dir = self.traces_path
-
- if not db_path.exists():
- raise FileNotFoundError(f"Master database not found at {db_path}")
-
- if not output_dir:
- output_dir = self.test_data_dir / f"{session_id}_chunked"
- else:
- output_dir = Path(output_dir)
-
- if output_dir.exists() or output_dir.is_symlink():
- # Safety check: only delete directories that are named *_chunked
- if output_dir.name.endswith("_chunked"):
- if output_dir.is_symlink():
- output_dir.unlink()
- elif (
- self.test_data_dir in output_dir.parents
- or output_dir.parent == self.test_data_dir
- or output_dir.parent.name == "inputs"
- ):
- shutil.rmtree(output_dir)
- output_dir.mkdir(parents=True, exist_ok=True)
- print(f"Chunking traces for session: {session_id}")
- print(f"Using database: {db_path}")
- print(f"Output directory: {output_dir}")
-
- conn = sqlite3.connect(db_path)
- conn.row_factory = sqlite3.Row
- cursor = conn.cursor()
-
- cursor.execute("SELECT * FROM sessions WHERE session_id = ?", (session_id,))
- session_row = cursor.fetchone()
-
- cursor.execute(
- "SELECT * FROM steps WHERE session_id = ? ORDER BY step_number ASC",
- (session_id,),
- )
- steps = cursor.fetchall()
- print(f"Found {len(steps)} steps for session {session_id}")
-
- if not session_row and not steps:
- conn.close()
- raise ValueError(f"Session {session_id} not found in database.")
-
- for step in steps:
- step_number = step["step_number"]
- step_id = step["step_id"]
- step_dir = output_dir / f"step_{step_number:02d}"
- step_dir.mkdir(parents=True, exist_ok=True)
-
- # 1. Process and save the step's primary metadata (step.json)
- step_data = {}
- for key in step.keys():
- val = step[key]
- if (
- key
- in (
- "action_taken",
- "last_execution_result",
- "extra_metadata",
- )
- and val
- ):
- try:
- step_data[key] = json.loads(val)
- except Exception:
- step_data[key] = val
- else:
- step_data[key] = val
-
- step_json_path = step_dir / "step.json"
- with open(step_json_path, "w", encoding="utf-8") as f:
- json.dump(step_data, f, indent=2, ensure_ascii=False)
-
- # 2. Retrieve and save trace logs recorded during this step (traces.json)
- cursor.execute(
- "SELECT * FROM traces WHERE step_id = ? ORDER BY timestamp ASC",
- (step_id,),
- )
- traces = cursor.fetchall()
-
- traces_data = []
- for trace_row in traces:
- trace_dict = {}
- for key in trace_row.keys():
- val = trace_row[key]
- if key == "payload" and val:
- try:
- trace_dict[key] = json.loads(val)
- except Exception:
- trace_dict[key] = val
- else:
- trace_dict[key] = val
- traces_data.append(trace_dict)
-
- traces_json_path = step_dir / "traces.json"
- with open(traces_json_path, "w", encoding="utf-8") as f:
- json.dump(traces_data, f, indent=2, ensure_ascii=False)
-
- # 3. Copy the step's screenshots (pre-action and post-action screenshots)
- pre_image_name = step_data.get("pre_image_name")
- post_image_name = step_data.get("post_image_name")
- if pre_image_name and post_image_name == pre_image_name:
- post_image_name = None
- images_dir = traces_dir / "images"
-
- if pre_image_name:
- src_pre = images_dir / f"{pre_image_name}.jpg"
- if src_pre.exists():
- shutil.copy2(src_pre, step_dir / "pre.jpg")
- else:
- print(f"Warning: Pre-action screenshot not found at {src_pre}")
-
- if post_image_name:
- src_post = images_dir / f"{post_image_name}.jpg"
- if src_post.exists():
- shutil.copy2(src_post, step_dir / "post.jpg")
- else:
- print(f"Warning: Post-action screenshot not found at {src_post}")
-
- # 4. Save metadata of the pre-action screenshot (pre_image_meta.json)
- if pre_image_name:
- cursor.execute(
- "SELECT * FROM images WHERE image_name = ?",
- (pre_image_name,),
- )
- img_row = cursor.fetchone()
- if img_row:
- img_meta = {}
- for key in img_row.keys():
- val = img_row[key]
- if key in ("ocr_result", "ui_tree", "extra_metadata") and val:
- try:
- img_meta[key] = json.loads(val)
- except Exception:
- img_meta[key] = val
- else:
- img_meta[key] = val
-
- pre_img_meta_path = step_dir / "pre_image_meta.json"
- with open(pre_img_meta_path, "w", encoding="utf-8") as f:
- json.dump(img_meta, f, indent=2, ensure_ascii=False)
- else:
- print(
- f"Warning: Image metadata row for {pre_image_name} not"
- " found in images table."
- )
-
- # 5. Save metadata of the post-action screenshot (post_image_meta.json)
- if post_image_name:
- cursor.execute(
- "SELECT * FROM images WHERE image_name = ?",
- (post_image_name,),
- )
- img_row = cursor.fetchone()
- if img_row:
- img_meta = {}
- for key in img_row.keys():
- val = img_row[key]
- if key in ("ocr_result", "ui_tree", "extra_metadata") and val:
- try:
- img_meta[key] = json.loads(val)
- except Exception:
- img_meta[key] = val
- else:
- img_meta[key] = val
-
- post_img_meta_path = step_dir / "post_image_meta.json"
- with open(post_img_meta_path, "w", encoding="utf-8") as f:
- json.dump(img_meta, f, indent=2, ensure_ascii=False)
- else:
- print(
- f"Warning: Image metadata row for {post_image_name} not"
- " found in images table."
- )
-
- conn.close()
- # Write sentinel file to indicate successful chunking
- try:
- (output_dir / ".chunked").touch()
- except Exception as e:
- print(f"Warning: Failed to write chunked sentinel file: {e}")
- print("Chunking trace completion status: success")
- return output_dir
-
- def list_devices(self) -> list[dict]:
- """Dynamically queries the ADB server for connected Android devices."""
- try:
- from adbutils import AdbClient
-
- adb = AdbClient(host="localhost", port=5037)
- devices = adb.device_list()
- return [{"serial": d.serial, "status": "online"} for d in devices]
- except Exception as e:
- print(f"Warning: Failed to query device list from ADB: {e}")
- return []
-
- def load_session_goal(
- self, session_id: str, step_dir: Path, original_db_path: str = None
- ) -> str:
- """Loads the session goal from either session.json or the original session db."""
- session_json_path = step_dir.parent / "session.json"
- if session_json_path.exists():
- try:
- with open(session_json_path, encoding="utf-8") as f:
- data = json.load(f)
- if isinstance(data, dict) and "initial_goal" in data and data["initial_goal"]:
- return data["initial_goal"]
- except Exception as e:
- print(f"Warning: Failed to load goal from session.json: {e}")
-
- if original_db_path and Path(original_db_path).exists():
- try:
- conn = sqlite3.connect(original_db_path)
- conn.row_factory = sqlite3.Row
- cursor = conn.cursor()
- cursor.execute(
- "SELECT initial_goal FROM sessions WHERE session_id = ?",
- (str(session_id),),
- )
- row = cursor.fetchone()
- conn.close()
- if row and row["initial_goal"]:
- return row["initial_goal"]
- except Exception as e:
- print(f"Warning: Failed to load goal from original database: {e}")
-
- raise ValueError(
- f"Could not load initial_goal for session {session_id} from"
- f" session.json or original database {original_db_path}."
- )
-
- def extract_replay_data(
- self,
- step_dir,
- original_db_path=None,
- agent_name="explorer",
- tool_name="ask_explorer",
- ):
- """Extracts final answer and chronological tool call sequence from traces.json/db."""
- step_json_path = step_dir / "step.json"
- if not step_json_path.exists():
- raise FileNotFoundError(f"step.json not found in {step_dir}")
-
- with open(step_json_path, encoding="utf-8") as f:
- step_data = json.load(f)
- step_id = step_data["step_id"]
-
- traces_json_path = step_dir / "traces.json"
- traces = []
- if traces_json_path.exists():
- with open(traces_json_path, encoding="utf-8") as f:
- traces = json.load(f)
-
- # If traces.json is empty/missing or lacks agent trace, check original DB
- agent_trace = next(
- (t for t in traces if t.get("type") == "agent" and t.get("name") == agent_name),
- None,
- )
- if (not agent_trace or not traces) and original_db_path and Path(original_db_path).exists():
- conn = sqlite3.connect(original_db_path)
- conn.row_factory = sqlite3.Row
- cursor = conn.cursor()
- cursor.execute(
- "SELECT * FROM traces WHERE step_id = ? ORDER BY timestamp ASC",
- (step_id,),
- )
- rows = cursor.fetchall()
- traces = []
- for r in rows:
- td = dict(r)
- if td.get("payload"):
- try:
- td["payload"] = json.loads(td["payload"])
- except (ValueError, TypeError):
- # Non-JSON payload: keep the raw string.
- pass
- traces.append(td)
- conn.close()
- agent_trace = next(
- (t for t in traces if t.get("type") == "agent" and t.get("name") == agent_name),
- None,
- )
-
- if not agent_trace:
- print(
- f"Warning: {agent_name} agent trace not found. Replay will"
- " fallback to direct submit_answer."
- )
- return (
- [],
- {
- "candidates": [],
- "fallback_message": f"No {agent_name} trace found.",
- },
- traces,
- )
-
- payload = agent_trace.get("payload") or {}
- result_str = payload.get("result", "{}")
- if isinstance(result_str, str):
- try:
- final_submit_args = json.loads(result_str)
- except Exception:
- final_submit_args = {
- "candidates": [],
- "fallback_message": result_str,
- }
- else:
- final_submit_args = result_str
-
- # Build sequence of intermediate tool calls under the agent trace
- agent_trace_id = agent_trace["trace_id"]
-
- def get_descendants(pid):
- child_list = []
- for t in traces:
- if t.get("parent_trace_id") == pid:
- child_list.append(t)
- child_list.extend(get_descendants(t["trace_id"]))
- return child_list
-
- descendants = get_descendants(agent_trace_id)
- descendants.sort(key=lambda x: x.get("timestamp", 0))
-
- tool_calls_sequence = []
- for t in descendants:
- if t.get("type") == "tool" and t.get("name") not in (
- tool_name,
- "submit_answer",
- ):
- t_payload = t.get("payload") or {}
- t_args = t_payload.get("args") or {}
- tool_calls_sequence.append({"name": t.get("name"), "args": t_args})
-
- # Preload traces up to the point when agent is called
- traces_to_preload = []
- if agent_trace in traces:
- idx = traces.index(agent_trace)
- traces_to_preload = traces[:idx]
- else:
- agent_ts = agent_trace.get("timestamp", 0.0)
- traces_to_preload = [t for t in traces if t.get("timestamp", 0.0) < agent_ts]
-
- return tool_calls_sequence, final_submit_args, traces_to_preload
-
- def resolve_step_data_path(self, data: dict, path: str):
- """Resolves a value from a nested dict/list using a dot-separated path (e.g.
-
- 'action_taken.0.target_text').
- """
- parts = path.split(".")
- curr = data
- for p in parts:
- if isinstance(curr, dict):
- curr = curr.get(p)
- elif isinstance(curr, list):
- try:
- idx = int(p)
- if idx < len(curr):
- curr = curr[idx]
- else:
- return None
- except ValueError:
- return None
- else:
- return None
- return curr
-
- def setup_temporary_database(
- self,
- temp_db_path,
- temp_traces_dir,
- session_id,
- step_data,
- pre_image_meta,
- initial_goal,
- traces_to_preload=None,
- post_image_meta=None,
- replay_id=None,
- ):
- """Initializes a sandbox SQLite database with preloaded step and screen metadata."""
- if not initial_goal:
- raise ValueError("initial_goal must be provided and cannot be empty.")
-
- if temp_db_path.exists():
- try:
- temp_db_path.unlink()
- except OSError:
- pass
-
- if temp_traces_dir.exists():
- shutil.rmtree(temp_traces_dir)
- temp_traces_dir.mkdir(parents=True, exist_ok=True)
-
- try:
- from artemis.data_engine.storage import StorageManager
- from artemis.data_engine.models import (
- SessionMetadata,
- ImageRecord,
- StepRecord,
- TraceRecord,
- )
- except ImportError:
- raise ImportError(
- "Failed to import Artemis Data Engine modules. Ensure you are"
- " running in the correct virtual environment."
- )
-
- storage = StorageManager(temp_db_path, temp_traces_dir)
-
- # Preload session metadata
- session_meta = SessionMetadata(
- session_id=UUID(session_id) if isinstance(session_id, str) else session_id,
- initial_goal=initial_goal,
- status="running",
- )
- storage.create_session(session_meta)
-
- if replay_id:
- replay_session_meta = SessionMetadata(
- session_id=UUID(replay_id) if isinstance(replay_id, str) else replay_id,
- initial_goal=initial_goal,
- status="running",
- )
- storage.create_session(replay_session_meta)
-
- # Preload image metadata
- if pre_image_meta:
- image_record = ImageRecord(
- image_name=pre_image_meta["image_name"],
- timestamp=pre_image_meta.get("timestamp", 0.0),
- ocr_result=pre_image_meta.get("ocr_result"),
- ui_tree=pre_image_meta.get("ui_tree"),
- extra_metadata=pre_image_meta.get("extra_metadata", {}),
- )
- storage.create_image(image_record)
-
- if post_image_meta:
- image_record = ImageRecord(
- image_name=post_image_meta["image_name"],
- timestamp=post_image_meta.get("timestamp", 0.0),
- ocr_result=post_image_meta.get("ocr_result"),
- ui_tree=post_image_meta.get("ui_tree"),
- extra_metadata=post_image_meta.get("extra_metadata", {}),
- )
- storage.create_image(image_record)
-
- # Preload step metadata
- step_record = StepRecord(
- step_id=step_data["step_id"],
- session_id=step_data["session_id"],
- step_number=step_data["step_number"],
- timestamp=step_data.get("timestamp", 0.0),
- pre_image_name=step_data.get("pre_image_name"),
- post_image_name=step_data.get("post_image_name"),
- summary=step_data.get("summary"),
- action_taken=step_data.get("action_taken"),
- operator_raw_thinking=step_data.get("operator_raw_thinking"),
- operator_native_thinking=step_data.get("operator_native_thinking"),
- last_execution_result=step_data.get("last_execution_result"),
- extra_metadata=step_data.get("extra_metadata", {}),
- )
- storage.create_step(step_record)
-
- # Preload trace history
- if traces_to_preload:
- for t in traces_to_preload:
- try:
- payload_val = t.get("payload")
- if isinstance(payload_val, str):
- try:
- payload_val = json.loads(payload_val)
- except Exception:
- payload_val = {}
- elif not isinstance(payload_val, dict):
- payload_val = {}
-
- trace_rec = TraceRecord(
- trace_id=UUID(t["trace_id"])
- if isinstance(t["trace_id"], str)
- else t["trace_id"],
- session_id=UUID(t["session_id"])
- if isinstance(t["session_id"], str)
- else t["session_id"],
- step_id=UUID(t["step_id"]) if t.get("step_id") else None,
- parent_trace_id=UUID(t["parent_trace_id"])
- if t.get("parent_trace_id")
- else None,
- type=t["type"],
- name=t["name"],
- timestamp=t.get("timestamp", 0.0),
- duration=t.get("duration"),
- status=t.get("status", "success"),
- payload=payload_val,
- )
- storage.create_trace(trace_rec)
- except Exception as ex:
- print(f"Warning: Failed to preload trace {t.get('trace_id')}: {ex}")
-
- return storage
-
- def get_preloaded_trace_ids_for_step(self, session_id: str, step_num: int) -> set:
- traces_json_path = (
- self.test_data_dir / f"{session_id}_chunked" / f"step_{step_num:02d}" / "traces.json"
- )
- if traces_json_path.exists():
- try:
- with open(traces_json_path, encoding="utf-8") as f:
- traces = json.load(f)
- return {str(t.get("trace_id")).lower() for t in traces if t.get("trace_id")}
- except (OSError, ValueError) as e:
- print(
- f"Warning: Failed to read preloaded trace ids from {traces_json_path}: {e}",
- file=sys.stderr,
- )
- return set()
-
- def load_traces_from_db(self, db_path: Path, step_num: int, preloaded_trace_ids: set) -> list:
- if not db_path.exists():
- return []
- try:
- conn = sqlite3.connect(db_path)
- conn.row_factory = sqlite3.Row
- cursor = conn.cursor()
- cursor.execute(
- "SELECT trace_id, session_id, step_id, parent_trace_id, type,"
- " name, status, timestamp, duration, payload FROM traces ORDER"
- " BY timestamp ASC"
- )
- rows = cursor.fetchall()
- conn.close()
-
- preloaded_set = {str(tid).lower() for tid in preloaded_trace_ids if tid}
- step_traces = []
- for r in rows:
- td = dict(r)
- if td.get("payload"):
- try:
- td["payload"] = json.loads(td["payload"])
- except (ValueError, TypeError):
- # Non-JSON payload: normalized to {} just below.
- pass
- if not isinstance(td.get("payload"), dict):
- td["payload"] = {}
-
- tid = str(r["trace_id"]).lower()
- if tid in preloaded_set:
- td["is_preloaded"] = True
- else:
- td["is_preloaded"] = False
-
- td["step_number"] = step_num
- step_traces.append(td)
- return step_traces
- except Exception as e:
- print(
- f"Warning: Failed to load traces from database {db_path}: {e}",
- file=sys.stderr,
- )
- return []
-
- def normalize_trace_payload(self, t: dict) -> dict:
- if t.get("type") == "tool":
- payload = t.get("payload") or {}
- args = payload.get("args") or {}
- positional_args = payload.get("positional_args") or []
-
- if (not args or args == {}) and positional_args:
- name = t.get("name")
- if name == "search_by_coords" and len(positional_args) >= 2:
- try:
- args = {
- "nx": int(positional_args[0]),
- "ny": int(positional_args[1]),
- }
- except ValueError:
- args = {
- "nx": positional_args[0],
- "ny": positional_args[1],
- }
- elif name == "search_ui" and len(positional_args) >= 1:
- args = {"search_query": positional_args[0]}
- elif name == "detect_objects" and len(positional_args) >= 1:
- args = {"labels": positional_args[0]}
- else:
- args = {"positional_args": positional_args}
-
- payload["args"] = args
- t["payload"] = payload
- return t
-
- def load_all_session_traces(
- self,
- temp_db_path: Path,
- preloaded_trace_ids: set,
- current_step_number: int,
- ) -> list:
- """Loads and merges all traces across all steps in the session."""
- session_id = None
- try:
- session_id = temp_db_path.parents[1].name.split("_")[0]
- except IndexError:
- # Path too shallow to carry a session dir: fall back to the DB probe.
- pass
-
- if not session_id:
- try:
- conn = sqlite3.connect(temp_db_path)
- cursor = conn.cursor()
- cursor.execute("SELECT session_id FROM traces LIMIT 1")
- row = cursor.fetchone()
- if row:
- session_id = str(row[0])
- conn.close()
- except sqlite3.Error:
- # Unreadable sandbox DB: fall back to single-step trace loading.
- pass
-
- if not session_id:
- return self.load_traces_from_db(temp_db_path, current_step_number, preloaded_trace_ids)
-
- session_chunked_dir = self.test_data_dir / f"{session_id}_chunked"
-
- all_traces = []
- if session_chunked_dir.exists():
- step_subdirs = sorted(
- [
- d
- for d in session_chunked_dir.iterdir()
- if d.is_dir() and d.name.startswith("step_")
- ],
- key=lambda d: d.name,
- )
-
- for subdir in step_subdirs:
- try:
- step_num = int(subdir.name.split("_")[1])
- except (IndexError, ValueError):
- continue
-
- step_outputs_dir = self.test_outputs_dir / f"{session_id}_step_{step_num:02d}"
- step_db_path = step_outputs_dir / "temp_traces" / "data_engine.db"
-
- step_preloaded_ids = self.get_preloaded_trace_ids_for_step(session_id, step_num)
-
- if step_db_path.exists():
- step_traces = self.load_traces_from_db(
- step_db_path, step_num, step_preloaded_ids
- )
- all_traces.extend(step_traces)
- else:
- traces_json_path = subdir / "traces.json"
- if traces_json_path.exists():
- try:
- with open(traces_json_path, encoding="utf-8") as f:
- step_traces = json.load(f)
- for t in step_traces:
- t["is_preloaded"] = True
- t["step_number"] = step_num
- if not isinstance(t.get("payload"), dict):
- t["payload"] = {}
- all_traces.append(t)
- except Exception as e:
- print(
- f"Warning: Failed to load traces from {traces_json_path}: {e}",
- file=sys.stderr,
- )
- else:
- all_traces = self.load_traces_from_db(
- temp_db_path, current_step_number, preloaded_trace_ids
- )
-
- # Align live replay traces to the appropriate preloaded step/operator trace
- for step_num in range(1, 100):
- current_step_id = None
- current_step_dir = self.test_data_dir / f"{session_id}_chunked" / f"step_{step_num:02d}"
- if not current_step_dir.exists():
- continue
-
- step_json_path = current_step_dir / "step.json"
- if step_json_path.exists():
- try:
- with open(step_json_path, encoding="utf-8") as f:
- step_data = json.load(f)
- current_step_id = step_data.get("step_id")
- except (OSError, ValueError) as e:
- print(
- f"Warning: Failed to read step id from {step_json_path}: {e}",
- file=sys.stderr,
- )
-
- current_step_id_str = str(current_step_id).lower() if current_step_id else None
-
- # 1. Find live root trace (no parent_trace_id and not preloaded)
- live_root_trace = None
- for t in all_traces:
- if t.get("step_number") != step_num:
- continue
- if not t.get("is_preloaded") and not t.get("parent_trace_id"):
- live_root_trace = t
- break
-
- # 2. Find all preloaded traces for this step
- step_preloaded_traces = [
- t
- for t in all_traces
- if t.get("step_number") == step_num
- and t.get("is_preloaded")
- and str(t.get("step_id") or "").lower() == current_step_id_str
- ]
-
- # Sort chronologically to identify the final response of the step
- step_preloaded_traces.sort(key=lambda x: x.get("timestamp", 0.0))
- final_response_trace = step_preloaded_traces[-1] if step_preloaded_traces else None
-
- # 3. Branch immediately before the final response of the step
- if live_root_trace and final_response_trace:
- # Resolve the tool name from the live root trace name
- tool_name = "ask_explorer"
- agent_name = live_root_trace.get("name")
- for t_name, cfg in REPLAY_TOOLS_CONFIG.items():
- if cfg.get("agent_name") == agent_name:
- tool_name = t_name
- break
-
- import uuid
-
- virtual_id = str(uuid.uuid4())
-
- # Branch from the same parent as the final response
- parent_id = final_response_trace.get("parent_trace_id")
-
- # Set timestamp to be immediately before the final response
- virtual_timestamp = final_response_trace.get("timestamp", 0.0) - 0.001
-
- live_query = None
- live_cf = None
- if live_root_trace.get("payload") and live_root_trace["payload"].get("args"):
- args = live_root_trace["payload"]["args"]
- live_query = (
- args.get("q") or args.get("query") or args.get("arg2") or args.get("goal")
- )
- live_cf = (
- args.get("cf") or args.get("context_feedback") or args.get("arg3") or ""
- )
- if not live_query:
- live_query = "Replay Execution"
-
- virtual_tool_trace = {
- "trace_id": virtual_id,
- "session_id": session_id,
- "step_id": current_step_id,
- "parent_trace_id": parent_id,
- "type": "tool",
- "name": tool_name,
- "status": live_root_trace.get("status", "success"),
- "timestamp": virtual_timestamp,
- "duration": live_root_trace.get("duration", 0.0),
- "payload": {
- "args": {
- "query": live_query,
- "context_feedback": live_cf,
- },
- "result": (live_root_trace.get("payload", {}).get("result")),
- },
- "is_preloaded": False,
- "step_number": step_num,
- }
-
- # Link the live root trace to our new virtual tool trace
- live_root_trace["parent_trace_id"] = virtual_id
- all_traces.append(virtual_tool_trace)
-
- all_traces = [self.normalize_trace_payload(t) for t in all_traces]
- all_traces.sort(key=lambda t: t.get("timestamp", 0.0))
- return all_traces
-
- def enrich_all_session_traces(self, all_traces: list, session_id: str) -> list:
- """Enriches all preloaded traces (across all steps) with file URLs to their corresponding screenshots."""
- session_chunked_dir = self.test_data_dir / f"{session_id}_chunked"
-
- step_dirs = {}
- if session_chunked_dir.exists():
- for subdir in session_chunked_dir.iterdir():
- if subdir.is_dir() and subdir.name.startswith("step_"):
- step_json_path = subdir / "step.json"
- if step_json_path.exists():
- try:
- with open(step_json_path, encoding="utf-8") as f:
- step_data = json.load(f)
- sid = step_data.get("step_id")
- if sid:
- step_dirs[str(sid).lower()] = subdir
- except (OSError, ValueError):
- # Unreadable step.json: screenshots for this step
- # simply are not linked into the trace payloads.
- pass
-
- for td in all_traces:
- if td.get("is_preloaded"):
- step_id = str(td.get("step_id") or "").lower()
- if step_id and step_id in step_dirs:
- step_dir = step_dirs[step_id]
- if td.get("type") == "agent":
- pre_jpg = step_dir / "pre.jpg"
- if pre_jpg.exists():
- td["payload"]["pre_screenshot_path"] = f"file://{pre_jpg.resolve()}"
- elif td.get("type") == "action":
- post_jpg = step_dir / "post.jpg"
- if post_jpg.exists():
- td["payload"]["post_screenshot_path"] = f"file://{post_jpg.resolve()}"
-
- return all_traces
-
- def load_steps_metadata(self, session_id: str) -> dict[int, dict]:
- """Loads metadata for all steps in the session from the chunked data."""
- session_chunked_dir = self.test_data_dir / f"{session_id}_chunked"
-
- steps = {}
- if session_chunked_dir.exists():
- for subdir in session_chunked_dir.iterdir():
- if subdir.is_dir() and subdir.name.startswith("step_"):
- step_json_path = subdir / "step.json"
- if step_json_path.exists():
- try:
- with open(step_json_path, encoding="utf-8") as f:
- step_data = json.load(f)
- step_num = step_data.get("step_number")
- if step_num is not None:
- steps[int(step_num)] = step_data
- except Exception as e:
- print(f"Warning: Failed to load step data from {step_json_path}: {e}")
- return steps
-
- def make_file_url(self, val):
- if isinstance(val, str) and (val.endswith(".jpg") or val.endswith(".png") or "/" in val):
- try:
- p = Path(val)
- if p.exists():
- abs_path = str(p.resolve())
- return (
- f"file://{abs_path}" if abs_path.startswith("/") else f"file:///{abs_path}"
- )
- except (OSError, ValueError):
- # Not a resolvable filesystem path: return the value verbatim.
- pass
- return val
-
- def clean_payload_paths(self, obj):
- if isinstance(obj, dict):
- return {k: self.clean_payload_paths(v) for k, v in obj.items()}
- elif isinstance(obj, list):
- return [self.clean_payload_paths(v) for v in obj]
- elif isinstance(obj, str):
- return self.make_file_url(obj)
- return obj
-
- def sanitize_nodes(self, nodes_list):
- sanitized = []
- for node in nodes_list:
- node["children"] = self.sanitize_nodes(node["children"])
- if node["name"] == "node":
- sanitized.extend(node["children"])
- else:
- sanitized.append(node)
- return sanitized
-
- def sort_tree_nodes_chronologically(self, nodes_list: list[dict]) -> list[dict]:
- for node in nodes_list:
- if "children" in node and node["children"]:
- node["children"] = self.sort_tree_nodes_chronologically(node["children"])
- return sorted(nodes_list, key=lambda x: x.get("timestamp", 0.0) or 0.0)
-
- def extract_tool_calls_from_db(self, temp_db_path: Path, preloaded_trace_ids: set) -> list:
- """Extracts tool calls executed during the current step replay from the sandbox database."""
- preloaded_set = {str(tid).lower() for tid in preloaded_trace_ids if tid}
-
- if not temp_db_path.exists():
- return []
-
- try:
- conn = sqlite3.connect(temp_db_path)
- conn.row_factory = sqlite3.Row
- cursor = conn.cursor()
- cursor.execute(
- "SELECT trace_id, name, status, payload FROM traces WHERE type"
- " = 'tool' ORDER BY timestamp ASC"
- )
- rows = cursor.fetchall()
- conn.close()
- except Exception as e:
- print(
- f"Warning: Failed to query tool calls from database: {e}",
- file=sys.stderr,
- )
- return []
-
- tool_calls = []
- for r in rows:
- tid = str(r["trace_id"]).lower()
- if tid not in preloaded_set:
- payload = {}
- if r["payload"]:
- try:
- payload = json.loads(r["payload"])
- except (ValueError, TypeError):
- # Non-JSON payload: treated as an empty tool payload.
- pass
-
- response_val = payload.get("result")
- if response_val is None:
- response_val = payload.get("error")
- else:
- if isinstance(response_val, str) and (
- response_val.startswith("{") or response_val.startswith("[")
- ):
- try:
- response_val = json.loads(response_val)
- except ValueError:
- # Looks like JSON but is not: keep the raw string.
- pass
-
- args = payload.get("args", {})
- positional_args = payload.get("positional_args", [])
- if positional_args:
- if r["name"] == "search_by_coords" and len(positional_args) >= 2:
- try:
- args = {
- "nx": int(positional_args[0]),
- "ny": int(positional_args[1]),
- }
- except ValueError:
- args = {
- "nx": positional_args[0],
- "ny": positional_args[1],
- }
- elif r["name"] == "search_ui" and len(positional_args) >= 1:
- args = {"search_query": positional_args[0]}
- elif r["name"] == "detect_objects" and len(positional_args) >= 1:
- args = {"labels": positional_args[0]}
- else:
- args = {"positional_args": positional_args}
-
- tool_calls.append(
- {
- "name": r["name"],
- "args": args,
- "status": r["status"],
- "response": response_val,
- }
- )
- return tool_calls
-
- def extract_state_prepopulation_data(self, step_dir, step_data, pre_image_meta):
- """Dynamically extracts state prepopulation fields from chunked test step files."""
- latest_ui_hierarchy = None
- if pre_image_meta and "ui_tree" in pre_image_meta:
- latest_ui_hierarchy = pre_image_meta["ui_tree"]
-
- structured_decisions = None
- traces_json_path = step_dir / "traces.json"
- if traces_json_path.exists():
- try:
- with open(traces_json_path, encoding="utf-8") as f:
- traces = json.load(f)
- operator_trace = next(
- (t for t in traces if t.get("type") == "agent" and t.get("name") == "operator"),
- None,
- )
- if operator_trace:
- payload = operator_trace.get("payload") or {}
- result_val = payload.get("result")
- if isinstance(result_val, str):
- try:
- res_dict = json.loads(result_val)
- structured_decisions = res_dict.get("structured_decisions")
- except Exception as e:
- print(f"Warning: Failed to parse operator trace result JSON: {e}")
- elif isinstance(result_val, dict):
- structured_decisions = result_val.get("structured_decisions")
- except Exception as e:
- print(f"Warning: Failed to read structured_decisions from traces.json: {e}")
- pass
-
- if not structured_decisions and step_data:
- action_taken = step_data.get("action_taken")
- if action_taken:
- try:
- structured_decisions = json.dumps(action_taken)
- except Exception as e:
- print(f"Warning: Failed to serialize action_taken fallback to JSON: {e}")
- pass
-
- focused_app_info = None
- if latest_ui_hierarchy:
- system_packages = {
- "com.android.systemui",
- "android",
- "com.google.android.apps.nexuslauncher",
- }
- packages = []
- for node in latest_ui_hierarchy:
- if isinstance(node, dict):
- pkg = node.get("package")
- if pkg and pkg not in system_packages and "launcher" not in pkg.lower():
- packages.append(pkg)
- if packages:
- from collections import Counter
-
- focused_app_info = Counter(packages).most_common(1)[0][0]
-
- device_date = None
- timestamp = None
- if step_data and "timestamp" in step_data:
- timestamp = step_data["timestamp"]
- elif pre_image_meta and "timestamp" in pre_image_meta:
- timestamp = pre_image_meta["timestamp"]
-
- if timestamp is not None:
- from datetime import datetime, UTC
-
- try:
- dt = datetime.fromtimestamp(timestamp, UTC)
- device_date = dt.strftime("%a %b %d %H:%M:%S UTC %Y")
- except Exception as e:
- print(
- f"Warning: Failed to parse timestamp {timestamp} into device date string: {e}"
- )
-
- return (
- latest_ui_hierarchy,
- structured_decisions,
- focused_app_info,
- device_date,
- )
-
- def _ensure_session_chunked(self, session_id: str) -> Path:
- """Ensures that the session traces are chunked and ready for replay."""
- session_chunked_dir = self.test_data_dir / f"{session_id}_chunked"
-
- db_steps_count = 0
- if self.db_path.exists():
- try:
- conn = sqlite3.connect(self.db_path)
- cursor = conn.cursor()
- cursor.execute(
- "SELECT COUNT(*) FROM steps WHERE session_id = ?",
- (session_id,),
- )
- db_steps_count = cursor.fetchone()[0]
- conn.close()
- except Exception as e:
- print(f"Warning: Failed to query step count from DB: {e}")
-
- chunked_steps_count = 0
- if session_chunked_dir.exists():
- chunked_steps_count = len(list(session_chunked_dir.glob("step_*")))
-
- # Check if directory does not exist, does not contain the sentinel file, or count of chunked steps is out of sync
- needs_chunking = (
- not session_chunked_dir.exists()
- or not (session_chunked_dir / ".chunked").exists()
- or db_steps_count != chunked_steps_count
- )
-
- if needs_chunking:
- try:
- print(
- f"Chunked data needs update for session {session_id} (DB"
- f" steps: {db_steps_count}, chunked steps:"
- f" {chunked_steps_count}). Chunking on-the-fly..."
- )
- self.chunk_session_traces(session_id, session_chunked_dir)
- except Exception as e:
- print(f"Failed to chunk session {session_id} on-the-fly: {e}")
- return session_chunked_dir
-
- def resolve_master_video_path(self, db_recorded_path_str: str) -> Path | None:
- """Resolves the physical master video recording path on disk, searching for renamed folders."""
- db_video_path = Path(db_recorded_path_str)
-
- # 1. Direct absolute path check
- if db_video_path.exists():
- return db_video_path
-
- # 2. Check if relative to workspace
- workspace_rel_path = self.workspace_root / db_video_path
- if workspace_rel_path.exists():
- return workspace_rel_path.resolve()
-
- # 3. Check if under traces_path
- traces_rel_path = self.traces_path / db_video_path.name
- if traces_rel_path.exists():
- return traces_rel_path.resolve()
-
- # 4. Resolve folder renaming prefix
- parent_dir_name = db_video_path.parent.name
- filename = db_video_path.name
- for search_dir in [
- self.traces_path,
- self.workspace_root / "artemis-traces",
- ]:
- if parent_dir_name and search_dir.exists():
- try:
- for child in search_dir.iterdir():
- if child.is_dir() and (
- child.name.startswith(parent_dir_name) or parent_dir_name in child.name
- ):
- candidate = child / filename
- if candidate.exists():
- return candidate.resolve()
- except Exception as e:
- print(f"Warning: Error while scanning {search_dir} for renamed folder: {e}")
-
- # 5. Check if under traces_path or artemis-traces with parent_dir_name
- for search_dir in [
- self.traces_path,
- self.workspace_root / "artemis-traces",
- ]:
- fallback_path = search_dir / parent_dir_name / filename
- if fallback_path.exists():
- return fallback_path.resolve()
-
- return None
-
- def calculate_virtual_start_time(self, session_id: str, step_number: int) -> float | None:
- """Calculates the virtual start time based on the database's final response timestamp."""
- if not self.db_path.exists():
- return None
-
- try:
- conn = sqlite3.connect(self.db_path)
- cursor = conn.cursor()
-
- # Fetch fallback current step timestamp
- cursor.execute(
- "SELECT timestamp FROM steps WHERE session_id = ? AND step_number = ?",
- (session_id, step_number),
- )
- row = cursor.fetchone()
- step_timestamp = row[0] if (row and row[0] is not None) else None
-
- if step_number <= 1:
- # First step starts at session start
- cursor.execute(
- "SELECT start_time FROM sessions WHERE session_id = ?",
- (session_id,),
- )
- row = cursor.fetchone()
- db_session_start_time = row[0] if (row and row[0] is not None) else step_timestamp
- conn.close()
- return db_session_start_time
-
- # Get step_id of previous step
- cursor.execute(
- "SELECT step_id FROM steps WHERE session_id = ? AND step_number = ?",
- (session_id, step_number - 1),
- )
- row = cursor.fetchone()
- if not row:
- conn.close()
- return step_timestamp
-
- prev_step_id = row[0]
-
- # Get maximum trace timestamp for that step (final response timestamp)
- cursor.execute(
- "SELECT MAX(timestamp) FROM traces WHERE step_id = ?",
- (prev_step_id,),
- )
- row = cursor.fetchone()
- conn.close()
-
- if row and row[0] is not None:
- db_final_response_timestamp = row[0]
- return db_final_response_timestamp - 0.001
-
- return step_timestamp
- except Exception as e:
- print(f"Warning: Failed to calculate virtual start time from database: {e}")
- return None
-
- def _preemptive_clip_video(
- self,
- session_id: str,
- step_number: int,
- step_data: dict,
- temp_db_path: Path,
- outputs_dir: Path,
- replay_id: str = None,
- ):
- """Locates the master recording, calculates step time bounds, trims the video and updates the temp DB."""
- import subprocess
- import uuid
-
- # 1. Query the master database's video_recordings table
- if not self.db_path.exists():
- print(
- f"Warning: Master database not found at {self.db_path}."
- " Skipping preemptive video clip."
- )
- return
-
- try:
- conn_master = sqlite3.connect(self.db_path)
- cursor = conn_master.cursor()
- cursor.execute(
- "SELECT video_id, device_id, start_time, end_time,"
- " local_video_path FROM video_recordings WHERE session_id = ?",
- (session_id,),
- )
- row = cursor.fetchone()
- conn_master.close()
- except Exception as e:
- print(f"Warning: Failed to query video_recordings from master database: {e}")
- return
-
- if not row:
- print(
- f"No video recording found for session {session_id} in master"
- " database. Skipping clip."
- )
- return
-
- (
- video_id,
- device_id,
- video_start_time,
- video_end_time,
- db_recorded_path,
- ) = row
- if not db_recorded_path:
- print("Warning: Recorded path is empty in video_recordings. Skipping clip.")
- return
-
- # 2. Locate master recording on disk (renaming logic)
- master_video_path = self.resolve_master_video_path(db_recorded_path)
- if not master_video_path:
- print(
- f"Warning: Could not locate master recording on disk for path: {db_recorded_path}"
- )
- return
-
- # 3. Calculate step bounds using virtual start time
- step_start = self.calculate_virtual_start_time(session_id, step_number)
- if step_start is None:
- step_start = step_data.get("timestamp")
-
- if step_start is None:
- print("Warning: Step start timestamp could not be resolved. Skipping clip.")
- return
-
- # Look for next step start
- step_dir = self.test_data_dir / f"{session_id}_chunked" / f"step_{step_number:02d}"
- next_step_dir = step_dir.parent / f"step_{step_number + 1:02d}"
- next_step_json_path = next_step_dir / "step.json"
-
- if next_step_json_path.exists():
- try:
- with open(next_step_json_path, encoding="utf-8") as f:
- next_step_data = json.load(f)
- next_step_start = next_step_data.get("timestamp", step_start + 15.0)
- except Exception as e:
- print(f"Warning: Failed to read next step json: {e}")
- next_step_start = step_start + 15.0
- else:
- next_step_start = step_start + 15.0
-
- # Calculate offsets
- start_offset = step_start - video_start_time
- # clamp start_offset to be non-negative
- start_offset = max(0.0, start_offset)
- end_offset = next_step_start - video_start_time
- duration = max(0.1, end_offset - start_offset)
-
- trimmed_video_path = outputs_dir / "recording_trimmed.mp4"
-
- # 4. Execute FFmpeg trim
- cmd = [
- "ffmpeg",
- "-y",
- "-i",
- str(master_video_path),
- "-ss",
- f"{start_offset:.3f}",
- "-t",
- f"{duration:.3f}",
- "-c:v",
- "libx264",
- str(trimmed_video_path),
- ]
- print(
- f"Trimming video segment from {master_video_path}:"
- f" offset={start_offset:.3f}s, duration={duration:.3f}s"
- )
- try:
- res = subprocess.run(cmd, capture_output=True, text=True)
- if res.returncode != 0:
- print(
- f"Warning: ffmpeg failed with exit code {res.returncode}. stderr: {res.stderr}"
- )
- return
- except Exception as e:
- print(f"Warning: Failed to execute ffmpeg command: {e}")
- return
-
- # 5. Update Sandbox Database
- try:
- conn_temp = sqlite3.connect(temp_db_path)
- conn_temp.execute("""
- CREATE TABLE IF NOT EXISTS video_recordings (
- video_id TEXT PRIMARY KEY,
- session_id TEXT,
- device_id TEXT,
- start_time REAL,
- end_time REAL,
- local_video_path TEXT,
- FOREIGN KEY(session_id) REFERENCES sessions(session_id)
- )
- """)
-
- # Insert original metadata first if not present
- conn_temp.execute(
- """
- INSERT OR IGNORE INTO video_recordings (video_id, session_id, device_id, start_time, end_time, local_video_path)
- VALUES (?, ?, ?, ?, ?, ?)
- """,
- (
- video_id,
- session_id,
- device_id,
- video_start_time,
- video_end_time,
- db_recorded_path,
- ),
- )
-
- # Update values
- conn_temp.execute(
- """
- UPDATE video_recordings
- SET local_video_path = ?, start_time = ?, end_time = ?
- WHERE session_id = ?
- """,
- (
- str(trimmed_video_path),
- step_start,
- next_step_start,
- session_id,
- ),
- )
-
- if replay_id:
- # Also copy / update for replay_id
- conn_temp.execute(
- """
- INSERT OR IGNORE INTO video_recordings (video_id, session_id, device_id, start_time, end_time, local_video_path)
- VALUES (?, ?, ?, ?, ?, ?)
- """,
- (
- str(uuid.uuid4()),
- replay_id,
- device_id,
- video_start_time,
- video_end_time,
- db_recorded_path,
- ),
- )
-
- conn_temp.execute(
- """
- UPDATE video_recordings
- SET local_video_path = ?, start_time = ?, end_time = ?
- WHERE session_id = ?
- """,
- (
- str(trimmed_video_path),
- step_start,
- next_step_start,
- replay_id,
- ),
- )
-
- conn_temp.commit()
- conn_temp.close()
- print(
- f"Successfully updated sandbox database at {temp_db_path} with"
- " trimmed video segment."
- )
- except Exception as e:
- print(f"Warning: Failed to update sandbox SQLite database: {e}")
-
- def create_ctx(
- self,
- session_id: str,
- step_number: int,
- agent_name: str = "explorer",
- replay_id: str = None,
- ) -> ArtemisContext:
- """Creates a sandboxed ArtemisContext and DataEngine for the given session and step."""
- self._ensure_session_chunked(session_id)
- step_dir = self.test_data_dir / f"{session_id}_chunked" / f"step_{step_number:02d}"
- if not step_dir.exists():
- raise FileNotFoundError(f"Step directory not found at {step_dir}")
-
- step_json_path = step_dir / "step.json"
- pre_image_meta_path = step_dir / "pre_image_meta.json"
- post_image_meta_path = step_dir / "post_image_meta.json"
-
- with open(step_json_path, encoding="utf-8") as f:
- step_data = json.load(f)
-
- pre_image_meta = None
- if pre_image_meta_path.exists():
- with open(pre_image_meta_path, encoding="utf-8") as f:
- pre_image_meta = json.load(f)
-
- post_image_meta = None
- if post_image_meta_path.exists():
- with open(post_image_meta_path, encoding="utf-8") as f:
- post_image_meta = json.load(f)
-
- outputs_dir = self.test_outputs_dir / f"{session_id}_step_{step_number:02d}"
- outputs_dir.mkdir(parents=True, exist_ok=True)
-
- temp_traces_dir = outputs_dir / "temp_traces"
- temp_db_path = temp_traces_dir / "data_engine.db"
-
- _, _, traces_to_preload = self.extract_replay_data(
- step_dir, str(self.db_path), agent_name=agent_name
- )
- initial_goal = self.load_session_goal(session_id, step_dir, str(self.db_path))
-
- self.setup_temporary_database(
- temp_db_path,
- temp_traces_dir,
- session_id,
- step_data,
- pre_image_meta,
- initial_goal,
- traces_to_preload,
- post_image_meta=post_image_meta,
- replay_id=replay_id,
- )
-
- try:
- self._preemptive_clip_video(
- session_id=session_id,
- step_number=step_number,
- step_data=step_data,
- temp_db_path=temp_db_path,
- outputs_dir=outputs_dir,
- replay_id=replay_id,
- )
- except Exception as e:
- print(f"Warning: Pre-emptive video clipping failed: {e}")
-
- try:
- from artemis.context import (
- ArtemisContext,
- DeviceContext,
- DevicePlatform,
- ExecutionSetup,
- )
- from artemis.data_engine.engine import DataEngine
-
- ArtemisContext.model_rebuild()
- except ImportError as import_err:
- raise ImportError(f"Failed to import Artemis core modules: {import_err}")
-
- import sqlite3
-
- conn = sqlite3.connect(self.db_path)
- cursor = conn.cursor()
- cursor.execute(
- "SELECT device_info FROM sessions WHERE session_id = ?",
- (session_id,),
- )
- row = cursor.fetchone()
- conn.close()
-
- device_info = {}
- if row and row[0]:
- try:
- device_info = json.loads(row[0])
- except (ValueError, TypeError):
- # Malformed device_info: simulate with the defaults below.
- pass
-
- sim_device_id = device_info.get("device_id", "replay-device")
- sim_w = device_info.get("device_width", 1080)
- sim_h = device_info.get("device_height", 2400)
-
- device_context = DeviceContext(
- host_platform="LINUX",
- mobile_platform=DevicePlatform.ANDROID,
- device_id=sim_device_id,
- device_width=sim_w,
- device_height=sim_h,
- )
- print(f"Replay simulated device: {sim_device_id} ({sim_w}x{sim_h})")
-
- from artemis.config import get_default_llm_config
-
- try:
- llm_cfg = get_default_llm_config()
- except Exception as e:
- print(f"Warning: Failed to load default LLM config: {e}")
- llm_cfg = None
-
- ctx = ArtemisContext(
- device=device_context,
- adb_client=None,
- ui_adb_client=None,
- execution_setup=ExecutionSetup(traces_path=temp_traces_dir),
- llm_config=llm_cfg,
- )
-
- data_engine = DataEngine(ctx)
- data_engine.current_session_id = replay_id or session_id
- data_engine.current_step_id = step_data["step_id"]
- data_engine.current_step_dir = outputs_dir
- ctx.data_engine = data_engine
-
- settings.DATA_ENGINE_DB_PATH = temp_db_path
- settings.TRACES_PATH = temp_traces_dir
- os.environ["DATA_ENGINE_DB_PATH"] = str(temp_db_path)
-
- return ctx
-
- def instantiate_state(
- self,
- session_id: str,
- step_number: int,
- ) -> State:
- """Instantiates the State object for the given session and step number."""
- self._ensure_session_chunked(session_id)
- step_dir = self.test_data_dir / f"{session_id}_chunked" / f"step_{step_number:02d}"
- if not step_dir.exists():
- raise FileNotFoundError(f"Step directory not found at {step_dir}")
-
- step_json_path = step_dir / "step.json"
- pre_image_meta_path = step_dir / "pre_image_meta.json"
-
- with open(step_json_path, encoding="utf-8") as f:
- step_data = json.load(f)
-
- pre_image_meta = None
- if pre_image_meta_path.exists():
- with open(pre_image_meta_path, encoding="utf-8") as f:
- pre_image_meta = json.load(f)
-
- initial_goal = self.load_session_goal(session_id, step_dir, str(self.db_path))
-
- # State has no fields for the app info or device date (extra="forbid").
- ui_hier, decisions, _app_info, _dev_date = self.extract_state_prepopulation_data(
- step_dir, step_data, pre_image_meta
- )
-
- try:
- from artemis.graph.state import State
- except ImportError as import_err:
- raise ImportError(f"Failed to import State module: {import_err}")
-
- state = State(
- initial_goal=initial_goal,
- latest_screenshot=str(step_dir / "pre.jpg")
- if (step_dir / "pre.jpg").exists()
- else str(step_dir / "post.jpg"),
- operator_raw_data={"width": self.w, "height": self.h},
- current_step_id=step_data["step_id"],
- subagent_calls=step_data.get("subagent_calls") or [],
- latest_ui_hierarchy=ui_hier,
- structured_decisions=decisions,
- )
- return state
-
- def generate_replay_id(self) -> str:
- """Generates a new unique replay ID."""
- import uuid
-
- return str(uuid.uuid4())
-
- async def replay_step_tool(
- self,
- session_id: str,
- step_number: int,
- override_device_id: str = None,
- user_submits: dict = None,
- tool_name: str = "ask_explorer",
- replay_id: str = None,
- ) -> list[dict]:
- """Replays a specific step tool in a sandboxed execution environment."""
- if not replay_id:
- replay_id = self.generate_replay_id()
-
- replay_result = {
- "session_id": session_id,
- "step_number": step_number,
- "replay_id": replay_id,
- "success": False,
- }
- tool_cfg = REPLAY_TOOLS_CONFIG.get(tool_name)
- if not tool_cfg:
- raise ValueError(f"Unknown tool for replay: {tool_name}")
- agent_name = tool_cfg["agent_name"]
-
- outputs_parent = self.test_outputs_dir
- if outputs_parent.exists():
- for item in outputs_parent.iterdir():
- if item.is_dir() and item.name.startswith(f"{session_id}_step_"):
- archive_dir = self.replay_base_dir / "data" / "older"
- archive_dir.mkdir(parents=True, exist_ok=True)
-
- from datetime import datetime
-
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- target_name = f"{item.name}_{timestamp}"
- target_path = archive_dir / target_name
-
- counter = 1
- while target_path.exists():
- target_name = f"{item.name}_{timestamp}_{counter}"
- target_path = archive_dir / target_name
- counter += 1
-
- print(
- f"Archiving previous run output directory {item.name} to {target_path}..."
- )
- try:
- shutil.move(str(item), str(target_path))
- except Exception as archive_err:
- print(
- f"Warning: Failed to archive {item.name}: {archive_err}",
- file=sys.stderr,
- )
-
- outputs_dir = self.test_outputs_dir / f"{session_id}_step_{step_number:02d}"
- temp_db_path = outputs_dir / "temp_traces" / "data_engine.db"
-
- # Load step data locally for fallback mapping resolutions
- step_dir = self.test_data_dir / f"{session_id}_chunked" / f"step_{step_number:02d}"
- step_json_path = step_dir / "step.json"
- with open(step_json_path, encoding="utf-8") as f:
- step_data = json.load(f)
-
- # Retrieve preloaded trace IDs from chunked trace files to filter new tool calls
- _, _, traces_to_preload = self.extract_replay_data(
- step_dir, str(self.db_path), agent_name=agent_name
- )
- preloaded_trace_ids = {t.get("trace_id") for t in traces_to_preload if t.get("trace_id")}
- llm_calls_log = []
-
- # Device initialization removed for step replay
-
- ctx = self.create_ctx(
- session_id=session_id,
- step_number=step_number,
- agent_name=agent_name,
- replay_id=replay_id,
- )
-
- state = self.instantiate_state(
- session_id=session_id,
- step_number=step_number,
- )
-
- data_engine = ctx.data_engine
-
- print("Executing using Live Gemini Client...")
-
- from google.genai.models import AsyncModels
- from unittest.mock import patch
-
- original_generate_content = AsyncModels.generate_content
-
- async def wrapped_generate_content(self, *, model, contents, config=None, **kwargs):
- system_instruction = None
- if config and config.system_instruction:
- if isinstance(config.system_instruction, str):
- system_instruction = config.system_instruction
- elif hasattr(config.system_instruction, "parts"):
- parts = []
- for p in config.system_instruction.parts:
- if hasattr(p, "text") and p.text:
- parts.append(p.text)
- system_instruction = " ".join(parts)
- else:
- system_instruction = str(config.system_instruction)
-
- prompt_data = {
- "model": model,
- "system_instruction": system_instruction,
- "contents": [],
- }
-
- contents_list = contents if isinstance(contents, list) else [contents]
- for content in contents_list:
- parts_desc = []
- if hasattr(content, "parts") and content.parts:
- for part in content.parts:
- if hasattr(part, "text") and part.text:
- parts_desc.append({"type": "text", "text": part.text})
- elif hasattr(part, "inline_data") and part.inline_data:
- parts_desc.append(
- {
- "type": "image",
- "mime_type": part.inline_data.mime_type,
- "length": len(part.inline_data.data),
- }
- )
- elif hasattr(part, "function_call") and part.function_call:
- parts_desc.append(
- {
- "type": "function_call",
- "name": part.function_call.name,
- "args": (
- dict(part.function_call.args)
- if part.function_call.args
- else {}
- ),
- }
- )
- elif hasattr(part, "function_call") and part.function_response:
- parts_desc.append(
- {
- "type": "function_response",
- "name": part.function_response.name,
- "response": (
- dict(part.function_response.response)
- if part.function_response.response
- else {}
- ),
- }
- )
- else:
- parts_desc.append({"type": "raw", "value": str(content)})
-
- prompt_data["contents"].append(
- {
- "role": getattr(content, "role", "unknown"),
- "parts": parts_desc,
- }
- )
-
- record = {"prompt": prompt_data, "response": None, "error": None}
- llm_calls_log.append(record)
-
- try:
- res = await original_generate_content(
- self,
- model=model,
- contents=contents,
- config=config,
- **kwargs,
- )
-
- thinking_parts = []
- if hasattr(res, "candidates") and res.candidates and res.candidates[0].content:
- for part in res.candidates[0].content.parts:
- if getattr(part, "thought", False):
- thinking_parts.append(part.text)
-
- resp_data = {
- "text": res.text if hasattr(res, "text") else None,
- "function_calls": [],
- "thought": ("\n".join(thinking_parts) if thinking_parts else None),
- }
- if hasattr(res, "function_calls") and res.function_calls:
- for fc in res.function_calls:
- resp_data["function_calls"].append(
- {
- "name": fc.name,
- "args": dict(fc.args) if fc.args else {},
- }
- )
- record["response"] = resp_data
- return res
- except Exception as err:
- err_str = str(err)
- record["error"] = (
- f"{err.__class__.__name__}: {err_str}" if err_str else err.__class__.__name__
- )
- raise err
-
- try:
- try:
- import inspect
- import importlib
-
- mod = importlib.import_module(tool_cfg["module"])
- tool_fn = getattr(mod, tool_cfg["function"])
-
- user_submits = user_submits or {}
- sig = inspect.signature(tool_fn)
- tool_args = {}
- denylist = tool_cfg.get("denylist_args", [])
- fallback_mappings = tool_cfg.get("fallback_mappings", {})
-
- user_submits = user_submits or {}
- sig = inspect.signature(tool_fn)
- tool_args = {}
- for name, param in sig.parameters.items():
- if name in denylist:
- if name == "ctx":
- tool_args["ctx"] = ctx
- elif name == "state":
- tool_args["state"] = state
- continue
-
- if name in user_submits:
- val = user_submits[name]
- if param.annotation is bool and isinstance(val, str):
- val = val.lower() == "true"
- tool_args[name] = val
- else:
- val = ""
- if param.default is not inspect.Parameter.empty:
- val = param.default
- if name in fallback_mappings:
- fallback_path = fallback_mappings[name]
- resolved_val = self.resolve_step_data_path(step_data, fallback_path)
- if resolved_val is not None:
- val = resolved_val
- tool_args[name] = val
-
- with patch.object(AsyncModels, "generate_content", wrapped_generate_content):
- outcome = await tool_fn(**tool_args)
-
- replay_result["success"] = True
- replay_result["result"] = outcome
- print(f"Replay Execution complete. Outcome: {outcome}")
-
- except Exception as replay_err:
- print(f"Error during step replay: {replay_err}", file=sys.stderr)
- traceback.print_exc()
-
- replay_result["success"] = False
- replay_result["error"] = (
- f"{replay_err.__class__.__name__}: {str(replay_err)}"
- if str(replay_err)
- else replay_err.__class__.__name__
- )
- raise replay_err
- finally:
- await data_engine.shutdown()
-
- tool_calls = []
- if temp_db_path.exists():
- try:
- tool_calls = self.extract_tool_calls_from_db(temp_db_path, preloaded_trace_ids)
- except Exception as ex:
- print(
- f"Warning: Failed to extract tool calls: {ex}",
- file=sys.stderr,
- )
-
- replay_result["tool_calls"] = tool_calls
- replay_result["llm_calls"] = llm_calls_log
-
- outcome_path = outputs_dir / "result.json"
- try:
- with open(outcome_path, "w", encoding="utf-8") as f:
- json.dump(replay_result, f, indent=2, ensure_ascii=False)
- except Exception as write_err:
- print(
- f"Warning: Failed to write result.json: {write_err}",
- file=sys.stderr,
- )
-
- return llm_calls_log
-
- def get_replay_config(self, tool_name: str = "ask_explorer") -> list[dict]:
- """Returns the parameter list and types of the selected tool logic."""
- tool_cfg = REPLAY_TOOLS_CONFIG.get(tool_name)
- if not tool_cfg:
- raise ValueError(f"Unknown tool for replay: {tool_name}")
-
- try:
- import inspect
- import importlib
-
- mod = importlib.import_module(tool_cfg["module"])
- tool_fn = getattr(mod, tool_cfg["function"])
- sig = inspect.signature(tool_fn)
-
- denylist = tool_cfg.get("denylist_args", [])
- explorer_params = []
- for name, param in sig.parameters.items():
- if name in denylist:
- continue
- default_val = ""
- if param.default is not inspect.Parameter.empty:
- default_val = param.default
-
- input_type = "text"
- if any(
- keyword in name.lower()
- for keyword in (
- "feedback",
- "context",
- "description",
- "note",
- )
- ):
- input_type = "textarea"
- elif param.annotation is bool:
- input_type = "checkbox"
- elif param.annotation in (int, float):
- input_type = "number"
-
- explorer_params.append(
- {
- "name": name,
- "default": default_val,
- "input_type": input_type,
- }
- )
- return explorer_params
- except Exception as e:
- raise Exception(f"Failed to load signature for {tool_name}: {str(e)}")
-
- def get_replay_tools(self) -> list[dict]:
- """Returns the list of registered tools available for replay."""
- return [
- {
- "name": name,
- "display_name": cfg["display_name"],
- "description": cfg["description"],
- }
- for name, cfg in REPLAY_TOOLS_CONFIG.items()
- ]
-
- def get_replay_steps(self, session_id: str) -> list[dict]:
- """Loads and formats metadata for all chunked steps in the session."""
- session_chunked_dir = self._ensure_session_chunked(session_id)
- if not session_chunked_dir.exists():
- raise FileNotFoundError("Step replay chunked data not found for this session.")
-
- try:
- steps_metadata = self.load_steps_metadata(session_id)
- step_numbers = sorted(list(steps_metadata.keys()))
-
- import inspect
- import importlib
-
- # Pre-load signatures and configs for all registered tools
- tool_signatures = {}
- for t_name, t_cfg in REPLAY_TOOLS_CONFIG.items():
- try:
- mod = importlib.import_module(t_cfg["module"])
- fn = getattr(mod, t_cfg["function"])
- tool_signatures[t_name] = inspect.signature(fn)
- except Exception as e:
- print(
- f"Warning: Failed to load signature for tool {t_name}: {e}",
- file=sys.stderr,
- )
-
- steps_list = []
- for num in step_numbers:
- s_data = steps_metadata.get(num, {})
- sid = s_data.get("step_id")
-
- pre_url = None
- post_url = None
- step_dir = session_chunked_dir / f"step_{num:02d}"
- if step_dir.exists():
- pre_jpg = step_dir / "pre.jpg"
- if pre_jpg.exists():
- pre_url = f"file://{pre_jpg.resolve()}"
- post_jpg = step_dir / "post.jpg"
- if post_jpg.exists():
- post_url = f"file://{post_jpg.resolve()}"
- if pre_url and post_url and pre_url == post_url:
- post_url = None
-
- # Generate param_defaults for all registered tools
- all_tool_defaults = {}
- for t_name, sig in tool_signatures.items():
- t_cfg = REPLAY_TOOLS_CONFIG[t_name]
- denylist = t_cfg.get("denylist_args", [])
- fallback_mappings = t_cfg.get("fallback_mappings", {})
-
- defaults = {}
- for name, param in sig.parameters.items():
- if name in denylist:
- continue
-
- val = ""
- if param.default is not inspect.Parameter.empty:
- val = param.default
-
- # Resolve fallback using mappings
- if name in fallback_mappings:
- mapping_path = fallback_mappings[name]
- parts = mapping_path.split(".")
- curr = s_data
- for part in parts:
- if isinstance(curr, dict):
- curr = curr.get(part)
- elif isinstance(curr, list):
- try:
- idx = int(part)
- if idx < len(curr):
- curr = curr[idx]
- else:
- curr = None
- except ValueError:
- curr = None
- else:
- curr = None
-
- if curr is not None:
- val = curr
-
- defaults[name] = val
- all_tool_defaults[t_name] = defaults
-
- # Check if this step has already been replayed by scanning outputs folder
- step_outputs_dir = self.test_outputs_dir / f"{session_id}_step_{num:02d}"
- step_db_path = step_outputs_dir / "temp_traces" / "data_engine.db"
- is_replayed = step_db_path.exists()
-
- steps_list.append(
- {
- "step_id": str(sid).lower() if sid else None,
- "step_number": num,
- "timestamp": s_data.get("timestamp"),
- "summary": s_data.get("summary", "No summary available."),
- "action_taken": s_data.get("action_taken", []),
- "query": "",
- "param_defaults": all_tool_defaults,
- "pre_screenshot": pre_url,
- "post_screenshot": post_url,
- "operator_raw_thinking": s_data.get("operator_raw_thinking"),
- "operator_native_thinking": s_data.get("operator_native_thinking"),
- "is_replayed": is_replayed,
- }
- )
-
- return steps_list
- except Exception as e:
- traceback.print_exc()
- raise e
-
- async def run_step_replay(
- self,
- session_id: str,
- step_number: int,
- device_id: str,
- user_submits: dict,
- tool_name: str = "ask_explorer",
- replay_id: str = None,
- ) -> dict:
- """Triggers sandbox execution of step replay and returns the resulting traces trees."""
- tool_cfg = REPLAY_TOOLS_CONFIG.get(tool_name)
- if not tool_cfg:
- raise ValueError(f"Unknown tool for replay: {tool_name}")
- agent_name = tool_cfg["agent_name"]
-
- # 1. Run live step replay
- llm_calls_log = await self.replay_step_tool(
- session_id=session_id,
- step_number=step_number,
- override_device_id=device_id,
- user_submits=user_submits,
- tool_name=tool_name,
- replay_id=replay_id,
- )
-
- # 2. Extract freshly generated traces
- outputs_dir = self.test_outputs_dir / f"{session_id}_step_{step_number:02d}"
- temp_db_path = outputs_dir / "temp_traces" / "data_engine.db"
-
- self._ensure_session_chunked(session_id)
- step_dir = self.test_data_dir / f"{session_id}_chunked" / f"step_{step_number:02d}"
- _, _, traces_to_preload = self.extract_replay_data(
- step_dir, str(self.db_path), agent_name=agent_name
- )
- preloaded_trace_ids = {t.get("trace_id") for t in traces_to_preload if t.get("trace_id")}
-
- all_traces = self.load_all_session_traces(temp_db_path, preloaded_trace_ids, step_number)
-
- # Align LLM calls and extract final tool result from result.json if available
- replay_outcome = None
- replay_id = None
- result_json_path = outputs_dir / "result.json"
- if result_json_path.exists():
- try:
- with open(result_json_path, encoding="utf-8") as f:
- res_data = json.load(f)
- replay_outcome = res_data.get("result")
- replay_id = res_data.get("replay_id")
- except (OSError, ValueError) as e:
- print(
- f"Warning: Failed to read replay result from {result_json_path}: {e}",
- file=sys.stderr,
- )
-
- # Update virtual tool trace result with the actual formatted outcome if available
- if replay_outcome is not None:
- for node in all_traces:
- if (
- not node.get("is_preloaded")
- and node["type"] == "tool"
- and node["name"] == tool_name
- ):
- node["payload"]["result"] = replay_outcome
-
- # Collect all valid LLM span names from REPLAY_TOOLS_CONFIG to align subagent calls too
- all_llm_span_names = {
- cfg.get("llm_span_name")
- for cfg in REPLAY_TOOLS_CONFIG.values()
- if cfg.get("llm_span_name")
- }
-
- llm_call_index = 0
- for node in all_traces:
- if (
- not node.get("is_preloaded")
- and node["type"] == "span"
- and node["name"] in all_llm_span_names
- and llm_calls_log
- and llm_call_index < len(llm_calls_log)
- ):
- node["type"] = "llm_call"
- node["llm_call"] = llm_calls_log[llm_call_index]
- llm_call_index += 1
-
- # Clean payload paths
- for node in all_traces:
- if node.get("payload"):
- node["payload"] = self.clean_payload_paths(node["payload"])
- if node.get("llm_call"):
- node["llm_call"] = self.clean_payload_paths(node["llm_call"])
-
- # Filter all_traces for ONLY this step's traces
- step_traces = [t for t in all_traces if t.get("step_number") == step_number]
-
- preloaded_traces = [t for t in step_traces if t.get("is_preloaded")]
- live_traces = [t for t in step_traces if not t.get("is_preloaded")]
-
- # Build preloaded hierarchy
- preloaded_nodes = {
- str(t["trace_id"]).lower(): {**t, "children": []} for t in preloaded_traces
- }
- preloaded_root_nodes = []
- for node_id, node in preloaded_nodes.items():
- parent_id = node.get("parent_trace_id")
- if parent_id:
- parent_id_str = str(parent_id).lower()
- if parent_id_str in preloaded_nodes:
- preloaded_nodes[parent_id_str]["children"].append(node)
- else:
- preloaded_root_nodes.append(node)
- else:
- preloaded_root_nodes.append(node)
- preloaded_root_nodes = self.sanitize_nodes(preloaded_root_nodes)
- preloaded_root_nodes = self.sort_tree_nodes_chronologically(preloaded_root_nodes)
-
- # Build live hierarchy
- live_nodes = {str(t["trace_id"]).lower(): {**t, "children": []} for t in live_traces}
-
- # Add a virtual "Response" child node at the end of the virtual tool trace to show the final outcome at the bottom of the tree
- if replay_outcome is not None:
- virtual_tool_node_id = None
- for node_id, node in live_nodes.items():
- if node.get("type") == "tool" and node.get("name") == tool_name:
- virtual_tool_node_id = node_id
- break
-
- if virtual_tool_node_id:
- import uuid
-
- outcome_node_id = str(uuid.uuid4())
- max_ts = (
- max([t.get("timestamp", 0.0) for t in live_traces])
- if live_traces
- else live_nodes[virtual_tool_node_id].get("timestamp", 0.0)
- )
-
- outcome_node = {
- "trace_id": outcome_node_id,
- "session_id": session_id,
- "step_id": live_nodes[virtual_tool_node_id].get("step_id"),
- "parent_trace_id": live_nodes[virtual_tool_node_id]["trace_id"],
- "type": "tool",
- "name": f"{tool_name} Response",
- "status": (live_nodes[virtual_tool_node_id].get("status", "success")),
- "timestamp": max_ts + 0.001,
- "duration": 0.0,
- "payload": {"result": replay_outcome},
- "is_preloaded": False,
- "step_number": step_number,
- "children": [],
- }
- live_nodes[outcome_node_id] = outcome_node
-
- live_root_nodes = []
- for node_id, node in live_nodes.items():
- parent_id = node.get("parent_trace_id")
- if parent_id:
- parent_id_str = str(parent_id).lower()
- if parent_id_str in live_nodes:
- live_nodes[parent_id_str]["children"].append(node)
- else:
- live_root_nodes.append(node)
- else:
- live_root_nodes.append(node)
- live_root_nodes = self.sanitize_nodes(live_root_nodes)
- live_root_nodes = self.sort_tree_nodes_chronologically(live_root_nodes)
-
- return {
- "success": True,
- "preloaded": preloaded_root_nodes,
- "live": live_root_nodes,
- "replay_id": replay_id,
- }
-
- def get_step_replay_traces(
- self, session_id: str, step_number: int, tool_name: str = "ask_explorer"
- ) -> dict:
- """Retrieves previously generated step replay traces if they exist."""
- tool_cfg = REPLAY_TOOLS_CONFIG.get(tool_name)
- if not tool_cfg:
- raise ValueError(f"Unknown tool for replay: {tool_name}")
- agent_name = tool_cfg["agent_name"]
-
- outputs_dir = self.test_outputs_dir / f"{session_id}_step_{step_number:02d}"
- temp_db_path = outputs_dir / "temp_traces" / "data_engine.db"
-
- if not temp_db_path.exists():
- return {"success": True, "live": []}
-
- self._ensure_session_chunked(session_id)
- step_dir = self.test_data_dir / f"{session_id}_chunked" / f"step_{step_number:02d}"
- preloaded_trace_ids = set()
- if step_dir.exists():
- _, _, traces_to_preload = self.extract_replay_data(
- step_dir,
- str(self.db_path),
- agent_name=agent_name,
- tool_name=tool_name,
- )
- preloaded_trace_ids = {
- t.get("trace_id") for t in traces_to_preload if t.get("trace_id")
- }
-
- all_traces = self.load_all_session_traces(temp_db_path, preloaded_trace_ids, step_number)
-
- llm_span_name = tool_cfg.get("llm_span_name")
- if not llm_span_name:
- raise ValueError(f"Tool {tool_name} config is missing 'llm_span_name'")
-
- # Align LLM calls and extract final tool result from result.json if available
- llm_calls_log = []
- replay_outcome = None
- replay_id = None
- result_json_path = outputs_dir / "result.json"
- if result_json_path.exists():
- try:
- with open(result_json_path, encoding="utf-8") as f:
- res_data = json.load(f)
- llm_calls_log = res_data.get("llm_calls") or []
- replay_outcome = res_data.get("result")
- replay_id = res_data.get("replay_id")
- except (OSError, ValueError) as e:
- print(
- f"Warning: Failed to read replay result from {result_json_path}: {e}",
- file=sys.stderr,
- )
-
- # Update virtual tool trace result with the actual formatted outcome if available
- if replay_outcome is not None:
- for node in all_traces:
- if (
- not node.get("is_preloaded")
- and node["type"] == "tool"
- and node["name"] == tool_name
- ):
- node["payload"]["result"] = replay_outcome
-
- # Collect all valid LLM span names from REPLAY_TOOLS_CONFIG to align subagent calls too
- all_llm_span_names = {
- cfg.get("llm_span_name")
- for cfg in REPLAY_TOOLS_CONFIG.values()
- if cfg.get("llm_span_name")
- }
-
- llm_call_index = 0
- for node in all_traces:
- if (
- not node.get("is_preloaded")
- and node["type"] == "span"
- and node["name"] in all_llm_span_names
- and llm_calls_log
- and llm_call_index < len(llm_calls_log)
- ):
- node["type"] = "llm_call"
- node["llm_call"] = llm_calls_log[llm_call_index]
- llm_call_index += 1
-
- # Clean payload paths
- for node in all_traces:
- if node.get("payload"):
- node["payload"] = self.clean_payload_paths(node["payload"])
- if node.get("llm_call"):
- node["llm_call"] = self.clean_payload_paths(node["llm_call"])
-
- # Filter all_traces for ONLY this step's traces
- step_traces = [t for t in all_traces if t.get("step_number") == step_number]
- live_traces = [t for t in step_traces if not t.get("is_preloaded")]
-
- # Build live hierarchy
- live_nodes = {str(t["trace_id"]).lower(): {**t, "children": []} for t in live_traces}
-
- # Add a virtual "Response" child node at the end of the virtual tool trace to show the final outcome at the bottom of the tree
- if replay_outcome is not None:
- virtual_tool_node_id = None
- for node_id, node in live_nodes.items():
- if node.get("type") == "tool" and node.get("name") == tool_name:
- virtual_tool_node_id = node_id
- break
-
- if virtual_tool_node_id:
- import uuid
-
- outcome_node_id = str(uuid.uuid4())
- max_ts = (
- max([t.get("timestamp", 0.0) for t in live_traces])
- if live_traces
- else live_nodes[virtual_tool_node_id].get("timestamp", 0.0)
- )
-
- outcome_node = {
- "trace_id": outcome_node_id,
- "session_id": session_id,
- "step_id": live_nodes[virtual_tool_node_id].get("step_id"),
- "parent_trace_id": live_nodes[virtual_tool_node_id]["trace_id"],
- "type": "tool",
- "name": f"{tool_name} Response",
- "status": (live_nodes[virtual_tool_node_id].get("status", "success")),
- "timestamp": max_ts + 0.001,
- "duration": 0.0,
- "payload": {"result": replay_outcome},
- "is_preloaded": False,
- "step_number": step_number,
- "children": [],
- }
- live_nodes[outcome_node_id] = outcome_node
-
- live_root_nodes = []
- for node_id, node in live_nodes.items():
- parent_id = node.get("parent_trace_id")
- if parent_id:
- parent_id_str = str(parent_id).lower()
- if parent_id_str in live_nodes:
- live_nodes[parent_id_str]["children"].append(node)
- else:
- live_root_nodes.append(node)
- else:
- live_root_nodes.append(node)
- live_root_nodes = self.sanitize_nodes(live_root_nodes)
- live_root_nodes = self.sort_tree_nodes_chronologically(live_root_nodes)
-
- return {
- "success": True,
- "live": live_root_nodes,
- "replay_id": replay_id,
- }
diff --git a/apps/admin_console/routers/__init__.py b/apps/admin_console/routers/__init__.py
deleted file mode 100644
index 58d482ea3..000000000
--- a/apps/admin_console/routers/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
diff --git a/apps/admin_console/routers/media.py b/apps/admin_console/routers/media.py
deleted file mode 100644
index ec9bd0c9a..000000000
--- a/apps/admin_console/routers/media.py
+++ /dev/null
@@ -1,198 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import asyncio
-from pathlib import Path
-from fastapi import APIRouter, HTTPException
-from fastapi.responses import FileResponse, HTMLResponse
-
-from artemis.config import IMAGES_DIR, TRACES_PATH, WORKSPACE_ROOT
-
-try:
- from admin_console.database.repositories.session_repository import session_repo
- from admin_console.services.media_service import media_service
-except ImportError:
- from apps.admin_console.database.repositories.session_repository import session_repo
- from apps.admin_console.services.media_service import media_service
-
-router = APIRouter(tags=["media"])
-
-_VIDEO_MEDIA_TYPES = {
- ".mp4": "video/mp4",
- ".webm": "video/webm",
- ".mkv": "video/x-matroska",
-}
-
-
-def _allowed_media_roots() -> list[Path]:
- """Directories the media endpoints are permitted to serve from."""
- roots = []
- for root in (WORKSPACE_ROOT, TRACES_PATH):
- try:
- roots.append(Path(root).resolve())
- except OSError:
- continue
- return roots
-
-
-def _resolve_media_path(raw_path: str, allowed_suffixes: set[str]) -> Path:
- """Resolve a client-supplied path strictly inside the allowed media roots.
-
- Every candidate is fully resolved (symlinks, `..`, mixed separators) and
- then re-checked against the allowed roots and an extension allowlist, so a
- crafted URL can never address source code, databases, or dotfiles.
- """
- roots = _allowed_media_roots()
- candidate = Path(raw_path)
- attempts = [candidate] if candidate.is_absolute() else [root / candidate for root in roots]
- for attempt in attempts:
- try:
- resolved = attempt.resolve(strict=True)
- except (OSError, ValueError):
- continue
- if not resolved.is_file():
- continue
- if not any(resolved.is_relative_to(root) for root in roots):
- continue
- if resolved.suffix.lower() not in allowed_suffixes:
- raise HTTPException(status_code=403, detail="File type is not served by the media API.")
- return resolved
- raise HTTPException(status_code=404, detail="Media file not found")
-
-
-@router.get("/admin", response_class=HTMLResponse)
-@router.get("/debug", response_class=HTMLResponse)
-async def get_admin_index():
- admin_index = Path(__file__).resolve().parent.parent / "index.html"
- if admin_index.exists():
- return admin_index.read_text(encoding="utf-8")
-
- admin_index_alt = WORKSPACE_ROOT / "apps" / "admin_console" / "index.html"
- if admin_index_alt.exists():
- return admin_index_alt.read_text(encoding="utf-8")
-
- return "Admin Console index.html not found "
-
-
-@router.get("/images/{image_name}")
-@router.get("/api/images/{image_name}")
-async def get_image(image_name: str):
- if not image_name.endswith(".jpg"):
- image_name += ".jpg"
- try:
- images_root = IMAGES_DIR.resolve()
- image_path = (images_root / image_name).resolve(strict=True)
- except (OSError, ValueError):
- raise HTTPException(status_code=404, detail="Image not found")
- if not image_path.is_file() or not image_path.is_relative_to(images_root):
- raise HTTPException(status_code=404, detail="Image not found")
-
- return FileResponse(image_path, media_type="image/jpeg")
-
-
-@router.get("/videos/{video_path:path}")
-async def get_video(video_path: str):
- path = _resolve_media_path(video_path, set(_VIDEO_MEDIA_TYPES))
- return FileResponse(path, media_type=_VIDEO_MEDIA_TYPES[path.suffix.lower()])
-
-
-@router.get("/api/sessions/{session_id}/video")
-async def get_session_video(session_id: str):
- # Blocking work (sqlite, filesystem scan, possible ffmpeg conversion) runs
- # off the event loop.
- return await asyncio.to_thread(_get_session_video_sync, session_id)
-
-
-def _get_session_video_sync(session_id: str):
- video_rec_map = session_repo.get_video_recordings_map()
- video_idx = media_service.build_video_index()
- row = session_repo.get_session_by_id(session_id)
- row_dict = dict(row) if row else {"session_id": session_id}
- recording = session_repo.get_video_recording_for_session(session_id)
- recording_status = str((recording or {}).get("status") or "")
-
- if recording_status in ("recording", "finalizing"):
- return {
- "session_id": session_id,
- "status": "processing",
- "has_video": False,
- "video_url": None,
- "video_segments": [],
- "retry_after_ms": 750,
- }
-
- if recording_status == "failed":
- v_url = media_service.resolve_video_url(row_dict, video_rec_map, video_idx)
- if not v_url:
- return {
- "session_id": session_id,
- "status": "failed",
- "has_video": False,
- "video_url": None,
- "video_segments": [],
- "message": recording.get("error") or "Recording finalization failed",
- }
- # If a video was recovered/found, update DB to ready and proceed to serve it
- session_repo.mark_recording_ready(session_id, v_url)
-
- v_url = media_service.resolve_video_url(row_dict, video_rec_map, video_idx)
- video_segments = media_service.resolve_video_segments(v_url)
- if v_url:
- version = int(
- float((recording or {}).get("end_time") or row_dict.get("end_time") or 0) * 1000
- )
- separator = "&" if "?" in v_url else "?"
- versioned_url = f"{v_url}{separator}v={version}" if version else v_url
- for segment in video_segments:
- segment_separator = "&" if "?" in segment["url"] else "?"
- segment["url"] = (
- f"{segment['url']}{segment_separator}v={version}" if version else segment["url"]
- )
- return {
- "session_id": session_id,
- "status": "ready",
- "has_video": True,
- "video_url": versioned_url,
- "video_segments": video_segments,
- }
-
- return {
- "session_id": session_id,
- "status": "unavailable",
- "has_video": False,
- "video_url": None,
- "video_segments": [],
- }
-
-
-@router.get("/local_file")
-async def get_local_file(path: str):
- p, media_type = media_service.get_safe_local_file(path)
- return FileResponse(p, media_type=media_type)
-
-
-@router.get("/api/sessions/{session_id}/plan")
-async def get_task_plan(session_id: str):
- return {"plan": media_service.get_task_plan_content(session_id)}
-
-
-@router.get("/api/sessions/{session_id}/notes")
-async def get_all_notes(session_id: str):
- return {"notes": media_service.get_session_notes_content(session_id)}
-
-
-@router.get("/api/sessions/{session_id}/checks")
-async def get_session_checks(session_id: str):
- """Checker verdict ledger + run outcome (backfill for the Checker panel)."""
- return media_service.get_session_checks(session_id)
diff --git a/apps/admin_console/routers/replay.py b/apps/admin_console/routers/replay.py
deleted file mode 100644
index 2a83b6e24..000000000
--- a/apps/admin_console/routers/replay.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import traceback
-from fastapi import APIRouter, HTTPException
-
-from artemis.config import WORKSPACE_ROOT
-
-try:
- from admin_console.schemas.task_schema import ReplayRequest
- from admin_console.replay_manager import ReplayManager
-except ImportError:
- from apps.admin_console.schemas.task_schema import ReplayRequest
- from apps.admin_console.replay_manager import ReplayManager
-
-replay_manager = ReplayManager(WORKSPACE_ROOT)
-
-
-router = APIRouter(tags=["replay"])
-
-
-@router.get("/api/devices")
-async def list_devices():
- """Dynamically queries the ADB server for connected Android devices."""
- return replay_manager.list_devices()
-
-
-@router.get("/api/replay/tools")
-async def get_replay_tools():
- """Returns the list of registered tools available for replay."""
- return replay_manager.get_replay_tools()
-
-
-@router.get("/api/replay/config")
-async def get_replay_config(tool_name: str = "ask_explorer"):
- """Returns the parameter list and types of the selected tool logic."""
- try:
- return replay_manager.get_replay_config(tool_name)
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.get("/api/sessions/{session_id}/replay_steps")
-async def get_replay_steps(session_id: str):
- """Loads and formats metadata for all chunked steps in the session."""
- try:
- return replay_manager.get_replay_steps(session_id)
- except FileNotFoundError as e:
- raise HTTPException(status_code=404, detail=str(e))
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.post("/api/sessions/{session_id}/steps/{step_number}/replay")
-async def trigger_step_replay_endpoint(session_id: str, step_number: int, req: ReplayRequest):
- """Triggers sandbox execution of step replay and returns the resulting traces trees."""
- try:
- return await replay_manager.run_step_replay(
- session_id=session_id,
- step_number=step_number,
- device_id=req.device_id,
- user_submits=req.user_submits,
- tool_name=req.tool_name,
- replay_id=req.replay_id,
- )
- except Exception as e:
- traceback.print_exc()
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.get("/api/sessions/{session_id}/steps/{step_number}/replay_traces")
-async def get_step_replay_traces_endpoint(
- session_id: str, step_number: int, tool_name: str = "ask_explorer"
-):
- """Retrieves previously generated step replay traces if they exist."""
- try:
- return replay_manager.get_step_replay_traces(session_id, step_number, tool_name)
- except Exception as e:
- traceback.print_exc()
- raise HTTPException(status_code=500, detail=str(e))
diff --git a/apps/admin_console/routers/sessions.py b/apps/admin_console/routers/sessions.py
deleted file mode 100644
index 6d828b772..000000000
--- a/apps/admin_console/routers/sessions.py
+++ /dev/null
@@ -1,239 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import asyncio
-import json
-import time
-from uuid import UUID
-from fastapi import APIRouter, HTTPException
-
-from artemis.config import DB_PATH, TRACES_PATH
-from artemis.runtime import trace_store
-
-try:
- from admin_console.core.state import state
- from admin_console.database.repositories.session_repository import session_repo
- from admin_console.database.repositories.trace_repository import trace_repo
- from admin_console.services.media_service import media_service
- from admin_console.services.model_service import model_service
-except ImportError:
- from apps.admin_console.core.state import state
- from apps.admin_console.database.repositories.session_repository import session_repo
- from apps.admin_console.database.repositories.trace_repository import trace_repo
- from apps.admin_console.services.media_service import media_service
- from apps.admin_console.services.model_service import model_service
-
-
-router = APIRouter(tags=["sessions"])
-
-
-@router.get("/api/sessions")
-async def list_sessions():
- # The body does blocking work (sqlite queries, filesystem scans, ffmpeg
- # conversion on first sight of a new recording), so run it off the event
- # loop — the frontend polls this endpoint and it must not stall other
- # requests.
- try:
- return await asyncio.to_thread(_list_sessions_sync)
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-def _list_sessions_sync():
- rows = session_repo.get_all_sessions()
- video_rec_map = session_repo.get_video_recordings_map()
- latest_recordings = session_repo.get_latest_video_recordings_map()
- agent_names_by_session = session_repo.get_agent_trace_names_map()
- video_idx = media_service.build_video_index()
- default_model_info = model_service.get_active_model_info()
-
- orphaned_ids = []
- unresolved_profiles = []
- result = []
-
- try:
- from artemis.runtime import DeviceExecutionLock
-
- active_owners = DeviceExecutionLock.get_active_owners()
- active_owner_sids = {
- str(owner.session_id) for owner in active_owners.values() if owner.session_id
- }
- except Exception:
- active_owner_sids = set()
-
- for row_dict in rows:
- s_id = str(row_dict.get("session_id"))
- recording = latest_recordings.get(s_id)
- d_info_raw = row_dict.get("device_info")
- device_id = None
- if d_info_raw:
- try:
- d_info = json.loads(d_info_raw) if isinstance(d_info_raw, str) else d_info_raw
- if isinstance(d_info, dict):
- device_id = d_info.get("device_id") or d_info.get("device_serial")
- except (ValueError, TypeError):
- # Malformed device_info JSON: fall back to the recording's device_id.
- pass
- if not device_id and recording:
- device_id = recording.get("device_id")
- row_dict["device_id"] = device_id
- row_dict["device_serial"] = device_id
-
- if row_dict.get("status") == "running":
- is_active = (
- s_id in active_owner_sids
- or s_id in state.active_connections
- or (state.is_running and s_id == str(state.active_session_id))
- )
- worker_is_alive = session_repo.process_is_alive(row_dict.get("pid"))
- if not is_active and not worker_is_alive:
- row_dict["status"] = "failed"
- row_dict["end_time"] = time.time()
- orphaned_ids.append(s_id)
-
- recording_status = str((recording or {}).get("status") or "unavailable")
- resolved_v_url = (
- media_service.resolve_video_url(row_dict, video_rec_map, video_idx)
- if recording_status != "recording"
- else None
- )
- if resolved_v_url:
- recording_status = "ready"
- row_dict["video_url"] = resolved_v_url
- else:
- row_dict["video_url"] = None
- row_dict["recording_status"] = recording_status
-
- agent_names = agent_names_by_session.get(s_id, [])
- sess_profile = model_service.resolve_session_profile(
- row_dict, None, state.current_profile, agent_names=agent_names
- )
- row_dict["model_info"] = (
- model_service.get_active_model_info(sess_profile)
- if sess_profile
- else default_model_info
- )
- if not sess_profile:
- unresolved_profiles.append(row_dict)
- result.append(row_dict)
-
- # Most sessions carry a profile in device_info or an agent trace name.
- # Only inspect the much larger LLM payload for legacy rows that remain
- # ambiguous after those cheap checks.
- if unresolved_profiles:
- unresolved_ids = [str(row.get("session_id")) for row in unresolved_profiles]
- llm_traces_by_session = session_repo.get_llm_traces_for_profiles_map(unresolved_ids)
- for row_dict in unresolved_profiles:
- s_id = str(row_dict.get("session_id"))
- llm_traces = llm_traces_by_session.get(s_id)
- if not llm_traces:
- continue
- sess_profile = model_service.resolve_session_profile(
- row_dict, llm_traces, state.current_profile
- )
- if sess_profile:
- row_dict["model_info"] = model_service.get_active_model_info(sess_profile)
-
- if orphaned_ids:
- try:
- harvested = session_repo.harvest_orphaned_sessions(orphaned_ids)
- print(f"[list_sessions] Auto-harvested {harvested} orphaned running session(s).")
- try:
- for o_id in orphaned_ids:
- if trace_store.read_status(str(o_id)):
- trace_store.update_trace_status(
- str(o_id),
- "failed",
- error="Process terminated unexpectedly (auto-harvested).",
- )
- except OSError as e:
- # read_status never raises; this guards the lock/write side
- # of update_trace_status.
- print(f"[list_sessions] Could not mark harvested traces failed: {e}")
- except Exception as e:
- print(f"[list_sessions] Failed to update orphaned sessions: {e}")
-
- return result
-
-
-@router.get("/api/sessions/{session_id}")
-async def get_session_details(session_id: str):
- """Retrieve details for a single automation session."""
- row = session_repo.get_session_by_id(session_id)
- if not row:
- raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
- return dict(row)
-
-
-@router.get("/api/sessions/{session_id}/usage")
-async def get_session_usage(session_id: str):
- """Session-wide LLM token totals, live executor context size and run tuning."""
- try:
- return await asyncio.to_thread(session_repo.get_session_usage, session_id)
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.get("/api/sessions/{session_id}/tree")
-async def get_tree(session_id: str):
- try:
- return trace_repo.get_trace_tree(session_id)
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.get("/api/sessions/{session_id}/background_tasks")
-async def get_session_background_tasks(session_id: str):
- try:
- return session_repo.get_background_tasks(session_id)
- except Exception:
- return []
-
-
-@router.get("/api/sessions/{session_id}/startup_progress")
-async def get_session_startup_progress(session_id: str):
- try:
- return state.get_startup_progress(session_id)
- except Exception:
- return []
-
-
-@router.post("/api/cleanup")
-async def cleanup_history_endpoint():
- try:
- from artemis.data_engine.storage import StorageManager
-
- storage = StorageManager(DB_PATH, TRACES_PATH)
- storage.clear_all_data()
- return {
- "status": "success",
- "message": "History cleaned up successfully",
- }
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.post("/api/sessions/{session_id}/delete")
-async def delete_session_endpoint(session_id: str):
- try:
- from artemis.data_engine.storage import StorageManager
-
- storage = StorageManager(DB_PATH, TRACES_PATH)
- storage.delete_session(UUID(session_id))
- return {
- "status": "success",
- "message": f"Session {session_id} deleted successfully",
- }
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
diff --git a/apps/admin_console/routers/steps.py b/apps/admin_console/routers/steps.py
deleted file mode 100644
index d6823966a..000000000
--- a/apps/admin_console/routers/steps.py
+++ /dev/null
@@ -1,109 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import json
-import traceback
-from fastapi import APIRouter, HTTPException
-from fastapi.responses import PlainTextResponse
-
-from artemis.config import TEST_OUTPUTS_DIR
-
-try:
- from admin_console.database.repositories.step_repository import step_repo
- from admin_console.database.repositories.trace_repository import trace_repo
- from admin_console.services.media_service import media_service
-except ImportError:
- from apps.admin_console.database.repositories.step_repository import step_repo
- from apps.admin_console.database.repositories.trace_repository import trace_repo
- from apps.admin_console.services.media_service import media_service
-
-
-router = APIRouter(tags=["steps"])
-
-
-@router.get("/api/sessions/{session_id}/steps")
-async def get_session_steps(session_id: str, client: str | None = None):
- try:
- return step_repo.get_session_steps(session_id, client=client)
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.get("/api/steps/{step_id}/traces")
-async def get_step_traces_endpoint(step_id: str):
- try:
- session_id = step_repo.get_step_session_id(step_id)
- if not session_id:
- raise HTTPException(status_code=404, detail="Step not found")
- return trace_repo.get_step_traces_tree(session_id, step_id)
- except HTTPException as e:
- raise e
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-def _resolve_trace_db_path(session_id: str | None, step_number: int | None):
- if session_id and step_number is not None:
- sandbox_db_path = (
- TEST_OUTPUTS_DIR
- / f"{session_id}_step_{step_number:02d}"
- / "temp_traces"
- / "data_engine.db"
- )
- if sandbox_db_path.exists():
- return sandbox_db_path
- return None
-
-
-@router.get("/api/traces/{trace_id}")
-async def get_trace(trace_id: str, session_id: str = None, step_number: int = None):
- try:
- db_path = _resolve_trace_db_path(session_id, step_number)
- trace_dict = trace_repo.get_trace_by_id(trace_id, db_path=db_path)
- if not trace_dict:
- raise HTTPException(status_code=404, detail="Trace not found")
-
- if trace_dict.get("payload"):
- try:
- payload_obj = json.loads(trace_dict["payload"])
- trace_dict["payload"] = media_service.unwrap_payload(payload_obj)
- except (ValueError, TypeError, KeyError, AttributeError):
- # Non-JSON or unexpectedly shaped payload: serve it raw.
- pass
-
- return trace_dict
- except HTTPException as e:
- raise e
- except Exception as e:
- traceback.print_exc()
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.get("/api/traces/{trace_id}/download")
-async def download_trace(trace_id: str, session_id: str = None, step_number: int = None):
- try:
- db_path = _resolve_trace_db_path(session_id, step_number)
- trace_dict = trace_repo.get_trace_by_id(trace_id, db_path=db_path)
- if not trace_dict or not trace_dict.get("payload"):
- raise HTTPException(status_code=404, detail="Payload not found")
-
- return PlainTextResponse(
- trace_dict["payload"],
- headers={"Content-Disposition": f'attachment; filename="trace_{trace_id}.json"'},
- )
- except HTTPException as e:
- raise e
- except Exception as e:
- traceback.print_exc()
- raise HTTPException(status_code=500, detail=str(e))
diff --git a/apps/admin_console/routers/stream.py b/apps/admin_console/routers/stream.py
deleted file mode 100644
index 5bc3c6889..000000000
--- a/apps/admin_console/routers/stream.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Device Screen Live Streaming Router.
-
-Exposes real-time screen streaming endpoints for the Web UI.
-"""
-
-from fastapi import APIRouter
-from fastapi.responses import JSONResponse, StreamingResponse
-
-try:
- from admin_console.services.device_stream_service import device_stream_service
-except ImportError:
- from apps.admin_console.services.device_stream_service import device_stream_service
-
-router = APIRouter(tags=["stream"])
-
-
-@router.get("/api/stream/device-live")
-async def stream_device_live():
- """Streams live device screen frames as multipart MJPEG."""
- return StreamingResponse(
- device_stream_service.mjpeg_frame_generator(),
- media_type="multipart/x-mixed-replace; boundary=frame",
- headers={
- "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
- "Pragma": "no-cache",
- "Expires": "0",
- "Connection": "keep-alive",
- },
- )
-
-
-@router.get("/api/stream/device-state")
-async def get_device_stream_state():
- """Returns whether an ADB device is connected and live streaming is available."""
- serial = await device_stream_service.get_device_serial()
- return JSONResponse(
- {
- "connected": serial is not None,
- "serial": serial,
- "live_stream_url": "/api/stream/device-live" if serial else None,
- }
- )
diff --git a/apps/admin_console/routers/system.py b/apps/admin_console/routers/system.py
deleted file mode 100644
index c8ce18343..000000000
--- a/apps/admin_console/routers/system.py
+++ /dev/null
@@ -1,626 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""System Readiness & Diagnostics Router for Artemis Admin Console."""
-
-import ipaddress
-import os
-import secrets
-from urllib.parse import urlsplit
-
-from fastapi import APIRouter, HTTPException, Request
-from pydantic import BaseModel, Field
-
-from artemis.core.diagnostics import readiness_engine
-from artemis.core.diagnostics.adb_server_connection import (
- InvalidAdbServerEndpoint,
- adb_server_connection,
-)
-from artemis.core.diagnostics.schema import SystemReadinessReport
-
-router = APIRouter(prefix="/api/system", tags=["system"])
-
-
-def _require_local_admin_request(request: Request) -> None:
- """Keep endpoint probing and mutation on the local administration boundary."""
- client_host = request.client.host if request.client else None
- allow_remote = os.getenv("ARTEMIS_ALLOW_REMOTE_ADB_CONFIGURATION", "").lower() in {
- "1",
- "true",
- "yes",
- }
- if client_host and not allow_remote:
- try:
- is_loopback = ipaddress.ip_address(client_host).is_loopback
- except ValueError:
- is_loopback = client_host.lower() == "localhost"
- if not is_loopback:
- raise HTTPException(
- status_code=403,
- detail=(
- "ADB server settings are local-only. Set "
- "ARTEMIS_ALLOW_REMOTE_ADB_CONFIGURATION=true to manage them from another "
- "computer."
- ),
- )
-
- origin = request.headers.get("origin")
- host = request.headers.get("host")
- if not origin or not host:
- return
- origin_host = urlsplit(origin).netloc.lower()
- if origin_host != host.lower():
- raise HTTPException(
- status_code=403,
- detail="ADB server settings can only be changed from the Artemis console.",
- )
-
-
-def _require_loopback_request(request: Request, detail: str) -> None:
- """Reject requests whose TCP peer is not the local machine."""
- client_host = request.client.host if request.client else None
- try:
- is_loopback = bool(client_host and ipaddress.ip_address(client_host).is_loopback)
- except ValueError:
- is_loopback = bool(client_host and client_host.lower() == "localhost")
- if not is_loopback:
- raise HTTPException(status_code=403, detail=detail)
-
-
-def _require_local_lifecycle_request(request: Request) -> None:
- """Authorize a process-lifecycle request from the local CLI only."""
- _require_loopback_request(request, "Server lifecycle controls are local-only.")
-
- expected = getattr(request.app.state, "lifecycle_token", None)
- supplied = request.headers.get("x-artemis-lifecycle-token")
- if not (
- isinstance(expected, str)
- and isinstance(supplied, str)
- and secrets.compare_digest(expected, supplied)
- ):
- raise HTTPException(status_code=403, detail="Invalid server lifecycle token.")
-
-
-class SelectDeviceRequest(BaseModel):
- """Payload to select an active target Android device."""
-
- serial: str = Field(description="Serial number or identifier of the Android device to select")
-
-
-@router.get("/readiness", response_model=SystemReadinessReport)
-async def get_system_readiness(force: bool = False) -> SystemReadinessReport:
- """Execute all diagnostic probes and return a comprehensive system readiness report."""
- return await readiness_engine.run_all(force_refresh=force)
-
-
-@router.post("/devices/select")
-async def select_active_device(request: SelectDeviceRequest):
- """Select the active Android device or emulator for subsequent automated tasks."""
- serial = request.serial.strip()
- if not serial:
- raise HTTPException(status_code=400, detail="Device serial cannot be empty.")
-
- readiness_engine.set_probe_target_serial(serial)
- # Return updated readiness
- report = await readiness_engine.run_all(force_refresh=True)
- return {
- "status": "success",
- "selected_serial": serial,
- "report": report,
- }
-
-
-@router.post("/adb/restart")
-async def restart_adb_server():
- """Restart local ADB server and return an updated readiness check."""
- restart_result = await readiness_engine.restart_adb_server()
- readiness_engine.invalidate_cache()
- updated_report = await readiness_engine.run_all(force_refresh=True)
- return {
- "restart_result": restart_result,
- "report": updated_report,
- }
-
-
-@router.post("/adb/heal-keys")
-async def heal_adb_keys():
- """Auto-heal corrupted ADB authentication RSA keys and return updated readiness."""
- heal_result = await readiness_engine.heal_adb_keys()
- updated_report = await readiness_engine.run_all()
- return {
- "heal_result": heal_result,
- "report": updated_report,
- }
-
-
-class ConnectAdbRequest(BaseModel):
- """Payload to connect to an Android device over Wi-Fi."""
-
- host: str = Field(description="IP address of Android device")
- port: int = Field(default=5555, description="Port number")
-
-
-@router.post("/adb/connect")
-async def connect_wireless_adb(request: ConnectAdbRequest):
- """Connect to a device over Wi-Fi and return updated readiness."""
- connect_result = await readiness_engine.connect_wireless_adb(request.host, request.port)
- readiness_engine.invalidate_cache()
- updated_report = await readiness_engine.run_all(force_refresh=True)
- return {
- "connect_result": connect_result,
- "report": updated_report,
- }
-
-
-class ConnectAdbServerRequest(BaseModel):
- """Payload to select an ADB server endpoint accessible from this computer."""
-
- host: str = Field(description="Host name or IP address of the ADB server")
- port: int = Field(default=5037, ge=1, le=65535, description="ADB server port")
- persist: bool = Field(default=True, description="Persist the endpoint for future launches")
-
-
-@router.get("/adb/server")
-async def get_adb_server_status():
- """Return the process-wide ADB server endpoint currently used by Artemis."""
- return adb_server_connection.status()
-
-
-@router.post("/adb/server/connect")
-async def connect_adb_server(payload: ConnectAdbServerRequest, request: Request):
- """Validate and activate an ADB server endpoint."""
- _require_local_admin_request(request)
- try:
- connection_result = await adb_server_connection.connect(
- payload.host,
- payload.port,
- persist=payload.persist,
- )
- except InvalidAdbServerEndpoint as exc:
- raise HTTPException(status_code=400, detail=str(exc)) from exc
-
- response: dict[str, object] = {"connection_result": connection_result}
- if connection_result["success"]:
- readiness_engine.set_probe_target_serial(None)
- readiness_engine.invalidate_cache()
- response["report"] = await readiness_engine.run_all(force_refresh=True)
- return response
-
-
-@router.post("/adb/server/probe")
-async def probe_adb_server(payload: ConnectAdbServerRequest, request: Request):
- """Test an ADB server endpoint without changing the active endpoint."""
- _require_local_admin_request(request)
- try:
- connection_result = await adb_server_connection.probe(payload.host, payload.port)
- except InvalidAdbServerEndpoint as exc:
- raise HTTPException(status_code=400, detail=str(exc)) from exc
- return {"connection_result": connection_result}
-
-
-@router.post("/adb/server/local")
-async def use_local_adb_server(request: Request, persist: bool = True):
- """Restore the standard local ADB server without touching a remote daemon."""
- _require_local_admin_request(request)
- connection_result = await adb_server_connection.use_local_server(persist=persist)
- readiness_engine.set_probe_target_serial(None)
- readiness_engine.invalidate_cache()
- updated_report = await readiness_engine.run_all(force_refresh=True)
- return {
- "connection_result": connection_result,
- "report": updated_report,
- }
-
-
-class LaunchEmulatorRequest(BaseModel):
- """Payload to launch a local Android Virtual Device (AVD)."""
-
- avd_name: str = Field(
- description="Name of the installed AVD emulator to launch (e.g. Android_2)"
- )
-
-
-@router.post("/emulator/launch")
-async def launch_emulator(request: LaunchEmulatorRequest):
- """Launch an Android emulator in the background and return initiation status."""
- avd_name = request.avd_name.strip()
- if not avd_name:
- raise HTTPException(status_code=400, detail="AVD name cannot be empty.")
-
- launch_res = await readiness_engine.launch_emulator(avd_name)
- return launch_res
-
-
-@router.get("/emulator/status")
-async def get_emulator_status():
- """Query real-time progress and logs of background emulator launch."""
- return readiness_engine.get_emulator_status()
-
-
-@router.post("/emulator/stop")
-async def stop_emulator():
- """Stop active emulator process."""
- return await readiness_engine.stop_emulator()
-
-
-@router.post("/emulator/dismiss")
-async def dismiss_emulator():
- """Dismiss emulator launch tracking state."""
- return readiness_engine.dismiss_emulator()
-
-
-class UpdateCredentialsRequest(BaseModel):
- """Payload to update and configure LLM or Vision OCR API credentials."""
-
- provider: str = Field(
- default="google",
- description="Provider identifier (e.g. google, gemini, openai, anthropic, openrouter, ocr)",
- )
- api_key: str = Field(description="The secret API key string to configure")
- persist_to_env: bool = Field(
- default=True, description="Whether to persist the key to .env file"
- )
-
-
-class ValidateCredentialsRequest(BaseModel):
- """Payload to test and verify LLM or Vision OCR API credentials without saving."""
-
- provider: str = Field(
- default="google",
- description="Provider identifier (e.g. google, gemini, openai, anthropic, openrouter, ocr)",
- )
- api_key: str = Field(description="The secret API key string to test")
- base_url: str | None = Field(default=None, description="Optional custom base URL")
-
-
-@router.get("/credentials")
-async def get_credentials():
- """Report which providers have an API key configured.
-
- Secret values never leave the process: this endpoint intentionally returns
- presence booleans only. Keys are written via POST /credentials and used
- server-side.
- """
- from artemis.config import settings
-
- providers = ("google", "openai", "anthropic", "openrouter", "ocr")
- status = {name: bool(settings.get_api_key(name)) for name in providers}
- status["gemini"] = status["google"]
- return {
- "providers": [
- {"name": name, "configured": configured} for name, configured in status.items()
- ]
- }
-
-
-@router.post("/credentials/test")
-async def test_credentials(request: ValidateCredentialsRequest):
- """Test and verify whether an API key is valid and usable with the corresponding provider endpoint."""
- from artemis.utils.credentials_validator import validate_api_key
-
- provider = request.provider.strip().lower()
- key = request.api_key.strip()
-
- if not key:
- raise HTTPException(status_code=400, detail="API key cannot be empty.")
-
- is_valid, message = await validate_api_key(
- provider=provider,
- api_key=key,
- base_url=request.base_url,
- )
- if not is_valid:
- raise HTTPException(status_code=400, detail=message)
-
- return {
- "valid": True,
- "provider": provider,
- "message": message,
- }
-
-
-@router.post("/credentials")
-async def update_credentials(request: UpdateCredentialsRequest):
- """Dynamically configure and persist LLM or Vision API key, returning updated readiness report."""
- from artemis.utils.credentials_validator import validate_api_key
-
- provider = request.provider.strip().lower()
- key = request.api_key.strip()
-
- # If a non-empty key is provided, verify it before saving
- if key:
- is_valid, validation_msg = await validate_api_key(provider=provider, api_key=key)
- if not is_valid:
- raise HTTPException(
- status_code=400,
- detail=f"API key verification failed: {validation_msg}",
- )
-
- try:
- from artemis.config import settings
-
- settings.set_api_key(provider, key, persist_to_env=request.persist_to_env)
-
- # Re-run all diagnostic probes to build updated report
- readiness_engine.invalidate_cache()
- updated_report = await readiness_engine.run_all(force_refresh=True)
- action_desc = (
- "successfully verified, updated, and applied" if key else "successfully cleared"
- )
- return {
- "status": "success",
- "message": f"API key for {provider} {action_desc}.",
- "provider": provider,
- "report": updated_report,
- }
- except HTTPException:
- raise
- except Exception as exc:
- raise HTTPException(status_code=500, detail=f"Failed to update credentials: {exc}")
-
-
-@router.get("/model-config-env")
-async def get_model_config_and_env():
- """Retrieve the current active artemis.jsonc configuration and .env status for custom setup."""
- import os
- from artemis.config.paths import get_config_path, get_env_file
- from artemis.config import settings
- from third_party.mobile_use.utils.file import load_jsonc
-
- from artemis.config.settings import is_placeholder_key
-
- # 1. Config file resolution
- config_path = None
- config_content = ""
- parsed_config = {}
- try:
- config_path_obj = get_config_path("artemis.jsonc")
- config_path = str(config_path_obj)
- config_content = config_path_obj.read_text(encoding="utf-8")
- with open(config_path_obj, encoding="utf-8") as f:
- parsed_config = load_jsonc(f)
- except Exception as e:
- config_content = f"// Error reading config: {e}"
-
- # 2. Env file resolution
- env_path = get_env_file()
-
- # 3. Relevant Env Variables status
- def get_real_env_value(key_name: str, provider: str) -> str | None:
- k = settings.get_api_key(provider)
- if k and not is_placeholder_key(k):
- return k.get_secret_value()
- val = os.environ.get(key_name)
- if val and val.strip() and not is_placeholder_key(val):
- return val.strip()
- return None
-
- def mask_key(k: str | None) -> str | None:
- # Expose only enough to recognize which key is active, never a usable
- # fragment of the secret itself.
- if not k:
- return None
- if len(k) <= 8:
- return "****"
- return f"****{k[-4:]}"
-
- gemini_real = get_real_env_value("GEMINI_API_KEY", "google") or get_real_env_value(
- "GOOGLE_API_KEY", "google"
- )
- openai_real = get_real_env_value("OPENAI_API_KEY", "openai")
- anthropic_real = get_real_env_value("ANTHROPIC_API_KEY", "anthropic")
- openrouter_real = get_real_env_value("OPEN_ROUTER_API_KEY", "openrouter")
- xai_real = get_real_env_value("XAI_API_KEY", "xai")
- base_url_val = settings.OPENAI_BASE_URL or os.environ.get("OPENAI_BASE_URL")
- if base_url_val and is_placeholder_key(base_url_val):
- base_url_val = None
- ocr_real = get_real_env_value("OCR_API_KEY", "ocr") or get_real_env_value(
- "VISION_API_KEY", "ocr"
- )
-
- env_vars = [
- {
- "name": "GEMINI_API_KEY",
- "provider": "google",
- "is_set": bool(gemini_real),
- "preview": mask_key(gemini_real),
- "description": "Google Gemini multimodal vision API key (free tier available)",
- },
- {
- "name": "OPENAI_API_KEY",
- "provider": "openai",
- "is_set": bool(openai_real),
- "preview": mask_key(openai_real),
- "description": "OpenAI API key (GPT-4o, GPT-4o-mini)",
- },
- {
- "name": "ANTHROPIC_API_KEY",
- "provider": "anthropic",
- "is_set": bool(anthropic_real),
- "preview": mask_key(anthropic_real),
- "description": "Anthropic Claude API key (Claude 3.5 Sonnet, Claude 3.7)",
- },
- {
- "name": "OPEN_ROUTER_API_KEY",
- "provider": "openrouter",
- "is_set": bool(openrouter_real),
- "preview": mask_key(openrouter_real),
- "description": "OpenRouter unified API gateway key",
- },
- {
- "name": "XAI_API_KEY",
- "provider": "xai",
- "is_set": bool(xai_real),
- "preview": mask_key(xai_real),
- "description": "xAI Grok vision API key",
- },
- {
- "name": "OPENAI_BASE_URL",
- "provider": "custom",
- "is_set": bool(base_url_val),
- "preview": base_url_val,
- "description": "Custom API endpoint (for local Ollama, vLLM, DeepSeek, or proxies)",
- },
- {
- "name": "VISION_API_KEY",
- "provider": "ocr",
- "is_set": bool(ocr_real),
- "preview": mask_key(ocr_real),
- "description": "Google Cloud Vision OCR key for screen text detection (optional)",
- },
- ]
-
- return {
- "config_path": config_path or "config/artemis.jsonc",
- "config_filename": "artemis.jsonc",
- "config_content": config_content,
- "default_model": parsed_config.get("default", {}),
- "presets": parsed_config.get("presets", {}),
- "env_path": str(env_path),
- "env_filename": ".env",
- "env_vars": env_vars,
- }
-
-
-@router.get("/server-status")
-async def get_server_runtime_status():
- """Retrieve runtime status, PID, port, and uptime of the Artemis server.
-
- Answers from in-process state. ``server_lifecycle.get_server_status`` is
- not used here: it runs ``lsof``/``fuser``, which can take over a second and
- is too slow for the ``is_artemis_daemon`` probe.
- """
- import os
- import time
-
- from artemis.runtime.process_probe import pid_is_alive
- from artemis.runtime.server_lifecycle import read_server_info
-
- try:
- from apps.admin_console.core.state import state
- except ImportError:
- from admin_console.core.state import state
-
- port = getattr(state, "port", 8000)
- current_pid = os.getpid()
- pids = {current_pid}
- started_at = None
-
- info = read_server_info()
- if info and info.get("port") == port:
- saved_pid = info.get("pid")
- if isinstance(saved_pid, int) and saved_pid != current_pid and pid_is_alive(saved_pid):
- pids.add(saved_pid)
- if isinstance(info.get("started_at"), (int, float)):
- started_at = float(info["started_at"])
- if started_at is None:
- try:
- import psutil
-
- started_at = psutil.Process(current_pid).create_time()
- except Exception: # pylint: disable=broad-exception-caught
- # psutil is optional; uptime is best-effort.
- started_at = None
-
- uptime_seconds = max(0.0, time.time() - started_at) if started_at is not None else None
- # Explicit DTO: the raw metadata file additionally holds the lifecycle
- # token, cmdline, and filesystem paths, none of which belong on the wire.
- return {
- "running": True,
- "port": port,
- "pids": sorted(pids),
- "active_pid": current_pid,
- "uptime_seconds": uptime_seconds,
- "url": f"http://localhost:{port}",
- "admin_url": f"http://localhost:{port}/admin",
- "current_pid": current_pid,
- }
-
-
-@router.post("/restart")
-async def restart_server_endpoint(request: Request):
- """Request a graceful restart of the Artemis server from thin clients/UI."""
- import asyncio
- import os
- import sys
- import threading
-
- _require_loopback_request(request, "Server lifecycle controls are local-only.")
-
- try:
- from apps.admin_console.core.state import state
- except ImportError:
- from admin_console.core.state import state
-
- port = getattr(state, "port", 8000)
- current_pid = os.getpid()
-
- def _restart_worker():
- import time
-
- time.sleep(0.6)
- if sys.platform != "win32":
- try:
- os.execv(sys.executable, [sys.executable] + sys.argv)
- except Exception:
- import subprocess
-
- subprocess.Popen([sys.executable] + sys.argv)
- os._exit(0)
- else:
- import subprocess
-
- subprocess.Popen([sys.executable] + sys.argv)
- os._exit(0)
-
- threading.Thread(target=_restart_worker, daemon=True).start()
-
- return {
- "status": "restarting",
- "message": "Artemis server is restarting. Client reconnection should occur in 2-3 seconds.",
- "previous_pid": current_pid,
- "port": port,
- }
-
-
-@router.post("/shutdown", status_code=202)
-async def shutdown_server_endpoint(request: Request):
- """Request a graceful shutdown of the Artemis server."""
- import asyncio
-
- try:
- from apps.admin_console.core.state import state
- except ImportError:
- from admin_console.core.state import state
-
- _require_local_lifecycle_request(request)
- server = getattr(request.app.state, "uvicorn_server", None)
- if server is None:
- raise HTTPException(status_code=503, detail="Server lifecycle controller is unavailable.")
-
- async def _shutdown_after_response() -> None:
- # Let Starlette flush the accepted response before Uvicorn leaves its
- # main loop and invokes the FastAPI shutdown lifecycle.
- await asyncio.sleep(0.05)
- state.is_shutting_down = True
- state.shutdown_event.set()
- server.should_exit = True
-
- asyncio.create_task(_shutdown_after_response())
-
- return {
- "status": "shutting_down",
- "message": "Artemis server is shutting down.",
- "pid": os.getpid(),
- }
diff --git a/apps/admin_console/routers/tasks.py b/apps/admin_console/routers/tasks.py
deleted file mode 100644
index 26fca4611..000000000
--- a/apps/admin_console/routers/tasks.py
+++ /dev/null
@@ -1,517 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import asyncio
-from contextlib import suppress
-import json
-from typing import Any
-from fastapi import APIRouter, HTTPException, Request
-from fastapi.responses import StreamingResponse
-from artemis.core.diagnostics import readiness_engine
-from artemis.runtime import DeviceExecutionLock, device_pool
-
-try:
- from admin_console.core.state import state
- from admin_console.database.repositories.session_repository import session_repo
- from admin_console.schemas.task_schema import RunRequest
- from admin_console.services.ipc_service import ipc_service
- from admin_console.services.model_service import model_service
- from admin_console.services.task_preset_catalog import task_recommendation_engine
- from admin_console.services.task_queue_service import task_queue_service
-except ImportError:
- from apps.admin_console.core.state import state
- from apps.admin_console.database.repositories.session_repository import session_repo
- from apps.admin_console.schemas.task_schema import RunRequest
- from apps.admin_console.services.ipc_service import ipc_service
- from apps.admin_console.services.model_service import model_service
- from apps.admin_console.services.task_preset_catalog import task_recommendation_engine
- from apps.admin_console.services.task_queue_service import task_queue_service
-
-
-router = APIRouter(tags=["tasks"])
-
-
-@router.get("/api/tasks/presets")
-async def get_task_presets(
- category: str = "recommended",
- packages: str | None = None,
- limit: int = 24,
-):
- """Retrieve smart task recommendations optionally tailored to detected device packages."""
- pkg_list = [p.strip() for p in packages.split(",") if p.strip()] if packages else None
- return task_recommendation_engine.recommend_tasks(
- installed_packages=pkg_list,
- category=category,
- limit=limit,
- )
-
-
-@router.get("/api/tasks/catalog")
-async def get_task_catalog():
- """Retrieve full catalog of predefined tasks and app package registry."""
- return {
- "tasks": [t.model_dump() for t in task_recommendation_engine.get_all_tasks()],
- "app_registry": task_recommendation_engine.get_app_registry(),
- }
-
-
-@router.post("/api/run")
-async def run_task(request: RunRequest):
- incoming_goals = []
- if request.goals:
- incoming_goals = request.goals
- elif request.goal:
- incoming_goals = [request.goal]
-
- if not incoming_goals:
- raise HTTPException(
- status_code=400,
- detail="Either 'goal' or 'goals' list must be provided.",
- )
-
- # Idempotent SDK retries must never re-run device readiness checks. A task
- # can hold the device while its admission response is lost in transit; in
- # that state, probing the same device again may fail or block even though
- # the original task was accepted successfully.
- if request.session_id and len(incoming_goals) == 1:
- requested_sid = str(request.session_id)
- existing_item = next(
- (
- item
- for item in state.queue_items
- if isinstance(item, dict) and str(item.get("session_id")) == requested_sid
- ),
- None,
- )
- persisted_session = session_repo.get_session_by_id(requested_sid)
- is_active = (
- str(state.active_session_id) == requested_sid
- or requested_sid in state.active_connections
- )
- if existing_item or persisted_session or is_active:
- task_payload = dict(existing_item or persisted_session or {})
- task_payload.setdefault("session_id", requested_sid)
- task_payload.setdefault("goal", incoming_goals[0])
- task_payload.setdefault("profile", request.profile or "flash")
- task_payload.setdefault("device_serial", request.device_serial)
- task_payload.setdefault("status", "running" if is_active else "queued")
- return {
- "status": task_payload["status"],
- "tasks": [task_payload],
- "enqueued_count": 0,
- "total_queued": len(state.queue_tasks),
- }
-
- # Reject an explicit unknown/offline target before running the more
- # expensive readiness probe. Besides producing a stable SDK response,
- # this avoids probing the currently active device for a serial that can
- # never be selected. Only a successful, non-empty enumeration may reject:
- # an indeterminate one (adb blip, startup) lets the submission queue and
- # fail downstream with a clear error instead.
- if request.device_serial:
- try:
- rejection = await device_pool.validate_explicit_serial_async(request.device_serial)
- except Exception:
- rejection = None
- if rejection:
- return {
- "status": "rejected",
- "error": rejection,
- "tasks": [],
- "enqueued_count": 0,
- "total_queued": len(state.queue_tasks),
- }
-
- # Re-check immediately before enqueueing so a device locked between UI
- # polling intervals cannot start through a stale Ready state. Use the
- # bounded submission probe: the full diagnostics path also scans packages,
- # emulator installations, Android version, and screen size.
- # With no explicit serial the probe itself resolves a live target (it
- # prefers the diagnostics target preference, then any unlocked ready
- # device); the verified serial is bound below.
- target_serial = request.device_serial
- device_probe = await readiness_engine.run_device_submission_probe(target_serial=target_serial)
- if device_probe and device_probe.summary in {"Device Locked", "Lock State Unknown"}:
- locked_serial = (
- device_probe.metadata.get("active_device", {}).get("serial") or target_serial or ""
- )
- detail = (
- f"Android device {locked_serial} is locked. Unlock it and enter the home screen before running a task.".replace(
- " ", " "
- ).strip()
- if device_probe.summary == "Device Locked"
- else f"Android device {locked_serial} lock state could not be verified. Keep it unlocked on the home screen and try again.".replace(
- " ", " "
- ).strip()
- )
- raise HTTPException(status_code=409, detail=detail)
-
- if device_probe and device_probe.metadata.get("active_device"):
- verified_serial = device_probe.metadata["active_device"].get("serial")
- # Only auto-selected targets may be re-bound to the probed device. An
- # explicitly requested serial is never silently replaced -- if it is
- # invalid, enqueue_tasks rejects the submission with a clear error.
- if verified_serial and not request.device_serial:
- target_serial = verified_serial
-
- return await task_queue_service.enqueue_tasks(
- incoming_goals,
- profile=request.profile or "flash",
- expected_output=request.expected_output,
- enable_outputter=request.enable_outputter,
- verification_level=request.verification_level,
- explorer_mode=request.explorer_mode,
- locked_app_package=request.locked_app_package,
- app_path=request.app_path,
- device_serial=target_serial,
- ingress=request.ingress or "frontend",
- session_id=request.session_id,
- conversation_id=request.conversation_id,
- )
-
-
-@router.get("/api/run/defaults")
-async def get_run_defaults():
- """Effective Pro-profile tuning defaults from the agent config.
-
- The launcher's sliders start here so they reflect ``artemis.jsonc`` (and the
- ``ARTEMIS_EXPLORER_VERSION`` override) instead of a hard-coded guess.
- """
- from artemis.config import load_agent_config, verification_level_for_checker
-
- agent_cfg = load_agent_config()
- return {
- "verification_level": verification_level_for_checker(agent_cfg.checker),
- "explorer_mode": agent_cfg.explorer.resolve(profile="pro"),
- }
-
-
-@router.get("/api/devices")
-async def list_devices():
- """List all connected Android devices with their busy / idle status."""
- devices = await device_pool.list_devices_async()
- return {"devices": [d.to_dict() for d in devices]}
-
-
-@router.post("/api/stop")
-async def stop_task(
- request: Request,
- all: bool = False,
- session_id: str | None = None,
- device_id: str | None = None,
-):
- target_all = all
- target_sid = session_id
- target_dev = device_id
-
- try:
- body = await request.json()
- if isinstance(body, dict):
- if "all" in body:
- target_all = bool(body["all"]) or target_all
- if body.get("session_id"):
- target_sid = str(body["session_id"])
- if body.get("device_id"):
- target_dev = str(body["device_id"])
- except ValueError:
- # Empty or non-JSON body: fall back to the query parameters.
- pass
-
- # stop_tasks updates scheduler state and asyncio events owned by this loop.
- stopped = task_queue_service.stop_tasks(
- clear_all=target_all,
- session_id=target_sid,
- device_id=target_dev,
- )
- if stopped:
- return {"status": "stopped", "session_id": target_sid}
- return {"status": "no_running_task"}
-
-
-@router.post("/api/resume")
-async def resume_task():
- resumed = task_queue_service.resume_task()
- if resumed:
- return {"status": "resumed"}
- return {"status": "not_paused"}
-
-
-@router.get("/api/status")
-async def get_status():
- # Watchdog check to ensure background worker is alive
- task_queue_service.ensure_worker_running()
-
- latest_session = session_repo.get_latest_session()
- latest_session_id = latest_session.get("session_id") if latest_session else None
- bg_tasks = session_repo.get_background_tasks(latest_session_id) if latest_session_id else []
-
- global_owner = DeviceExecutionLock.get_active_owner()
- is_running = state.is_running or global_owner is not None
- running_task = next(
- (t for t in state.queue_items if isinstance(t, dict) and t.get("status") == "running"), None
- )
- if not running_task and is_running:
- running_task = next(
- (t for t in state.queue_items if isinstance(t, dict) and t.get("status") == "pending"),
- None,
- )
-
- active_owners = DeviceExecutionLock.get_active_owners()
- active_tasks = [
- {
- "device_id": owner.device_id,
- "session_id": owner.session_id,
- "goal": owner.description,
- "pid": owner.pid,
- "ingress": owner.ingress,
- "acquired_at": owner.acquired_at,
- }
- for owner in active_owners.values()
- ]
-
- running_sid = (
- (global_owner.session_id if global_owner else None)
- or state.active_session_id
- or (running_task.get("session_id") if running_task else None)
- or (
- active_tasks[0]["session_id"]
- if active_tasks and active_tasks[0].get("session_id")
- else None
- )
- or (session_repo.get_running_session_id() if is_running else None)
- )
- owner_connection = state.active_connections.get(str(running_sid), {}) if running_sid else {}
- running_goal = (
- owner_connection.get("goal")
- or (global_owner.description if global_owner else None)
- or state.current_goal
- or (running_task.get("goal") if running_task else None)
- or (active_tasks[0]["goal"] if active_tasks else None)
- )
-
- active_profile = (
- owner_connection.get("profile")
- or state.current_profile
- or (running_task.get("profile") if running_task and not global_owner else None)
- )
- if not active_profile and (running_sid or latest_session_id):
- check_sid = running_sid or latest_session_id
- sess_row = session_repo.get_session_by_id(check_sid)
- if sess_row:
- llm_traces = session_repo.get_llm_traces_for_profile(check_sid)
- agent_names = session_repo.get_agent_trace_names(check_sid)
- active_profile = model_service.resolve_session_profile(
- sess_row, llm_traces, agent_names=agent_names
- )
-
- model_info = model_service.get_active_model_info(active_profile)
-
- # Unified Global Queue: merge web tasks and external SDK/CLI device queue tickets
- global_queued = DeviceExecutionLock.get_queued_tasks()
- seen_ids = set()
- queue_data: list[dict[str, Any]] = []
-
- for item in state.queue_tasks:
- sid = item.get("session_id")
- if sid:
- seen_ids.add(str(sid))
- queue_data.append(item)
-
- for g_item in global_queued:
- sid = str(g_item.get("session_id"))
- if sid not in seen_ids:
- seen_ids.add(sid)
- queue_data.append(g_item)
-
- if is_running:
- is_paused = state.is_paused
- return {
- "status": "paused" if is_paused else "running",
- "paused_error": state.paused_error if is_paused else None,
- "goal": running_goal,
- "pid": (
- global_owner.pid
- if global_owner
- else state.current_process.pid
- if state.current_process
- else None
- ),
- "session_id": running_sid,
- "background_tasks": bg_tasks,
- "queue": queue_data,
- "model_info": model_info,
- "active_tasks": active_tasks,
- }
-
- if latest_session_id and str(latest_session_id) in state.active_connections:
- conn_info = state.active_connections[str(latest_session_id)]
- is_paused = state.is_paused
- conn_profile = conn_info.get("profile") or active_profile
- conn_model_info = model_service.get_active_model_info(conn_profile)
- return {
- "status": "paused" if is_paused else "running",
- "paused_error": state.paused_error if is_paused else None,
- "goal": conn_info.get("goal"),
- "pid": conn_info.get("pid"),
- "session_id": latest_session_id,
- "background_tasks": bg_tasks,
- "queue": queue_data,
- "model_info": conn_model_info,
- "active_tasks": active_tasks,
- "ipc_port": state.ipc_port,
- }
-
- return {
- "status": "idle",
- "session_id": latest_session_id,
- "background_tasks": bg_tasks,
- "queue": queue_data,
- "active_tasks": active_tasks,
- "model_info": model_info,
- "ipc_port": state.ipc_port,
- }
-
-
-@router.get("/api/stream")
-@router.get("/api/stream/{session_id}")
-async def stream_events(session_id: str = "active", client: str | None = None):
- async def event_generator():
- queue = asyncio.Queue()
- event_loop = asyncio.get_running_loop()
-
- def callback(event_type, data):
- try:
- # Global queue lifecycle events should always be delivered
- if event_type not in (
- "session_started",
- "session_ended",
- "background_tasks_updated",
- ):
- # Filter events by session_id when subscribed to a specific session
- if session_id and session_id not in ("all", "active"):
- evt_session_id = None
- if isinstance(data, dict):
- evt_session_id = data.get("session_id")
-
- if evt_session_id and str(evt_session_id) != str(session_id):
- return
-
- if (
- not evt_session_id
- and state.active_session_id
- and str(state.active_session_id) != str(session_id)
- ):
- return
-
- sanitized_data = ipc_service.sanitize_event_data(event_type, data)
- event_loop.call_soon_threadsafe(queue.put_nowait, (event_type, sanitized_data))
- except RuntimeError:
- pass
-
- state.add_subscriber(callback)
- yield f'event: info\ndata: {{"message": "Subscribed to session {session_id}"}}\n\n'
-
- if session_id in ("all", "active"):
- active_sid = state.active_session_id
- if not active_sid:
- running_item = next(
- (
- t
- for t in state.queue_items
- if isinstance(t, dict) and t.get("status") == "running"
- ),
- None,
- )
- if running_item:
- active_sid = running_item.get("session_id")
- if not active_sid:
- owner = DeviceExecutionLock.get_active_owner()
- if owner and owner.session_id:
- active_sid = owner.session_id
-
- if active_sid:
- goal = state.current_goal or ""
- profile = state.current_profile or "flash"
- yield (
- "event: session_started\n"
- f"data: {json.dumps({'session_id': str(active_sid), 'initial_goal': goal, 'profile': profile}, default=str)}\n\n"
- )
- for progress_event in state.get_startup_progress(str(active_sid)):
- yield (
- "event: startup_progress\n"
- f"data: {json.dumps(progress_event, default=str)}\n\n"
- )
- try:
- from apps.admin_console.database.repositories.step_repository import step_repo
-
- recorded_steps = step_repo.get_session_steps(str(active_sid))
- for step_dict in recorded_steps:
- yield (
- f"event: step_recorded\ndata: {json.dumps(step_dict, default=str)}\n\n"
- )
- except Exception as exc:
- print(f"[Stream] Could not replay active steps: {exc}")
- else:
- for progress_event in state.get_startup_progress(session_id):
- yield (
- f"event: startup_progress\ndata: {json.dumps(progress_event, default=str)}\n\n"
- )
-
- shutdown_waiter = asyncio.create_task(state.shutdown_event.wait())
- queue_waiter = None
- try:
- while not state.is_shutting_down:
- queue_waiter = asyncio.create_task(queue.get())
- done, _ = await asyncio.wait(
- {queue_waiter, shutdown_waiter},
- timeout=5.0,
- return_when=asyncio.FIRST_COMPLETED,
- )
- if shutdown_waiter in done:
- queue_waiter.cancel()
- with suppress(asyncio.CancelledError):
- await queue_waiter
- queue_waiter = None
- break
- if queue_waiter in done:
- event_type, data = queue_waiter.result()
- queue_waiter = None
- yield f"event: {event_type}\ndata: {json.dumps(data, default=str)}\n\n"
- else:
- queue_waiter.cancel()
- with suppress(asyncio.CancelledError):
- await queue_waiter
- queue_waiter = None
- yield "event: keep-alive\ndata: {}\n\n"
- except asyncio.CancelledError:
- raise
- finally:
- waiters = (queue_waiter, shutdown_waiter)
- for waiter in waiters:
- if waiter is not None and not waiter.done():
- waiter.cancel()
- for waiter in waiters:
- if waiter is not None:
- with suppress(asyncio.CancelledError):
- await waiter
- state.remove_subscriber(callback)
-
- return StreamingResponse(
- event_generator(),
- media_type="text/event-stream",
- headers={
- "Cache-Control": "no-cache, no-transform",
- "X-Accel-Buffering": "no",
- },
- )
diff --git a/apps/admin_console/schemas/__init__.py b/apps/admin_console/schemas/__init__.py
deleted file mode 100644
index 58d482ea3..000000000
--- a/apps/admin_console/schemas/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
diff --git a/apps/admin_console/schemas/task_schema.py b/apps/admin_console/schemas/task_schema.py
deleted file mode 100644
index 144a97203..000000000
--- a/apps/admin_console/schemas/task_schema.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from pydantic import BaseModel
-
-
-class RunRequest(BaseModel):
- goal: str | None = None
- goals: list[str] | None = None
- profile: str | None = "flash"
- expected_output: str | None = None
- enable_outputter: bool | None = None
- # Pro-profile tuning (ignored by the Flash profile): a coarse Checker preset
- # ('off' | 'final' | 'checkpoints' | 'strict') and the Explorer perception
- # version used by the Operator ('flash' | 'pro' | 'ultra').
- verification_level: str | None = None
- explorer_mode: str | None = None
- locked_app_package: str | None = None
- app_path: str | None = None
- device_serial: str | None = None
- ingress: str | None = "frontend"
- session_id: str | None = None
- conversation_id: str | None = None
-
-
-class ReplayRequest(BaseModel):
- device_id: str
- user_submits: dict
- tool_name: str = "ask_explorer"
- replay_id: str | None = None
-
-
-class StopRequest(BaseModel):
- session_id: str | None = None
- device_id: str | None = None
- all: bool = False
diff --git a/apps/admin_console/server.py b/apps/admin_console/server.py
deleted file mode 100644
index 151bb827b..000000000
--- a/apps/admin_console/server.py
+++ /dev/null
@@ -1,508 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Artemis Admin & Trace Console Server
-
-Modular entrypoint for full trace inspection, step replay, and task execution management.
-"""
-
-import argparse
-import asyncio
-from contextlib import asynccontextmanager
-import os
-from pathlib import Path
-import secrets
-import signal
-import sys
-from types import FrameType
-
-# Bootstrap sys.path to allow running from any CWD
-_current_p = Path(__file__).resolve().parent
-while _current_p != _current_p.parent:
- if (_current_p / "pyproject.toml").exists() or (_current_p / "artemis").is_dir():
- _workspace_root = _current_p
- break
- _current_p = _current_p.parent
-else:
- _workspace_root = Path(__file__).resolve().parent.parent.parent
-
-_apps_dir = _workspace_root / "apps"
-_admin_console_dir = _apps_dir / "admin_console"
-_cloud_service_dir = _apps_dir / "cloud_service"
-
-for _p in (str(_workspace_root), str(_apps_dir), str(_admin_console_dir), str(_cloud_service_dir)):
- if _p not in sys.path:
- sys.path.insert(0, _p)
-
-from fastapi import FastAPI
-from fastapi.responses import FileResponse, HTMLResponse
-import uvicorn
-
-from artemis.runtime import (
- DeviceExecutionLock,
- clear_server_info,
- device_pool,
- shutdown_awake_service,
- start_awake_service,
- write_server_info,
-)
-
-from artemis.config import (
- DB_PATH,
- IMAGES_DIR,
- REPLAY_BASE_DIR,
- TEST_DATA_DIR,
- TEST_OUTPUTS_DIR,
- TRACES_PATH,
- WORKSPACE_ROOT,
- init_ls_address,
-)
-from artemis.resources import get_bundled_showcase_dist
-
-try:
- from admin_console.core.security import SameOriginBoundaryMiddleware
- from admin_console.core.state import ServerState, state
- from admin_console.database.connection import db_session, get_db
- from admin_console.database.repositories.session_repository import session_repo
- from admin_console.database.repositories.step_repository import step_repo
- from admin_console.database.repositories.trace_repository import trace_repo
- from admin_console.routers import media, replay, sessions, steps, stream, system, tasks
- from admin_console.routers.replay import replay_manager
- from admin_console.services.ipc_service import ipc_service
- from admin_console.services.media_service import media_service
- from admin_console.services.model_service import model_service
- from admin_console.services.task_queue_service import task_queue_service
-except ImportError:
- from apps.admin_console.core.security import SameOriginBoundaryMiddleware
- from apps.admin_console.core.state import state
- from apps.admin_console.database.repositories.session_repository import session_repo
- from apps.admin_console.routers import media, replay, sessions, steps, stream, system, tasks
- from apps.admin_console.routers.replay import replay_manager
- from apps.admin_console.services.ipc_service import ipc_service
- from apps.admin_console.services.model_service import model_service
- from apps.admin_console.services.task_queue_service import task_queue_service
-
-# Initialize language server synchronization address
-init_ls_address()
-
-
-@asynccontextmanager
-async def _lifespan(_app: "FastAPI"):
- """Start server resources and release them on shutdown."""
- await on_startup()
- try:
- yield
- finally:
- await on_shutdown()
-
-
-# Initialize FastAPI application
-app = FastAPI(title="Artemis Admin & Trace Console", lifespan=_lifespan)
-LIFECYCLE_TOKEN = os.environ.get("ARTEMIS_LIFECYCLE_TOKEN") or secrets.token_urlsafe(32)
-app.state.lifecycle_token = LIFECYCLE_TOKEN
-
-# The console UI is served same-origin from this process, so no CORS grants
-# exist at all; the boundary middleware rejects cross-origin browser traffic
-# and unrecognized Host headers (DNS rebinding) instead.
-app.add_middleware(SameOriginBoundaryMiddleware)
-
-
-async def on_startup():
- """Startup lifecycle hooks."""
- state.is_shutting_down = False
- state.shutdown_event.clear()
- write_server_info(
- port=getattr(state, "port", 8000),
- host=getattr(state, "host", "127.0.0.1"),
- lifecycle_token=LIFECYCLE_TOKEN,
- )
- await asyncio.to_thread(start_awake_service)
- cleaned_device_locks = DeviceExecutionLock.cleanup_stale_locks()
- if cleaned_device_locks:
- print(f"[ServerStartup] Removed {cleaned_device_locks} stale device lock(s).")
-
- # Warm the adb server and complete one enumeration before requests are
- # served, so submissions in the first seconds never race an adb cold start
- # and get a false "no devices attached" rejection.
- try:
- adb_warmed = await device_pool.warm_up_async()
- except Exception as exc:
- adb_warmed = False
- print(f"[ServerStartup] ADB warm-up failed: {exc}")
- if not adb_warmed:
- print(
- "[ServerStartup] ADB warm-up did not confirm a responding adb server; "
- "device enumeration will retry on demand."
- )
- asyncio.create_task(asyncio.to_thread(task_queue_service.archive_older_replays_on_launch))
- asyncio.create_task(
- asyncio.to_thread(task_queue_service.verify_chunks_exist_on_launch, replay_manager)
- )
-
- cleaned = session_repo.cleanup_orphans_on_startup()
- if cleaned > 0:
- print(f"[ServerStartup] Marked {cleaned} orphaned running session(s) as failed.")
- # Workers killed together with a previous daemon never remuxed their
- # recordings; publish whatever raw files they left behind.
- asyncio.create_task(asyncio.to_thread(task_queue_service.recover_orphaned_recordings_on_launch))
-
- await ipc_service.start_server()
- state.worker_task = asyncio.create_task(task_queue_service.queue_worker())
-
-
-async def on_shutdown():
- """Stop task and IPC children before the UI server exits."""
- state.is_shutting_down = True
- state.shutdown_event.set()
- task_queue_service._broadcast_event("server_shutdown", {"status": "stopping"})
- owned_session_ids = {
- str(item["session_id"])
- for item in state.queue_items
- if isinstance(item, dict) and item.get("status") == "running" and item.get("session_id")
- }
-
- worker = state.worker_task
- if worker is not None and not worker.done():
- worker.cancel()
- try:
- await asyncio.wait_for(worker, timeout=5.0)
- except (asyncio.CancelledError, TimeoutError):
- pass
- state.worker_task = None
-
- # Cancel in-flight run coroutines and wait for their finalizers (DB status,
- # session_ended broadcast, trace sync, recording recovery) to run.
- run_tasks = [t for t in list(task_queue_service._run_tasks) if not t.done()]
- for run_task in run_tasks:
- run_task.cancel()
- if run_tasks:
- try:
- await asyncio.wait_for(asyncio.gather(*run_tasks, return_exceptions=True), timeout=5.0)
- except (TimeoutError, asyncio.CancelledError):
- pass
- for run in list(state.active_runs.values()):
- run_proc = run.get("process")
- if run_proc is not None and run_proc.returncode is None:
- await task_queue_service._terminate_worker_process(run_proc)
- state.active_runs.clear()
- if state.current_process is not None and state.current_process.returncode is None:
- await task_queue_service._terminate_worker_process(state.current_process)
- for item in state.queue_items:
- if isinstance(item, dict):
- DeviceExecutionLock.cancel_reservation(item.get("queue_ticket"))
- DeviceExecutionLock.cleanup_stale_locks()
- state.current_process = None
- state.queue_items.clear()
- for session_id in owned_session_ids:
- session_repo.update_session_status(session_id, "cancelled")
-
- await ipc_service.stop_server()
- state.ipc_subscribers.clear()
- await asyncio.to_thread(shutdown_awake_service)
- clear_server_info(
- port=getattr(state, "port", 8000),
- lifecycle_token=LIFECYCLE_TOKEN,
- )
-
-
-# Mount modular routers
-app.include_router(stream.router)
-app.include_router(media.router)
-app.include_router(sessions.router)
-app.include_router(steps.router)
-app.include_router(tasks.router)
-app.include_router(replay.router)
-app.include_router(system.router)
-
-# Mount cloud gateway router for Frappe / Cloud integration if present
-try:
- from cloud_service.gateway import cloud_router
-
- app.include_router(cloud_router)
-except ImportError:
- try:
- from apps.cloud_service.gateway import cloud_router
-
- app.include_router(cloud_router)
- except ImportError:
- pass
-
-
-# ------------------------------------------------------------------------------
-# Showcase UI (Angular 19) Unified Single-Port SPA Static Hosting
-# ------------------------------------------------------------------------------
-def _get_showcase_dist() -> Path:
- """Return the source build, or the immutable build bundled in a wheel."""
- base_dist = _workspace_root / "apps" / "showcase_ui" / "dist"
- candidates = [
- base_dist / "frontend" / "browser",
- base_dist / "browser",
- base_dist / "frontend",
- base_dist,
- ]
- for candidate in candidates:
- if candidate.is_dir() and (candidate / "index.html").exists():
- return candidate
- bundled_dist = get_bundled_showcase_dist()
- if bundled_dist is not None:
- return bundled_dist
- return base_dist / "frontend" / "browser"
-
-
-def _resolve_static_file(root: Path, relative_path: str) -> Path | None:
- """Return the requested file only when it truly lives under ``root``."""
- try:
- root = root.resolve()
- target = (root / relative_path).resolve()
- except (OSError, ValueError):
- return None
- if target.is_file() and target.is_relative_to(root):
- return target
- return None
-
-
-@app.get("/", include_in_schema=False)
-async def serve_showcase_root():
- """Explicitly serve the Showcase UI at the root path by default."""
- return await serve_showcase_spa("")
-
-
-@app.get("/{full_path:path}", include_in_schema=False)
-async def serve_showcase_spa(full_path: str):
- # Do not intercept API, media, or replay paths
- if (
- full_path.startswith("api/")
- or full_path.startswith("images/")
- or full_path.startswith("videos/")
- or full_path.startswith("local_file")
- or full_path == "docs"
- or full_path == "openapi.json"
- or full_path.startswith("redoc")
- ):
- from fastapi import HTTPException
-
- raise HTTPException(status_code=404, detail="Endpoint not found")
-
- clean_path = full_path.strip("/")
-
- # Admin / Debug Console routes
- if (
- clean_path in ("admin", "debug")
- or clean_path.startswith("admin/")
- or clean_path.startswith("debug/")
- ):
- admin_index = _admin_console_dir / "index.html"
- if admin_index.exists():
- return HTMLResponse(admin_index.read_text(encoding="utf-8"))
-
- showcase_dist = _get_showcase_dist()
-
- # Exact static file match (js, css, images, fonts, favicon, logo).
- # Resolve and re-anchor under the static root so encoded traversal
- # sequences cannot address files outside the build directories.
- if clean_path:
- target_file = _resolve_static_file(showcase_dist, clean_path)
- if target_file is not None:
- return FileResponse(target_file)
-
- # Fallback for icons/logos if showcase UI hasn't been built yet
- public_root = _workspace_root / "apps" / "showcase_ui" / "public"
- public_file = _resolve_static_file(public_root, clean_path)
- if public_file is not None:
- return FileResponse(public_file)
-
- # Serve Showcase UI Angular SPA index.html
- index_file = showcase_dist / "index.html"
- if index_file.exists():
- return HTMLResponse(index_file.read_text(encoding="utf-8"))
-
- # Fallback message if Showcase UI is not built
- fallback_html = """
-
-
-
-
- Artemis Showcase UI - Not Built
-
-
-
-
-
✨ Artemis Showcase UI
-
The Showcase UI (Angular frontend) has not been built yet.
-
To compile the Showcase UI, run:
-
cd apps/showcase_ui && npm install && npm run build
-
Or launch using ./start.sh or artemis ui to build automatically.
-
Go to Admin Debug Console →
-
-
-"""
- return HTMLResponse(fallback_html)
-
-
-# ------------------------------------------------------------------------------
-# Backward compatibility exports
-# ------------------------------------------------------------------------------
-get_active_model_info = model_service.get_active_model_info
-start_ipc_server = ipc_service.start_server
-queue_worker = task_queue_service.queue_worker
-active_connections = state.active_connections
-ipc_subscribers = state.ipc_subscribers
-task_queue = state.task_queue
-queue_goals = state.queue_goals
-
-
-class ArtemisUvicornServer(uvicorn.Server):
- """Notify application streams before Uvicorn waits for them to close."""
-
- @staticmethod
- def _task_is_active() -> bool:
- """Check task state without doing process I/O from a signal handler."""
- proc = state.current_process
- if proc is not None and proc.returncode is None:
- return True
- return any(
- isinstance(item, dict) and item.get("status") == "running" for item in state.queue_items
- )
-
- @staticmethod
- def _schedule_signal_report(message: str) -> None:
- """Defer console output so a signal cannot re-enter Colorama writes."""
- try:
- asyncio.get_running_loop().call_soon(print, message)
- except RuntimeError:
- try:
- os.write(2, f"{message}\n".encode(errors="replace"))
- except OSError:
- pass
-
- def handle_exit(self, sig: int, frame: FrameType | None) -> None:
- try:
- signal_name = signal.Signals(sig).name
- except ValueError:
- signal_name = "UNKNOWN"
- frame_location = "unknown"
- if frame is not None:
- frame_location = f"{frame.f_code.co_filename}:{frame.f_lineno}"
- signal_report = (
- f"[ServerSignal] Received {signal_name} ({sig}); "
- f"server PID={os.getpid()}, parent PID={os.getppid()}, frame={frame_location}"
- )
- self._schedule_signal_report(signal_report)
-
- if sys.platform == "win32" and sig == signal.SIGINT and self._task_is_active():
- self._schedule_signal_report(
- "[ServerSignal] Ignored Windows SIGINT while a mobile task is active. "
- "Stop the task first, or press Ctrl+Break to force server shutdown."
- )
- return
-
- state.is_shutting_down = True
- state.shutdown_event.set()
- super().handle_exit(sig, frame)
-
-
-def run_ui_server(host: str, port: int, reload: bool = False) -> None:
- """Run the UI server with bounded, signal-aware graceful shutdown."""
- state.host = host
- state.port = port
- write_server_info(port=port, host=host, lifecycle_token=LIFECYCLE_TOKEN)
-
- try:
- if reload:
- uvicorn.run(
- "apps.admin_console.server:app",
- host=host,
- port=port,
- reload=True,
- timeout_graceful_shutdown=5,
- )
- return
-
- config = uvicorn.Config(
- app,
- host=host,
- port=port,
- timeout_graceful_shutdown=5,
- )
- server = ArtemisUvicornServer(config)
- app.state.uvicorn_server = server
- server.run()
- finally:
- app.state.uvicorn_server = None
- clear_server_info(port=port, lifecycle_token=LIFECYCLE_TOKEN)
-
-
-def main(argv: list[str] | None = None) -> None:
- """Parse the daemon/standalone server bind address and run Uvicorn."""
- parser = argparse.ArgumentParser(description="Run the Artemis Admin Console server")
- parser.add_argument(
- "--host",
- default=os.environ.get("ARTEMIS_SERVER_HOST", "127.0.0.1"),
- help=(
- "Address to bind the HTTP server to. Defaults to loopback; expose "
- "remotely via a Tailscale/SSH tunnel rather than a wide bind."
- ),
- )
- parser.add_argument(
- "--port",
- type=int,
- default=int(os.environ.get("ANTIGRAVITY_SIDECAR_WEB_PORT", "8000")),
- help="TCP port for the HTTP server.",
- )
- args = parser.parse_args(argv)
- run_ui_server(host=args.host, port=args.port)
-
-
-if __name__ == "__main__":
- main()
diff --git a/apps/admin_console/services/__init__.py b/apps/admin_console/services/__init__.py
deleted file mode 100644
index 58d482ea3..000000000
--- a/apps/admin_console/services/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
diff --git a/apps/admin_console/services/device_stream_service.py b/apps/admin_console/services/device_stream_service.py
deleted file mode 100644
index 6455a3e77..000000000
--- a/apps/admin_console/services/device_stream_service.py
+++ /dev/null
@@ -1,137 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Device Live Screen Streaming Service.
-
-Provides real-time, low-latency device screen frames over HTTP MJPEG and WebSocket.
-Operates concurrently with ADB agent actions with zero interference.
-"""
-
-import asyncio
-import logging
-import subprocess
-import time
-from collections.abc import AsyncGenerator
-
-from artemis.toolchain import find_adb
-
-logger = logging.getLogger("artemis.stream_service")
-
-
-class DeviceStreamService:
- """Manages real-time screen capture and distribution to web clients."""
-
- def __init__(self):
- self._active_listeners = 0
- self._lock = asyncio.Lock()
- self._latest_frame: bytes | None = None
- self._last_frame_time: float = 0.0
- self._is_capturing = False
- self._capture_task: asyncio.Task | None = None
-
- async def get_device_serial(self) -> str | None:
- """Find the currently connected active ADB device serial."""
- try:
- adb_bin = find_adb()
- proc = await asyncio.create_subprocess_exec(
- adb_bin,
- "devices",
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- )
- stdout, _ = await proc.communicate()
- lines = stdout.decode().strip().splitlines()
- for line in lines[1:]:
- parts = line.strip().split()
- if len(parts) >= 2 and parts[1] == "device":
- return parts[0]
- except Exception as e:
- logger.warning(f"Error checking adb devices: {e}")
- return None
-
- async def _capture_loop(self):
- """Background frame capture loop that runs while listeners > 0."""
- logger.info("[StreamService] Starting live screen capture loop...")
- while self._active_listeners > 0:
- try:
- start_t = time.time()
- serial = await self.get_device_serial()
- adb_bin = find_adb()
- cmd = (
- [adb_bin, "-s", serial, "exec-out", "screencap", "-p"]
- if serial
- else [adb_bin, "exec-out", "screencap", "-p"]
- )
- proc = await asyncio.create_subprocess_exec(
- *cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- )
- stdout, _ = await proc.communicate()
- if proc.returncode == 0 and len(stdout) > 1000:
- self._latest_frame = stdout
- self._last_frame_time = time.time()
-
- elapsed = time.time() - start_t
- delay = max(0.03, 0.08 - elapsed)
- await asyncio.sleep(delay)
- except asyncio.CancelledError:
- break
- except Exception as e:
- logger.error(f"[StreamService] Capture error: {e}")
- await asyncio.sleep(0.5)
-
- logger.info("[StreamService] Stopping live screen capture loop (0 listeners).")
- self._is_capturing = False
-
- async def start_capturing(self):
- """Register a new listener and start background capture if needed."""
- async with self._lock:
- self._active_listeners += 1
- if not self._is_capturing or self._capture_task is None or self._capture_task.done():
- self._is_capturing = True
- self._capture_task = asyncio.create_task(self._capture_loop())
-
- async def stop_capturing(self):
- """Deregister a listener and stop capture loop when count reaches 0."""
- async with self._lock:
- self._active_listeners = max(0, self._active_listeners - 1)
- if self._active_listeners == 0 and self._capture_task and not self._capture_task.done():
- self._capture_task.cancel()
- self._is_capturing = False
-
- async def mjpeg_frame_generator(self) -> AsyncGenerator[bytes, None]:
- """Async generator streaming MJPEG multipart bytes to HTTP response."""
- await self.start_capturing()
- try:
- last_sent_time = 0.0
- while True:
- if self._latest_frame and self._last_frame_time > last_sent_time:
- last_sent_time = self._last_frame_time
- frame_bytes = self._latest_frame
- yield (
- b"--frame\r\n"
- b"Content-Type: image/png\r\n"
- b"Content-Length: "
- + str(len(frame_bytes)).encode()
- + b"\r\n\r\n"
- + frame_bytes
- + b"\r\n"
- )
- await asyncio.sleep(0.04)
- finally:
- await self.stop_capturing()
-
-
-device_stream_service = DeviceStreamService()
diff --git a/apps/admin_console/services/ipc_service.py b/apps/admin_console/services/ipc_service.py
deleted file mode 100644
index 712b1d805..000000000
--- a/apps/admin_console/services/ipc_service.py
+++ /dev/null
@@ -1,262 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import asyncio
-from contextlib import suppress
-import json
-import logging
-from typing import Any
-
-logger = logging.getLogger(__name__)
-
-try:
- from admin_console.core.state import state
-except ImportError:
- from apps.admin_console.core.state import state
-
-
-class IPCService:
- """Service managing internal IPC socket server and event distribution."""
-
- @staticmethod
- def _clean_value(val: Any) -> Any:
- """Recursively cleans values to remove non-serializable and internal
- Python runtime representations.
- """
- if val is None:
- return None
- if isinstance(val, (bool, int, float)):
- return val
- if isinstance(val, str):
- if (
- "object at 0x" in val
- or val.startswith(" Any:
- """Sanitizes stream event payloads for all clients, stripping bulky
- raw bytes and internal memory references.
- """
- if not isinstance(data, dict):
- return data
-
- if event_type == "trace_recorded":
- tr_type = data.get("type")
- payload = data.get("payload")
- if isinstance(payload, str):
- try:
- payload = json.loads(payload)
- except ValueError:
- # Non-JSON payload string: cleaned as-is below.
- pass
-
- if tr_type in ("tool", "action") and isinstance(payload, dict):
- args = payload.get("args", payload)
- data["payload"] = {"args": cls._clean_value(args)}
- elif tr_type == "llm_call" and isinstance(payload, dict):
- data["payload"] = {"error": cls._clean_value(payload.get("error"))}
- elif payload is not None:
- data["payload"] = cls._clean_value(payload)
-
- elif event_type in ("step_recorded", "step_updated"):
- # Strip heavy raw image bytes from SSE broadcast (clients fetch via image URLs)
- data["pre_screenshot_bytes"] = None
- data["post_screenshot_bytes"] = None
-
- if "generic_tools" in data and isinstance(data["generic_tools"], list):
- sanitized_tools = []
- for tr in data["generic_tools"]:
- if not isinstance(tr, dict):
- continue
- tr_dict = dict(tr)
- pl = tr_dict.get("payload")
- if isinstance(pl, str):
- try:
- pl = json.loads(pl)
- except ValueError:
- # Non-JSON payload string: cleaned as-is below.
- pass
-
- if tr_dict.get("type") == "llm_call":
- tr_dict["payload"] = (
- {"error": cls._clean_value(pl.get("error"))}
- if isinstance(pl, dict)
- else None
- )
- elif isinstance(pl, dict):
- args = pl.get("args", pl)
- tr_dict["payload"] = {"args": cls._clean_value(args)}
- else:
- tr_dict["payload"] = cls._clean_value(pl)
- sanitized_tools.append(tr_dict)
- data["generic_tools"] = sanitized_tools
-
- try:
- from artemis.utils.coordinates import normalize_step_actions
-
- data = normalize_step_actions(data)
- except ImportError:
- pass
-
- return data
-
- # Backward compatibility alias
- filter_event_for_angular = sanitize_event_data
-
- @classmethod
- async def start_server(cls):
- async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
- current_session_id = None
- try:
- while not state.is_shutting_down:
- line = await reader.readline()
- if not line:
- break
- try:
- payload = json.loads(line.decode("utf-8"))
- event_type = payload.get("event_type")
- data = payload.get("data")
-
- if event_type == "session_started" and data:
- incoming_sid = data.get("session_id")
- if incoming_sid and str(incoming_sid) in getattr(
- state, "cancelled_session_ids", set()
- ):
- print(
- f"[IPC] Ignoring session_started for cancelled session: {incoming_sid}"
- )
- continue
-
- state.active_session_id = incoming_sid
- current_session_id = incoming_sid
- pid = data.get("pid")
- goal = data.get("initial_goal")
- device_info = data.get("device_info")
- if isinstance(device_info, dict) and device_info.get("profile"):
- state.current_profile = device_info.get("profile")
- if goal:
- state.current_goal = goal
- print(
- f"[IPC] Session started: {state.active_session_id} (profile: {state.current_profile})"
- )
- if current_session_id:
- state.active_connections[str(current_session_id)] = {
- "writer": writer,
- "pid": pid,
- "goal": goal,
- "profile": state.current_profile,
- }
- elif event_type == "session_ended" and data:
- ended_sid = data.get("session_id")
- if state.active_session_id and str(state.active_session_id) == str(
- ended_sid
- ):
- state.active_session_id = None
- state.current_goal = None
- state.current_profile = None
-
- if event_type == "startup_progress" and isinstance(data, dict):
- state.record_startup_progress(data)
-
- for cb in list(state.ipc_subscribers):
- try:
- cb(event_type, data)
- except Exception:
- # One broken subscriber must not block the
- # others, but a silent drop hides it entirely.
- logger.warning(
- "IPC subscriber %r failed for event %s",
- cb,
- event_type,
- exc_info=True,
- )
- except (ValueError, UnicodeDecodeError, TypeError) as e:
- print(f"IPC parse error: {e}")
- except asyncio.CancelledError:
- raise
- except (ConnectionResetError, BrokenPipeError, OSError):
- # Normal when a task or the UI server is shutting down on Windows.
- pass
- finally:
- if current_session_id and str(current_session_id) in state.active_connections:
- state.active_connections.pop(str(current_session_id), None)
- writer.close()
- with suppress(ConnectionResetError, BrokenPipeError, OSError):
- await writer.wait_closed()
-
- server = await asyncio.start_server(handle_client, "127.0.0.1", 0, limit=1024 * 1024 * 100)
- state.ipc_port = server.sockets[0].getsockname()[1]
- state.ipc_server = server
- state.ipc_serve_task = asyncio.create_task(server.serve_forever())
- print(f"Internal IPC server started on port {state.ipc_port}")
- try:
- from artemis.config import write_ipc_port
-
- write_ipc_port(state.ipc_port)
- except Exception as e:
- print(f"Failed to write IPC port file: {e}")
-
- @classmethod
- async def stop_server(cls):
- """Close IPC listeners and task connections during application shutdown."""
- for connection in list(state.active_connections.values()):
- writer = connection.get("writer")
- if writer is not None:
- writer.close()
- with suppress(ConnectionResetError, BrokenPipeError, OSError):
- await writer.wait_closed()
- state.active_connections.clear()
-
- if state.ipc_server is not None:
- state.ipc_server.close()
- await state.ipc_server.wait_closed()
- state.ipc_server = None
-
- if state.ipc_serve_task is not None:
- state.ipc_serve_task.cancel()
- with suppress(asyncio.CancelledError):
- await state.ipc_serve_task
- state.ipc_serve_task = None
- state.ipc_port = None
- try:
- from artemis.config import clear_ipc_port
-
- clear_ipc_port()
- except Exception as e:
- print(f"Failed to clear IPC port state: {e}")
-
-
-ipc_service = IPCService()
diff --git a/apps/admin_console/services/media_service.py b/apps/admin_console/services/media_service.py
deleted file mode 100644
index e114d043e..000000000
--- a/apps/admin_console/services/media_service.py
+++ /dev/null
@@ -1,623 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import base64
-from datetime import datetime
-import hashlib
-import json
-import logging
-import os
-from pathlib import Path
-import subprocess
-import threading
-import time
-from typing import Any
-import urllib.parse
-from fastapi import HTTPException
-
-from artemis.config import IMAGES_DIR, TRACES_PATH, WORKSPACE_ROOT
-
-logger = logging.getLogger(__name__)
-
-
-class MediaService:
- """Service handling media indexing, local file security, payload unwrapping and notes/plans."""
-
- # Conversion results keyed by source path -> (mtime, size, resolved path).
- # Failed conversions are cached too (resolved path == source path) so a broken
- # file doesn't re-run ffmpeg (up to 45s) on every /api/sessions poll.
- _playable_cache: dict[str, tuple[float, int, str]] = {}
- _playable_cache_lock = threading.Lock()
-
- @classmethod
- def ensure_browser_playable_video(cls, p: Path) -> Path:
- """Ensure the video file is in a browser-supported container (MP4).
-
- If given an MKV, check if a converted MP4 already exists or convert it.
- """
- try:
- if not p.exists():
- return p
- suffix = p.suffix.lower()
- if suffix == ".mp4":
- return p
- if suffix in (".mkv", ".webm"):
- if cls.is_live_recording(p):
- # scrcpy is still appending to this file: converting now
- # would freeze a few-seconds partial as recording.mp4.
- return p
- src_stat = p.stat()
- cache_key = str(p)
- cached = cls._playable_cache.get(cache_key)
- if cached and cached[0] == src_stat.st_mtime and cached[1] == src_stat.st_size:
- return Path(cached[2])
- mp4_cand = p.with_suffix(".mp4")
- if (
- mp4_cand.exists()
- and mp4_cand.stat().st_size > 0
- and mp4_cand.stat().st_mtime >= src_stat.st_mtime
- ):
- # An MP4 older than its source is a stale partial left by a
- # mid-run scan; fall through and convert again.
- cls._playable_cache[cache_key] = (
- src_stat.st_mtime,
- src_stat.st_size,
- str(mp4_cand),
- )
- return mp4_cand
- with cls._playable_cache_lock:
- # Another thread may have converted this file while we waited.
- cached = cls._playable_cache.get(cache_key)
- if cached and cached[0] == src_stat.st_mtime and cached[1] == src_stat.st_size:
- return Path(cached[2])
- try:
- result = cls._convert_to_mp4(p, mp4_cand)
- except Exception:
- result = p
- cls._playable_cache[cache_key] = (
- src_stat.st_mtime,
- src_stat.st_size,
- str(result),
- )
- return result
- except OSError:
- # Filesystem probe failed (missing/locked file): serve the
- # original path and let the media endpoint report the error.
- pass
- return p
-
- @staticmethod
- def _convert_to_mp4(p: Path, mp4_cand: Path) -> Path:
- from artemis.utils.video import get_ffmpeg_path
-
- ffmpeg = get_ffmpeg_path()
- # 1. Attempt fast copy remux
- subprocess.run(
- [
- ffmpeg,
- "-y",
- "-fflags",
- "+genpts",
- "-i",
- str(p),
- "-c",
- "copy",
- "-movflags",
- "+faststart",
- str(mp4_cand),
- ],
- capture_output=True,
- timeout=15,
- )
- if mp4_cand.exists() and mp4_cand.stat().st_size > 0:
- return mp4_cand
- # 2. If copy remux failed, fallback to ultrafast re-encode
- subprocess.run(
- [
- ffmpeg,
- "-y",
- "-fflags",
- "+genpts+discardcorrupt",
- "-i",
- str(p),
- "-c:v",
- "libx264",
- "-preset",
- "ultrafast",
- "-pix_fmt",
- "yuv420p",
- "-movflags",
- "+faststart",
- str(mp4_cand),
- ],
- capture_output=True,
- timeout=30,
- )
- if mp4_cand.exists() and mp4_cand.stat().st_size > 0:
- return mp4_cand
- return p
-
- LIVE_RECORDING_GRACE_SECONDS = 10.0
- TRACE_STATUS_SUFFIXES = ("_PASS_", "_FAIL_", "_TESTFAIL_")
-
- @classmethod
- def is_live_recording(cls, p: Path) -> bool:
- """Whether ``p`` is a raw .mkv that scrcpy is still appending to.
-
- scrcpy touches the file continuously while recording; a raw file that
- has not changed for a few seconds belongs to a finished (or killed)
- recording and is safe to convert.
- """
- if p.suffix.lower() != ".mkv":
- return False
- try:
- return (time.time() - p.stat().st_mtime) < cls.LIVE_RECORDING_GRACE_SECONDS
- except OSError:
- return False
-
- @staticmethod
- def is_browser_playable(p: Path) -> bool:
- return p.suffix.lower() in (".mp4", ".webm")
-
- @classmethod
- def strip_trace_status_suffix(cls, folder_name: str) -> str:
- for marker in cls.TRACE_STATUS_SUFFIXES:
- folder_name = folder_name.split(marker)[0]
- return folder_name
-
- @classmethod
- def _finalize_trace_folder_name(cls, folder: Path, session_start_time: float | None) -> Path:
- """Give an unfinished temp trace folder the terminal name the worker uses."""
- try:
- if folder.resolve().parent != Path(TRACES_PATH).resolve():
- return folder
- except OSError:
- return folder
- if any(marker in folder.name for marker in cls.TRACE_STATUS_SUFFIXES):
- return folder
- try:
- stamp = datetime.fromtimestamp(float(session_start_time or time.time())).strftime(
- "%Y-%m-%dT%H-%M-%S"
- )
- except (TypeError, ValueError, OSError, OverflowError):
- stamp = datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
- target = folder.with_name(f"{folder.name}_FAIL_{stamp}")
- if target.exists():
- return folder
- try:
- folder.rename(target)
- except OSError as exc:
- logger.warning("Could not rename orphaned trace folder %s: %s", folder, exc)
- return folder
- return target
-
- @classmethod
- def recover_orphaned_recording(
- cls,
- local_video_path: str | Path | None,
- session_start_time: float | None = None,
- ) -> Path | None:
- """Finalize the recording of a worker that died before its own stop path ran.
-
- A hard-killed worker leaves the raw scrcpy ``recording.mkv`` behind
- (playable, but never remuxed) plus, possibly, a few-seconds partial
- ``recording.mp4`` that a session-list scan converted mid-run. The raw
- file is remuxed over that partial and removed on success, and the temp
- trace folder is renamed the way the worker would have done it.
-
- Returns the final browser-playable path, or None when nothing usable
- exists (the caller then records a terminal recording failure).
- """
- if not local_video_path:
- return None
- source = Path(str(local_video_path))
- folder = source.parent
- if not folder.is_dir():
- return None
- raw = source if source.suffix.lower() == ".mkv" else folder / "recording.mkv"
- mp4 = folder / "recording.mp4"
-
- def _non_empty(path: Path) -> bool:
- try:
- return path.is_file() and path.stat().st_size > 0
- except OSError:
- return False
-
- if _non_empty(raw):
- if cls.is_live_recording(raw):
- return None
- with cls._playable_cache_lock:
- converted = cls._convert_to_mp4(raw, mp4)
- cls._playable_cache.pop(str(raw), None)
- if converted != mp4 or not _non_empty(mp4):
- return None
- try:
- raw.unlink()
- except OSError:
- pass
- final = mp4
- elif _non_empty(mp4):
- final = mp4
- elif _non_empty(source) and cls.is_browser_playable(source):
- final = source
- else:
- return None
-
- folder = cls._finalize_trace_folder_name(folder, session_start_time)
- return folder / final.name
-
- @staticmethod
- def path_to_video_url(p: Path) -> str:
- resolved = p.resolve()
- try:
- rel = resolved.relative_to(WORKSPACE_ROOT)
- return f"/videos/{urllib.parse.quote(rel.as_posix(), safe='/')}"
- except ValueError:
- return f"/videos/{urllib.parse.quote(resolved.as_posix().lstrip('/'), safe='/:')}"
-
- @classmethod
- def build_video_index(cls) -> dict[str, str]:
- idx = {}
- dirs = [
- TRACES_PATH,
- WORKSPACE_ROOT / "artemis-traces",
- WORKSPACE_ROOT / ".benchmarks" / "diagnoser" / "outputs" / "artifacts",
- ]
- for d in dirs:
- if not d.exists():
- continue
- try:
- entries = list(d.iterdir())
- except OSError as exc:
- logger.warning("Video index scan failed under %s: %s", d, exc)
- continue
- for item in entries:
- # One unreadable entry must not abort the rest of the directory.
- try:
- if item.is_dir():
- for ext in [".mp4", ".mkv", ".webm"]:
- vfile = item / f"recording{ext}"
- if vfile.exists():
- vfile = cls.ensure_browser_playable_video(vfile)
- if not cls.is_browser_playable(vfile):
- # Live (still recording) or unconvertible raw file.
- break
- url = cls.path_to_video_url(vfile)
- idx[item.name] = url
- prefix = cls.strip_trace_status_suffix(item.name)
- idx[prefix] = url
- break
- elif item.suffix in [".mp4", ".mkv", ".webm"]:
- item = cls.ensure_browser_playable_video(item)
- if not cls.is_browser_playable(item):
- continue
- url = cls.path_to_video_url(item)
- idx[item.stem] = url
- idx[item.name] = url
- except OSError as exc:
- logger.warning("Skipping unindexable video entry %s: %s", item, exc)
- return idx
-
- @classmethod
- def resolve_video_url(
- cls,
- row_dict: dict[str, Any],
- video_rec_map: dict[str, str],
- video_idx: dict[str, str],
- ) -> str | None:
- raw_sid = row_dict.get("session_id")
- s_id = (
- str(raw_sid).strip()
- if raw_sid is not None and str(raw_sid).strip().lower() != "none"
- else ""
- )
- v_url = None
- v_fp = row_dict.get("video_filepath")
- if v_fp and os.path.exists(v_fp):
- p = cls.ensure_browser_playable_video(Path(v_fp))
- if cls.is_browser_playable(p):
- v_url = cls.path_to_video_url(p)
-
- orig_rec = video_rec_map.get(s_id)
- if not v_url and orig_rec:
- p = Path(orig_rec)
- if p.exists():
- p = cls.ensure_browser_playable_video(p)
- if cls.is_browser_playable(p):
- v_url = cls.path_to_video_url(p)
- if not v_url:
- folder_name = p.parent.name
- v_url = video_idx.get(folder_name) or video_idx.get(folder_name.split("_")[0])
-
- if not v_url:
- v_url = video_idx.get(s_id)
-
- if not v_url and s_id:
- sess_dir = TRACES_PATH / s_id
- if sess_dir.is_dir():
- for ext in [".mp4", ".webm", ".mkv"]:
- vfile = sess_dir / f"recording{ext}"
- if vfile.exists():
- vfile = cls.ensure_browser_playable_video(vfile)
- if cls.is_browser_playable(vfile):
- v_url = cls.path_to_video_url(vfile)
- break
- if not v_url:
- try:
- for item in sess_dir.iterdir():
- if item.is_file() and item.suffix.lower() in [".mp4", ".webm", ".mkv"]:
- item = cls.ensure_browser_playable_video(item)
- if not cls.is_browser_playable(item):
- continue
- v_url = cls.path_to_video_url(item)
- break
- except OSError:
- # Unreadable session dir: no recording to resolve.
- pass
-
- if not v_url and row_dict.get("initial_goal"):
- goal_str = row_dict["initial_goal"]
- task_hint = (
- goal_str.split("Task: ")[1].split()[0].strip()
- if "Task: " in goal_str
- else goal_str.split("\n")[0].strip()
- )
- v_url = video_idx.get(task_hint)
-
- return v_url
-
- @classmethod
- def resolve_video_segments(cls, video_url: str | None) -> list[dict[str, Any]]:
- """Resolve a recording sidecar manifest into browser-safe segment URLs."""
- if not video_url or not video_url.startswith("/videos/"):
- return []
- relative = urllib.parse.unquote(video_url.removeprefix("/videos/"))
- video_path = (WORKSPACE_ROOT / relative).resolve()
- manifest_path = video_path.parent / "recording.json"
- if not manifest_path.is_file():
- return []
- try:
- payload = json.loads(manifest_path.read_text(encoding="utf-8"))
- segments = []
- for item in payload.get("segments", []):
- segment_path = (manifest_path.parent / str(item["file"])).resolve()
- if (
- segment_path.parent != manifest_path.parent.resolve()
- or not segment_path.is_file()
- ):
- continue
- segment = {
- "url": cls.path_to_video_url(segment_path),
- "start": float(item.get("start", 0)),
- "duration": float(item.get("duration", 0)),
- "width": int(item.get("width", 0)),
- "height": int(item.get("height", 0)),
- }
- # Manifest v2 carries the real session-relative timeline so the
- # UI can seek by step time across scrcpy restart gaps.
- if item.get("offset_ms") is not None and item.get("duration_ms") is not None:
- segment["offset_ms"] = int(item["offset_ms"])
- segment["duration_ms"] = int(item["duration_ms"])
- segments.append(segment)
- return segments
- except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError):
- return []
-
- @staticmethod
- def unwrap_payload(obj: Any) -> Any:
- """Unwraps trace payloads, storing base64 images as disk files and
- truncating large dumps.
- """
- if isinstance(obj, str):
- try:
- if (obj.startswith("{") and obj.endswith("}")) or (
- obj.startswith("[") and obj.endswith("]")
- ):
- return MediaService.unwrap_payload(json.loads(obj))
- except ValueError:
- # Looks like JSON but is not: treat it as a plain string.
- pass
-
- # Check if the string is a Base64 image
- is_base64_img = False
- base64_data = None
- if obj.startswith("data:image/") and ";base64," in obj:
- try:
- _, base64_data = obj.split(";base64,", 1)
- is_base64_img = True
- except ValueError:
- # Malformed data URI: fall through to plain-string handling.
- pass
- elif obj.startswith("iVBORw0KGgo") or obj.startswith("/9j/"):
- is_base64_img = True
- base64_data = obj
-
- if is_base64_img and base64_data:
- try:
- image_bytes = base64.b64decode(base64_data)
- image_hash = hashlib.sha256(image_bytes).hexdigest()
-
- IMAGES_DIR.mkdir(parents=True, exist_ok=True)
- image_path = IMAGES_DIR / f"{image_hash}.jpg"
- if not image_path.exists():
- image_path.write_bytes(image_bytes)
-
- return f"image://{image_hash}"
- except (ValueError, OSError) as exc:
- # Invalid base64 or image cache write failure: fall back
- # to the truncated raw string below.
- logger.debug("Could not persist inline image: %s", exc)
-
- if len(obj) > 2000:
- preview_start = obj[:100].replace("\n", " ")
- preview_end = obj[-100:].replace("\n", " ")
- return (
- f""
- )
- return obj
- elif isinstance(obj, dict):
- return {k: MediaService.unwrap_payload(v) for k, v in obj.items()}
- elif isinstance(obj, list):
- if len(obj) > 20 and any(
- isinstance(x, dict) and ("bounds" in x or "resource-id" in x) for x in obj
- ):
- return [f""]
- return [MediaService.unwrap_payload(x) for x in obj]
- return obj
-
- _LOCAL_FILE_MEDIA_TYPES = {
- ".jpg": "image/jpeg",
- ".jpeg": "image/jpeg",
- ".png": "image/png",
- ".gif": "image/gif",
- ".webp": "image/webp",
- ".mp4": "video/mp4",
- ".webm": "video/webm",
- ".mkv": "video/x-matroska",
- }
-
- @classmethod
- def get_safe_local_file(cls, path_str: str) -> tuple[Path, str]:
- """Serve trace media only: resolved path must sit inside the workspace
- or traces tree AND carry a media extension, so this endpoint can never
- hand out .env, databases, or source files."""
- decoded_path = urllib.parse.unquote(path_str)
- if decoded_path.startswith("file://"):
- decoded_path = decoded_path[len("file://") :]
- try:
- p = Path(decoded_path).resolve(strict=True)
- except (OSError, ValueError):
- raise HTTPException(status_code=404, detail="File not found")
-
- if not p.is_file():
- raise HTTPException(status_code=404, detail="File not found")
-
- allowed_roots = []
- for root in (WORKSPACE_ROOT, TRACES_PATH):
- try:
- allowed_roots.append(Path(root).resolve())
- except OSError:
- continue
- if not any(p.is_relative_to(root) for root in allowed_roots):
- raise HTTPException(
- status_code=403,
- detail="Access denied. Path is outside workspace root.",
- )
-
- media_type = cls._LOCAL_FILE_MEDIA_TYPES.get(p.suffix.lower())
- if media_type is None:
- raise HTTPException(
- status_code=403,
- detail="Access denied. Only media files are served.",
- )
-
- return p, media_type
-
- @staticmethod
- def get_task_plan_content(session_id: str) -> str:
- plan_path = TRACES_PATH / session_id / "notes" / "task_plan.md"
- if not plan_path.exists():
- plan_path = TRACES_PATH / "notes" / "task_plan.md"
-
- if plan_path.exists():
- try:
- return plan_path.read_text(encoding="utf-8")
- except Exception as e:
- return f"Error reading task plan: {e}"
- return "No task plan created yet."
-
- @staticmethod
- def _read_jsonl_records(path: Path, label: str, session_id: str) -> list[dict[str, Any]]:
- records: list[dict[str, Any]] = []
- if not path.is_file():
- return records
- try:
- for line in path.read_text(encoding="utf-8").splitlines():
- line = line.strip()
- if not line:
- continue
- try:
- rec = json.loads(line)
- except ValueError:
- continue
- if isinstance(rec, dict):
- records.append(rec)
- except (OSError, ValueError) as e:
- logger.warning(f"Failed to read {label} for {session_id}: {e}")
- return records
-
- @staticmethod
- def get_session_checks(session_id: str) -> dict[str, Any]:
- """Checker material of one session for UI backfill: the append-only
- verdict ledger (``check_ledger.jsonl``), the per-attempt transcripts of
- the Checker's streamed reasoning (``check_streams.jsonl``: one record
- per attempt with timestamped ``segments``) and the machine-readable run
- outcome (``run_outcome.json``), all written by the Checker /
- ``artemis.graph.checkpoints``. Missing files mean "no checks ran"
- (sessions recorded before transcripts existed simply have no streams).
- """
- session_dir = TRACES_PATH / session_id
- records = MediaService._read_jsonl_records(
- session_dir / "check_ledger.jsonl", "check ledger", session_id
- )
- streams = MediaService._read_jsonl_records(
- session_dir / "check_streams.jsonl", "check streams", session_id
- )
-
- run_outcome: dict[str, Any] | None = None
- outcome_path = session_dir / "run_outcome.json"
- if outcome_path.is_file():
- try:
- loaded = json.loads(outcome_path.read_text(encoding="utf-8"))
- if isinstance(loaded, dict):
- run_outcome = loaded
- except (OSError, ValueError) as e:
- logger.warning(f"Failed to read run outcome for {session_id}: {e}")
-
- return {"records": records, "streams": streams, "run_outcome": run_outcome}
-
- @staticmethod
- def get_session_notes_content(session_id: str) -> dict[str, str]:
- notes_dir = TRACES_PATH / session_id / "notes"
- notes_content = {}
-
- if notes_dir.exists() and notes_dir.is_dir():
- for file in notes_dir.iterdir():
- if file.is_file() and file.suffix in [".md", ".txt", ".json", ".yaml"]:
- try:
- notes_content[file.name] = file.read_text(encoding="utf-8")
- except Exception as e:
- notes_content[file.name] = f"Error reading file: {e}"
-
- if not notes_content:
- global_notes_dir = TRACES_PATH / "notes"
- if global_notes_dir.exists() and global_notes_dir.is_dir():
- for file in global_notes_dir.iterdir():
- if file.is_file() and file.suffix in [
- ".md",
- ".txt",
- ".json",
- ".yaml",
- ]:
- try:
- notes_content[file.name] = file.read_text(encoding="utf-8")
- except Exception as e:
- notes_content[file.name] = f"Error reading file: {e}"
-
- return notes_content
-
-
-media_service = MediaService()
diff --git a/apps/admin_console/services/model_service.py b/apps/admin_console/services/model_service.py
deleted file mode 100644
index 0ccc0bc04..000000000
--- a/apps/admin_console/services/model_service.py
+++ /dev/null
@@ -1,148 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import json
-import logging
-import time
-from typing import Any
-
-logger = logging.getLogger(__name__)
-
-
-class ModelService:
- """Service handling agent architecture profile detection and metadata formatting."""
-
- # parse_llm_config re-reads and re-validates the config file on every call;
- # /api/sessions calls this once per session row, so cache (provider, model_id)
- # for a short TTL. Config edits take effect within _LLM_INFO_TTL seconds.
- _LLM_INFO_TTL = 10.0
- _llm_info_cache: tuple[float, str, str] | None = None
-
- @classmethod
- def _get_llm_provider_and_model(cls) -> tuple[str, str]:
- cached = cls._llm_info_cache
- now = time.monotonic()
- if cached and now - cached[0] < cls._LLM_INFO_TTL:
- return cached[1], cached[2]
- provider = "google"
- model_id = "gemini-3.7-flash"
- try:
- from artemis.config import parse_llm_config
-
- llm_cfg = parse_llm_config()
- if llm_cfg and llm_cfg.operator:
- provider = str(llm_cfg.operator.provider)
- model_id = str(llm_cfg.operator.model)
- elif llm_cfg and llm_cfg.default:
- provider = str(llm_cfg.default.provider)
- model_id = str(llm_cfg.default.model)
- except Exception as exc:
- # Unreadable/invalid LLM config: fall back to display defaults
- # (cached for the TTL, so this logs at most once per window).
- logger.warning("Could not resolve LLM config for display: %s", exc)
- cls._llm_info_cache = (now, provider, model_id)
- return provider, model_id
-
- @classmethod
- def get_active_model_info(cls, profile: str | None = None) -> dict[str, str]:
- """Return active architecture and underlying LLM model configuration."""
- # 1. Determine underlying LLM model and provider from config (cached)
- provider, model_id = cls._get_llm_provider_and_model()
-
- # 2. Determine agent architecture name (Flash vs Pro)
- arch_name = "Flash"
- if profile:
- p_lower = str(profile).lower()
- if "pro" in p_lower:
- arch_name = "Pro"
- elif "flash" in p_lower:
- arch_name = "Flash"
- else:
- arch_name = str(profile).capitalize()
-
- return {
- "name": arch_name,
- "id": model_id,
- "provider": provider,
- "architecture": f"ARTEMIS {arch_name}",
- }
-
- @staticmethod
- def resolve_session_profile(
- row_dict: dict[str, Any],
- llm_trace_payloads: list[str] | None = None,
- running_profile: str | None = None,
- agent_names: list[str] | None = None,
- ) -> str | None:
- """Resolve the Artemis agent architecture profile ('flash' or 'pro') for a session."""
- sess_profile = None
- d_info_raw = row_dict.get("device_info")
- if d_info_raw:
- try:
- d_info = json.loads(d_info_raw) if isinstance(d_info_raw, str) else d_info_raw
- if isinstance(d_info, dict):
- p = d_info.get("profile")
- if p:
- p_str = str(p).lower()
- if "pro" in p_str:
- return "pro"
- elif "flash" in p_str:
- return "flash"
- except (ValueError, TypeError):
- # Malformed device_info JSON: try the other profile sources.
- pass
-
- if (row_dict.get("status") == "running") and running_profile:
- p_str = str(running_profile).lower()
- if "pro" in p_str:
- return "pro"
- elif "flash" in p_str:
- return "flash"
-
- # Check Agent/Trace names - the most definitive indicator of architecture
- if agent_names:
- agent_names_lower = [str(a).lower() for a in agent_names]
- if any(
- name in ("planner", "validator", "summarizer", "operator", "checker", "diagnoser")
- for name in agent_names_lower
- ):
- return "pro"
- if any("flashrunner" in name for name in agent_names_lower):
- return "flash"
-
- # Check LLM traces for agent name
- if llm_trace_payloads:
- for tr_payload in llm_trace_payloads:
- if tr_payload:
- try:
- p_obj = (
- json.loads(tr_payload) if isinstance(tr_payload, str) else tr_payload
- )
- if isinstance(p_obj, dict):
- agent_name = str(p_obj.get("agent") or p_obj.get("name") or "").lower()
- if "flashrunner" in agent_name:
- return "flash"
- if any(
- x in agent_name
- for x in ("planner", "validator", "operator", "checker")
- ):
- return "pro"
- except (ValueError, TypeError):
- # Malformed trace payload JSON: skip this trace.
- pass
-
- return None
-
-
-model_service = ModelService()
diff --git a/apps/admin_console/services/task_preset_catalog.py b/apps/admin_console/services/task_preset_catalog.py
deleted file mode 100644
index b87836056..000000000
--- a/apps/admin_console/services/task_preset_catalog.py
+++ /dev/null
@@ -1,514 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Preset Task Catalog & Dynamic Recommendation Engine for Artemis."""
-
-from typing import Any, Literal
-from pydantic import BaseModel, Field
-
-
-class AppInfo(BaseModel):
- """Application metadata."""
-
- name: str
- icon: str
- pkg: str
- category: str = "general"
-
-
-class TaskPreset(BaseModel):
- """Smart suggestion task definition."""
-
- id: str
- title: str
- description: str
- goal: str
- profile: Literal["flash", "pro"]
- category: Literal["flash", "pro", "cross_app", "monitor"]
- tag: str
- apps: list[AppInfo]
- required_packages: list[str] = Field(default_factory=list)
- match_mode: Literal["any", "all"] = "any"
- priority: int = 50
-
-
-# ============================================================================
-# APP PACKAGE REGISTRY
-# ============================================================================
-
-APP_REGISTRY: dict[str, dict[str, str]] = {
- # Google Suite & System
- "com.google.android.apps.maps": {"name": "Maps", "icon": "explore", "category": "navigation"},
- "com.google.android.gm": {"name": "Gmail", "icon": "mail", "category": "productivity"},
- "com.android.chrome": {"name": "Chrome", "icon": "public", "category": "browser"},
- "com.google.android.youtube": {
- "name": "YouTube",
- "icon": "smart_display",
- "category": "entertainment",
- },
- "com.android.settings": {"name": "Settings", "icon": "settings", "category": "system"},
- "com.google.android.deskclock": {"name": "Clock", "icon": "timer", "category": "utility"},
- "com.android.deskclock": {"name": "Clock", "icon": "timer", "category": "utility"},
- "com.google.android.calculator": {
- "name": "Calculator",
- "icon": "calculate",
- "category": "utility",
- },
- "com.android.calculator2": {"name": "Calculator", "icon": "calculate", "category": "utility"},
- "com.google.android.apps.photos": {
- "name": "Photos",
- "icon": "photo_library",
- "category": "media",
- },
- "com.google.android.calendar": {
- "name": "Calendar",
- "icon": "calendar_month",
- "category": "productivity",
- },
- "com.google.android.keep": {
- "name": "Keep Notes",
- "icon": "note_alt",
- "category": "productivity",
- },
- "com.android.vending": {"name": "Play Store", "icon": "storefront", "category": "tools"},
- "com.google.android.apps.messaging": {
- "name": "Messages",
- "icon": "chat",
- "category": "communication",
- },
- # Popular Ecosystem Apps
- "com.tencent.mm": {"name": "WeChat", "icon": "forum", "category": "social"},
- "com.xingin.xhs": {"name": "Xiaohongshu", "icon": "auto_stories", "category": "social"},
- "com.sankuai.meituan": {"name": "Meituan", "icon": "restaurant", "category": "lifestyle"},
- "com.dianping.v1": {"name": "Dianping", "icon": "star", "category": "lifestyle"},
- "tv.danmaku.bili": {"name": "Bilibili", "icon": "video_library", "category": "entertainment"},
- "com.eg.android.AlipayGphone": {
- "name": "Alipay",
- "icon": "account_balance_wallet",
- "category": "finance",
- },
- "com.netease.cloudmusic": {
- "name": "NetEase Music",
- "icon": "headphones",
- "category": "entertainment",
- },
- "com.spotify.music": {"name": "Spotify", "icon": "music_note", "category": "entertainment"},
-}
-
-
-# ============================================================================
-# CURATED TASK PRESET LIBRARY (1-2 curated tasks per common app)
-# ============================================================================
-
-PRESET_TASK_CATALOG: list[TaskPreset] = [
- # 1. Google Maps
- TaskPreset(
- id="maps_coffee",
- title="Find Specialty Coffee",
- description="Search nearby top-rated cafes in Google Maps",
- goal="Open Google Maps, search for top-rated specialty coffee shops nearby, and view the top result details.",
- profile="flash",
- category="flash",
- tag="Maps",
- apps=[
- AppInfo(
- name="Maps",
- icon="explore",
- pkg="com.google.android.apps.maps",
- category="navigation",
- )
- ],
- required_packages=["com.google.android.apps.maps"],
- priority=95,
- ),
- TaskPreset(
- id="pro_commute_share",
- title="Commute ETA & Message Draft",
- description="Check transit time on Maps and draft arrival ETA in Messages",
- goal="Open Google Maps to check commute time to the International Airport, calculate arrival time, then open Messages and draft an ETA text message.",
- profile="pro",
- category="cross_app",
- tag="Maps + Messages",
- apps=[
- AppInfo(
- name="Maps",
- icon="explore",
- pkg="com.google.android.apps.maps",
- category="navigation",
- ),
- AppInfo(
- name="Messages",
- icon="chat",
- pkg="com.google.android.apps.messaging",
- category="communication",
- ),
- ],
- required_packages=["com.google.android.apps.maps", "com.google.android.apps.messaging"],
- match_mode="all",
- priority=92,
- ),
- # 2. Gmail
- TaskPreset(
- id="gmail_receipts",
- title="Search Order Receipts",
- description="Find recent flight or delivery confirmation emails in Gmail",
- goal="Open Gmail and search for recent flight or package delivery confirmation emails.",
- profile="flash",
- category="flash",
- tag="Gmail",
- apps=[
- AppInfo(name="Gmail", icon="mail", pkg="com.google.android.gm", category="productivity")
- ],
- required_packages=["com.google.android.gm"],
- priority=90,
- ),
- TaskPreset(
- id="pro_email_to_calendar",
- title="Email Itinerary to Calendar",
- description="Extract flight or event dates from Gmail and schedule in Calendar",
- goal="Open Gmail to find the latest event invitation or itinerary, extract dates and location, then open Google Calendar and create a corresponding calendar event.",
- profile="pro",
- category="cross_app",
- tag="Gmail + Calendar",
- apps=[
- AppInfo(
- name="Gmail", icon="mail", pkg="com.google.android.gm", category="productivity"
- ),
- AppInfo(
- name="Calendar",
- icon="calendar_month",
- pkg="com.google.android.calendar",
- category="productivity",
- ),
- ],
- required_packages=["com.google.android.gm"],
- priority=94,
- ),
- # 3. Chrome
- TaskPreset(
- id="chrome_research",
- title="Search AI News Breakthroughs",
- description="Search latest multimodal AI developments in Chrome browser",
- goal="Open Chrome browser and search for latest breakthroughs in multimodal mobile AI agents.",
- profile="flash",
- category="flash",
- tag="Chrome",
- apps=[AppInfo(name="Chrome", icon="public", pkg="com.android.chrome", category="browser")],
- required_packages=["com.android.chrome"],
- priority=88,
- ),
- TaskPreset(
- id="pro_research_keep",
- title="Product Research & Notes Note",
- description="Compare top 3 headphones on Chrome and record comparison in Keep",
- goal="Open Chrome, research top 3 noise-cancelling headphones comparing price and battery life, then write a structured comparison summary note in Keep Notes.",
- profile="pro",
- category="pro",
- tag="Chrome + Keep",
- apps=[
- AppInfo(name="Chrome", icon="public", pkg="com.android.chrome", category="browser"),
- AppInfo(
- name="Keep Notes",
- icon="note_alt",
- pkg="com.google.android.keep",
- category="productivity",
- ),
- ],
- required_packages=["com.android.chrome"],
- priority=91,
- ),
- # 4. YouTube
- TaskPreset(
- id="youtube_lofi",
- title="Play Lo-Fi Music Radio",
- description="Search and play a Lo-Fi hip hop live stream on YouTube",
- goal='Open YouTube, search for "Lofi hip hop beats relaxing radio" and tap on the live stream.',
- profile="flash",
- category="flash",
- tag="YouTube",
- apps=[
- AppInfo(
- name="YouTube",
- icon="smart_display",
- pkg="com.google.android.youtube",
- category="entertainment",
- )
- ],
- required_packages=["com.google.android.youtube"],
- priority=85,
- ),
- # 5. Settings
- TaskPreset(
- id="settings_display_wifi",
- title="Dark Mode & Wi-Fi Check",
- description="Toggle dark theme and verify network connection in Settings",
- goal="Open Settings app, navigate to Display settings, ensure Dark theme is enabled, and check Wi-Fi connection status.",
- profile="flash",
- category="flash",
- tag="Settings",
- apps=[
- AppInfo(name="Settings", icon="settings", pkg="com.android.settings", category="system")
- ],
- required_packages=["com.android.settings"],
- priority=87,
- ),
- TaskPreset(
- id="pro_settings_qa",
- title="Subsystem Health & Crash Probe",
- description="Traverse Settings submenus to verify screens and check for crash dialogs",
- goal="Explore Settings submenus (Network, Connected devices, Apps, Battery, Storage), verify each screen loads properly without ANR or crash dialogs, and summarize results.",
- profile="pro",
- category="monitor",
- tag="Settings QA",
- apps=[
- AppInfo(name="Settings", icon="settings", pkg="com.android.settings", category="system")
- ],
- required_packages=["com.android.settings"],
- priority=93,
- ),
- # 6. Clock
- TaskPreset(
- id="clock_timer",
- title="25-Min Pomodoro Timer",
- description="Start a 25-minute focus countdown timer in Clock app",
- goal="Open Clock app, switch to Timer tab, set 25 minutes and start the countdown timer.",
- profile="flash",
- category="flash",
- tag="Clock",
- apps=[
- AppInfo(
- name="Clock", icon="timer", pkg="com.google.android.deskclock", category="utility"
- )
- ],
- required_packages=["com.google.android.deskclock", "com.android.deskclock"],
- priority=86,
- ),
- # 7. Calculator
- TaskPreset(
- id="calc_gratuity",
- title="Split Bill & Calculate Tip",
- description="Calculate 18% gratuity on $186.40 for 3 people in Calculator",
- goal="Open Calculator and calculate 18% tip on a bill of $186.40, then divide by 3 people.",
- profile="flash",
- category="flash",
- tag="Calculator",
- apps=[
- AppInfo(
- name="Calculator",
- icon="calculate",
- pkg="com.google.android.calculator",
- category="utility",
- )
- ],
- required_packages=["com.google.android.calculator", "com.android.calculator2"],
- priority=84,
- ),
- # 8. Photos
- TaskPreset(
- id="photos_inspect",
- title="Inspect Recent Screenshot",
- description="Open Google Photos and review the latest screenshot taken",
- goal="Open Google Photos and view the most recent screenshot in the screenshots album.",
- profile="flash",
- category="flash",
- tag="Photos",
- apps=[
- AppInfo(
- name="Photos",
- icon="photo_library",
- pkg="com.google.android.apps.photos",
- category="media",
- )
- ],
- required_packages=["com.google.android.apps.photos"],
- priority=82,
- ),
- # 9. WeChat
- TaskPreset(
- id="wechat_browse",
- title="Check WeChat Messages",
- description="Open WeChat and view top recent chat conversations",
- goal="Open WeChat and view the top recent chat messages.",
- profile="flash",
- category="flash",
- tag="WeChat",
- apps=[AppInfo(name="WeChat", icon="forum", pkg="com.tencent.mm", category="social")],
- required_packages=["com.tencent.mm"],
- priority=89,
- ),
- TaskPreset(
- id="pro_wechat_to_calendar",
- title="WeChat Notice to Calendar",
- description="Extract meeting notice from WeChat chat and add to Calendar",
- goal="Open WeChat, locate the latest meeting announcement or event message in the top chat, extract the time and topic, then open Calendar and schedule an event.",
- profile="pro",
- category="cross_app",
- tag="WeChat + Calendar",
- apps=[
- AppInfo(name="WeChat", icon="forum", pkg="com.tencent.mm", category="social"),
- AppInfo(
- name="Calendar",
- icon="calendar_month",
- pkg="com.google.android.calendar",
- category="productivity",
- ),
- ],
- required_packages=["com.tencent.mm"],
- priority=93,
- ),
- # 10. Xiaohongshu
- TaskPreset(
- id="xhs_coffee_guide",
- title="RED Cafe Guide Search",
- description="Search trending specialty cafe reviews on Xiaohongshu",
- goal="Open Xiaohongshu, search for top-rated specialty coffee shops, and view the top post.",
- profile="flash",
- category="flash",
- tag="Xiaohongshu",
- apps=[
- AppInfo(
- name="Xiaohongshu", icon="auto_stories", pkg="com.xingin.xhs", category="social"
- )
- ],
- required_packages=["com.xingin.xhs"],
- priority=87,
- ),
- # 11. Meituan / Dianping
- TaskPreset(
- id="meituan_ramen_search",
- title="Meituan Food Search",
- description="Search top-rated Ramen nearby on Meituan or Dianping",
- goal="Open Meituan or Dianping, search for top-rated Ramen nearby, and view top restaurant rating.",
- profile="flash",
- category="flash",
- tag="Meituan",
- apps=[
- AppInfo(
- name="Meituan", icon="restaurant", pkg="com.sankuai.meituan", category="lifestyle"
- )
- ],
- required_packages=["com.sankuai.meituan", "com.dianping.v1"],
- priority=86,
- ),
- # 12. Bilibili
- TaskPreset(
- id="bilibili_stream",
- title="Bilibili Tech Video",
- description="Search and play an AI Agent tutorial video on Bilibili",
- goal='Open Bilibili, search for "AI Agent Architecture", and play the top matching video.',
- profile="flash",
- category="flash",
- tag="Bilibili",
- apps=[
- AppInfo(
- name="Bilibili",
- icon="video_library",
- pkg="tv.danmaku.bili",
- category="entertainment",
- )
- ],
- required_packages=["tv.danmaku.bili"],
- priority=85,
- ),
- # 13. Play Store
- TaskPreset(
- id="pro_playstore_review",
- title="Play Store App Review Study",
- description="Compare top task management apps and ratings on Google Play",
- goal="Open Google Play Store, search for top rated task management apps, compare ratings and latest user reviews of the top 2 candidates, and record recommendations.",
- profile="pro",
- category="pro",
- tag="Play Store",
- apps=[
- AppInfo(
- name="Play Store", icon="storefront", pkg="com.android.vending", category="tools"
- )
- ],
- required_packages=["com.android.vending"],
- priority=88,
- ),
-]
-
-
-# ============================================================================
-# RECOMMENDATION ENGINE
-# ============================================================================
-
-
-class TaskRecommendationEngine:
- """Intelligent recommendation engine matching device capabilities."""
-
- def __init__(self, catalog: list[TaskPreset] | None = None):
- self.catalog = catalog or PRESET_TASK_CATALOG
-
- def get_all_tasks(self) -> list[dict[str, Any]]:
- return [task.model_dump() for task in self.catalog]
-
- def recommend_tasks(
- self, installed_packages: list[str] | set[str], category: str = "all", limit: int = 12
- ) -> list[dict[str, Any]]:
- pkgs_set = (
- set(installed_packages) if isinstance(installed_packages, list) else installed_packages
- )
-
- scored_tasks: list[tuple[int, TaskPreset, bool]] = []
-
- for task in self.catalog:
- # Check package presence
- is_matched = False
- if pkgs_set:
- if not task.required_packages:
- is_matched = True
- elif task.match_mode == "all":
- is_matched = all(pkg in pkgs_set for pkg in task.required_packages)
- else:
- is_matched = any(pkg in pkgs_set for pkg in task.required_packages)
- else:
- is_matched = False
-
- # Calculate score
- score = task.priority
- if pkgs_set:
- if is_matched:
- score += 100
- if len(task.required_packages) > 1 and task.match_mode == "all":
- score += 30
- else:
- score -= 40
-
- # Filter by category
- if category == "flash" and task.profile != "flash":
- continue
- elif category == "pro" and task.profile != "pro":
- continue
- elif category == "cross_app" and task.category != "cross_app":
- continue
- elif category == "monitor" and task.category != "monitor":
- continue
-
- scored_tasks.append((score, task, is_matched))
-
- # Sort descending by score
- scored_tasks.sort(key=lambda item: item[0], reverse=True)
-
- results: list[dict[str, Any]] = []
- for _, task, matched in scored_tasks[:limit]:
- d = task.model_dump()
- d["is_device_matched"] = matched
- results.append(d)
-
- return results
-
-
-task_recommendation_engine = TaskRecommendationEngine()
diff --git a/apps/admin_console/services/task_queue_service.py b/apps/admin_console/services/task_queue_service.py
deleted file mode 100644
index b99b4c919..000000000
--- a/apps/admin_console/services/task_queue_service.py
+++ /dev/null
@@ -1,1719 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import asyncio
-from datetime import datetime
-import logging
-import os
-from pathlib import Path
-import shutil
-import sys
-import threading
-import time
-from typing import Any
-import uuid
-
-try:
- from admin_console.core.state import state
- from admin_console.database.repositories.session_repository import session_repo
- from admin_console.services import worker_process_io
- from admin_console.services.media_service import media_service
-except ImportError:
- from apps.admin_console.core.state import state
- from apps.admin_console.database.repositories.session_repository import session_repo
- from apps.admin_console.services import worker_process_io
- from apps.admin_console.services.media_service import media_service
-
-from artemis.config import (
- PAUSE_FILE,
- TEST_DATA_DIR,
- TEST_OUTPUTS_DIR,
- WORKSPACE_ROOT,
-)
-from artemis.runtime import (
- AdbEndpoint,
- AdbTarget,
- DeviceExecutionLock,
- clear_cancel_request,
- current_adb_endpoint,
- pid_is_alive,
- process_supervisor,
- request_cancel,
- trace_store,
-)
-
-logger = logging.getLogger(__name__)
-
-
-class TaskQueueService:
- """Service managing FIFO task execution, background worker, subprocess lifecycle,
- and startup tasks.
- """
-
- # Strong references to in-flight _execute_task_item tasks (asyncio itself only
- # keeps weak references to running tasks).
- _run_tasks: set[asyncio.Task] = set()
- # Deadline enforcers for graceful stops (see _stop_worker_gracefully).
- _forced_stop_tasks: set[asyncio.Task] = set()
-
- DEFAULT_CANCEL_GRACE_SECONDS = 45.0
-
- @classmethod
- def _cancel_grace_seconds(cls) -> float:
- """How long a worker may finalize itself before it is killed.
-
- ``ARTEMIS_CANCEL_GRACE_SECONDS=0`` restores the legacy immediate kill.
- """
- raw = os.getenv("ARTEMIS_CANCEL_GRACE_SECONDS")
- if raw is None or not raw.strip():
- return cls.DEFAULT_CANCEL_GRACE_SECONDS
- try:
- return max(0.0, float(raw))
- except ValueError:
- return cls.DEFAULT_CANCEL_GRACE_SECONDS
-
- @staticmethod
- def _hard_kill(pid: int, process_created_at: float = 0.0) -> bool:
- if not pid:
- return False
- if process_created_at and process_created_at > 0:
- return process_supervisor.terminate_tree_verified(pid, process_created_at)
- try:
- return process_supervisor.terminate_tree(pid)
- except Exception:
- return False
-
- @classmethod
- def _stop_worker_gracefully(
- cls,
- pid: Any,
- process_created_at: float = 0.0,
- session_id: str | None = None,
- reason: str = "Task stopped from the Artemis frontend.",
- ) -> tuple[bool, bool]:
- """Ask a worker to cancel itself; hard-kill it once the grace period lapses.
-
- Workers are isolated from the daemon's console (and may belong to
- another ingress process), so instead of a signal the daemon drops a
- cancel marker the worker polls for. Honouring it runs the worker's
- normal cancellation path: the screen recording is stopped and remuxed,
- the trace folder is compiled, and the device lease is released. A
- worker that never picks the marker up is killed after the grace period.
-
- Returns ``(stopped, deferred)``: ``stopped`` mirrors the legacy kill
- result, ``deferred`` is True when the kill was handed to the deadline
- enforcer instead of happening now.
- """
- try:
- pid_int = int(pid) if pid else 0
- except (TypeError, ValueError):
- pid_int = 0
- grace = cls._cancel_grace_seconds()
- if not pid_int or grace <= 0:
- return cls._hard_kill(pid_int, process_created_at), False
-
- created_at = float(process_created_at or 0.0)
- if created_at <= 0:
- # PID markers carry the creation time so a leftover marker can never
- # cancel a future process that reuses this PID.
- try:
- import psutil
-
- created_at = float(psutil.Process(pid_int).create_time())
- except Exception:
- created_at = 0.0
- if not pid_is_alive(pid_int, created_at or None):
- return True, False
-
- written = request_cancel(
- session_id=str(session_id) if session_id else None,
- pid=pid_int,
- process_created_at=created_at,
- reason=reason,
- )
- if not written:
- return cls._hard_kill(pid_int, created_at), False
-
- print(
- f"[stop_tasks] Cancel requested for worker {pid_int}"
- f" (session {session_id or 'n/a'}); forcing termination after {grace:.0f}s"
- )
- cls._schedule_forced_stop(pid_int, created_at, grace, session_id)
- return True, True
-
- @classmethod
- def _stop_proc_gracefully(cls, proc: Any, session_id: Any) -> bool:
- """Graceful variant for a locally spawned process; True when deferred."""
- pid = getattr(proc, "pid", None)
- if not pid or cls._cancel_grace_seconds() <= 0:
- return False
- try:
- _stopped, deferred = cls._stop_worker_gracefully(
- pid, session_id=str(session_id) if session_id else None
- )
- except Exception:
- return False
- return deferred
-
- @classmethod
- def _schedule_forced_stop(
- cls, pid: int, process_created_at: float, grace: float, session_id: Any
- ) -> None:
- """Kill ``pid`` if it is still alive once ``grace`` seconds have passed."""
-
- def _still_alive() -> bool:
- return pid_is_alive(pid, process_created_at or None)
-
- def _force() -> None:
- print(
- f"[stop_tasks] Worker {pid} (session {session_id or 'n/a'}) did not exit"
- f" within {grace:.0f}s of the cancel request; forcing termination."
- )
- cls._hard_kill(pid, process_created_at)
-
- async def _enforce_async() -> None:
- deadline = time.monotonic() + grace
- while time.monotonic() < deadline:
- if not _still_alive():
- break
- await asyncio.sleep(0.5)
- else:
- _force()
- clear_cancel_request(session_id=str(session_id) if session_id else None, pid=pid)
-
- def _enforce_sync() -> None:
- deadline = time.monotonic() + grace
- while time.monotonic() < deadline:
- if not _still_alive():
- break
- time.sleep(0.5)
- else:
- _force()
- clear_cancel_request(session_id=str(session_id) if session_id else None, pid=pid)
-
- try:
- loop = asyncio.get_running_loop()
- except RuntimeError:
- loop = None
- if loop is not None:
- task = loop.create_task(_enforce_async())
- cls._forced_stop_tasks.add(task)
- task.add_done_callback(cls._forced_stop_tasks.discard)
- return
- threading.Thread(
- target=_enforce_sync, name=f"artemis-forced-stop-{pid}", daemon=True
- ).start()
-
- @staticmethod
- def _task_target(task_item: dict[str, Any]) -> AdbTarget:
- endpoint_data = task_item.get("adb_endpoint")
- endpoint = (
- AdbEndpoint.from_mapping(endpoint_data)
- if isinstance(endpoint_data, dict)
- else current_adb_endpoint()
- )
- serial = task_item.get("device_serial")
- return AdbTarget(endpoint=endpoint, serial=str(serial) if serial else None)
-
- @classmethod
- def _broadcast_event(cls, event_type: str, data: Any):
- """Broadcasts an event safely to all registered subscribers."""
- for cb in list(state.ipc_subscribers):
- try:
- cb(event_type, data)
- except Exception:
- # One broken subscriber must not block the others, but a
- # silent drop hides it entirely.
- logger.warning(
- "Event subscriber %r failed for event %s",
- cb,
- event_type,
- exc_info=True,
- )
-
- @classmethod
- def _broadcast_startup_progress(cls, session_id: str | None, stage: str, message: str) -> None:
- if not session_id:
- return
- data = {
- "session_id": str(session_id),
- "stage": stage,
- "message": message,
- "timestamp": time.time(),
- }
- state.record_startup_progress(data)
- cls._broadcast_event("startup_progress", data)
-
- @classmethod
- def _get_next_pending_task(cls) -> dict[str, Any] | None:
- """Finds and returns the first pending task from queue_items."""
- for item in state.queue_items:
- if isinstance(item, dict) and item.get("status") == "pending":
- return item
- return None
-
- @classmethod
- def _remove_task(cls, session_id: str | None):
- """Removes a task from queue_items by session_id."""
- if not session_id:
- return
- removed_items = [
- t
- for t in state.queue_items
- if isinstance(t, dict) and t.get("session_id") == session_id
- ]
- for item in removed_items:
- DeviceExecutionLock.cancel_reservation(item.get("queue_ticket"))
- state.queue_items = [
- t
- for t in state.queue_items
- if not (isinstance(t, dict) and t.get("session_id") == session_id)
- ]
-
- @staticmethod
- def _resolve_terminal_status(
- current_status: str | None,
- returncode: int,
- was_stopped_manually: bool,
- ) -> tuple[str, bool]:
- """Resolve final status while preserving an authoritative task result.
-
- The worker exit code is only a fallback for sessions that have not
- reached a terminal state. ``success`` is the DataEngine alias for the
- UI-facing ``completed`` status and is normalized here.
-
- Returns:
- A tuple of ``(resolved_status, should_persist)``.
- """
- if was_stopped_manually:
- return "cancelled", True
-
- normalized = current_status.lower().strip() if isinstance(current_status, str) else None
- if normalized == "success":
- return "completed", True
- if normalized in {"completed", "failed", "cancelled"}:
- return normalized, False
- return ("completed" if returncode == 0 else "failed"), True
-
- # Worker subprocess I/O plumbing lives in worker_process_io; the historical
- # private names stay bound here so callers and tests keep working unchanged.
- _subprocess_creation_kwargs = staticmethod(worker_process_io.subprocess_creation_kwargs)
- _forward_worker_output = staticmethod(worker_process_io.forward_worker_output)
- _finish_output_forwarder = staticmethod(worker_process_io.finish_output_forwarder)
- _wait_for_worker_process = staticmethod(worker_process_io.wait_for_worker_process)
-
- @staticmethod
- async def _terminate_worker_process(proc: asyncio.subprocess.Process | None) -> None:
- """Stop a worker without changing the established POSIX behavior."""
- if proc is None or proc.returncode is not None:
- return
- if sys.platform == "win32":
- await process_supervisor.stop_process(proc)
- return
- try:
- proc.kill()
- except ProcessLookupError:
- pass
-
- @classmethod
- def ensure_worker_running(cls):
- """Guarantees that the background queue worker task is active and running."""
- try:
- loop = asyncio.get_running_loop()
- if state.worker_task is None or state.worker_task.done():
- state.worker_task = loop.create_task(cls.queue_worker())
- try:
- state.wake_event.set()
- except AttributeError:
- # Partially initialized state (unit tests): the worker
- # loop polls anyway.
- pass
- except RuntimeError:
- pass
-
- @classmethod
- def _concurrency_limit(cls) -> int:
- """Return 0 for per-device concurrency, or the global task limit."""
- return DeviceExecutionLock.resolve_env_concurrency()
-
- @classmethod
- async def queue_worker(cls):
- """Persistent dispatcher scheduling pending tasks onto devices.
-
- Scans the queue and launches each eligible task as an independent
- coroutine. Admission is governed by _concurrency_limit(): 0 admits one
- task per device so distinct devices execute concurrently, N>=1 admits
- at most N tasks across all devices. Per-device FIFO ordering and
- cross-process mutual exclusion remain enforced by DeviceExecutionLock
- inside each worker process.
- """
- print(
- f"[QueueWorker] Dispatcher initialized (concurrency limit: {cls._concurrency_limit()})."
- )
- try:
- DeviceExecutionLock.cleanup_stale_locks()
- except Exception as exc:
- print(f"[QueueWorker] Initial stale lock cleanup notice: {exc}")
-
- try:
- while True:
- try:
- cls._dispatch_pending_tasks()
- except Exception as exc:
- print(f"[QueueWorker] Dispatch error: {exc}")
- try:
- await asyncio.wait_for(state.wake_event.wait(), timeout=0.3)
- state.wake_event.clear()
- except TimeoutError:
- pass
- except asyncio.CancelledError:
- print("[QueueWorker] Dispatcher received cancellation signal.")
- for run in list(state.active_runs.values()):
- run_proc = run.get("process")
- if run_proc is not None and run_proc.returncode is None:
- await cls._terminate_worker_process(run_proc)
- raise
-
- @classmethod
- def _dispatch_pending_tasks(cls) -> None:
- """Launch every pending task admissible under the current concurrency limit."""
- limit = cls._concurrency_limit()
- state.prune_finished_runs()
- if limit == 1 and state.is_running:
- return
-
- # Dispatched-but-not-yet-spawned runs are only visible as queue items in
- # "running" state, so admission must count those too -- active_runs alone
- # lags behind by the subprocess startup latency.
- in_flight = [
- i for i in state.queue_items if isinstance(i, dict) and i.get("status") == "running"
- ]
- capacity = None
- if limit >= 1:
- # Registered workers still have running queue rows until finalization.
- active_pids = {
- getattr(run.get("process"), "pid", None) for run in state.active_runs.values()
- } - {None}
- starting = sum(
- str(item.get("session_id")) not in state.active_runs
- and item.get("pid") not in active_pids
- for item in in_flight
- )
- capacity = limit - len(state.active_runs) - starting
- if capacity <= 0:
- return
- busy_devices = state.busy_device_ids | {
- cls._task_target(i).lock_key for i in in_flight if i.get("device_serial")
- }
- dispatched_any = False
- loop = asyncio.get_running_loop()
- for item in list(state.queue_items):
- if not (isinstance(item, dict) and item.get("status") == "pending"):
- continue
- sess_id = item.get("session_id")
- if sess_id and str(sess_id) in state.cancelled_session_ids:
- cls._remove_task(sess_id)
- continue
-
- device = item.get("device_serial")
- target = cls._task_target(item)
- if limit == 0:
- # A task without a resolved device may bind to any serial, so it
- # only launches on an otherwise idle scheduler; the device lock
- # then allocates freely without contending against active runs.
- if device is None and (state.active_runs or in_flight or dispatched_any):
- continue
- if device is not None and target.lock_key in busy_devices:
- continue
- elif limit > 1 and device is not None and target.lock_key in busy_devices:
- # A second worker for this device would wait on its lock.
- continue
-
- item["status"] = "running"
- dispatched_any = True
- if device is not None:
- busy_devices.add(target.lock_key)
- run_task = loop.create_task(cls._execute_task_item(item))
- # Hold a strong reference: asyncio keeps only weak refs to running
- # tasks, and a collected run would strand its queue item forever.
- cls._run_tasks.add(run_task)
- run_task.add_done_callback(cls._run_tasks.discard)
- if capacity is not None:
- capacity -= 1
- if capacity <= 0:
- break
-
- @classmethod
- def _begin_task_run(
- cls,
- task_item: dict[str, Any],
- run_key: str,
- sess_id: Any,
- goal: str,
- profile: str,
- ) -> None:
- """Mark the task as running and announce the launch to subscribers."""
- task_item["status"] = "running"
- task_item["start_time"] = time.time()
-
- # A fresh launch clears a stale stop request left over for this run
- # key from a previous task. Manual stops are tracked per run in
- # manually_stopped_run_ids so stopping one device's task never
- # affects concurrent runs. Per-session stops are tracked in
- # cancelled_session_ids and are unaffected.
- state.manually_stopped_run_ids.discard(run_key)
- state.current_goal = goal
- state.current_profile = profile
- state.active_session_id = sess_id
-
- cls._broadcast_startup_progress(sess_id, "launching", "Starting the execution process")
-
- # Broadcast session_started so all connected clients know the task has started
- cls._broadcast_event(
- "session_started",
- {
- "session_id": sess_id,
- "initial_goal": goal,
- "profile": profile,
- "device_serial": task_item.get("device_serial"),
- },
- )
-
- @classmethod
- def _build_worker_invocation(
- cls,
- task_item: dict[str, Any],
- run_key: str,
- sess_id: Any,
- goal: str,
- profile: str,
- target: AdbTarget,
- ) -> tuple[list[str], dict[str, str]]:
- """Assemble the worker subprocess command line and environment."""
- expected_output = task_item.get("expected_output")
- enable_outputter = task_item.get("enable_outputter")
- verification_level = task_item.get("verification_level")
- explorer_mode = task_item.get("explorer_mode")
- locked_app = task_item.get("locked_app_package") or task_item.get("locked_app")
- app_path = task_item.get("app_path")
-
- test_name = f"web_{int(time.time())}_{run_key[:8]}"
- env = os.environ.copy()
- pythonpath_parts = [
- str(WORKSPACE_ROOT),
- str(WORKSPACE_ROOT / "apps" / "admin_console"),
- str(WORKSPACE_ROOT / "apps" / "cloud_service"),
- env.get("PYTHONPATH", ""),
- ]
- env["PYTHONPATH"] = os.pathsep.join([p for p in pythonpath_parts if p])
- env["PYTHONUTF8"] = "1"
- env["PYTHONUNBUFFERED"] = "1"
- if state.ipc_port is not None:
- env["ARTEMIS_IPC_PORT"] = str(state.ipc_port)
- if sess_id:
- env["ARTEMIS_SESSION_ID"] = str(sess_id)
- env["ARTEMIS_TASK_INGRESS"] = str(task_item.get("ingress", "frontend"))
- env["ARTEMIS_TASK_WORKER"] = "1"
- target.endpoint.apply_to_environment(env)
- env[DeviceExecutionLock.LOCK_SCOPE_ENV] = target.lock_scope
- queue_ticket = task_item.get("queue_ticket")
- if queue_ticket:
- env[DeviceExecutionLock.QUEUE_TICKET_ENV] = str(queue_ticket)
-
- cmd = [
- sys.executable,
- "-m",
- "artemis.main",
- goal,
- "--profile",
- profile,
- "--test-name",
- test_name,
- ]
- if sess_id:
- cmd.extend(["--session-id", str(sess_id)])
- if expected_output:
- cmd.extend(["--output-description", str(expected_output)])
- if enable_outputter is not None:
- cmd.append("--enable-outputter" if enable_outputter else "--disable-outputter")
- if verification_level:
- cmd.extend(["--verification-level", str(verification_level)])
- if explorer_mode:
- cmd.extend(["--explorer-pro-mode", str(explorer_mode)])
- if locked_app:
- cmd.extend(["--locked-app", str(locked_app)])
- if app_path:
- cmd.extend(["--app-path", str(app_path)])
- device_serial = task_item.get("device_serial")
- if device_serial:
- cmd.extend(["--device-serial", str(device_serial)])
- env["ADB_DEVICE_SERIAL"] = str(device_serial)
- return cmd, env
-
- @classmethod
- def _register_worker_run(
- cls,
- task_item: dict[str, Any],
- run_key: str,
- sess_id: Any,
- goal: str,
- profile: str,
- target: AdbTarget,
- proc: asyncio.subprocess.Process,
- ) -> None:
- """Record the spawned worker in shared state and hand it the device reservation."""
- device_serial = task_item.get("device_serial")
- state.current_process = proc
- task_item["pid"] = proc.pid
- state.active_runs[run_key] = {
- "process": proc,
- "device_id": str(device_serial) if device_serial else None,
- "lock_key": target.lock_key if device_serial else None,
- "adb_endpoint": target.endpoint.to_dict(),
- "goal": goal,
- "profile": profile,
- }
- if sess_id:
- try:
- # Preserve status updates written concurrently by the worker.
- trace_store.update_trace_pid(str(sess_id), proc.pid)
- except OSError as exc:
- # Status probes fall back to DB PID bookkeeping, but note it.
- print(
- f"[QueueWorker] Could not record worker pid in status.json for {sess_id}: {exc}"
- )
- cls._broadcast_startup_progress(sess_id, "process_ready", "Execution process started")
- ingress_type = str(task_item.get("ingress", "frontend"))
- queue_ticket = task_item.get("queue_ticket")
- if queue_ticket:
- transferred = DeviceExecutionLock.transfer_reservation(
- str(queue_ticket),
- proc.pid,
- description=f"{ingress_type} task: {goal[:120]}",
- device_id=device_serial or "pending",
- session_id=str(sess_id) if sess_id else None,
- ingress=ingress_type,
- lock_scope=target.lock_scope,
- )
- if not transferred:
- print(
- f"[QueueWorker] Could not transfer queue ticket {queue_ticket} to worker"
- f" pid {proc.pid}; the worker will queue a fresh ticket itself."
- )
-
- @classmethod
- def _start_output_forwarder(
- cls, sess_id: Any, proc: asyncio.subprocess.Process
- ) -> asyncio.Task[None] | None:
- """Start forwarding the worker's output; returns the forwarder task, if any."""
- # Forward the worker's combined output and tee it into the trace's
- # stdout.log so the log paths advertised by the MCP API exist.
- log_path = None
- if sess_id:
- try:
- log_path = trace_store.get_trace_stdout_log_path(str(sess_id))
- except Exception:
- log_path = None
- if isinstance(proc.stdout, asyncio.StreamReader):
- return asyncio.create_task(cls._forward_worker_output(proc.stdout, log_path))
- return None
-
- @classmethod
- async def _terminate_if_cancelled_during_launch(
- cls, run_key: str, sess_id: Any, proc: asyncio.subprocess.Process
- ) -> None:
- """Terminate a worker whose task was cancelled while it was being launched."""
- if sess_id and (
- str(sess_id) in getattr(state, "cancelled_session_ids", set())
- or run_key in state.manually_stopped_run_ids
- ):
- print(f"[QueueWorker] Task [{sess_id}] was cancelled during launch. Terminating.")
- await cls._terminate_worker_process(proc)
-
- @classmethod
- async def _persist_terminal_session_status(
- cls, sess_id: Any, returncode: int, manual_stop: bool
- ) -> str:
- """Resolve and persist the session's terminal status in the DB and trace store."""
- current_status = session_repo.get_session_status(sess_id)
- new_status, should_persist = cls._resolve_terminal_status(
- current_status,
- returncode,
- manual_stop,
- )
- if should_persist:
- if session_repo.update_session_status(sess_id, new_status, time.time()):
- print(f"[QueueWorker] Updated session {sess_id} status to '{new_status}'")
- else:
- # The DB row is the fallback MCP pollers reconcile
- # against when status.json is stale, so a failed DB
- # write must not pass silently.
- logger.error(
- "[QueueWorker] Could not persist terminal DB status '%s' for session %s",
- new_status,
- sess_id,
- )
- else:
- print(f"[QueueWorker] Preserved authoritative session {sess_id} status '{new_status}'")
- # A stale status.json would leave MCP pollers seeing "running"
- # until their next DB reconcile, so retry transient write
- # failures before giving up.
- for attempt in range(3):
- try:
- if trace_store.read_status(str(sess_id)):
- canonical_mcp_status = (
- "completed" if new_status in ("completed", "success") else new_status
- )
- trace_store.update_trace_status(
- str(sess_id),
- canonical_mcp_status,
- )
- break
- except OSError as exc:
- if attempt < 2:
- await asyncio.sleep(0.5 * (attempt + 1))
- continue
- logger.error(
- "[QueueWorker] Could not persist terminal MCP status "
- "for %s after %d attempts: %s",
- sess_id,
- attempt + 1,
- exc,
- )
- return new_status
-
- @classmethod
- async def _recover_or_fail_recording(cls, sess_id: Any) -> None:
- """Finalize a recording whose worker died before it could, else mark it failed.
-
- A worker that stops gracefully finalizes its own recording and this is
- a no-op. A hard-killed (or crashed) worker leaves the raw scrcpy file it
- registered at recording start; remuxing that file is all that is
- needed to publish the full video.
- """
- rec_info = session_repo.get_video_recording_for_session(sess_id)
- rec_status = (rec_info or {}).get("status")
- if rec_status == "ready":
- return
-
- recovered_url = None
- # 1. Direct recovery from the recording row written at recording start.
- try:
- local_video_path = (rec_info or {}).get("local_video_path")
- if local_video_path:
- start_time = (rec_info or {}).get("start_time")
- if not start_time:
- session_row = session_repo.get_session_by_id(sess_id)
- start_time = dict(session_row).get("start_time") if session_row else None
- final_path = await asyncio.to_thread(
- media_service.recover_orphaned_recording,
- local_video_path,
- start_time,
- )
- if final_path:
- session_repo.mark_recording_ready(sess_id, str(final_path))
- recovered_url = media_service.path_to_video_url(Path(final_path))
- except Exception as rec_err:
- print(f"[QueueWorker] Error finalizing orphaned recording: {rec_err}")
-
- # 2. Fallback: locate an already finalized file by folder / session naming.
- if not recovered_url:
- try:
- video_rec_map = session_repo.get_video_recordings_map()
- video_idx = await asyncio.to_thread(media_service.build_video_index)
- fallback_url = await asyncio.to_thread(
- media_service.resolve_video_url,
- {"session_id": sess_id},
- video_rec_map,
- video_idx,
- )
- if fallback_url:
- session_repo.mark_recording_ready(sess_id, fallback_url)
- recovered_url = fallback_url
- except Exception as rec_err:
- print(f"[QueueWorker] Error attempting recording recovery: {rec_err}")
-
- if recovered_url:
- cls._broadcast_event(
- "recording_ready",
- {"session_id": sess_id, "video_url": recovered_url},
- )
- return
-
- recording_error = "Task worker exited before recording finalization completed"
- if session_repo.mark_recording_failed_if_pending(sess_id, recording_error):
- cls._broadcast_event(
- "recording_failed",
- {"session_id": sess_id, "error": recording_error},
- )
-
- @classmethod
- def _announce_session_end(
- cls,
- task_item: dict[str, Any],
- sess_id: Any,
- goal: str,
- new_status: str,
- manual_stop: bool,
- ) -> None:
- """Broadcast session_ended and dispatch the external completion notification."""
- state.active_connections.pop(sess_id, None)
- cls._broadcast_event(
- "session_ended",
- {
- "session_id": sess_id,
- "status": new_status,
- "was_stopped_manually": manual_stop,
- },
- )
-
- conversation_id = task_item.get("conversation_id") if task_item else None
- if conversation_id or task_item.get("ingress") == "mcp":
- try:
- from mcp_server.notifiers import notify
-
- notify(
- conversation_id=conversation_id or "",
- message=f"Artemis autonomous task '{goal}' finished with status '{new_status}'.\nTrace ID: {sess_id}",
- title=f"Task {new_status.capitalize()}: {goal[:40]}",
- event_type=new_status,
- payload={
- "trace_id": sess_id,
- "session_id": sess_id,
- "status": new_status,
- "goal": goal,
- },
- )
- except Exception as notif_err:
- print(f"[QueueWorker] Notification dispatch notice: {notif_err}")
-
- @classmethod
- def _release_run_slot(
- cls, sess_id: Any, run_key: str, proc: asyncio.subprocess.Process | None
- ) -> None:
- """Clean up the finished task and release this run's scheduling slot."""
- if sess_id:
- cls._remove_task(sess_id)
- state.cancelled_session_ids.discard(str(sess_id))
- state.cancelled_session_ids.discard(run_key)
- state.manually_stopped_run_ids.discard(run_key)
- try:
- clear_cancel_request(
- session_id=str(sess_id) if sess_id else None,
- pid=getattr(proc, "pid", None),
- )
- except (OSError, TypeError, ValueError):
- # Marker cleanup is best-effort: an unwritable temp dir or an odd
- # pid value must not block releasing the run slot.
- pass
- state.active_runs.pop(run_key, None)
- if proc is not None and state.current_process is proc:
- state.current_process = None
- if not state.active_runs:
- state.active_session_id = None
- state.current_goal = None
- state.current_profile = None
- # No runs left: any not-yet-consumed manual-stop markers are
- # stale and must not leak into future runs.
- state.manually_stopped_run_ids.clear()
- state.wake_event.set()
-
- @classmethod
- async def _execute_task_item(cls, task_item: dict[str, Any]) -> None:
- """Run one queued task to completion in its own worker subprocess."""
- sess_id = task_item.get("session_id")
- run_key = str(sess_id) if sess_id else uuid.uuid4().hex
- goal = task_item.get("goal")
- profile = task_item.get("profile", "flash")
- proc: asyncio.subprocess.Process | None = None
- output_task: asyncio.Task[None] | None = None
- try:
- if not isinstance(goal, str) or not goal.strip():
- raise ValueError("Queued task must contain a non-empty string goal.")
- cls._begin_task_run(task_item, run_key, sess_id, goal, profile)
-
- target = cls._task_target(task_item)
- cmd, env = cls._build_worker_invocation(
- task_item, run_key, sess_id, goal, profile, target
- )
-
- device_serial = task_item.get("device_serial")
- print(
- f"[QueueWorker] Starting task [{sess_id}]: '{goal}' (profile: {profile}, device: {device_serial or 'auto'}, outputter: {bool(task_item.get('expected_output') or task_item.get('enable_outputter'))})"
- )
- proc = await asyncio.create_subprocess_exec(
- *cmd,
- cwd=str(WORKSPACE_ROOT),
- env=env,
- **cls._subprocess_creation_kwargs(),
- )
- cls._register_worker_run(task_item, run_key, sess_id, goal, profile, target, proc)
- output_task = cls._start_output_forwarder(sess_id, proc)
- await cls._terminate_if_cancelled_during_launch(run_key, sess_id, proc)
-
- # 3. Await subprocess completion
- returncode = await cls._wait_for_worker_process(proc)
- print(f"[QueueWorker] Task [{sess_id}] exited with returncode {returncode}")
-
- manual_stop = run_key in state.manually_stopped_run_ids or bool(
- sess_id and str(sess_id) in state.cancelled_session_ids
- )
-
- # 4. Perform fallback database status update and notification
- if sess_id:
- new_status = await cls._persist_terminal_session_status(
- sess_id, returncode, manual_stop
- )
- await cls._recover_or_fail_recording(sess_id)
- cls._announce_session_end(task_item, sess_id, goal, new_status, manual_stop)
-
- except asyncio.CancelledError:
- print(f"[QueueWorker] Task [{sess_id}] received cancellation signal.")
- if proc is not None and proc.returncode is None:
- await cls._terminate_worker_process(proc)
- raise
- except Exception:
- logger.exception(f"[QueueWorker] Unexpected error executing task [{sess_id}]")
- # A spawn failure must also leave a terminal session status.
- if sess_id:
- try:
- new_status = await cls._persist_terminal_session_status(
- sess_id, returncode=1, manual_stop=False
- )
- cls._announce_session_end(task_item, sess_id, goal or "", new_status, False)
- except (OSError, RuntimeError, ValueError):
- logger.exception(
- f"[QueueWorker] Could not persist failure status for [{sess_id}]"
- )
- finally:
- await cls._finish_output_forwarder(output_task)
- # 5. Clean up the finished task and release this run's scheduling slot
- cls._release_run_slot(sess_id, run_key, proc)
-
- @classmethod
- def _find_duplicate_submission(
- cls,
- goals: list[str],
- session_id: str | None,
- device_serial: str | None,
- endpoint: AdbEndpoint,
- now: float,
- ) -> dict[str, Any] | None:
- """Return the short-circuit response for a duplicate submission, if any."""
- # 1. Deduplication by session_id: if session_id is already running or queued, do not re-enqueue
- if session_id and len(goals) == 1:
- target_sid = str(session_id)
- if state.active_session_id and str(state.active_session_id) == target_sid:
- return {
- "status": "started",
- "tasks": [{"session_id": target_sid, "status": "running"}],
- "enqueued_count": 0,
- "total_queued": len(state.queue_tasks),
- }
- existing_item = next(
- (
- item
- for item in state.queue_items
- if isinstance(item, dict) and str(item.get("session_id")) == target_sid
- ),
- None,
- )
- if existing_item:
- return {
- "status": existing_item.get("status", "queued"),
- "tasks": [existing_item],
- "enqueued_count": 0,
- "total_queued": len(state.queue_tasks),
- }
-
- # 2. Debounce duplicate rapid submissions (e.g. UI double-click or network retry within 1s)
- if len(goals) == 1:
- first_goal = goals[0]
- recent_duplicate = next(
- (
- item
- for item in reversed(state.queue_items)
- if isinstance(item, dict)
- and item.get("status") == "pending"
- and item.get("goal") == first_goal
- and (not device_serial or item.get("device_serial") == device_serial)
- and item.get("adb_endpoint", {}).get("identity") == endpoint.identity
- and (now - float(item.get("created_at", 0))) < 1.0
- ),
- None,
- )
- if recent_duplicate:
- return {
- "status": "queued",
- "tasks": [recent_duplicate],
- "enqueued_count": 0,
- "total_queued": len(state.queue_tasks),
- }
- return None
-
- @classmethod
- async def _reject_unavailable_device(cls, device_serial: str | None) -> dict[str, Any] | None:
- """Return the rejection response for an unattached explicit serial, if any."""
- # Strict device binding: reject an explicitly requested serial that is not
- # attached and authorized, instead of silently running on another device.
- # The shared validator fails open on an indeterminate/empty enumeration so
- # the task can proceed and fail downstream with a clear no-device error.
- if device_serial:
- try:
- from artemis.runtime import device_pool
-
- rejection = await device_pool.validate_explicit_serial_async(device_serial)
- except Exception:
- rejection = None
- if rejection:
- return {
- "status": "rejected",
- "error": rejection,
- "tasks": [],
- "enqueued_count": 0,
- "total_queued": len(state.queue_tasks),
- }
- return None
-
- @classmethod
- def _create_queue_item(
- cls,
- goal: str,
- index: int,
- now: float,
- endpoint: AdbEndpoint,
- single_session_id: str | None,
- profile: str,
- expected_output: str | None,
- enable_outputter: bool | None,
- locked_app_package: str | None,
- app_path: str | None,
- device_serial: str | None,
- ingress: str,
- conversation_id: str | None,
- verification_level: str | None = None,
- explorer_mode: str | None = None,
- ) -> dict[str, Any]:
- """Reserve a device slot and build one pending queue item for a goal."""
- sess_id = single_session_id if single_session_id else str(uuid.uuid4())
- # enqueue_tasks resolves the device before creating queue items.
- assigned_serial = device_serial
-
- queue_ticket = DeviceExecutionLock.reserve(
- description=f"{ingress} task: {goal[:120]}",
- device_id=assigned_serial or "pending",
- session_id=sess_id,
- ingress=ingress,
- lock_scope=endpoint.identity,
- )
- return {
- "session_id": sess_id,
- "goal": goal,
- "profile": profile or "flash",
- "expected_output": expected_output,
- "enable_outputter": enable_outputter,
- "verification_level": verification_level,
- "explorer_mode": explorer_mode,
- "locked_app_package": locked_app_package,
- "app_path": app_path,
- "device_serial": assigned_serial,
- "adb_endpoint": endpoint.to_dict(),
- "ingress": ingress,
- "conversation_id": conversation_id,
- "status": "pending",
- "queue_ticket": queue_ticket,
- "created_at": now + index * 0.001,
- "start_time": now + index * 0.001,
- }
-
- @classmethod
- async def enqueue_tasks(
- cls,
- goals: list[str],
- profile: str = "flash",
- expected_output: str | None = None,
- enable_outputter: bool | None = None,
- locked_app_package: str | None = None,
- app_path: str | None = None,
- device_serial: str | None = None,
- ingress: str = "frontend",
- session_id: str | None = None,
- conversation_id: str | None = None,
- verification_level: str | None = None,
- explorer_mode: str | None = None,
- ) -> dict[str, Any]:
- """Enqueues one or more goals and wakes up the background worker.
-
- ``verification_level`` and ``explorer_mode`` are Pro-profile tuning knobs
- forwarded to the worker as ``--verification-level`` / ``--explorer-pro-mode``;
- they are normalised here so the queue item and the CLI see one spelling.
- """
- verification_level = (
- str(verification_level).strip().lower() or None if verification_level else None
- )
- explorer_mode = str(explorer_mode).strip().lower() or None if explorer_mode else None
- cls.ensure_worker_running()
-
- enqueued_tasks = []
- now = time.time()
- endpoint = current_adb_endpoint()
-
- duplicate_response = cls._find_duplicate_submission(
- goals, session_id, device_serial, endpoint, now
- )
- if duplicate_response is not None:
- return duplicate_response
-
- rejection_response = await cls._reject_unavailable_device(device_serial)
- if rejection_response is not None:
- return rejection_response
-
- single_session_id = session_id if (session_id and len(goals) == 1) else None
- if not device_serial:
- # Device enumeration may block on ADB.
- from artemis.runtime import device_pool
-
- try:
- device_serial = await device_pool.select_device_async()
- except Exception:
- device_serial = None
- for i, goal in enumerate(goals):
- task_item = cls._create_queue_item(
- goal,
- i,
- now,
- endpoint,
- single_session_id,
- profile,
- expected_output,
- enable_outputter,
- locked_app_package,
- app_path,
- device_serial,
- ingress,
- conversation_id,
- verification_level=verification_level,
- explorer_mode=explorer_mode,
- )
- state.queue_items.append(task_item)
- enqueued_tasks.append(task_item)
- cls._broadcast_startup_progress(
- task_item["session_id"], "queued", "Task received and queued"
- )
-
- # Wake worker immediately
- state.wake_event.set()
-
- return {
- "status": "queued" if state.is_running else "started",
- "tasks": enqueued_tasks,
- "enqueued_count": len(goals),
- "total_queued": len(state.queue_tasks),
- }
-
- @staticmethod
- def _clear_pause_file() -> None:
- """Remove a leftover pause marker after a stop request."""
- if PAUSE_FILE.exists():
- try:
- PAUSE_FILE.unlink()
- except OSError:
- # Best-effort cleanup of the pause marker; a leftover file
- # only pauses until the next resume request.
- pass
-
- @classmethod
- def _terminate_all_device_owners(cls) -> None:
- """Terminate every device lock owner and persist each cancellation."""
- active_owners: dict[str, Any] = {}
- try:
- active_owners = DeviceExecutionLock.get_active_owners()
- except OSError as exc:
- # Unreadable lock dir: fall back to the single-owner probe below.
- print(f"[stop_tasks] Could not enumerate device owners: {exc}")
- fallback = DeviceExecutionLock.get_active_owner()
- if fallback and not active_owners:
- active_owners["default"] = fallback
-
- for dev_owner in list(active_owners.values()):
- if dev_owner and DeviceExecutionLock.is_active_owner(dev_owner):
- cls._stop_worker_gracefully(
- dev_owner.pid,
- dev_owner.process_created_at,
- session_id=dev_owner.session_id,
- )
- DeviceExecutionLock.cleanup_stale_locks(dev_owner.device_id)
- sid = dev_owner.session_id
- if sid:
- session_repo.update_session_status(str(sid), "cancelled", time.time())
- try:
- is_mcp = bool(dev_owner and dev_owner.ingress == "mcp")
- if is_mcp or trace_store.read_status(str(sid)):
- trace_store.update_trace_status(
- str(sid),
- "cancelled",
- error="Task stopped from the Artemis frontend.",
- )
- except OSError as exc:
- print(
- f"[stop_tasks] Could not persist cancelled MCP status for {sid}: {exc}"
- )
- cls._broadcast_event(
- "session_ended",
- {
- "session_id": sid,
- "status": "cancelled",
- "was_stopped_manually": True,
- },
- )
-
- @classmethod
- def _kill_all_local_runs(cls) -> None:
- """Mark every locally-managed run manually stopped and kill its process."""
- for run_key, run in list(state.active_runs.items()):
- # Mark each run individually: the global "stopped manually"
- # boolean was shared process-wide and polluted the terminal
- # status of unrelated concurrent runs.
- state.manually_stopped_run_ids.add(str(run_key))
- # Also track it per session so each finalizer resolves its
- # terminal status as cancelled.
- state.cancelled_session_ids.add(str(run_key))
- run_proc = run.get("process")
- if run_proc is not None and run_proc.returncode is None:
- if not cls._stop_proc_gracefully(run_proc, run.get("session_id") or run_key):
- try:
- run_proc.kill()
- except (ProcessLookupError, OSError):
- # Process already exited between the check and the kill.
- pass
- # active_runs entries are popped by each run's finalizer once the
- # process exit is observed; clearing them here would free the device
- # slots before the processes are actually gone.
- if state.current_process:
- if not cls._stop_proc_gracefully(state.current_process, state.active_session_id):
- try:
- state.current_process.kill()
- except (ProcessLookupError, OSError):
- # Process already exited between the check and the kill.
- pass
- state.current_process = None
-
- @classmethod
- def _stop_all_tasks(cls) -> bool:
- """Terminate all active device owners and clear pending queue submissions."""
- # 1. Cancel local queue reservations
- for item in state.queue_items:
- if isinstance(item, dict) and item.get("status") != "running":
- DeviceExecutionLock.cancel_reservation(item.get("queue_ticket"))
- state.clear_queue()
-
- # 2. Terminate all active owners across all devices
- cls._terminate_all_device_owners()
- cls._kill_all_local_runs()
-
- state.active_connections.clear()
- state.active_session_id = None
- state.current_goal = None
- state.current_profile = None
-
- cls._clear_pause_file()
-
- cls.ensure_worker_running()
- state.wake_event.set()
- return True
-
- @classmethod
- def _resolve_stop_owner(
- cls,
- active_owners: dict[str, Any],
- target_sid: str | None,
- target_device: str | None,
- ) -> Any:
- """Resolve the device lock owner targeted by this stop request, if any."""
- owner = None
- if target_sid:
- for dev_owner in active_owners.values():
- if dev_owner.session_id and str(dev_owner.session_id) == target_sid:
- owner = dev_owner
- break
- if owner is None and target_device:
- clean_target_dev = DeviceExecutionLock._normalize_device_id(target_device)
- for dev_key, dev_owner in active_owners.items():
- if dev_key == clean_target_dev or dev_owner.device_id == target_device:
- owner = dev_owner
- break
- if owner is None:
- # Covers scoped/legacy lock records that get_active_owners cannot
- # key by session or device. A stale owner for a different session
- # is never adopted: with no live owner, a session-targeted stop
- # falls through to the queue/session-repository fallbacks below.
- fallback_owner = DeviceExecutionLock.get_active_owner(target_device)
- if fallback_owner and (
- not target_sid
- or (fallback_owner.session_id and str(fallback_owner.session_id) == target_sid)
- ):
- owner = fallback_owner
- return owner
-
- @classmethod
- def _resolve_local_run(
- cls, target_sid: str | None, target_device: str | None
- ) -> tuple[str | None, dict[str, Any] | None, Any]:
- """Resolve the locally-managed run for this target.
-
- Concurrent scheduling keeps one subprocess per run in
- state.active_runs. Returns ``(local_run_key, local_run, local_proc)``.
- """
- local_run = None
- local_run_key: str | None = None
- if target_sid:
- local_run = state.active_runs.get(target_sid)
- if local_run is not None:
- local_run_key = target_sid
- elif target_device:
- local_run_key, local_run = next(
- (
- (key, run)
- for key, run in state.active_runs.items()
- if run.get("device_id") and str(run["device_id"]) == target_device
- ),
- (None, None),
- )
- elif len(state.active_runs) == 1:
- # Untargeted stop (legacy single-device gesture): with exactly one
- # live run the target is unambiguous.
- local_run_key, local_run = next(iter(state.active_runs.items()))
- local_proc = (local_run or {}).get("process")
- if local_proc is None and not target_sid and not target_device:
- # Untargeted legacy fallback only: the last-started process may
- # stand in when no per-run entry resolved. An explicitly targeted
- # stop that matched no run must never grab current_process -- it
- # mirrors the newest run, which can be an unrelated concurrent one.
- local_proc = state.current_process
- if local_run is None and local_proc is not None:
- # Legacy path: current_process without a resolved run entry. Find
- # the run that owns this process so a manual stop can be attributed
- # to it instead of to the whole scheduler.
- local_run_key = next(
- (
- str(key)
- for key, run in state.active_runs.items()
- if run.get("process") is local_proc
- ),
- None,
- )
- return local_run_key, local_run, local_proc
-
- @classmethod
- def _find_target_queue_item(cls, target_sid: str | None) -> dict[str, Any] | None:
- """Find the running (preferred) or pending queue item for this target."""
- running_item = next(
- (
- item
- for item in state.queue_items
- if isinstance(item, dict)
- and item.get("status") == "running"
- and (not target_sid or str(item.get("session_id")) == target_sid)
- ),
- None,
- )
- return running_item or next(
- (
- item
- for item in state.queue_items
- if isinstance(item, dict)
- and item.get("status") == "pending"
- and (not target_sid or str(item.get("session_id")) == target_sid)
- ),
- None,
- )
-
- @classmethod
- def _resolve_stopped_session_id(
- cls,
- owner: Any,
- target_sid: str | None,
- local_run_key: str | None,
- is_local_owner: bool,
- local_item: dict[str, Any] | None,
- ) -> Any:
- """Attribute the stop request to a session id, if one can be resolved."""
- # active_session_id mirrors the most recently launched run, so with
- # concurrent runs the resolved local run key (== its session id) must
- # take precedence to avoid attributing the stop to an unrelated run.
- return (
- owner.session_id
- if owner and owner.session_id
- else target_sid
- if target_sid
- else local_run_key
- if local_run_key is not None
- else state.active_session_id
- if is_local_owner
- else local_item.get("session_id")
- if local_item
- else None
- )
-
- @classmethod
- def _terminate_stop_target(
- cls,
- owner: Any,
- owner_record_exists: bool,
- target_sid: str | None,
- local_run: dict[str, Any] | None,
- local_run_key: str | None,
- local_proc: Any,
- local_pid: Any,
- local_item: dict[str, Any] | None,
- is_local_owner: bool,
- ) -> tuple[bool, bool, bool] | None:
- """Terminate the resolved stop target.
-
- Returns ``(stopped, is_local_owner, reservation_cancelled)``, or
- ``None`` when the caller must refuse the stop outright.
- """
- stopped = False
- reservation_cancelled = False
- if owner and DeviceExecutionLock.is_active_owner(owner):
- stopped, _deferred = cls._stop_worker_gracefully(
- owner.pid,
- owner.process_created_at,
- session_id=owner.session_id or target_sid,
- )
- DeviceExecutionLock.cleanup_stale_locks(owner.device_id)
- elif owner is None and owner_record_exists and not target_sid:
- # Never fall back to a frontend PID while another process has an
- # owner record that is still being published or cannot be parsed.
- return None
- elif (
- owner is None
- and local_proc is not None
- and (
- local_run is not None
- or not target_sid
- or str(state.active_session_id) == target_sid
- )
- ):
- # The locally-managed worker can be stopped during its short
- # initialization window before Agent acquires the device lease.
- if target_sid:
- state.cancelled_session_ids.add(target_sid)
- elif local_run_key is not None:
- state.manually_stopped_run_ids.add(str(local_run_key))
- deferred = False
- if local_pid:
- try:
- stopped, deferred = cls._stop_worker_gracefully(
- local_pid,
- session_id=target_sid or (local_run or {}).get("session_id"),
- )
- except Exception:
- stopped = False
- if not deferred:
- try:
- local_proc.kill()
- stopped = True
- except (ProcessLookupError, OSError):
- # Process already exited; the stop above may have got it.
- pass
- stopped = True
- is_local_owner = True
- elif (
- owner is None
- and local_item
- and (not target_sid or str(local_item.get("session_id")) == target_sid)
- ):
- # Cancel a frontend submission before its worker has started. This
- # does not touch pending reservations created by other ingresses.
- DeviceExecutionLock.cancel_reservation(local_item.get("queue_ticket"))
- reservation_cancelled = True
- stopped = True
- is_local_owner = True
- elif target_sid:
- # Fallback 1: check global device queue
- global_queued = DeviceExecutionLock.get_queued_tasks()
- for q_item in global_queued:
- if str(q_item.get("session_id")) == target_sid:
- # get_queued_tasks exposes the reservation as "token".
- stopped = DeviceExecutionLock.cancel_reservation(q_item.get("token"))
- break
- # Fallback 2: check session repository for a running session with a live worker PID
- if not stopped:
- row = session_repo.get_session_by_id(target_sid)
- if row and row.get("status") == "running":
- row_pid = row.get("pid")
- if row_pid and session_repo.process_is_alive(row_pid):
- try:
- cls._stop_worker_gracefully(int(row_pid), session_id=target_sid)
- except Exception as exc:
- # Report it: a surviving worker keeps the device busy.
- print(f"[stop_tasks] Could not terminate worker pid {row_pid}: {exc}")
- DeviceExecutionLock.cleanup_stale_locks()
- stopped = True
- return stopped, is_local_owner, reservation_cancelled
-
- @classmethod
- def _finalize_targeted_stop(
- cls,
- stopped_session_id: Any,
- owner: Any,
- owner_pid: Any,
- is_local_owner: bool,
- local_run_key: str | None,
- local_proc: Any,
- ) -> None:
- """Propagate the stop into shared state, the DB, and event subscribers."""
- if is_local_owner:
- if stopped_session_id:
- # Per-session cancellation: the run's own finalizer resolves the
- # terminal status without affecting other concurrent runs.
- state.cancelled_session_ids.add(str(stopped_session_id))
- elif local_run_key is not None:
- state.manually_stopped_run_ids.add(str(local_run_key))
- if local_proc is not None and state.current_process is local_proc:
- state.current_process = None
-
- for sid, conn_info in list(state.active_connections.items()):
- if (stopped_session_id and str(sid) == str(stopped_session_id)) or (
- owner_pid and conn_info.get("pid") == owner_pid
- ):
- state.active_connections.pop(sid, None)
-
- if stopped_session_id:
- session_repo.update_session_status(str(stopped_session_id), "cancelled", time.time())
- try:
- is_mcp = bool(owner and owner.ingress == "mcp")
- if is_mcp or trace_store.read_status(str(stopped_session_id)):
- trace_store.update_trace_status(
- str(stopped_session_id),
- "cancelled",
- error="Task stopped from the Artemis frontend.",
- )
- except Exception as exc:
- print(f"Failed to update MCP cancellation status: {exc}")
- cls._broadcast_event(
- "session_ended",
- {
- "session_id": stopped_session_id,
- "status": "cancelled",
- "was_stopped_manually": True,
- },
- )
-
- if state.active_session_id and (
- not stopped_session_id or str(state.active_session_id) == str(stopped_session_id)
- ):
- state.active_session_id = None
- state.current_goal = None
- state.current_profile = None
-
- @classmethod
- def _remove_stopped_queue_item(
- cls, stopped_session_id: Any, reservation_cancelled: bool
- ) -> None:
- """Drop the stopped session's queue item and cancel its reservation."""
- if not stopped_session_id:
- return
- stopped_item = next(
- (
- item
- for item in state.queue_items
- if isinstance(item, dict) and str(item.get("session_id")) == str(stopped_session_id)
- ),
- None,
- )
- if stopped_item and not reservation_cancelled:
- DeviceExecutionLock.cancel_reservation(stopped_item.get("queue_ticket"))
- state.queue_items = [
- item
- for item in state.queue_items
- if not (
- isinstance(item, dict) and str(item.get("session_id")) == str(stopped_session_id)
- )
- ]
-
- @classmethod
- def _stop_targeted_task(cls, target_sid: str | None, target_device: str | None) -> bool:
- """Stop a specific task (or default single-device active task)."""
- active_owners = {}
- try:
- active_owners = DeviceExecutionLock.get_active_owners()
- except OSError as exc:
- # Unreadable lock dir: the local-process fallbacks below still apply.
- print(f"[stop_tasks] Could not enumerate device owners: {exc}")
-
- owner = cls._resolve_stop_owner(active_owners, target_sid, target_device)
-
- owner_record_exists = DeviceExecutionLock.has_owner_record(target_device)
- owner_pid = owner.pid if owner else None
-
- local_run_key, local_run, local_proc = cls._resolve_local_run(target_sid, target_device)
- local_pid = getattr(local_proc, "pid", None)
- is_local_owner = bool(owner_pid and local_pid and owner_pid == local_pid)
-
- local_item = cls._find_target_queue_item(target_sid)
- stopped_session_id = cls._resolve_stopped_session_id(
- owner, target_sid, local_run_key, is_local_owner, local_item
- )
-
- outcome = cls._terminate_stop_target(
- owner,
- owner_record_exists,
- target_sid,
- local_run,
- local_run_key,
- local_proc,
- local_pid,
- local_item,
- is_local_owner,
- )
- if outcome is None:
- return False
- stopped, is_local_owner, reservation_cancelled = outcome
-
- if not stopped and not target_sid:
- return False
-
- cls._finalize_targeted_stop(
- stopped_session_id,
- owner,
- owner_pid,
- is_local_owner,
- local_run_key,
- local_proc,
- )
- cls._remove_stopped_queue_item(stopped_session_id, reservation_cancelled)
-
- cls._clear_pause_file()
-
- cls.ensure_worker_running()
- state.wake_event.set()
- return True
-
- @classmethod
- def stop_tasks(
- cls,
- clear_all: bool = False,
- session_id: str | None = None,
- device_id: str | None = None,
- ) -> bool:
- """Stop the active task controlling a mobile device or all tasks.
-
- The active lease is shared by frontend, MCP, CLI, SDK, and other UI
- processes across all connected devices.
- - If ``session_id`` or ``device_id`` is specified, only the corresponding
- running or queued task is cancelled.
- - If ``clear_all`` is requested, all active device owners are terminated
- and pending queue submissions are cleared.
- - If no specific task is specified and ``clear_all`` is False, stops the
- currently active task in single-device mode for backward compatibility.
- """
- target_sid = str(session_id).strip() if session_id else None
- target_device = str(device_id).strip() if device_id else None
-
- if clear_all:
- return cls._stop_all_tasks()
-
- return cls._stop_targeted_task(target_sid, target_device)
-
- @classmethod
- def resume_task(cls) -> bool:
- if PAUSE_FILE.exists():
- PAUSE_FILE.unlink()
- return True
- return False
-
- @classmethod
- def recover_orphaned_recordings_on_launch(cls) -> int:
- """Finalize recordings left behind by workers that died with the daemon.
-
- The per-run finalizer handles workers that exit while the daemon is up;
- this sweep covers everything else (daemon crash, machine reboot, tasks
- cancelled before this recovery path existed). Returns the number of
- recordings published.
- """
- recovered = 0
- try:
- pending = session_repo.get_unfinalized_video_recordings()
- except Exception as exc:
- print(f"[ServerStartup] Could not enumerate unfinalized recordings: {exc}")
- return 0
- for row in pending:
- sess_id = row.get("session_id")
- try:
- final_path = media_service.recover_orphaned_recording(
- row.get("local_video_path"),
- row.get("start_time") or row.get("session_start_time"),
- )
- except Exception as exc:
- print(f"[ServerStartup] Recording recovery failed for {sess_id}: {exc}")
- continue
- if not final_path:
- continue
- if session_repo.mark_recording_ready(str(sess_id), str(final_path)):
- recovered += 1
- print(f"[ServerStartup] Recovered recording for session {sess_id}: {final_path}")
- if recovered:
- print(f"[ServerStartup] Recovered {recovered} orphaned recording(s).")
- return recovered
-
- @staticmethod
- def archive_older_replays_on_launch():
- """Archives all existing step replay output folders on server launch."""
- if TEST_OUTPUTS_DIR.exists():
- archive_dir = TEST_DATA_DIR / "older"
- for item in TEST_OUTPUTS_DIR.iterdir():
- if item.is_dir():
- archive_dir.mkdir(parents=True, exist_ok=True)
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- target_name = f"{item.name}_{timestamp}"
- target_path = archive_dir / target_name
-
- counter = 1
- while target_path.exists():
- target_name = f"{item.name}_{timestamp}_{counter}"
- target_path = archive_dir / target_name
- counter += 1
-
- print(
- f"Archiving older replay record on server launch: "
- f"{item.name} -> {target_path}"
- )
- try:
- shutil.move(str(item), str(target_path))
- except Exception as e:
- print(
- f"Warning: Failed to archive {item.name} during server launch: {e}",
- file=sys.stderr,
- )
-
- @staticmethod
- def verify_chunks_exist_on_launch(replay_manager):
- """Verifies that chunked directories exist for all sessions in the database."""
- print("Verifying session chunks on launch...")
- try:
- sessions = session_repo.get_all_sessions()
- session_ids = [s.get("session_id") for s in sessions if s.get("session_id")]
- print(f"Found {len(session_ids)} sessions in database to verify.")
- for session_id in session_ids:
- replay_manager._ensure_session_chunked(str(session_id))
- except Exception as e:
- print(
- f"Warning: Failed to verify chunks during server launch: {e}",
- file=sys.stderr,
- )
-
-
-task_queue_service = TaskQueueService()
diff --git a/apps/admin_console/services/worker_process_io.py b/apps/admin_console/services/worker_process_io.py
deleted file mode 100644
index f94ab8282..000000000
--- a/apps/admin_console/services/worker_process_io.py
+++ /dev/null
@@ -1,144 +0,0 @@
-# Copyright 2026 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Low-level worker subprocess I/O plumbing for the task queue.
-
-These helpers are deliberately free of admin-console state: they deal only
-with spawning options, output forwarding, and process-exit observation for a
-single worker subprocess. TaskQueueService exposes them as its private
-static methods so existing callers and tests keep working unchanged.
-"""
-
-import asyncio
-import codecs
-import os
-import psutil
-import subprocess
-import sys
-from typing import Any
-
-
-def subprocess_creation_kwargs() -> dict[str, Any]:
- """Isolate task workers from the UI server's Windows console.
-
- A new process group alone is insufficient on Windows: the worker still
- shares the parent's console, so a CTRL_C_EVENT generated anywhere in
- that console can reach the UI server. CREATE_NO_WINDOW removes that
- shared console boundary.
-
- Output is captured on every platform so it can be forwarded to the
- server terminal and teed into the trace's stdout.log (the daemon itself
- is often spawned with its stdio discarded, so inheriting would lose the
- worker's logs entirely).
- """
- kwargs: dict[str, Any] = {
- "stdout": asyncio.subprocess.PIPE,
- "stderr": asyncio.subprocess.STDOUT,
- }
- if sys.platform == "win32":
- kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_NO_WINDOW
- return kwargs
-
-
-async def forward_worker_output(
- stream: asyncio.StreamReader | None, log_path: str | None = None
-) -> None:
- """Forward a worker's combined output without corrupting UTF-8.
-
- When ``log_path`` is given, the output is also teed into that file so
- the trace's advertised stdout.log actually exists for diagnostics.
- """
- if stream is None:
- return
-
- log_file = None
- if log_path:
- try:
- os.makedirs(os.path.dirname(log_path), exist_ok=True)
- log_file = open(log_path, "w", buffering=1, encoding="utf-8", errors="replace")
- except Exception as exc:
- print(f"[QueueWorker] Could not open worker log file '{log_path}': {exc}")
-
- def _emit(text: str) -> None:
- sys.stdout.write(text)
- sys.stdout.flush()
- if log_file is not None:
- try:
- log_file.write(text)
- except (OSError, ValueError):
- # Best-effort tee into stdout.log; console output above
- # already carried the text.
- pass
-
- try:
- decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
- while True:
- chunk = await stream.read(4096)
- if not chunk:
- break
- text = decoder.decode(chunk)
- if text:
- _emit(text)
-
- tail = decoder.decode(b"", final=True)
- if tail:
- _emit(tail)
- finally:
- if log_file is not None:
- try:
- log_file.close()
- except OSError:
- # Flush-on-close of the best-effort tee failed; nothing to do.
- pass
-
-
-async def finish_output_forwarder(output_task: asyncio.Task[None] | None) -> None:
- """Drain final worker output without allowing inherited handles to stall the queue."""
- if output_task is None:
- return
- try:
- await asyncio.wait_for(asyncio.shield(output_task), timeout=2.0)
- except asyncio.CancelledError:
- if output_task.cancelled():
- return
- raise
- except TimeoutError:
- output_task.cancel()
- try:
- await output_task
- except asyncio.CancelledError:
- pass
- except Exception as exc:
- print(f"[QueueWorker] Failed to forward detached worker output: {exc}")
-
-
-async def wait_for_worker_process(proc: asyncio.subprocess.Process) -> int:
- """Wait for worker process to exit, with watchdog fallback if PID was reaped externally."""
- while True:
- try:
- return await asyncio.wait_for(proc.wait(), timeout=1.0)
- except TimeoutError:
- pid = getattr(proc, "pid", None)
- if pid:
- try:
- p = psutil.Process(pid)
- if not p.is_running() or p.status() == psutil.STATUS_ZOMBIE:
- return proc.returncode if proc.returncode is not None else -15
- except (psutil.NoSuchProcess, ProcessLookupError):
- return proc.returncode if proc.returncode is not None else -15
- except psutil.Error:
- # Transient probe failure (e.g. AccessDenied): keep waiting.
- pass
- else:
- return proc.returncode if proc.returncode is not None else -15
diff --git a/apps/showcase_ui/README.md b/apps/showcase_ui/README.md
deleted file mode 100644
index 09bad9861..000000000
--- a/apps/showcase_ui/README.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# ✨ Artemis Showcase & User Workspace UI
-
-A modern, highly aesthetic presentation and interaction frontend built with **Angular 19** and **SCSS** for showcasing the Artemis mobile autonomous agent.
-
-## 🎨 Visual Design Highlights
-
-* **Aurora Glow Dynamics**: Ambient background lighting with smooth gradients.
-* **Glassmorphism Aesthetic**: Translucent frosted-glass panels with subtle borders.
-* **Real-time Dual-Pane Workspace**:
- - **Left Pane (`app-agent-stream`)**: Live streaming of Agent thought steps, plan breakdowns, tool actions, and status.
- - **Right Pane (`app-chat-interface`)**: Natural language chat input sidebar with interactive feedback and status pills.
-
-## 🚀 How to Run
-
-### Prerequisites
-- Node.js (>= 18)
-- Backend API running on `http://localhost:8000` (via `apps/admin_console` or `artemis ui`)
-
-### Development Server
-```bash
-cd apps/showcase_ui
-npm install
-npm start
-```
-This runs `ng serve --proxy-config proxy.conf.json` on **`http://localhost:4200/`**.
-
-### Production Build
-```bash
-npm run build
-```
-Build artifacts will be emitted to `dist/`, which can be served statically by `apps.admin_console` or `artemis ui`.
diff --git a/apps/showcase_ui/angular.json b/apps/showcase_ui/angular.json
deleted file mode 100644
index 1ddf7d1d9..000000000
--- a/apps/showcase_ui/angular.json
+++ /dev/null
@@ -1,132 +0,0 @@
-{
- "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
- "version": 1,
- "cli": {
- "packageManager": "npm",
- "analytics": false
- },
- "newProjectRoot": "projects",
- "projects": {
- "frontend": {
- "projectType": "application",
- "schematics": {
- "@schematics/angular:component": {
- "style": "scss"
- }
- },
- "root": "",
- "sourceRoot": "src",
- "prefix": "app",
- "architect": {
- "build": {
- "builder": "@angular/build:application",
- "options": {
- "outputPath": "dist/frontend",
- "index": "src/index.html",
- "browser": "src/main.ts",
- "polyfills": [
- "zone.js"
- ],
- "tsConfig": "tsconfig.app.json",
- "inlineStyleLanguage": "scss",
- "assets": [
- {
- "glob": "**/*",
- "input": "public"
- }
- ],
- "styles": [
- "src/styles.scss"
- ],
- "scripts": []
- },
- "configurations": {
- "production": {
- "budgets": [
- {
- "type": "initial",
- "maximumWarning": "2MB",
- "maximumError": "4MB"
- },
- {
- "type": "anyComponentStyle",
- "maximumWarning": "200kB",
- "maximumError": "400kB"
- }
- ],
- "outputHashing": "all"
- },
- "development": {
- "optimization": false,
- "extractLicenses": false,
- "sourceMap": true
- }
- },
- "defaultConfiguration": "production"
- },
- "serve": {
- "builder": "@angular/build:dev-server",
- "configurations": {
- "production": {
- "buildTarget": "frontend:build:production"
- },
- "development": {
- "buildTarget": "frontend:build:development"
- }
- },
- "defaultConfiguration": "development"
- },
- "extract-i18n": {
- "builder": "@angular/build:extract-i18n"
- },
- "test": {
- "builder": "@angular/build:karma",
- "options": {
- "polyfills": [
- "zone.js",
- "zone.js/testing"
- ],
- "tsConfig": "tsconfig.spec.json",
- "inlineStyleLanguage": "scss",
- "assets": [
- {
- "glob": "**/*",
- "input": "public"
- }
- ],
- "styles": [
- "src/styles.scss"
- ],
- "scripts": []
- }
- }
- }
- }
- },
- "schematics": {
- "@schematics/angular:component": {
- "type": "component"
- },
- "@schematics/angular:directive": {
- "type": "directive"
- },
- "@schematics/angular:service": {
- "type": "service"
- },
- "@schematics/angular:guard": {
- "typeSeparator": "."
- },
- "@schematics/angular:interceptor": {
- "typeSeparator": "."
- },
- "@schematics/angular:module": {
- "typeSeparator": "."
- },
- "@schematics/angular:pipe": {
- "typeSeparator": "."
- },
- "@schematics/angular:resolver": {
- "typeSeparator": "."
- }
- }
-}
diff --git a/apps/showcase_ui/package-lock.json b/apps/showcase_ui/package-lock.json
deleted file mode 100644
index bab81b328..000000000
--- a/apps/showcase_ui/package-lock.json
+++ /dev/null
@@ -1,9472 +0,0 @@
-{
- "name": "artemis-showcase-ui",
- "version": "1.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "artemis-showcase-ui",
- "version": "1.0.0",
- "dependencies": {
- "@angular/cdk": "^22.1.5",
- "@angular/common": "^22.1.4",
- "@angular/compiler": "^22.1.4",
- "@angular/core": "^22.1.4",
- "@angular/forms": "^22.1.4",
- "@angular/platform-browser": "^22.1.4",
- "@angular/platform-browser-dynamic": "^22.1.4",
- "@angular/router": "^22.1.4",
- "rxjs": "~7.8.0",
- "tslib": "^2.3.0",
- "zone.js": "~0.15.0"
- },
- "devDependencies": {
- "@angular/build": "^22.1.6",
- "@angular/cli": "^22.1.6",
- "@angular/compiler-cli": "^22.1.4",
- "@types/jasmine": "~5.1.0",
- "istanbul-lib-instrument": "^6.0.3",
- "jasmine-core": "~5.6.0",
- "karma": "~6.4.0",
- "karma-chrome-launcher": "~3.2.0",
- "karma-coverage": "~2.2.0",
- "karma-jasmine": "~5.1.0",
- "karma-jasmine-html-reporter": "~2.1.0",
- "typescript": "~6.0.3"
- }
- },
- "node_modules/@ampproject/remapping": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
- "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@angular-devkit/architect": {
- "version": "0.2201.6",
- "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2201.6.tgz",
- "integrity": "sha512-oGQEdof2/1bZk58PN9dvpGi8pvtbgE5vkP1xRtCqN8dSVxwre1l0pKynOj/Uge1KxjCvs4c1QrQu0vsAnY0vpg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@angular-devkit/core": "22.1.6",
- "rxjs": "7.8.2"
- },
- "bin": {
- "architect": "bin/cli.js"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- }
- },
- "node_modules/@angular-devkit/core": {
- "version": "22.1.6",
- "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.1.6.tgz",
- "integrity": "sha512-KLBsZoc2RhOy0xSdeYcLoSOqV/fBP3feBmhY9o12ry2IuyW/17sCmWr6XbIfTIYQN6M98Y1ewmwVNFsvV5b0gA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ajv": "8.20.0",
- "ajv-formats": "3.0.1",
- "jsonc-parser": "3.3.1",
- "picomatch": "4.0.5",
- "rxjs": "7.8.2",
- "source-map": "0.7.6"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- },
- "peerDependencies": {
- "chokidar": "^5.0.0"
- },
- "peerDependenciesMeta": {
- "chokidar": {
- "optional": true
- }
- }
- },
- "node_modules/@angular-devkit/schematics": {
- "version": "22.1.6",
- "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.1.6.tgz",
- "integrity": "sha512-IbDO9KbQyQm20uinsfT8d9ZtQw41/WVutI/65mAw39WZfN8Ky+MOgOUJCGLiSccMprvZ9YdodBTnN9bLH149Ag==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@angular-devkit/core": "22.1.6",
- "jsonc-parser": "3.3.1",
- "magic-string": "1.0.0",
- "ora": "9.4.1",
- "rxjs": "7.8.2"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- }
- },
- "node_modules/@angular/build": {
- "version": "22.1.6",
- "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.1.6.tgz",
- "integrity": "sha512-J7JBd3hDAV4dlGNMavDC0KjtvbDd4KOddaom1inwkq92tYnD1ljmhDDigAsUBnRvQhJ82cvagD8zi0s9Ki4rTQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@ampproject/remapping": "2.3.0",
- "@angular-devkit/architect": "0.2201.6",
- "@babel/core": "8.0.1",
- "@babel/helper-annotate-as-pure": "8.0.0",
- "@babel/helper-split-export-declaration": "7.24.7",
- "@inquirer/confirm": "6.1.1",
- "@vitejs/plugin-basic-ssl": "2.3.0",
- "beasties": "0.4.3",
- "browserslist": "^4.26.0",
- "esbuild": "0.28.2",
- "https-proxy-agent": "9.1.0",
- "jsonc-parser": "3.3.1",
- "listr2": "11.0.0",
- "magic-string": "1.0.0",
- "mrmime": "2.0.1",
- "oxc-parser": "0.142.0",
- "parse5-html-rewriting-stream": "8.0.1",
- "picomatch": "4.0.5",
- "piscina": "5.2.0",
- "rolldown": "1.2.0",
- "sass": "1.101.0",
- "semver": "7.8.5",
- "source-map-support": "0.5.21",
- "tinyglobby": "0.2.17",
- "vite": "8.1.5",
- "watchpack": "2.5.2"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- },
- "optionalDependencies": {
- "lmdb": "3.5.6"
- },
- "peerDependencies": {
- "@angular/compiler": "^22.0.0",
- "@angular/compiler-cli": "^22.0.0",
- "@angular/core": "^22.0.0",
- "@angular/localize": "^22.0.0",
- "@angular/platform-browser": "^22.0.0",
- "@angular/platform-server": "^22.0.0",
- "@angular/service-worker": "^22.0.0",
- "@angular/ssr": "^22.1.6",
- "istanbul-lib-instrument": "^6.0.0",
- "karma": "^6.4.0",
- "less": "^4.2.0",
- "ng-packagr": "^22.0.0",
- "postcss": "^8.4.0",
- "rollup": "^4.0.0",
- "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0",
- "tslib": "^2.3.0",
- "typescript": ">=6.0 <6.1",
- "vitest": "^4.0.8"
- },
- "peerDependenciesMeta": {
- "@angular/core": {
- "optional": true
- },
- "@angular/localize": {
- "optional": true
- },
- "@angular/platform-browser": {
- "optional": true
- },
- "@angular/platform-server": {
- "optional": true
- },
- "@angular/service-worker": {
- "optional": true
- },
- "@angular/ssr": {
- "optional": true
- },
- "istanbul-lib-instrument": {
- "optional": true
- },
- "karma": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "ng-packagr": {
- "optional": true
- },
- "postcss": {
- "optional": true
- },
- "rollup": {
- "optional": true
- },
- "tailwindcss": {
- "optional": true
- },
- "vitest": {
- "optional": true
- }
- }
- },
- "node_modules/@angular/cdk": {
- "version": "22.1.5",
- "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-22.1.5.tgz",
- "integrity": "sha512-1tYAeI5KKIqNGNvyDuMM0Drs2FU3M4B25Q1BlAnwtZe3wvrpVETAGXgHaBX3Hs/pvWfgMNwgbcHkxlKVUBNXMA==",
- "license": "MIT",
- "dependencies": {
- "parse5": "^8.0.0",
- "tslib": "^2.3.0"
- },
- "peerDependencies": {
- "@angular/common": "^22.0.0 || ^23.0.0",
- "@angular/core": "^22.0.0 || ^23.0.0",
- "@angular/forms": "^22.0.0 || ^23.0.0",
- "@angular/platform-browser": "^22.0.0 || ^23.0.0",
- "rxjs": "^6.5.3 || ^7.4.0"
- }
- },
- "node_modules/@angular/cli": {
- "version": "22.1.6",
- "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.1.6.tgz",
- "integrity": "sha512-HT3OzYkSpCyXMTgD5G1tsS7vkrY8cYJ/JgSjRjrpwOAooSMtKF1hv7BgBcn79sKg4eaiPLrDpQLXP93vHxgSFg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@angular-devkit/architect": "0.2201.6",
- "@angular-devkit/core": "22.1.6",
- "@angular-devkit/schematics": "22.1.6",
- "@inquirer/prompts": "8.5.2",
- "@listr2/prompt-adapter-inquirer": "4.2.5",
- "@modelcontextprotocol/sdk": "1.30.0",
- "@schematics/angular": "22.1.6",
- "jsonc-parser": "3.3.1",
- "listr2": "11.0.0",
- "npm-package-arg": "14.0.0",
- "parse5-html-rewriting-stream": "8.0.1",
- "semver": "7.8.5",
- "yargs": "18.1.0",
- "zod": "4.4.3"
- },
- "bin": {
- "ng": "bin/ng.js"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- }
- },
- "node_modules/@angular/common": {
- "version": "22.1.4",
- "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.1.4.tgz",
- "integrity": "sha512-UtWCloA/d0I5twV9Lu/Fcc7T6uQb40f/k0Hj5HLohIsiLTyvXVxWINBfa7LWWLtcq/0UOIY/yCs9Zq9Sj4dWqA==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.3.0"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
- },
- "peerDependencies": {
- "@angular/core": "22.1.4",
- "rxjs": "^6.5.3 || ^7.4.0"
- }
- },
- "node_modules/@angular/compiler": {
- "version": "22.1.4",
- "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-22.1.4.tgz",
- "integrity": "sha512-o+W0shjR5KmPPWchFmqN9bLINYAy14zIW//g9T5WqKDMOqjt4IbeY6AKOoE1db/dXffprLEH9nyPOmi19iKNxg==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.3.0"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
- }
- },
- "node_modules/@angular/compiler-cli": {
- "version": "22.1.4",
- "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.1.4.tgz",
- "integrity": "sha512-h9CcVG614TJLbk+dswlzSjYARYJh2MLOEwcnpt9zUGtOffhrrSS9BFJGjSwzdDSVM3J5mvf5ZuW+0hPAuRRoOA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "8.0.1",
- "@jridgewell/sourcemap-codec": "^1.4.14",
- "chokidar": "^5.0.0",
- "convert-source-map": "^1.5.1",
- "reflect-metadata": "^0.2.0",
- "semver": "^7.0.0",
- "tslib": "^2.3.0",
- "yargs": "^18.0.0"
- },
- "bin": {
- "ng-xi18n": "bundles/src/bin/ng_xi18n.js",
- "ngc": "bundles/src/bin/ngc.js"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
- },
- "peerDependencies": {
- "@angular/compiler": "22.1.4",
- "typescript": ">=6.0 <6.1"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/@angular/core": {
- "version": "22.1.4",
- "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.1.4.tgz",
- "integrity": "sha512-0dW+lw8nHo6RGBUOXtjs02vjHIPJ8rEbw4fd0NH3Dii4p+gIB4ZaTw+o7Se3Nf4m1oZ8+2qAYmWVHXlfhKcCDg==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.3.0"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
- },
- "peerDependencies": {
- "@angular/compiler": "22.1.4",
- "rxjs": "^6.5.3 || ^7.4.0",
- "zone.js": "~0.15.0 || ~0.16.0"
- },
- "peerDependenciesMeta": {
- "@angular/compiler": {
- "optional": true
- },
- "zone.js": {
- "optional": true
- }
- }
- },
- "node_modules/@angular/forms": {
- "version": "22.1.4",
- "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.1.4.tgz",
- "integrity": "sha512-w8ZXe4oURrb/DS5zAFrR4AmGXC84sdLeOKXRM76ndRRgSCzyRLEe4wDU1KHnDejNVIGvoSZYJJjOajG1lpj8TA==",
- "license": "MIT",
- "dependencies": {
- "@standard-schema/spec": "^1.0.0",
- "tslib": "^2.3.0",
- "zod": "^4.0.10"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
- },
- "peerDependencies": {
- "@angular/common": "22.1.4",
- "@angular/core": "22.1.4",
- "@angular/platform-browser": "22.1.4",
- "rxjs": "^6.5.3 || ^7.4.0"
- }
- },
- "node_modules/@angular/platform-browser": {
- "version": "22.1.4",
- "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.1.4.tgz",
- "integrity": "sha512-zhEWnRShG6GjnEvCVROD68XfnrV6+HJ9bPjwZx4LsAz7spX823Nhiml8Uqpvijsa1w7UdWLU7nK/magr9IEMOQ==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.3.0"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
- },
- "peerDependencies": {
- "@angular/animations": "22.1.4",
- "@angular/common": "22.1.4",
- "@angular/core": "22.1.4"
- },
- "peerDependenciesMeta": {
- "@angular/animations": {
- "optional": true
- }
- }
- },
- "node_modules/@angular/platform-browser-dynamic": {
- "version": "22.1.4",
- "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-22.1.4.tgz",
- "integrity": "sha512-gtaQ05csHot468owe3AOXJYd2/VPlIQFr3tyXV8O2vhkfTokAyynpaMLc2q7EWbrDDuMvzETR2sBKBhgqIul+A==",
- "deprecated": "@angular/platform-browser-dynamic is deprecated. Use `@angular/platform-browser` instead.",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.3.0"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
- },
- "peerDependencies": {
- "@angular/common": "22.1.4",
- "@angular/compiler": "22.1.4",
- "@angular/core": "22.1.4",
- "@angular/platform-browser": "22.1.4"
- }
- },
- "node_modules/@angular/router": {
- "version": "22.1.4",
- "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.1.4.tgz",
- "integrity": "sha512-vCwJLRnFBSRH5LxSF8OXKBhdmJ526hVGb/5sfeLTQW/oWCdcfXb2RvHfB077m5cdqKX9sBwFVlkJ8Z8kgx/pLQ==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.3.0"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
- },
- "peerDependencies": {
- "@angular/common": "22.1.4",
- "@angular/core": "22.1.4",
- "@angular/platform-browser": "22.1.4",
- "rxjs": "^6.5.3 || ^7.4.0"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
- "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.29.7",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.1.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.0.tgz",
- "integrity": "sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.1.tgz",
- "integrity": "sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^8.0.0",
- "@babel/generator": "^8.0.0",
- "@babel/helper-compilation-targets": "^8.0.0",
- "@babel/helpers": "^8.0.0",
- "@babel/parser": "^8.0.0",
- "@babel/template": "^8.0.0",
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0",
- "@types/gensync": "^1.0.5",
- "convert-source-map": "^2.0.0",
- "empathic": "^2.0.1",
- "gensync": "^1.0.0-beta.2",
- "import-meta-resolve": "^4.2.0",
- "json5": "^2.2.3",
- "obug": "^2.1.1",
- "semver": "^7.7.3"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@babel/core/node_modules/@babel/code-frame": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz",
- "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^8.0.0",
- "js-tokens": "^10.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/core/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/core/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/core/node_modules/@babel/parser": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz",
- "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.4"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/core/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/core/node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@babel/core/node_modules/js-tokens": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
- "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@babel/generator": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz",
- "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^8.0.0",
- "@babel/types": "^8.0.0",
- "@jridgewell/gen-mapping": "^0.3.12",
- "@jridgewell/trace-mapping": "^0.3.28",
- "@types/jsesc": "^2.5.0",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/generator/node_modules/@babel/parser": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz",
- "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.4"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/generator/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-annotate-as-pure": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-8.0.0.tgz",
- "integrity": "sha512-NSpMkMsvvZqzThJ0p1B02cbtA2ObEyfBvq950bmNkyxsxvcxwhvvCB036rKhlEnuBBo30bOrk13u3FzlKSoRrw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.0.tgz",
- "integrity": "sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^8.0.0",
- "@babel/helper-validator-option": "^8.0.0",
- "browserslist": "^4.24.0",
- "lru-cache": "^11.0.0",
- "semver": "^7.7.3"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-globals": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.0.tgz",
- "integrity": "sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-split-export-declaration": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz",
- "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.24.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
- "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-option": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz",
- "integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helpers": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.0.tgz",
- "integrity": "sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helpers/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helpers/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helpers/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
- "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.29.8"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/template": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz",
- "integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^8.0.0",
- "@babel/parser": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/template/node_modules/@babel/code-frame": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz",
- "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^8.0.0",
- "js-tokens": "^10.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/template/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/template/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/template/node_modules/@babel/parser": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz",
- "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.4"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/template/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/template/node_modules/js-tokens": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
- "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@babel/traverse": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.4.tgz",
- "integrity": "sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^8.0.0",
- "@babel/generator": "^8.0.0",
- "@babel/helper-globals": "^8.0.0",
- "@babel/parser": "^8.0.4",
- "@babel/template": "^8.0.0",
- "@babel/types": "^8.0.4",
- "obug": "^2.1.1"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/@babel/code-frame": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz",
- "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^8.0.0",
- "js-tokens": "^10.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/@babel/parser": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz",
- "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.4"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/js-tokens": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
- "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@babel/types": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
- "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@colors/colors": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
- "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.1.90"
- }
- },
- "node_modules/@emnapi/core": {
- "version": "1.11.2",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz",
- "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.2.2",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/runtime": {
- "version": "1.11.2",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
- "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/wasi-threads": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
- "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
- "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
- "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
- "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
- "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
- "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
- "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
- "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
- "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
- "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
- "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
- "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
- "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
- "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
- "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
- "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
- "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
- "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
- "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
- "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
- "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
- "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
- "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
- "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
- "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
- "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
- "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@harperfast/extended-iterable": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz",
- "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true
- },
- "node_modules/@hono/node-server": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz",
- "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=20"
- },
- "peerDependencies": {
- "hono": "^4"
- }
- },
- "node_modules/@inquirer/ansi": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz",
- "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- }
- },
- "node_modules/@inquirer/checkbox": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.3.tgz",
- "integrity": "sha512-XEYX2WA8SBkLPczL6/yXPHLPCvDoptmh9v56Cy05BSV1Smk1vWy19bTC4qJBuIffw7+6l4CcaYYzGqG60RfW1g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/core": "^12.0.1",
- "@inquirer/figures": "^2.0.8",
- "@inquirer/type": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/checkbox/node_modules/@inquirer/core": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.1.tgz",
- "integrity": "sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/figures": "^2.0.8",
- "@inquirer/type": "^4.1.0",
- "cli-width": "^4.1.0",
- "fast-wrap-ansi": "^0.2.0",
- "mute-stream": "^3.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/confirm": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz",
- "integrity": "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^11.2.1",
- "@inquirer/type": "^4.0.7"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/core": {
- "version": "11.2.1",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz",
- "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/figures": "^2.0.7",
- "@inquirer/type": "^4.0.7",
- "cli-width": "^4.1.0",
- "fast-wrap-ansi": "^0.2.0",
- "mute-stream": "^3.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/editor": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.1.tgz",
- "integrity": "sha512-y43COoyVUjPWIobn2Qep/uI1drPS78aaZZZ9kVi94Tyu/GuW2N8d8Q4rifJXGAXCEAXCPTTMjD8gC1HyvM5ukA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^12.0.1",
- "@inquirer/external-editor": "^3.0.4",
- "@inquirer/type": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/editor/node_modules/@inquirer/core": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.1.tgz",
- "integrity": "sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/figures": "^2.0.8",
- "@inquirer/type": "^4.1.0",
- "cli-width": "^4.1.0",
- "fast-wrap-ansi": "^0.2.0",
- "mute-stream": "^3.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/expand": {
- "version": "5.1.3",
- "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.3.tgz",
- "integrity": "sha512-3NQJiXNJ/aj9wiAsr7pECdp5Qe9J0X9YUJCKsaFXS+ddOxfL6J4AIl3w3T4Gq3kK0WsQY5GMoDokK5X94m6lHw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^12.0.1",
- "@inquirer/type": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/expand/node_modules/@inquirer/core": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.1.tgz",
- "integrity": "sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/figures": "^2.0.8",
- "@inquirer/type": "^4.1.0",
- "cli-width": "^4.1.0",
- "fast-wrap-ansi": "^0.2.0",
- "mute-stream": "^3.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/external-editor": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.4.tgz",
- "integrity": "sha512-tZbbaK2ovq6vlrRBNQvjrypmrED/p5x2ncIHQ79cD55tei3dD96v5glMMA+6tiq7K104i/25DVYKWVPJuV6ptA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chardet": "^2.1.1",
- "iconv-lite": "^0.7.2"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/figures": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.8.tgz",
- "integrity": "sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- }
- },
- "node_modules/@inquirer/input": {
- "version": "5.1.4",
- "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.4.tgz",
- "integrity": "sha512-3xQkQrOvgOzpSN2ciTVdRDlg1FWMCA8l+0KfB6SNlILoTCGzJTzO/gc0Rwjcb3usuGyKdaGtI6OiyMdeMeLWkg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^12.0.1",
- "@inquirer/type": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/input/node_modules/@inquirer/core": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.1.tgz",
- "integrity": "sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/figures": "^2.0.8",
- "@inquirer/type": "^4.1.0",
- "cli-width": "^4.1.0",
- "fast-wrap-ansi": "^0.2.0",
- "mute-stream": "^3.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/number": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.1.tgz",
- "integrity": "sha512-5KaqwZNLRpUuWcoCrYghPP9TMaXL5v2Sk4xqePM7RCVegcJStoXdWibio60YIC1bec+z1fCyb67N6XPJIkZtGA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^12.0.1",
- "@inquirer/type": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/number/node_modules/@inquirer/core": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.1.tgz",
- "integrity": "sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/figures": "^2.0.8",
- "@inquirer/type": "^4.1.0",
- "cli-width": "^4.1.0",
- "fast-wrap-ansi": "^0.2.0",
- "mute-stream": "^3.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/password": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.2.0.tgz",
- "integrity": "sha512-CvVcW09emkBESEOW+4R8CjLNkP3fB3XrjeL8CDvfpjgrJN+V9oerXmJAXXM3l+4xqYPD5Yaujzy/Ph0PLOdDuA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/core": "^12.0.1",
- "@inquirer/type": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/password/node_modules/@inquirer/core": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.1.tgz",
- "integrity": "sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/figures": "^2.0.8",
- "@inquirer/type": "^4.1.0",
- "cli-width": "^4.1.0",
- "fast-wrap-ansi": "^0.2.0",
- "mute-stream": "^3.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/prompts": {
- "version": "8.5.2",
- "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.2.tgz",
- "integrity": "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/checkbox": "^5.2.1",
- "@inquirer/confirm": "^6.1.1",
- "@inquirer/editor": "^5.2.2",
- "@inquirer/expand": "^5.1.1",
- "@inquirer/input": "^5.1.2",
- "@inquirer/number": "^4.1.1",
- "@inquirer/password": "^5.1.1",
- "@inquirer/rawlist": "^5.3.1",
- "@inquirer/search": "^4.2.1",
- "@inquirer/select": "^5.2.1"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/rawlist": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.3.tgz",
- "integrity": "sha512-Mu7WrtmDLaXBDEyrRLS70SZgX9ZSm4Up1w0ZxiH8C1OOp9oaVCn2k8q3QGgmlnhsKYUhuaU3zFWhAP6wxkVIMA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^12.0.1",
- "@inquirer/type": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/rawlist/node_modules/@inquirer/core": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.1.tgz",
- "integrity": "sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/figures": "^2.0.8",
- "@inquirer/type": "^4.1.0",
- "cli-width": "^4.1.0",
- "fast-wrap-ansi": "^0.2.0",
- "mute-stream": "^3.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/search": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.1.tgz",
- "integrity": "sha512-0VWOvsHWI0rPj6CG70MoP4oXNCB6adcyN8bVFZXnh11eLDdPIK2f2XCmva78acPPDfJisfab7qNakwBh7hdBXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^12.0.1",
- "@inquirer/figures": "^2.0.8",
- "@inquirer/type": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/search/node_modules/@inquirer/core": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.1.tgz",
- "integrity": "sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/figures": "^2.0.8",
- "@inquirer/type": "^4.1.0",
- "cli-width": "^4.1.0",
- "fast-wrap-ansi": "^0.2.0",
- "mute-stream": "^3.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/select": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.3.tgz",
- "integrity": "sha512-KuRTodDa6xBXX2noIpjuitpX/QT7Sfav7dIZ/OfUY54Hxg95nrGoshSzxx6Ey7qqLbImKdiGkSDt7KjPXjQgmA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/core": "^12.0.1",
- "@inquirer/figures": "^2.0.8",
- "@inquirer/type": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/select/node_modules/@inquirer/core": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.1.tgz",
- "integrity": "sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^2.0.7",
- "@inquirer/figures": "^2.0.8",
- "@inquirer/type": "^4.1.0",
- "cli-width": "^4.1.0",
- "fast-wrap-ansi": "^0.2.0",
- "mute-stream": "^3.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/type": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.1.0.tgz",
- "integrity": "sha512-FMiJpuHUG3Dk0ex+UIXkre7i+i4OcwHWk9YdcVtZHFwb/r2rnrU2ipTCNAB7A+QOP0ryzIcqOfy76fRyyvOEAw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@istanbuljs/schema": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz",
- "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@listr2/prompt-adapter-inquirer": {
- "version": "4.2.5",
- "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-4.2.5.tgz",
- "integrity": "sha512-pYGy9dTdTwXdasPgyohkr0HoQ4FrkAzFnsUZl/gcnadDArbpZ8e+fgr+F9WBdNEl2y00mb9bCM4WgmoBkZJ27A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@inquirer/type": "^4.0.7"
- },
- "engines": {
- "node": ">=22.13.0"
- },
- "peerDependencies": {
- "@inquirer/prompts": ">= 3 < 9",
- "listr2": "11.0.0"
- }
- },
- "node_modules/@lmdb/lmdb-darwin-arm64": {
- "version": "3.5.6",
- "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.6.tgz",
- "integrity": "sha512-mY5FG4TjPAkY4P0w+OhHaUka5mDh2TX2WKYIwuKzJ1zeW3VvRgxdam/lGJTquI+bthTx5CSHDW+BAQCnNAzkEA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@lmdb/lmdb-darwin-x64": {
- "version": "3.5.6",
- "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.6.tgz",
- "integrity": "sha512-foa+pwitysO8k+xhs7psBFfTKnVgR69NlZRRTHaFVDqphh7AdGpLeyRzKw/ofatr/sN6TiHRRW6mmop0ZrrppQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@lmdb/lmdb-linux-arm": {
- "version": "3.5.6",
- "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.6.tgz",
- "integrity": "sha512-QR4YRyR5h5Z8eGXrNQjiyo2NNDfqi3tCc9dQG5Is1blCt+qWw1ZoBWhlWAr5d+jshkifMIJjVHzHGKbkKzF8Tw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@lmdb/lmdb-linux-arm64": {
- "version": "3.5.6",
- "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.6.tgz",
- "integrity": "sha512-HmiyFFdJa38s1heCMSooSPaBSFTHJ3C+ERPp28xAPlDX1YiALJVOgbry065nXd8Y7KISWjnw05zpG1RX8IfftA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@lmdb/lmdb-linux-x64": {
- "version": "3.5.6",
- "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.6.tgz",
- "integrity": "sha512-ADzCuCF2cTNiX9kDScqcz1fjnAkxPpQNneV3KFTdV3wWtVlI2sTGzySoMTgDpinkMMFj1NTJlxA6XR8fwc4hlA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@lmdb/lmdb-win32-arm64": {
- "version": "3.5.6",
- "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.6.tgz",
- "integrity": "sha512-J7A9aEQsQiv0TYtBGL7NDIPp2lOS8nnl+zm4sWZm1xlsTTaQ4PgD096Adzdrk27rw3UxCkDXdCUa4ax41oztBQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@lmdb/lmdb-win32-x64": {
- "version": "3.5.6",
- "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.6.tgz",
- "integrity": "sha512-1g7G0knRX2iV/voDu54yxrGqw5Dk0w2oIYb7dgJq8IkOi+m7wbD8Q3QpPFjh0C01G58S88dqGn03len6UPCXsg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@modelcontextprotocol/sdk": {
- "version": "1.30.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
- "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@hono/node-server": "^1.19.9 || ^2.0.5",
- "ajv": "^8.17.1",
- "ajv-formats": "^3.0.1",
- "content-type": "^1.0.5",
- "cors": "^2.8.5",
- "cross-spawn": "^7.0.5",
- "eventsource": "^3.0.2",
- "eventsource-parser": "^3.0.0",
- "express": "^5.2.1",
- "express-rate-limit": "^8.2.1",
- "hono": "^4.11.4",
- "jose": "^6.1.3",
- "json-schema-typed": "^8.0.2",
- "pkce-challenge": "^5.0.0",
- "raw-body": "^3.0.0",
- "zod": "^3.25 || ^4.0",
- "zod-to-json-schema": "^3.25.1"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@cfworker/json-schema": "^4.1.1",
- "zod": "^3.25 || ^4.0"
- },
- "peerDependenciesMeta": {
- "@cfworker/json-schema": {
- "optional": true
- },
- "zod": {
- "optional": false
- }
- }
- },
- "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
- "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.7.0",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz",
- "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz",
- "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz",
- "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz",
- "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz",
- "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz",
- "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@napi-rs/nice": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz",
- "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- },
- "optionalDependencies": {
- "@napi-rs/nice-android-arm-eabi": "1.1.1",
- "@napi-rs/nice-android-arm64": "1.1.1",
- "@napi-rs/nice-darwin-arm64": "1.1.1",
- "@napi-rs/nice-darwin-x64": "1.1.1",
- "@napi-rs/nice-freebsd-x64": "1.1.1",
- "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1",
- "@napi-rs/nice-linux-arm64-gnu": "1.1.1",
- "@napi-rs/nice-linux-arm64-musl": "1.1.1",
- "@napi-rs/nice-linux-ppc64-gnu": "1.1.1",
- "@napi-rs/nice-linux-riscv64-gnu": "1.1.1",
- "@napi-rs/nice-linux-s390x-gnu": "1.1.1",
- "@napi-rs/nice-linux-x64-gnu": "1.1.1",
- "@napi-rs/nice-linux-x64-musl": "1.1.1",
- "@napi-rs/nice-openharmony-arm64": "1.1.1",
- "@napi-rs/nice-win32-arm64-msvc": "1.1.1",
- "@napi-rs/nice-win32-ia32-msvc": "1.1.1",
- "@napi-rs/nice-win32-x64-msvc": "1.1.1"
- }
- },
- "node_modules/@napi-rs/nice-android-arm-eabi": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz",
- "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-android-arm64": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz",
- "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-darwin-arm64": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz",
- "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-darwin-x64": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz",
- "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-freebsd-x64": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz",
- "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-linux-arm-gnueabihf": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz",
- "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-linux-arm64-gnu": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz",
- "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-linux-arm64-musl": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz",
- "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-linux-ppc64-gnu": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz",
- "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-linux-riscv64-gnu": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz",
- "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-linux-s390x-gnu": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz",
- "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-linux-x64-gnu": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz",
- "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-linux-x64-musl": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz",
- "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-openharmony-arm64": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz",
- "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-win32-arm64-msvc": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz",
- "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-win32-ia32-msvc": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz",
- "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/nice-win32-x64-msvc": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz",
- "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/wasm-runtime": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz",
- "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@tybys/wasm-util": "^0.10.3"
- },
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- },
- "peerDependencies": {
- "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4",
- "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4"
- }
- },
- "node_modules/@oxc-parser/binding-android-arm-eabi": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.142.0.tgz",
- "integrity": "sha512-ZiRGDutGsv1G6bL/ozy/koC0Sv39T1DqyoC4KD1DOy9ZoACm1O5UWhEK2c02Qdk+4lfLVkvFa/mQ0fm/4h1BtQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-android-arm64": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.142.0.tgz",
- "integrity": "sha512-WZkvGRLNQTz8lR9zP5nLjUdlroRCopBu3g9zF1p/laE6DzT1UbQo8Rdz5MWhaJUPYg/6gp+jo7HUgsyKaN1FtQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-darwin-arm64": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.142.0.tgz",
- "integrity": "sha512-l4khS8LQOOVYsGRVARo1gSaCT/aBSceUVXgtovWc2+drnxVuDr082WA3OCHVdVzIz5JIrP/y9CWsSKxBDNmYGg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-darwin-x64": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.142.0.tgz",
- "integrity": "sha512-QBsNF3nqlXmcH2B1YOPqQYmCJoy4HuIjUxGbBO/k5JAJUl68ghU2psRY2zPk+RyBaWqKP/qfL4oaFgEMCdwskA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-freebsd-x64": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.142.0.tgz",
- "integrity": "sha512-b7Q7m4Cqc6XqNhri3R+QhU+GVy646Pn+bkdhrDdWym/Fdi0ZUa+d73H9dm5H91JtbtAQ/z1d8XKMW3oOV8a4tQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.142.0.tgz",
- "integrity": "sha512-3riVS5IhdH3uCZj1Y9ftDQlR0dvLsIlw/edrRqk8JhgNd5K0XSs+UBtgh50N13CAlW9/TXj6sVGXaKNBocd0Yg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-linux-arm-musleabihf": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.142.0.tgz",
- "integrity": "sha512-NmXUOpgpTSkhl795TiXmWppTwmSJ92RC1qvD6e4XOF+slgmo3e6Ah+kEu+6AN8s7NAOEwqGmir58MgSQSWmBSA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-linux-arm64-gnu": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.142.0.tgz",
- "integrity": "sha512-gc0EXsKtXgerujmU2Bql3u1L1HsSQ2774R83idq/FoNMPVV/RY/1ErFsvnit7KoiP/sLvzQixeUo4Ut0ic0wmw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-linux-arm64-musl": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.142.0.tgz",
- "integrity": "sha512-F2XvmWSE0uWpie+jHKKIFgdVOe9ypGhkEZxKx5DuW215K6cbAC274yYaPkcM7EqY4Df3Weyhpcz3lsURyH2LVg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-linux-ppc64-gnu": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.142.0.tgz",
- "integrity": "sha512-wLMbT21U/QxknQsk+VvNF0b9D2/aGWhcaQQQ+VYlE8FwD5+GoWZIPPXNzyHmkYyhm0KB3itL+TBavjMatqNnYA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-linux-riscv64-gnu": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.142.0.tgz",
- "integrity": "sha512-+G8F/4ckwT7FCJV4H2bt09xEzJbjNCfuL4Sp1AYNaFtFMVtgIGMuJlteT82U+K0UIZ/DzAR/LDlMFnEuajG7Kw==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-linux-riscv64-musl": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.142.0.tgz",
- "integrity": "sha512-hTsHtTLxMAfCo+rpF5K3qZJKW2NpPN/CHd4mYB3y7XlSdspHkd2gehDIofP64AacA9nWQw2tY3O7wR6UY8IVOA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-linux-s390x-gnu": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.142.0.tgz",
- "integrity": "sha512-6y7qYY3TCUDYjqswImdTGl92y+KA/80twALegQPN27kfY+bG7Ib1+L3jbmrCZQx6wrVnai9IPsEZp07I0hx7JQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-linux-x64-gnu": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.142.0.tgz",
- "integrity": "sha512-i69kAWU+2LgoH5bR+zWiiu+UzAw7Oxkwv7COeJTeY19pn4e70nKQcr9Pm6cL2Z0Z54d+gl9qADlK/0yyuCPiBA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-linux-x64-musl": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.142.0.tgz",
- "integrity": "sha512-4SQs678MmjYVrmhAgCWD4o0vpaFszXw9xLX5p2Z9MMFcltxiLkA88wQjh80YHjPrXtpyZ2CWI5m+1yNKM0m2Pw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-openharmony-arm64": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.142.0.tgz",
- "integrity": "sha512-YHpx9N7Ln3a++Tc8rv+H7mrK1zyJQOAwCFg8LZ3lTs1T5afGWeZrLPhPT9HLnIwSjCyJqPWVMIrMxbjcmBr2oQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-wasm32-wasi": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.142.0.tgz",
- "integrity": "sha512-3pLDyY3+oogW73RM5uehNgAiR/Xfb7fvO2Q1Z1gIqZ2+50XDVQmBVlRkHXZTU4gKnQHpwETNsYQVsJ3joVB2iA==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "1.11.2",
- "@emnapi/runtime": "1.11.2",
- "@napi-rs/wasm-runtime": "^1.1.6"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-win32-arm64-msvc": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.142.0.tgz",
- "integrity": "sha512-Had/VeVY28Oyb0K+Q4FV8KCzoBycIh93oDK6pCbya9lkzdq+ikMHMgBubsdqqlybjJmQRawCQRrnBRHyQwYvcQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-win32-ia32-msvc": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.142.0.tgz",
- "integrity": "sha512-GGi3+YphVHavvgs6gum2UXoNCqzHAmPt/nXkn8ZQZstV2Q1qZD1Mn8fz/nWrDkefHQtrG/+1/XrbMxsBTo6Svw==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-parser/binding-win32-x64-msvc": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.142.0.tgz",
- "integrity": "sha512-Ny/Wv4Us1LGC/ljwNTp+Hx3r/pH15EFfeDF0p+n898gt+TtRd6C9SccHcuUhDiNTb8s5tt7jdeAMDRQZ4Vq6hg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@oxc-project/types": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz",
- "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
- }
- },
- "node_modules/@parcel/watcher": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz",
- "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "detect-libc": "^2.0.3",
- "is-glob": "^4.0.3",
- "node-addon-api": "^7.0.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "@parcel/watcher-android-arm64": "2.6.0",
- "@parcel/watcher-darwin-arm64": "2.6.0",
- "@parcel/watcher-darwin-x64": "2.6.0",
- "@parcel/watcher-freebsd-x64": "2.6.0",
- "@parcel/watcher-linux-arm-glibc": "2.6.0",
- "@parcel/watcher-linux-arm-musl": "2.6.0",
- "@parcel/watcher-linux-arm64-glibc": "2.6.0",
- "@parcel/watcher-linux-arm64-musl": "2.6.0",
- "@parcel/watcher-linux-x64-glibc": "2.6.0",
- "@parcel/watcher-linux-x64-musl": "2.6.0",
- "@parcel/watcher-win32-arm64": "2.6.0",
- "@parcel/watcher-win32-x64": "2.6.0"
- }
- },
- "node_modules/@parcel/watcher-android-arm64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz",
- "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-darwin-arm64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz",
- "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-darwin-x64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz",
- "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-freebsd-x64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz",
- "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm-glibc": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz",
- "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm-musl": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz",
- "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm64-glibc": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz",
- "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm64-musl": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz",
- "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-x64-glibc": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz",
- "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-x64-musl": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz",
- "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-win32-arm64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz",
- "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-win32-x64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz",
- "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher/node_modules/node-addon-api": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
- "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
- "node_modules/@rolldown/binding-android-arm64": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.0.tgz",
- "integrity": "sha512-9yB1l95IrJuNGDFdOYe79vdApdz6WWBCObE+rQ2LUliYUlcyFwSYIb2xb5/Ifw7dAtMy2ZqNyd8QTSOc7duAKw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.0.tgz",
- "integrity": "sha512-pexNaW9ACLUOaBITOpU6qVu4VrsOFIjTv6bzgu0YUATo4eUJx0V605PxwZfndpPOn0ilqGqvGQ0M8UW0IE24jg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.0.tgz",
- "integrity": "sha512-NqKYaq0355ZmNMG4QGpxtEDxsc7tGDhjhCm4PpE0cwnBW+5Il95LJyq414niEiaKLVjnVHBEjSo1wngKxJNiFw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.0.tgz",
- "integrity": "sha512-3vPoHzh6eBTz9IbB0/qZdSr0Qeks2echn+I4cHu2joV74VriPDdldswksEDzrl1mBB+oPRi+67+3Ib59paxIPQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.0.tgz",
- "integrity": "sha512-E6NNefZ1bUVmKJq2tJkf45J4Zyczj7qm9rUT7NY+Xo2474Y13qWAwc2tvBt0BAVbmtXR1llkxXg0Ou1jbDf2SQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.0.tgz",
- "integrity": "sha512-D+TgkdgM1vu+7/Fpf8+v0ARW+RXEP9Ccazgm8zQ4JFFd9Q7SrYQ2TakU5S5ihazQDgpKyAgZDOcIFsvoHmTZ8w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.0.tgz",
- "integrity": "sha512-wUqdwJBbAv0APN87GecstdMUtLjjNTs0hBALpxETD73mccFxdmt/XeizXDtN5RAlBwNKmI+Tg+blect2G+8IeQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.0.tgz",
- "integrity": "sha512-9DtF35qR9/NrfhM4oxLplCzVVjE+KKm8Pjemi0i/sdhAWkUasjmSo8WTTubNJClhSHCfyk2yeyoXDQEDPtDAAw==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.0.tgz",
- "integrity": "sha512-RzuHrBh8X8Hntd2N4VR02QGEciq/9JhcZoTpR/Cee6otRrlILGCf3cg2ygHuih+ZebUnWmMrDX6ITI85btO6rQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.0.tgz",
- "integrity": "sha512-MK7L0018jjh1jR3mh21G2j1zAVcpscJBlPo2z19pRjv2XOYGRhaV4LyiD8HO6nCDdZln9IFgCMIV5yt4E3klGQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.0.tgz",
- "integrity": "sha512-gyrxLQ9NfGb/9LoVnC4kb9miUghw1mghnkfYvNHSnVIXriabnfgGPUP4RLcJm87q3KgYz4FYUG8IDiWUT+CpSw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.0.tgz",
- "integrity": "sha512-/6VFMQGRmrhP77KXDC+StIxGzcNp5JOIyYtw0CQ8gPlzhpiIRucYfoM5FaFamHd5BJYIdH86yfP46l1p3WdrFA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.0.tgz",
- "integrity": "sha512-rwdbUL465kisF24WEJLvP3JrEG6E5GRuIHt5wpMwHGERtHe4Wm2CIvtf5gTBgr2tGOHKh5NdKEAFS2VkOPE91g==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "1.11.2",
- "@emnapi/runtime": "1.11.2",
- "@napi-rs/wasm-runtime": "^1.1.6"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.0.tgz",
- "integrity": "sha512-+5suHwRiKGmhwyUaNT8a5QbrBvLFh2DbO910TEmGRH1aSxwrCezodvGQnulv4uiWEIv1Kq4ypRsJ5+O+ry1DiA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.0.tgz",
- "integrity": "sha512-WfFv6/qGufotqBSBzBYwgpCkJBk8Nj7697LL9vTz/XWc67e0r3oewu8iMRwQj3AUL45GVD7wVsPjCsAAtW66Wg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/pluginutils": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
- "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@schematics/angular": {
- "version": "22.1.6",
- "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-22.1.6.tgz",
- "integrity": "sha512-RiD4OZJ4yuaM1aXb3pt1gjDSWwkP0jrgiCuoaBls1eabcrDmlsNuQv0ekxbcGgCRPdSJvmHRZIHHmFPzei8w3Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@angular-devkit/core": "22.1.6",
- "@angular-devkit/schematics": "22.1.6",
- "jsonc-parser": "3.3.1",
- "typescript": "6.0.3"
- },
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- }
- },
- "node_modules/@socket.io/component-emitter": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
- "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@standard-schema/spec": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
- "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
- "license": "MIT"
- },
- "node_modules/@tybys/wasm-util": {
- "version": "0.10.3",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
- "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@types/cors": {
- "version": "2.8.19",
- "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
- "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/gensync": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz",
- "integrity": "sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/jasmine": {
- "version": "5.1.15",
- "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.15.tgz",
- "integrity": "sha512-ZAC8KjmV2MJxbNTrwXFN+HKeajpXQZp6KpPiR6Aa4XvaEnjP6qh23lL/Rqb7AYzlp3h/rcwDrQ7Gg7q28cQTQg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/jsesc": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz",
- "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "26.2.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
- "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~8.3.0"
- }
- },
- "node_modules/@types/ws": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
- "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@vitejs/plugin-basic-ssl": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz",
- "integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "peerDependencies": {
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/accepts/node_modules/negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/agent-base": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
- "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/ajv": {
- "version": "8.20.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
- "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ajv-formats": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
- "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependenciesMeta": {
- "ajv": {
- "optional": true
- }
- }
- },
- "node_modules/ansi-escapes": {
- "version": "7.3.0",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
- "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "environment": "^1.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ansi-regex": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
- "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/anymatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/anymatch/node_modules/picomatch": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
- "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/base64id": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
- "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^4.5.0 || >= 5.9"
- }
- },
- "node_modules/baseline-browser-mapping": {
- "version": "2.11.14",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz",
- "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "baseline-browser-mapping": "dist/cli.cjs"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/beasties": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.3.tgz",
- "integrity": "sha512-fIIeLOcbAB/K1kb1HBVJoiq1alHL4RCYBSo5e7HzrNkkgMggXR1Vqt/Z9JWnkfe/qdCo66Ux3QRwZioAIBdWRA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "css-select": "^6.0.0",
- "css-what": "^7.0.0",
- "dom-serializer": "^2.0.0",
- "domhandler": "^5.0.3",
- "htmlparser2": "^10.0.0",
- "picocolors": "^1.1.1",
- "postcss": "^8.4.49",
- "postcss-media-query-parser": "^0.2.3",
- "postcss-safe-parser": "^7.0.1"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/binary-extensions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/body-parser": {
- "version": "1.20.6",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
- "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "content-type": "~1.0.5",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "~1.2.0",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.4.24",
- "on-finished": "~2.4.1",
- "qs": "~6.15.1",
- "raw-body": "~2.5.3",
- "type-is": "~1.6.18",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/body-parser/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/body-parser/node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/body-parser/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/boolbase": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
- "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/brace-expansion": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
- "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fill-range": "^7.1.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/browserslist": {
- "version": "4.28.8",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
- "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "baseline-browser-mapping": "^2.11.12",
- "caniuse-lite": "^1.0.30001809",
- "electron-to-chromium": "^1.5.402",
- "node-releases": "^2.0.53",
- "update-browserslist-db": "^1.3.0"
- },
- "bin": {
- "browserslist": "cli.js"
- },
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
- }
- },
- "node_modules/buffer-from": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/call-bound": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/caniuse-lite": {
- "version": "1.0.30001809",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz",
- "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "CC-BY-4.0"
- },
- "node_modules/chalk": {
- "version": "5.6.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
- "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.17.0 || ^14.13 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/chardet": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz",
- "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/chokidar": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
- "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "readdirp": "^5.0.0"
- },
- "engines": {
- "node": ">= 20.19.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/cli-cursor": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
- "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "restore-cursor": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-spinners": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
- "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18.20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-truncate": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-6.1.1.tgz",
- "integrity": "sha512-06p9vyLahLa4zkGcgsGxU6iEkSOiuI4fhCH6Emhe2lPAcoUv73n72DnODsnHA+5wwXGnV0n9M9/qOQJSjYhFhw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "slice-ansi": "^9.0.0",
- "string-width": "^8.2.0"
- },
- "engines": {
- "node": ">=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-truncate/node_modules/string-width": {
- "version": "8.2.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
- "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.5.0",
- "strip-ansi": "^7.1.2"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-width": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
- "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">= 12"
- }
- },
- "node_modules/cliui": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
- "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^7.2.0",
- "strip-ansi": "^7.1.0",
- "wrap-ansi": "^9.0.0"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/cliui/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/cliui/node_modules/wrap-ansi": {
- "version": "9.0.2",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
- "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.1",
- "string-width": "^7.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/connect": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
- "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "finalhandler": "1.1.2",
- "parseurl": "~1.3.3",
- "utils-merge": "1.0.1"
- },
- "engines": {
- "node": ">= 0.10.0"
- }
- },
- "node_modules/connect/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/connect/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/content-disposition": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
- "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/content-type": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
- "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/convert-source-map": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
- "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cookie": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
- "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie-signature": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
- "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.6.0"
- }
- },
- "node_modules/cors": {
- "version": "2.8.6",
- "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
- "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "object-assign": "^4",
- "vary": "^1"
- },
- "engines": {
- "node": ">= 0.10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/cross-spawn/node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/css-select": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz",
- "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "boolbase": "^1.0.0",
- "css-what": "^7.0.0",
- "domhandler": "^5.0.3",
- "domutils": "^3.2.2",
- "nth-check": "^2.1.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/fb55"
- }
- },
- "node_modules/css-what": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz",
- "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">= 6"
- },
- "funding": {
- "url": "https://github.com/sponsors/fb55"
- }
- },
- "node_modules/custom-event": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
- "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/date-format": {
- "version": "4.0.14",
- "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
- "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/destroy": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
- "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/di": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
- "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/dom-serialize": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
- "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "custom-event": "~1.0.0",
- "ent": "~2.2.0",
- "extend": "^3.0.0",
- "void-elements": "^2.0.0"
- }
- },
- "node_modules/dom-serializer": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
- "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.2",
- "entities": "^4.2.0"
- },
- "funding": {
- "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
- }
- },
- "node_modules/domelementtype": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
- "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fb55"
- }
- ],
- "license": "BSD-2-Clause"
- },
- "node_modules/domhandler": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
- "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "domelementtype": "^2.3.0"
- },
- "engines": {
- "node": ">= 4"
- },
- "funding": {
- "url": "https://github.com/fb55/domhandler?sponsor=1"
- }
- },
- "node_modules/domutils": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
- "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "dom-serializer": "^2.0.0",
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.3"
- },
- "funding": {
- "url": "https://github.com/fb55/domutils?sponsor=1"
- }
- },
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.407",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.407.tgz",
- "integrity": "sha512-4R8XgQOdfxexCd/u63lRm6wCHjECwI45MV9wxAs2ggtfWe2hwlo1ql97jKsju2IcJ+jFSTwBssyYoiWhh7mauQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/emoji-regex": {
- "version": "10.6.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
- "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/empathic": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz",
- "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/engine.io": {
- "version": "6.6.9",
- "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz",
- "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/cors": "^2.8.12",
- "@types/node": ">=10.0.0",
- "@types/ws": "^8.5.12",
- "accepts": "~1.3.4",
- "base64id": "2.0.0",
- "cookie": "~0.7.2",
- "cors": "~2.8.5",
- "debug": "~4.4.1",
- "engine.io-parser": "~5.2.1",
- "ws": "~8.21.0"
- },
- "engines": {
- "node": ">=10.2.0"
- }
- },
- "node_modules/engine.io-parser": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
- "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/ent": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz",
- "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.3",
- "es-errors": "^1.3.0",
- "punycode": "^1.4.1",
- "safe-regex-test": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/entities": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
- "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/environment": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
- "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-object-atoms": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
- "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/esbuild": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
- "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.2",
- "@esbuild/android-arm": "0.28.2",
- "@esbuild/android-arm64": "0.28.2",
- "@esbuild/android-x64": "0.28.2",
- "@esbuild/darwin-arm64": "0.28.2",
- "@esbuild/darwin-x64": "0.28.2",
- "@esbuild/freebsd-arm64": "0.28.2",
- "@esbuild/freebsd-x64": "0.28.2",
- "@esbuild/linux-arm": "0.28.2",
- "@esbuild/linux-arm64": "0.28.2",
- "@esbuild/linux-ia32": "0.28.2",
- "@esbuild/linux-loong64": "0.28.2",
- "@esbuild/linux-mips64el": "0.28.2",
- "@esbuild/linux-ppc64": "0.28.2",
- "@esbuild/linux-riscv64": "0.28.2",
- "@esbuild/linux-s390x": "0.28.2",
- "@esbuild/linux-x64": "0.28.2",
- "@esbuild/netbsd-arm64": "0.28.2",
- "@esbuild/netbsd-x64": "0.28.2",
- "@esbuild/openbsd-arm64": "0.28.2",
- "@esbuild/openbsd-x64": "0.28.2",
- "@esbuild/openharmony-arm64": "0.28.2",
- "@esbuild/sunos-x64": "0.28.2",
- "@esbuild/win32-arm64": "0.28.2",
- "@esbuild/win32-ia32": "0.28.2",
- "@esbuild/win32-x64": "0.28.2"
- }
- },
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/eventemitter3": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
- "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/eventsource": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
- "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eventsource-parser": "^3.0.1"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/eventsource-parser": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
- "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/express": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
- "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "accepts": "^2.0.0",
- "body-parser": "^2.2.1",
- "content-disposition": "^1.0.0",
- "content-type": "^1.0.5",
- "cookie": "^0.7.1",
- "cookie-signature": "^1.2.1",
- "debug": "^4.4.0",
- "depd": "^2.0.0",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "etag": "^1.8.1",
- "finalhandler": "^2.1.0",
- "fresh": "^2.0.0",
- "http-errors": "^2.0.0",
- "merge-descriptors": "^2.0.0",
- "mime-types": "^3.0.0",
- "on-finished": "^2.4.1",
- "once": "^1.4.0",
- "parseurl": "^1.3.3",
- "proxy-addr": "^2.0.7",
- "qs": "^6.14.0",
- "range-parser": "^1.2.1",
- "router": "^2.2.0",
- "send": "^1.1.0",
- "serve-static": "^2.2.0",
- "statuses": "^2.0.1",
- "type-is": "^2.0.1",
- "vary": "^1.1.2"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/express-rate-limit": {
- "version": "8.7.0",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz",
- "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.3",
- "ip-address": "^10.2.0"
- },
- "engines": {
- "node": ">= 16"
- },
- "funding": {
- "url": "https://github.com/sponsors/express-rate-limit"
- },
- "peerDependencies": {
- "express": ">= 4.11"
- }
- },
- "node_modules/express/node_modules/accepts": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
- "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mime-types": "^3.0.0",
- "negotiator": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/express/node_modules/body-parser": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
- "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "bytes": "^3.1.2",
- "content-type": "^2.0.0",
- "debug": "^4.4.3",
- "http-errors": "^2.0.1",
- "iconv-lite": "^0.7.2",
- "on-finished": "^2.4.1",
- "qs": "^6.15.2",
- "raw-body": "^3.0.2",
- "type-is": "^2.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/express/node_modules/body-parser/node_modules/content-type": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
- "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/express/node_modules/encodeurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/express/node_modules/finalhandler": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
- "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.0",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "on-finished": "^2.4.1",
- "parseurl": "^1.3.3",
- "statuses": "^2.0.1"
- },
- "engines": {
- "node": ">= 18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/express/node_modules/media-typer": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
- "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/express/node_modules/mime-db": {
- "version": "1.54.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
- "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/express/node_modules/mime-types": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
- "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mime-db": "^1.54.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/express/node_modules/raw-body": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
- "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.7.0",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/express/node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/express/node_modules/type-is": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
- "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "content-type": "^2.0.0",
- "media-typer": "^1.1.0",
- "mime-types": "^3.0.0"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/express/node_modules/type-is/node_modules/content-type": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
- "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/extend": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-string-truncated-width": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
- "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-string-width": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz",
- "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-string-truncated-width": "^3.0.2"
- }
- },
- "node_modules/fast-uri": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
- "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fastify"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/fastify"
- }
- ],
- "license": "BSD-3-Clause"
- },
- "node_modules/fast-wrap-ansi": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz",
- "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-string-width": "^3.0.2"
- }
- },
- "node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/finalhandler": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
- "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "on-finished": "~2.3.0",
- "parseurl": "~1.3.3",
- "statuses": "~1.5.0",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/finalhandler/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/finalhandler/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/finalhandler/node_modules/on-finished": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
- "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/flatted": {
- "version": "3.4.4",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
- "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/follow-redirects": {
- "version": "1.16.0",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
- "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
- "dev": true,
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
- "node_modules/forwarded": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
- "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fresh": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
- "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/fs-extra": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
- "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.2.0",
- "jsonfile": "^4.0.0",
- "universalify": "^0.1.0"
- },
- "engines": {
- "node": ">=6 <7 || >=8"
- }
- },
- "node_modules/fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": "6.* || 8.* || >= 10.*"
- }
- },
- "node_modules/get-east-asian-width": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
- "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- },
- "engines": {
- "node": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-tostringtag": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
- "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-symbols": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
- "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/hono": {
- "version": "4.13.5",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.5.tgz",
- "integrity": "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=16.9.0"
- }
- },
- "node_modules/hosted-git-info": {
- "version": "10.1.1",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-10.1.1.tgz",
- "integrity": "sha512-DeOnSPAvOndYKfw075gt8yZzQ7S2hNztw34zBTfhIzLhmBTswIBg5/y+pqu/VD5cYWm5goAFTusDmUEmKZ0PEQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "lru-cache": "^11.1.0"
- },
- "engines": {
- "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
- }
- },
- "node_modules/html-escaper": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
- "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/htmlparser2": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
- "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
- "dev": true,
- "funding": [
- "https://github.com/fb55/htmlparser2?sponsor=1",
- {
- "type": "github",
- "url": "https://github.com/sponsors/fb55"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.3",
- "domutils": "^3.2.2",
- "entities": "^7.0.1"
- }
- },
- "node_modules/htmlparser2/node_modules/entities": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
- "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/http-errors": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
- "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "depd": "~2.0.0",
- "inherits": "~2.0.4",
- "setprototypeof": "~1.2.0",
- "statuses": "~2.0.2",
- "toidentifier": "~1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/http-errors/node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/http-proxy": {
- "version": "1.18.1",
- "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
- "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eventemitter3": "^4.0.0",
- "follow-redirects": "^1.0.0",
- "requires-port": "^1.0.0"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/https-proxy-agent": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz",
- "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "agent-base": "9.0.0",
- "debug": "^4.3.4",
- "proxy-agent-negotiate": "1.1.0"
- },
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/iconv-lite": {
- "version": "0.7.3",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
- "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/immutable": {
- "version": "5.1.9",
- "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
- "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/import-meta-resolve": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
- "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/ip-address": {
- "version": "10.7.0",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz",
- "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 12"
- }
- },
- "node_modules/ipaddr.js": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "binary-extensions": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-fullwidth-code-point": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
- "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.3.1"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-interactive": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
- "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/is-promise": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
- "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/is-regex": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
- "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "gopd": "^1.2.0",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-unicode-supported": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
- "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/isbinaryfile": {
- "version": "4.0.10",
- "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
- "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/gjtorikian/"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/istanbul-lib-coverage": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
- "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/istanbul-lib-instrument": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
- "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "@babel/core": "^7.23.9",
- "@babel/parser": "^7.23.9",
- "@istanbuljs/schema": "^0.1.3",
- "istanbul-lib-coverage": "^3.2.0",
- "semver": "^7.5.4"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/compat-data": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
- "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/core": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
- "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.7",
- "@babel/helper-compilation-targets": "^7.29.7",
- "@babel/helper-module-transforms": "^7.29.7",
- "@babel/helpers": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/template": "^7.29.7",
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7",
- "@jridgewell/remapping": "^2.3.5",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/core/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/generator": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
- "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.8",
- "@babel/types": "^7.29.8",
- "@jridgewell/gen-mapping": "^0.3.12",
- "@jridgewell/trace-mapping": "^0.3.28",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-compilation-targets": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
- "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.29.7",
- "@babel/helper-validator-option": "^7.29.7",
- "browserslist": "^4.24.0",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-compilation-targets/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-globals": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
- "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-module-imports": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
- "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-module-transforms": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
- "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7",
- "@babel/traverse": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-validator-option": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
- "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/helpers": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
- "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/template": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
- "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/@babel/traverse": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
- "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.8",
- "@babel/helper-globals": "^7.29.7",
- "@babel/parser": "^7.29.8",
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.8",
- "debug": "^4.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/istanbul-lib-instrument/node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/istanbul-lib-instrument/node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/istanbul-lib-report": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
- "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "istanbul-lib-coverage": "^3.0.0",
- "make-dir": "^4.0.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/istanbul-lib-source-maps": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
- "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "debug": "^4.1.1",
- "istanbul-lib-coverage": "^3.0.0",
- "source-map": "^0.6.1"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/istanbul-lib-source-maps/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/istanbul-reports": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
- "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "html-escaper": "^2.0.0",
- "istanbul-lib-report": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/jasmine-core": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.6.0.tgz",
- "integrity": "sha512-niVlkeYVRwKFpmfWg6suo6H9CrNnydfBLEqefM5UjibYS+UoTjZdmvPJSiuyrRLGnFj1eYRhFd/ch+5hSlsFVA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/jose": {
- "version": "6.2.10",
- "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.10.tgz",
- "integrity": "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/panva"
- }
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-schema-typed": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
- "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
- "dev": true,
- "license": "BSD-2-Clause"
- },
- "node_modules/json5": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "json5": "lib/cli.js"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/jsonc-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
- "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/jsonfile": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
- "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
- "dev": true,
- "license": "MIT",
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
- "node_modules/karma": {
- "version": "6.4.4",
- "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz",
- "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@colors/colors": "1.5.0",
- "body-parser": "^1.19.0",
- "braces": "^3.0.2",
- "chokidar": "^3.5.1",
- "connect": "^3.7.0",
- "di": "^0.0.1",
- "dom-serialize": "^2.2.1",
- "glob": "^7.1.7",
- "graceful-fs": "^4.2.6",
- "http-proxy": "^1.18.1",
- "isbinaryfile": "^4.0.8",
- "lodash": "^4.17.21",
- "log4js": "^6.4.1",
- "mime": "^2.5.2",
- "minimatch": "^3.0.4",
- "mkdirp": "^0.5.5",
- "qjobs": "^1.2.0",
- "range-parser": "^1.2.1",
- "rimraf": "^3.0.2",
- "socket.io": "^4.7.2",
- "source-map": "^0.6.1",
- "tmp": "^0.2.1",
- "ua-parser-js": "^0.7.30",
- "yargs": "^16.1.1"
- },
- "bin": {
- "karma": "bin/karma"
- },
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/karma-chrome-launcher": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz",
- "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "which": "^1.2.1"
- }
- },
- "node_modules/karma-coverage": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz",
- "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "istanbul-lib-coverage": "^3.2.0",
- "istanbul-lib-instrument": "^5.1.0",
- "istanbul-lib-report": "^3.0.0",
- "istanbul-lib-source-maps": "^4.0.1",
- "istanbul-reports": "^3.0.5",
- "minimatch": "^3.0.4"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/karma-coverage/node_modules/@babel/compat-data": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
- "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/karma-coverage/node_modules/@babel/core": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
- "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.7",
- "@babel/helper-compilation-targets": "^7.29.7",
- "@babel/helper-module-transforms": "^7.29.7",
- "@babel/helpers": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/template": "^7.29.7",
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7",
- "@jridgewell/remapping": "^2.3.5",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/karma-coverage/node_modules/@babel/generator": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
- "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.8",
- "@babel/types": "^7.29.8",
- "@jridgewell/gen-mapping": "^0.3.12",
- "@jridgewell/trace-mapping": "^0.3.28",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/karma-coverage/node_modules/@babel/helper-compilation-targets": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
- "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.29.7",
- "@babel/helper-validator-option": "^7.29.7",
- "browserslist": "^4.24.0",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/karma-coverage/node_modules/@babel/helper-globals": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
- "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/karma-coverage/node_modules/@babel/helper-module-imports": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
- "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/karma-coverage/node_modules/@babel/helper-module-transforms": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
- "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7",
- "@babel/traverse": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/karma-coverage/node_modules/@babel/helper-validator-option": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
- "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/karma-coverage/node_modules/@babel/helpers": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
- "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/karma-coverage/node_modules/@babel/template": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
- "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/karma-coverage/node_modules/@babel/traverse": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
- "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.8",
- "@babel/helper-globals": "^7.29.7",
- "@babel/parser": "^7.29.8",
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.8",
- "debug": "^4.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/karma-coverage/node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
- "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "@babel/core": "^7.12.3",
- "@babel/parser": "^7.14.7",
- "@istanbuljs/schema": "^0.1.2",
- "istanbul-lib-coverage": "^3.2.0",
- "semver": "^6.3.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/karma-coverage/node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/karma-coverage/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/karma-jasmine": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz",
- "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "jasmine-core": "^4.1.0"
- },
- "engines": {
- "node": ">=12"
- },
- "peerDependencies": {
- "karma": "^6.0.0"
- }
- },
- "node_modules/karma-jasmine-html-reporter": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz",
- "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "jasmine-core": "^4.0.0 || ^5.0.0",
- "karma": "^6.0.0",
- "karma-jasmine": "^5.0.0"
- }
- },
- "node_modules/karma-jasmine/node_modules/jasmine-core": {
- "version": "4.6.1",
- "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz",
- "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/karma/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/karma/node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
- },
- "engines": {
- "node": ">= 8.10.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/karma/node_modules/cliui": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
- "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.0",
- "wrap-ansi": "^7.0.0"
- }
- },
- "node_modules/karma/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/karma/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/karma/node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/karma/node_modules/picomatch": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
- "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/karma/node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "picomatch": "^2.2.1"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
- "node_modules/karma/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/karma/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/karma/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/karma/node_modules/wrap-ansi": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/karma/node_modules/yargs": {
- "version": "16.2.2",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz",
- "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cliui": "^7.0.2",
- "escalade": "^3.1.1",
- "get-caller-file": "^2.0.5",
- "require-directory": "^2.1.1",
- "string-width": "^4.2.0",
- "y18n": "^5.0.5",
- "yargs-parser": "^20.2.2"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/karma/node_modules/yargs-parser": {
- "version": "20.2.9",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
- "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/lightningcss": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
- "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
- "dev": true,
- "license": "MPL-2.0",
- "dependencies": {
- "detect-libc": "^2.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-android-arm64": "1.33.0",
- "lightningcss-darwin-arm64": "1.33.0",
- "lightningcss-darwin-x64": "1.33.0",
- "lightningcss-freebsd-x64": "1.33.0",
- "lightningcss-linux-arm-gnueabihf": "1.33.0",
- "lightningcss-linux-arm64-gnu": "1.33.0",
- "lightningcss-linux-arm64-musl": "1.33.0",
- "lightningcss-linux-x64-gnu": "1.33.0",
- "lightningcss-linux-x64-musl": "1.33.0",
- "lightningcss-win32-arm64-msvc": "1.33.0",
- "lightningcss-win32-x64-msvc": "1.33.0"
- }
- },
- "node_modules/lightningcss-android-arm64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
- "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
- "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
- "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
- "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
- "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
- "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
- "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
- "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
- "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
- "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
- "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/listr2": {
- "version": "11.0.0",
- "resolved": "https://registry.npmjs.org/listr2/-/listr2-11.0.0.tgz",
- "integrity": "sha512-8K88S0aSrcSXdJfiZtEy5BQMnR+TyjrCGLcgAvQs6ta0NEnIm0RJ72/Pv67Jvg07cfBhDbuN74V81lSSVYEFEw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cli-truncate": "^6.1.1",
- "log-update": "^8.0.0",
- "wrap-ansi": "^10.0.0"
- },
- "engines": {
- "node": ">=22.13.0"
- }
- },
- "node_modules/lmdb": {
- "version": "3.5.6",
- "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.6.tgz",
- "integrity": "sha512-j3uE8ReKNyUWDjhfEFSJqE/1DLtfTR5Z8yFzVHvBjAk37wNg7HdScjcv8ttPHRvrdgPQMPWxFFI0SsdBzI5lBw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@harperfast/extended-iterable": "^1.0.3",
- "msgpackr": "^1.11.2",
- "node-addon-api": "^6.1.0",
- "node-gyp-build-optional-packages": "5.2.2",
- "ordered-binary": "^1.5.3",
- "weak-lru-cache": "^1.2.2"
- },
- "bin": {
- "download-lmdb-prebuilds": "bin/download-prebuilds.js"
- },
- "optionalDependencies": {
- "@lmdb/lmdb-darwin-arm64": "3.5.6",
- "@lmdb/lmdb-darwin-x64": "3.5.6",
- "@lmdb/lmdb-linux-arm": "3.5.6",
- "@lmdb/lmdb-linux-arm64": "3.5.6",
- "@lmdb/lmdb-linux-x64": "3.5.6",
- "@lmdb/lmdb-win32-arm64": "3.5.6",
- "@lmdb/lmdb-win32-x64": "3.5.6"
- }
- },
- "node_modules/lodash": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
- "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/log-symbols": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
- "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-unicode-supported": "^2.0.0",
- "yoctocolors": "^2.1.1"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/log-update": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/log-update/-/log-update-8.0.0.tgz",
- "integrity": "sha512-lddSgOt3bPASrylL54ZSpy8nBHns+vBVSoILlVOx+dei300pnLRN958rj/EdlVLKuWlSESU3qdnDZdAI7FXYGg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-escapes": "^7.3.0",
- "cli-cursor": "^5.0.0",
- "slice-ansi": "^9.0.0",
- "string-width": "^8.2.0",
- "strip-ansi": "^7.2.0",
- "wrap-ansi": "^10.0.0"
- },
- "engines": {
- "node": ">=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/log-update/node_modules/string-width": {
- "version": "8.2.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
- "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.5.0",
- "strip-ansi": "^7.1.2"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/log4js": {
- "version": "6.9.1",
- "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz",
- "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "date-format": "^4.0.14",
- "debug": "^4.3.4",
- "flatted": "^3.2.7",
- "rfdc": "^1.3.0",
- "streamroller": "^3.1.5"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/lru-cache": {
- "version": "11.5.2",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
- "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/magic-string": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.0.0.tgz",
- "integrity": "sha512-CGvjzMN08iv6w1mm4/x3Gh1hLb4VnyRUA15FFpl6CsCIGGoe36k7kY5KNz9QDbSBN5I/fWHM6ZlIkUTa5xdUEA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/make-dir": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
- "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "semver": "^7.5.3"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/merge-descriptors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
- "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/mime": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
- "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "mime": "cli.js"
- },
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mimic-function": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
- "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/mkdirp": {
- "version": "0.5.6",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
- "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "minimist": "^1.2.6"
- },
- "bin": {
- "mkdirp": "bin/cmd.js"
- }
- },
- "node_modules/mrmime": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
- "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/msgpackr": {
- "version": "1.12.1",
- "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz",
- "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "optionalDependencies": {
- "msgpackr-extract": "^3.0.2"
- }
- },
- "node_modules/msgpackr-extract": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz",
- "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "node-gyp-build-optional-packages": "5.2.2"
- },
- "bin": {
- "download-msgpackr-prebuilds": "bin/download-prebuilds.js"
- },
- "optionalDependencies": {
- "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4",
- "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4",
- "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4",
- "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4",
- "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4",
- "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4"
- }
- },
- "node_modules/mute-stream": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz",
- "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": "^20.17.0 || >=22.9.0"
- }
- },
- "node_modules/nanoid": {
- "version": "3.3.18",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
- "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/negotiator": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
- "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "content-type": "^2.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/negotiator/node_modules/content-type": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
- "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/node-addon-api": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
- "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
- "node_modules/node-gyp-build-optional-packages": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
- "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "detect-libc": "^2.0.1"
- },
- "bin": {
- "node-gyp-build-optional-packages": "bin.js",
- "node-gyp-build-optional-packages-optional": "optional.js",
- "node-gyp-build-optional-packages-test": "build-test.js"
- }
- },
- "node_modules/node-releases": {
- "version": "2.0.53",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz",
- "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/npm-package-arg": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-14.0.0.tgz",
- "integrity": "sha512-69XQh3k+dtGa1p+7RaR57IuG3rCko96xr/nUfN4yDYBXbTYICiWcOpsFKLN2GtGE9cyIljE+f1exnaYt9MvM+Q==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "hosted-git-info": "^10.1.0",
- "proc-log": "^7.0.0",
- "semver": "^7.3.5",
- "validate-npm-package-name": "^8.0.0"
- },
- "engines": {
- "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
- }
- },
- "node_modules/nth-check": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
- "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "boolbase": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/fb55/nth-check?sponsor=1"
- }
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-inspect": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/obug": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
- "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
- "dev": true,
- "funding": [
- "https://github.com/sponsors/sxzz",
- "https://opencollective.com/debug"
- ],
- "license": "MIT",
- "engines": {
- "node": ">=12.20.0"
- }
- },
- "node_modules/on-finished": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "wrappy": "1"
- }
- },
- "node_modules/onetime": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
- "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mimic-function": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ora": {
- "version": "9.4.1",
- "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz",
- "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^5.6.2",
- "cli-cursor": "^5.0.0",
- "cli-spinners": "^3.2.0",
- "is-interactive": "^2.0.0",
- "is-unicode-supported": "^2.1.0",
- "log-symbols": "^7.0.1",
- "stdin-discarder": "^0.3.2",
- "string-width": "^8.1.0"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ora/node_modules/string-width": {
- "version": "8.2.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
- "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.5.0",
- "strip-ansi": "^7.1.2"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ordered-binary": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz",
- "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
- "node_modules/oxc-parser": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.142.0.tgz",
- "integrity": "sha512-kKR+jPiRJYJDexVoziIg/FVGvr1fT1FZSSJOk6tVoMKKSlsf1Cso+cgGCJkOEDWOP174vRntCPFKg+AS7InWvw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@oxc-project/types": "^0.142.0"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
- },
- "optionalDependencies": {
- "@oxc-parser/binding-android-arm-eabi": "0.142.0",
- "@oxc-parser/binding-android-arm64": "0.142.0",
- "@oxc-parser/binding-darwin-arm64": "0.142.0",
- "@oxc-parser/binding-darwin-x64": "0.142.0",
- "@oxc-parser/binding-freebsd-x64": "0.142.0",
- "@oxc-parser/binding-linux-arm-gnueabihf": "0.142.0",
- "@oxc-parser/binding-linux-arm-musleabihf": "0.142.0",
- "@oxc-parser/binding-linux-arm64-gnu": "0.142.0",
- "@oxc-parser/binding-linux-arm64-musl": "0.142.0",
- "@oxc-parser/binding-linux-ppc64-gnu": "0.142.0",
- "@oxc-parser/binding-linux-riscv64-gnu": "0.142.0",
- "@oxc-parser/binding-linux-riscv64-musl": "0.142.0",
- "@oxc-parser/binding-linux-s390x-gnu": "0.142.0",
- "@oxc-parser/binding-linux-x64-gnu": "0.142.0",
- "@oxc-parser/binding-linux-x64-musl": "0.142.0",
- "@oxc-parser/binding-openharmony-arm64": "0.142.0",
- "@oxc-parser/binding-wasm32-wasi": "0.142.0",
- "@oxc-parser/binding-win32-arm64-msvc": "0.142.0",
- "@oxc-parser/binding-win32-ia32-msvc": "0.142.0",
- "@oxc-parser/binding-win32-x64-msvc": "0.142.0"
- }
- },
- "node_modules/parse5": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
- "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
- "license": "MIT",
- "dependencies": {
- "entities": "^8.0.0"
- },
- "funding": {
- "url": "https://github.com/inikulin/parse5?sponsor=1"
- }
- },
- "node_modules/parse5-html-rewriting-stream": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.1.tgz",
- "integrity": "sha512-NaRku2aMpUN1Sh1Gyk1KWUh2A7EJx2c6qYzvwsPtqhoHoaURshdrceYK3LunVCm3WHhm6FS7Vcczbvdh3/UIVw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "entities": "^8.0.0",
- "parse5": "^8.0.0",
- "parse5-sax-parser": "^8.0.0"
- },
- "funding": {
- "url": "https://github.com/inikulin/parse5?sponsor=1"
- }
- },
- "node_modules/parse5-html-rewriting-stream/node_modules/entities": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
- "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=20.19.0"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/parse5-sax-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz",
- "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "parse5": "^8.0.0"
- },
- "funding": {
- "url": "https://github.com/inikulin/parse5?sponsor=1"
- }
- },
- "node_modules/parse5/node_modules/entities": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
- "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=20.19.0"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-to-regexp": {
- "version": "8.4.2",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
- "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/piscina": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.2.0.tgz",
- "integrity": "sha512-DszUCKeVN/5G5QKo6jAVHL8fmKnkJvQ0ACiVgY7YGCq3TUB2oznAOayvZPIAdEThvhczkXR+qm3IHsNXpFCYfA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=20.x"
- },
- "optionalDependencies": {
- "@napi-rs/nice": "^1.0.4"
- }
- },
- "node_modules/pkce-challenge": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
- "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.25",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
- "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.16",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/postcss-media-query-parser": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
- "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/postcss-safe-parser": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz",
- "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=18.0"
- },
- "peerDependencies": {
- "postcss": "^8.4.31"
- }
- },
- "node_modules/proc-log": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-7.0.0.tgz",
- "integrity": "sha512-FYgfaA69XZ93zaXLoMNQ+ViDXGGBgR8aLh03txzcFhV+9xOXx7+8DLCULrKKpR9+GsH9ZfHm82aSUPpozX0Ztg==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
- }
- },
- "node_modules/proxy-addr": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
- "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "forwarded": "0.2.0",
- "ipaddr.js": "1.9.1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/proxy-agent-negotiate": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz",
- "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 20"
- },
- "peerDependencies": {
- "kerberos": "^2.0.0"
- },
- "peerDependenciesMeta": {
- "kerberos": {
- "optional": true
- }
- }
- },
- "node_modules/punycode": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/qjobs": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz",
- "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.9"
- }
- },
- "node_modules/qs": {
- "version": "6.15.3",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
- "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "es-define-property": "^1.0.1",
- "side-channel": "^1.1.1"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/range-parser": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
- "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/raw-body": {
- "version": "2.5.3",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
- "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.4.24",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/raw-body/node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/readdirp": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
- "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 20.19.0"
- },
- "funding": {
- "type": "individual",
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/reflect-metadata": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
- "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/require-directory": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/require-from-string": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/requires-port": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
- "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/restore-cursor": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
- "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "onetime": "^7.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/rfdc": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
- "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "deprecated": "Rimraf versions prior to v4 are no longer supported",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "glob": "^7.1.3"
- },
- "bin": {
- "rimraf": "bin.js"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/rolldown": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.0.tgz",
- "integrity": "sha512-u7tgm5l4Yw1iTqUL4EcYOAt7fFvCgQMLeidrnD4GALlC6aOznCjezYajgxeyKw27u0Q5N7fwgCzjVyPIWzwuBA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@oxc-project/types": "=0.140.0",
- "@rolldown/pluginutils": "^1.0.0"
- },
- "bin": {
- "rolldown": "bin/cli.mjs"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.2.0",
- "@rolldown/binding-darwin-arm64": "1.2.0",
- "@rolldown/binding-darwin-x64": "1.2.0",
- "@rolldown/binding-freebsd-x64": "1.2.0",
- "@rolldown/binding-linux-arm-gnueabihf": "1.2.0",
- "@rolldown/binding-linux-arm64-gnu": "1.2.0",
- "@rolldown/binding-linux-arm64-musl": "1.2.0",
- "@rolldown/binding-linux-ppc64-gnu": "1.2.0",
- "@rolldown/binding-linux-s390x-gnu": "1.2.0",
- "@rolldown/binding-linux-x64-gnu": "1.2.0",
- "@rolldown/binding-linux-x64-musl": "1.2.0",
- "@rolldown/binding-openharmony-arm64": "1.2.0",
- "@rolldown/binding-wasm32-wasi": "1.2.0",
- "@rolldown/binding-win32-arm64-msvc": "1.2.0",
- "@rolldown/binding-win32-x64-msvc": "1.2.0"
- }
- },
- "node_modules/rolldown/node_modules/@oxc-project/types": {
- "version": "0.140.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.140.0.tgz",
- "integrity": "sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
- }
- },
- "node_modules/router": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
- "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.0",
- "depd": "^2.0.0",
- "is-promise": "^4.0.0",
- "parseurl": "^1.3.3",
- "path-to-regexp": "^8.0.0"
- },
- "engines": {
- "node": ">= 18"
- }
- },
- "node_modules/rxjs": {
- "version": "7.8.2",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
- "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.1.0"
- }
- },
- "node_modules/safe-regex-test": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
- "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "is-regex": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/sass": {
- "version": "1.101.0",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz",
- "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chokidar": "^5.0.0",
- "immutable": "^5.1.5",
- "source-map-js": ">=0.6.2 <2.0.0"
- },
- "bin": {
- "sass": "sass.js"
- },
- "engines": {
- "node": ">=20.19.0"
- },
- "optionalDependencies": {
- "@parcel/watcher": "^2.4.1"
- }
- },
- "node_modules/semver": {
- "version": "7.8.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/send": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
- "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.3",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "etag": "^1.8.1",
- "fresh": "^2.0.0",
- "http-errors": "^2.0.1",
- "mime-types": "^3.0.2",
- "ms": "^2.1.3",
- "on-finished": "^2.4.1",
- "range-parser": "^1.2.1",
- "statuses": "^2.0.2"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/send/node_modules/encodeurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/send/node_modules/mime-db": {
- "version": "1.54.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
- "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/send/node_modules/mime-types": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
- "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mime-db": "^1.54.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/send/node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/serve-static": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
- "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "parseurl": "^1.3.3",
- "send": "^1.2.0"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/serve-static/node_modules/encodeurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/setprototypeof": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/side-channel": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
- "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.4",
- "side-channel-list": "^1.0.1",
- "side-channel-map": "^1.0.1",
- "side-channel-weakmap": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-list": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
- "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.4"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-map": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
- "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-weakmap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
- "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3",
- "side-channel-map": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/slice-ansi": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-9.0.0.tgz",
- "integrity": "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.3",
- "is-fullwidth-code-point": "^5.1.0"
- },
- "engines": {
- "node": ">=22"
- },
- "funding": {
- "url": "https://github.com/chalk/slice-ansi?sponsor=1"
- }
- },
- "node_modules/slice-ansi/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/socket.io": {
- "version": "4.8.3",
- "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
- "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "accepts": "~1.3.4",
- "base64id": "~2.0.0",
- "cors": "~2.8.5",
- "debug": "~4.4.1",
- "engine.io": "~6.6.0",
- "socket.io-adapter": "~2.5.2",
- "socket.io-parser": "~4.2.4"
- },
- "engines": {
- "node": ">=10.2.0"
- }
- },
- "node_modules/socket.io-adapter": {
- "version": "2.5.8",
- "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz",
- "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "debug": "~4.4.1",
- "ws": "~8.21.0"
- }
- },
- "node_modules/socket.io-parser": {
- "version": "4.2.7",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz",
- "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@socket.io/component-emitter": "~3.1.0",
- "debug": "~4.4.1"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/source-map": {
- "version": "0.7.6",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
- "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">= 12"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/source-map-support": {
- "version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- }
- },
- "node_modules/source-map-support/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/statuses": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
- "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/stdin-discarder": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz",
- "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/streamroller": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
- "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "date-format": "^4.0.14",
- "debug": "^4.3.4",
- "fs-extra": "^8.1.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/string-width": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
- "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^10.3.0",
- "get-east-asian-width": "^1.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/strip-ansi": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
- "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.2.2"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/tinyglobby": {
- "version": "0.2.17",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tmp": {
- "version": "0.2.7",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
- "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.14"
- }
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/toidentifier": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
- },
- "node_modules/type-is": {
- "version": "1.6.18",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
- "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "media-typer": "0.3.0",
- "mime-types": "~2.1.24"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/typescript": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
- "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/ua-parser-js": {
- "version": "0.7.41",
- "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz",
- "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/ua-parser-js"
- },
- {
- "type": "paypal",
- "url": "https://paypal.me/faisalman"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/faisalman"
- }
- ],
- "license": "MIT",
- "bin": {
- "ua-parser-js": "script/cli.js"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/undici-types": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
- "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/universalify": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
- "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4.0.0"
- }
- },
- "node_modules/unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/update-browserslist-db": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz",
- "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "escalade": "^3.2.0",
- "picocolors": "^1.1.1"
- },
- "bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "browserslist": ">= 4.21.0"
- }
- },
- "node_modules/utils-merge": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4.0"
- }
- },
- "node_modules/validate-npm-package-name": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-8.0.0.tgz",
- "integrity": "sha512-SCv6OOV6Xj2/3cXy3dGmADluJTNcL3o7hZAglNPTe+WYuEuvxgJzxPrSDLZhF+CwyQOubqgecjMmTJGMVLWjYQ==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
- }
- },
- "node_modules/vary": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/vite": {
- "version": "8.1.5",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
- "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "lightningcss": "^1.32.0",
- "picomatch": "^4.0.5",
- "postcss": "^8.5.17",
- "rolldown": "~1.1.5",
- "tinyglobby": "^0.2.17"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.3.0",
- "esbuild": "^0.27.0 || ^0.28.0",
- "jiti": ">=1.21.0",
- "less": "^4.0.0",
- "sass": "^1.70.0",
- "sass-embedded": "^1.70.0",
- "stylus": ">=0.54.8",
- "sugarss": "^5.0.0",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "@vitejs/devtools": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/vite/node_modules/@emnapi/core": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
- "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.2.2",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/vite/node_modules/@emnapi/runtime": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
- "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/vite/node_modules/@oxc-project/types": {
- "version": "0.139.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
- "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-android-arm64": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
- "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
- "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
- "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
- "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
- "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
- "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
- "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
- "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
- "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
- "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
- "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
- "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
- "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "1.11.1",
- "@emnapi/runtime": "1.11.1",
- "@napi-rs/wasm-runtime": "^1.1.6"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
- "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
- "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/vite/node_modules/rolldown": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
- "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@oxc-project/types": "=0.139.0",
- "@rolldown/pluginutils": "^1.0.0"
- },
- "bin": {
- "rolldown": "bin/cli.mjs"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.1.5",
- "@rolldown/binding-darwin-arm64": "1.1.5",
- "@rolldown/binding-darwin-x64": "1.1.5",
- "@rolldown/binding-freebsd-x64": "1.1.5",
- "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
- "@rolldown/binding-linux-arm64-gnu": "1.1.5",
- "@rolldown/binding-linux-arm64-musl": "1.1.5",
- "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
- "@rolldown/binding-linux-s390x-gnu": "1.1.5",
- "@rolldown/binding-linux-x64-gnu": "1.1.5",
- "@rolldown/binding-linux-x64-musl": "1.1.5",
- "@rolldown/binding-openharmony-arm64": "1.1.5",
- "@rolldown/binding-wasm32-wasi": "1.1.5",
- "@rolldown/binding-win32-arm64-msvc": "1.1.5",
- "@rolldown/binding-win32-x64-msvc": "1.1.5"
- }
- },
- "node_modules/void-elements": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
- "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/watchpack": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz",
- "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.1.2"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/weak-lru-cache": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz",
- "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
- "node_modules/which": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
- "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "which": "bin/which"
- }
- },
- "node_modules/wrap-ansi": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.1.tgz",
- "integrity": "sha512-M0N4xzyzosiIok3svYlEo1sdLZts/8FPgYH/GPC3wvlmPoRvnoManGMrE54waYj3tISA8w6lsdesfVv67qSr8Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.3",
- "string-width": "^8.2.0"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/wrap-ansi/node_modules/string-width": {
- "version": "8.2.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
- "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.5.0",
- "strip-ansi": "^7.1.2"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/ws": {
- "version": "8.21.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
- "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "node_modules/y18n": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
- "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/yargs": {
- "version": "18.1.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz",
- "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cliui": "^9.0.1",
- "escalade": "^3.1.1",
- "get-caller-file": "^2.0.5",
- "string-width": "^8.2.1",
- "y18n": "^5.0.5",
- "yargs-parser": "^22.0.0"
- },
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=23"
- }
- },
- "node_modules/yargs-parser": {
- "version": "22.0.0",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
- "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=23"
- }
- },
- "node_modules/yargs/node_modules/string-width": {
- "version": "8.2.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
- "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.5.0",
- "strip-ansi": "^7.1.2"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/yoctocolors": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz",
- "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/zod": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
- "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/colinhacks"
- }
- },
- "node_modules/zod-to-json-schema": {
- "version": "3.25.2",
- "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
- "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
- "dev": true,
- "license": "ISC",
- "peerDependencies": {
- "zod": "^3.25.28 || ^4"
- }
- },
- "node_modules/zone.js": {
- "version": "0.15.1",
- "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
- "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
- "license": "MIT"
- }
- }
-}
diff --git a/apps/showcase_ui/package.json b/apps/showcase_ui/package.json
deleted file mode 100644
index 5969a2da0..000000000
--- a/apps/showcase_ui/package.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "name": "artemis-showcase-ui",
- "version": "1.0.0",
- "scripts": {
- "ng": "ng",
- "start": "ng serve --proxy-config proxy.conf.json",
- "build": "ng build",
- "watch": "ng build --watch --configuration development",
- "test": "ng test"
- },
- "private": true,
- "dependencies": {
- "@angular/cdk": "^22.1.5",
- "@angular/common": "^22.1.4",
- "@angular/compiler": "^22.1.4",
- "@angular/core": "^22.1.4",
- "@angular/forms": "^22.1.4",
- "@angular/platform-browser": "^22.1.4",
- "@angular/platform-browser-dynamic": "^22.1.4",
- "@angular/router": "^22.1.4",
- "rxjs": "~7.8.0",
- "tslib": "^2.3.0",
- "zone.js": "~0.15.0"
- },
- "devDependencies": {
- "@angular/build": "^22.1.6",
- "@angular/cli": "^22.1.6",
- "@angular/compiler-cli": "^22.1.4",
- "@types/jasmine": "~5.1.0",
- "istanbul-lib-instrument": "^6.0.3",
- "jasmine-core": "~5.6.0",
- "karma": "~6.4.0",
- "karma-chrome-launcher": "~3.2.0",
- "karma-coverage": "~2.2.0",
- "karma-jasmine": "~5.1.0",
- "karma-jasmine-html-reporter": "~2.1.0",
- "typescript": "~6.0.3"
- }
-}
diff --git a/apps/showcase_ui/proxy.conf.json b/apps/showcase_ui/proxy.conf.json
deleted file mode 100644
index fd09eea92..000000000
--- a/apps/showcase_ui/proxy.conf.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "/api": {
- "target": "http://localhost:8000",
- "secure": false,
- "logLevel": "debug"
- },
- "/images": {
- "target": "http://localhost:8000",
- "secure": false,
- "logLevel": "debug"
- },
- "/videos": {
- "target": "http://localhost:8000",
- "secure": false,
- "logLevel": "debug"
- },
- "/local_file": {
- "target": "http://localhost:8000",
- "secure": false,
- "logLevel": "debug"
- }
-}
-
diff --git a/apps/showcase_ui/public/favicon.ico b/apps/showcase_ui/public/favicon.ico
deleted file mode 100644
index df9d973c4..000000000
Binary files a/apps/showcase_ui/public/favicon.ico and /dev/null differ
diff --git a/apps/showcase_ui/public/logo.png b/apps/showcase_ui/public/logo.png
deleted file mode 100644
index 2d96e1400..000000000
Binary files a/apps/showcase_ui/public/logo.png and /dev/null differ
diff --git a/apps/showcase_ui/public/logo.svg b/apps/showcase_ui/public/logo.svg
deleted file mode 100644
index 53a9a18bf..000000000
--- a/apps/showcase_ui/public/logo.svg
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/apps/showcase_ui/src/app/app.component.html b/apps/showcase_ui/src/app/app.component.html
deleted file mode 100644
index c4d9d0434..000000000
--- a/apps/showcase_ui/src/app/app.component.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
diff --git a/apps/showcase_ui/src/app/app.component.scss b/apps/showcase_ui/src/app/app.component.scss
deleted file mode 100644
index 0f3fad224..000000000
--- a/apps/showcase_ui/src/app/app.component.scss
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
diff --git a/apps/showcase_ui/src/app/app.component.spec.ts b/apps/showcase_ui/src/app/app.component.spec.ts
deleted file mode 100644
index dcae6f074..000000000
--- a/apps/showcase_ui/src/app/app.component.spec.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { TestBed } from '@angular/core/testing';
-import { provideRouter } from '@angular/router';
-import { provideHttpClient, withXhr } from '@angular/common/http';
-import { AppComponent } from './app.component';
-
-describe('AppComponent', () => {
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [AppComponent],
- providers: [provideRouter([]), provideHttpClient(withXhr())]
- }).compileComponents();
- });
-
- it('should create the app', () => {
- const fixture = TestBed.createComponent(AppComponent);
- const app = fixture.componentInstance;
- expect(app).toBeTruthy();
- });
-
- it(`should have the 'frontend' title`, () => {
- const fixture = TestBed.createComponent(AppComponent);
- const app = fixture.componentInstance;
- expect(app.title).toEqual('frontend');
- });
-
-});
diff --git a/apps/showcase_ui/src/app/app.component.ts b/apps/showcase_ui/src/app/app.component.ts
deleted file mode 100644
index c9376cf21..000000000
--- a/apps/showcase_ui/src/app/app.component.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Component, ChangeDetectionStrategy } from '@angular/core';
-import { RouterOutlet } from '@angular/router';
-import { NavSwitcherComponent } from './components/nav-switcher/nav-switcher.component';
-
-@Component({
- selector: 'app-root',
- imports: [RouterOutlet, NavSwitcherComponent],
- templateUrl: './app.component.html',
- changeDetection: ChangeDetectionStrategy.Eager,
- styleUrl: './app.component.scss'
-})
-export class AppComponent {
- title = 'frontend';
-}
diff --git a/apps/showcase_ui/src/app/app.config.ts b/apps/showcase_ui/src/app/app.config.ts
deleted file mode 100644
index c5a0f8775..000000000
--- a/apps/showcase_ui/src/app/app.config.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
-import { provideRouter } from '@angular/router';
-import { provideHttpClient, withXhr } from '@angular/common/http';
-
-import { routes } from './app.routes';
-
-export const appConfig: ApplicationConfig = {
- providers: [
- provideZoneChangeDetection({ eventCoalescing: true }),
- provideRouter(routes),
- provideHttpClient(withXhr())
- ]
-};
diff --git a/apps/showcase_ui/src/app/app.routes.ts b/apps/showcase_ui/src/app/app.routes.ts
deleted file mode 100644
index 3c163ece9..000000000
--- a/apps/showcase_ui/src/app/app.routes.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Routes } from '@angular/router';
-import { HomeComponent } from './pages/home/home.component';
-import { WorkspaceComponent } from './pages/workspace/workspace.component';
-import { LegacyWorkspaceComponent } from './pages/legacy-workspace/legacy-workspace.component';
-
-export const routes: Routes = [
- { path: '', component: HomeComponent },
- { path: 'workspace', component: WorkspaceComponent },
- { path: 'check', component: LegacyWorkspaceComponent },
- { path: '**', redirectTo: '' }
-];
diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts b/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts
deleted file mode 100644
index 5c915fd12..000000000
--- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import { StartupProgressEvent } from '../../services/agent.service';
-import { buildStartupWorkItems } from './agent-stream.component';
-
-describe('startup Work block', () => {
- it('shows only the three device preparation operations', () => {
- const events: StartupProgressEvent[] = [
- { stage: 'submitting', message: 'Submitting the task', timestamp: 100 },
- { stage: 'queued', message: 'Task received and queued', timestamp: 100.9 },
- { stage: 'launching', message: 'Starting the execution process', timestamp: 101 },
- { stage: 'configuration', message: 'Loading the run configuration', timestamp: 103.1 },
- { stage: 'device_check', message: 'Checking the Android device', timestamp: 103.2 },
- { stage: 'device_ready', message: 'Android device connected', timestamp: 123 },
- { stage: 'model_warmup', message: 'Warming the model connection', timestamp: 123 },
- { stage: 'uiautomator', message: 'Connecting to UI Automator', timestamp: 124 },
- { stage: 'uiautomator_ready', message: 'UI Automator is ready', timestamp: 128 },
- { stage: 'environment', message: 'Preparing the device environment', timestamp: 128 },
- { stage: 'environment_ready', message: 'Device environment is ready', timestamp: 131 }
- ];
-
- const items = buildStartupWorkItems(events, 131, false, true);
-
- expect(items.map((item) => item.stage)).toEqual([
- 'device_ready',
- 'uiautomator_ready',
- 'environment_ready'
- ]);
- expect(items.map((item) => item.elapsed)).toEqual(['20s', '4.0s', '3.0s']);
- expect(items.every((item) => !item.isActive)).toBeTrue();
- });
-
- it('keeps the current preparation operation live without exposing first-action text', () => {
- const events: StartupProgressEvent[] = [
- { stage: 'device_check', message: 'Checking the Android device', timestamp: 10 }
- ];
-
- const [item] = buildStartupWorkItems(events, 11.4, false, true);
-
- expect(item.message).toBe('Checking the Android device');
- expect(item.elapsed).toBe('1.4s');
- expect(item.isActive).toBeTrue();
- });
-
- it('shows the helper install sub-step while the hierarchy service is connecting', () => {
- const events: StartupProgressEvent[] = [
- { stage: 'uiautomator', message: 'Connecting to the UI hierarchy service', timestamp: 10 },
- {
- stage: 'helper_install',
- message: 'Installing the Artemis accessibility helper on this device for the first time (v1.1.3, about 3 seconds)',
- timestamp: 10.2
- }
- ];
-
- const [item] = buildStartupWorkItems(events, 12, false, true);
-
- expect(item.isActive).toBeTrue();
- expect(item.message).toContain('Installing the Artemis accessibility helper');
- expect(item.elapsed).toBe('2.0s');
- });
-
- it('uses the hierarchy source line as the completion message of the second step', () => {
- const events: StartupProgressEvent[] = [
- { stage: 'uiautomator', message: 'Connecting to the UI hierarchy service', timestamp: 10 },
- { stage: 'helper_install', message: 'Installing ...', timestamp: 10.2 },
- { stage: 'uiautomator_ready', message: 'UI hierarchy service is ready (helper)', timestamp: 13 },
- {
- stage: 'hierarchy_backend',
- message: 'UI hierarchy source: Artemis accessibility helper v1.1.3',
- timestamp: 13.1
- },
- // A later mid-run switch must not rewrite the startup line.
- {
- stage: 'hierarchy_backend_changed',
- message: 'UI hierarchy source switched from Artemis accessibility helper to UIAutomator2 because ...',
- timestamp: 90
- }
- ];
-
- const [item] = buildStartupWorkItems(events, 100, true, true);
-
- expect(item.isActive).toBeFalse();
- expect(item.message).toBe('UI hierarchy source: Artemis accessibility helper v1.1.3');
- expect(item.elapsed).toBe('3.0s');
- });
-
- it('falls back to the generic completion line for runs without a source event', () => {
- const events: StartupProgressEvent[] = [
- { stage: 'uiautomator', message: 'Connecting to the UI hierarchy service', timestamp: 10 },
- { stage: 'environment', message: 'Preparing the device environment', timestamp: 14 }
- ];
-
- const [item] = buildStartupWorkItems(events, 20, false, true);
-
- expect(item.message).toBe('UI hierarchy service is ready');
- });
-
- it('uses old first_response events only as a hidden completion boundary', () => {
- const events: StartupProgressEvent[] = [
- { stage: 'environment', message: 'Preparing the device environment', timestamp: 20 },
- { stage: 'first_response', message: 'Generating the first action', timestamp: 23 }
- ];
-
- const [item] = buildStartupWorkItems(events, 24, false, true);
-
- expect(item.message).toBe('Device environment is ready');
- expect(item.elapsed).toBe('3.0s');
- expect(item.isActive).toBeFalse();
- });
-});
diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html
deleted file mode 100644
index 93172d380..000000000
--- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html
+++ /dev/null
@@ -1,1436 +0,0 @@
-
-
-
-
-
-
-
- @if (consolidatedBlocks().length > 0 || showPlanningLoader()) {
-
-
- @if (agentService.viewedModel(); as model) {
-
- {{ getModelIcon(model.name) }}
- Architecture:
- {{ getModelDisplayName(model.name) }}
-
-
-
-
-
- }
-
-
- @if (agentService.isCurrentSessionRunning()) {
-
- videocam
-
- }
- @if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() === 'processing') {
-
- progress_activity
-
- }
- @if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && agentService.currentSessionVideoUrl()) {
-
- smart_display
-
- }
- @if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && !agentService.currentSessionVideoUrl() && agentService.hasCurrentSessionStepFrames()) {
-
- slideshow
-
- }
- @if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && !agentService.currentSessionVideoUrl() && !agentService.hasCurrentSessionStepFrames() && agentService.currentSessionRecordingStatus() === 'failed') {
-
- videocam_off
-
- }
- @if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && !agentService.currentSessionVideoUrl() && !agentService.hasCurrentSessionStepFrames() && agentService.currentSessionRecordingStatus() !== 'failed') {
-
- videocam
-
- }
- @if (agentService.isCurrentSessionRunning()) {
- Recording...
- }
- @if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() === 'processing') {
- Preparing...
- }
- @if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && agentService.currentSessionVideoUrl()) {
- Play Recording
- }
- @if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && !agentService.currentSessionVideoUrl() && agentService.hasCurrentSessionStepFrames()) {
- Step Replay
- }
- @if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && !agentService.currentSessionVideoUrl() && !agentService.hasCurrentSessionStepFrames() && agentService.currentSessionRecordingStatus() === 'failed') {
- Recording Failed
- }
- @if (!agentService.isCurrentSessionRunning() && agentService.currentSessionRecordingStatus() !== 'processing' && !agentService.currentSessionVideoUrl() && !agentService.hasCurrentSessionStepFrames() && agentService.currentSessionRecordingStatus() !== 'failed') {
- Screen Recording
- }
- @if (agentService.isCurrentSessionRunning()) {
-
- }
- @if (!agentService.isCurrentSessionRunning() && !agentService.currentSessionVideoUrl() && agentService.hasCurrentSessionStepFrames()) {
-
- {{ agentService.currentSessionStepFrames().length }}
-
- }
-
-
- }
-
-
-
- @if (agentService.isSessionContentLoading()) {
-
- progress_activity
- Loading run history...
-
- }
-
- @if (!agentService.isSessionContentLoading() && consolidatedBlocks().length === 0 && !showPlanningLoader() && startupWorkItems().length === 0) {
-
-
sensors
-
No Stream Activity Selected
-
Please select a session from the Task List on the right to view its real-time log payloads.
-
- }
-
-
- @if (!agentService.isSessionContentLoading() && (phases().length > 0 || showPlanningLoader() || startupWorkItems().length > 0)) {
-
- @if (startupWorkItems().length > 0) {
-
-
-
-
-
-
-
- @for (item of startupWorkItems(); track item) {
-
-
-
-
- {{ item.message }} · {{ item.isActive ? 'Waiting ' : 'Waited ' }}{{ item.elapsed }}
-
-
-
- }
-
-
-
-
-
-
- }
- @for (phase of phases(); track trackPhase($index, phase); let first = $first) {
-
- @if (hasVisibleBlocks(phase)) {
-
- }
- @if (hasVisibleBlocks(phase)) {
-
- @for (block of phase.blocks; track trackBlock($index, block)) {
- @if (hasVisibleContent(block)) {
-
-
-
-
- @if (isCheckerBlock(block)) {
-
-
- @if (!isCheckerMuted(block) && getCheckerItems(block).length > 0) {
-
- @for (item of getCheckerItems(block); track $index) {
-
- {{ $index + 1 }}
- {{ item.text }}
- {{ getCheckMethodLabel(item, block) }}
-
- }
-
- }
-
- }
-
- @if (getSortedStepEvents(block.data).length > 0) {
-
- @for (item of getSortedStepEvents(block.data); track trackStepEvent(itemIdx, item); let itemIdx = $index) {
-
- @if (isCheckerBlock(block) && item.type === 'thinking') {
-
-
-
-
-
- @if (item.data.text && item.data.text.trim()) {
-
- }
-
- }
- @if (isCheckerBlock(block) && item.type === 'text' && isCheckerTextVisible(item)) {
- @if (item.data.isReset) {
-
-
-
-
- {{ formatResetMessage(getCheckerSegmentResetMessage(item), isCheckerSegmentRunning(block, itemIdx)) }}
-
-
-
- }
- @if (item.data.text && item.data.text.trim()) {
-
- }
- }
-
- @if (!isCheckerBlock(block) && item.type === 'thinking') {
- @if (getBlockResets(block, true).length > 0) {
-
- @for (reset of getBlockResets(block, true); track reset.id || $index) {
-
-
-
- {{ formatResetMessage(reset.message, reset.isWaiting) }}
-
-
- }
-
- }
- @if (getStreamDisplayText(block.id + '-native', item.data.text) || (isBlockRunning(block) && !isStreamRewinding(block.id + '-native') && !hasWaitingReset(block, true))) {
-
-
-
-
-
- @if (getStreamDisplayText(block.id + '-native', item.data.text)) {
-
- }
-
- }
- }
-
- @if (!isCheckerBlock(block) && item.type === 'text') {
- @if (getBlockResets(block, false).length > 0) {
-
- @for (reset of getBlockResets(block, false); track reset.id || $index) {
-
-
-
- {{ formatResetMessage(reset.message, reset.isWaiting) }}
-
-
- }
-
- }
- @if (getStreamDisplayText(block.id, item.data.text) || (isBlockRunning(block) && !isStreamRewinding(block.id) && !hasWaitingReset(block, false))) {
-
-
- @if (getStreamDisplayText(block.id, item.data.text)) {
-
- }
-
- }
- }
-
- @if (isReportStatusAction(item.data)) {
-
-
-
- @if (!isActionExpanded('action-' + block.id + '-' + itemIdx) && getReportStatusExplanation(item.data)) {
-
-
- Summary:
- "{{ getReportStatusExplanation(item.data) }}"
-
-
- }
-
- @if (isActionExpanded('action-' + block.id + '-' + itemIdx)) {
-
-
- @if (getReportStatusExplanation(item.data); as expText) {
-
- Report & Findings
-
-
- }
-
-
- @if (getStepPreImageUrl(block.data, item.data) || getStepPostImageUrl(block.data, item.data)) {
-
- @if (getStepPreImageUrl(block.data, item.data)) {
-
-
Final Verified State
-
-
-
-
-
-
- }
- @if (getStepPostImageUrl(block.data, item.data)) {
-
-
Post Verification
-
-
-
-
-
-
- }
-
- }
-
- }
-
- }
-
- @if (item.type === 'action' && !isReportStatusAction(item.data) && (isAndroidAction(item.data) || item.data?.type === 'action')) {
-
-
-
- @if (!isActionExpanded('action-' + block.id + '-' + itemIdx) && (getActionTargetText(item.data) || getActionInputText(item.data) || getActionCoords(item.data))) {
-
- @if (getActionTargetText(item.data)) {
-
- Target:
- "{{ getActionTargetText(item.data) }}"
-
- }
- @if (getActionInputText(item.data)) {
-
- {{ getActionInputLabel(item.data) }}:
- "{{ getActionInputText(item.data) }}"
-
- }
- @if (getActionCoords(item.data)) {
-
- Coordinates:
- {{ getActionCoords(item.data) }}
-
- }
-
- }
-
- @if (isActionExpanded('action-' + block.id + '-' + itemIdx)) {
-
-
- @if (getActionTargetText(item.data)) {
-
- Target
- "{{ getActionTargetText(item.data) }}"
-
- }
- @if (getActionInputText(item.data)) {
-
- {{ getActionInputLabel(item.data) }}
- "{{ getActionInputText(item.data) }}"
-
- }
- @if (getActionCoords(item.data)) {
-
- Coordinates
- {{ getActionCoords(item.data) }}
-
- }
- @if (getActionBounds(item.data)) {
-
- Target Bounds
- [{{ getActionBounds(item.data) }}]
-
- }
- @if (getActionClass(item.data)) {
-
- Target Class
- {{ getActionClass(item.data) }}
-
- }
- @for (param of getActionExtraParams(item.data); track trackParam($index, param)) {
-
- {{ param.key }}
- {{ param.value }}
-
- }
-
- @if (!getActionTargetText(item.data) && !getActionInputText(item.data) && !getActionCoords(item.data) && !getActionBounds(item.data) && !getActionClass(item.data) && getActionExtraParams(item.data).length === 0) {
-
- Action
- {{ getActionTitle(item.data) }}
-
- }
-
-
- @if (getStepPreImageUrl(block.data, item.data) || getStepPostImageUrl(block.data, item.data)) {
-
- @if (getStepPreImageUrl(block.data, item.data)) {
-
-
Pre-Action (Before)
-
-
-
-
-
-
-
- }
- @if (getStepPostImageUrl(block.data, item.data)) {
-
-
Post-Action (After)
-
-
-
-
-
-
- }
-
- }
- @if (isActionFailed(item.data, block.data)) {
-
- error
- {{ getActionErrorMessage(item.data, block.data) }}
-
- }
-
- }
-
- }
-
- @if (item.type === 'tool') {
-
- @if (isBackendSwitchNote(item.data)) {
-
-
-
-
- {{ getBackendSwitchMessage(item.data) }}
-
-
-
- }
-
- @if (isLLMRetry(item.data)) {
-
- @for (retry of getLLMRetryEntries(item.data); track retry; let retryIndex = $index) {
-
-
-
- @if (isLLMRetryEntryWaiting(retry)) {
-
- LLM temporarily unavailable
- @if (getLLMRetryEntryDelay(retry); as retryDelay) {
- · Retrying in {{ retryDelay }}…
- }
-
-
- Frequent retries may indicate a low API rate limit. Consider increasing the limit or reducing request concurrency.
-
- }
- @if (!isLLMRetryEntryWaiting(retry)) {
-
- Retrying LLM request · Waited {{ getLLMRetryEntryWaited(retry) }}
-
- }
-
-
- }
-
- }
-
- @if (isDisplayableLLMFailure(item.data)) {
-
-
- @if (isTerminalLLMFailure(item.data)) {
-
-
- play_arrow
- Continue task
- arrow_forward
-
- Task paused · no steps are running
-
- }
-
- }
-
- @if (shouldShowTool(item.data, block.data) && !isDeviceActionTool(item.data)) {
-
- }
-
- @if (shouldShowTool(item.data, block.data) && isDeviceActionTool(item.data)) {
-
- }
- }
- }
-
- }
-
- @if (hasCheckerResult(block)) {
-
- @for (verdict of getCheckerVerdicts(block); track $index) {
-
- {{ getVerdictItemNumber(block, verdict, $index) }}
- {{ getVerdictIcon(verdict.status) }}
-
- {{ verdict.evidence || getVerdictStatusText(verdict.status) }}
- @if (verdict.suggestion) {
- {{ verdict.suggestion }}
- }
-
-
- }
- @for (note of getCheckerNotes(block); track note) {
-
{{ note }}
- }
-
- }
-
-
- }
- }
-
- }
-
- }
-
- @if (outputterReport(); as outReport) {
-
-
-
- @if (parsedOutputReport(); as parsedOut) {
-
- @if (parsedOut.title) {
-
{{ parsedOut.title }}
- }
- @if (parsedOut.milestones.length > 0) {
-
- @for (m of parsedOut.milestones; track trackMilestone($index, m)) {
-
-
- {{ m.type === 'checked' ? 'check_circle' : (m.type === 'progress' ? 'pending' : 'radio_button_unchecked') }}
-
-
- @for (seg of m.segments; track seg) {
- @if (seg.bold) {
- {{ seg.text }}
- }
- @if (seg.code) {
- {{ seg.text }}
- }
- @if (!seg.bold && !seg.code) {
- {{ seg.text }}
- }
- }
-
-
- }
-
- }
- @if (parsedOut.otherLines.length > 0) {
-
- @for (line of parsedOut.otherLines; track trackMarkdownLine($index, line)) {
-
- @if (line.type === 'verify') {
- {{ line.checkKind || 'verify' }}{{ line.atEnd ? '@end' : '' }}
- }
- @if (line.type === 'finding') {
- finding
- }
- @for (seg of line.segments; track seg) {
- @if (seg.bold) {
- {{ seg.text }}
- }
- @if (seg.code) {
- {{ seg.text }}
- }
- @if (!seg.bold && !seg.code) {
- {{ seg.text }}
- }
- }
-
- }
-
- }
-
- }
-
-
- }
-
- @if (showPlanningLoader() && (startupWorkItems().length === 0 || startupPreparationIsComplete() || consolidatedBlocks().length > 0)) {
-
-
- progress_activity
- {{ currentPlanningText() }}
-
-
- }
-
- }
-
-
-
- @if (selectedZoomImage()) {
-
-
-
- close
-
-
-
-
-
-
-
- }
-
diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss
deleted file mode 100644
index cb1c34f71..000000000
--- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss
+++ /dev/null
@@ -1,3973 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-$border-color: rgba(0, 0, 0, 0.08);
-$text-primary: #1f1f1f;
-$text-secondary: #1f1f1f;
-$text-muted: #1f1f1f;
-$bg-main: #f9fafb;
-$uniform-gap: 8px;
-$stream-row-height: 25px;
-
-:host {
- display: flex;
- flex-direction: column;
- height: 100%;
- width: 100%;
- overflow: hidden;
-}
-
-.agent-stream-container {
- display: flex;
- flex-direction: column;
- height: 100%;
- background: #f8f9fa;
- box-sizing: border-box;
- position: relative;
- container-type: inline-size;
- container-name: stream-panel;
-
- .vscode-nav-header {
- position: relative;
- height: 60px;
- flex-shrink: 0;
- background: #ffffff;
- border-bottom: 1px solid rgba(0, 0, 0, 0.08);
- display: flex;
- align-items: center;
- justify-content: flex-end;
- padding: 0 16px;
- box-sizing: border-box;
- user-select: none;
- box-shadow: none;
- z-index: 10;
-
- @media (min-width: 1151px) {
- display: none !important;
- }
-
- .vscode-tab-container {
- display: flex;
- height: 36px;
- box-sizing: border-box;
- background: rgba(241, 245, 249, 0.72);
- backdrop-filter: blur(16px) saturate(180%);
- -webkit-backdrop-filter: blur(16px) saturate(180%);
- border: 1px solid rgba(226, 232, 240, 0.85);
- border-radius: 18px;
- padding: 3px;
- gap: 3px;
- align-items: center;
- flex-shrink: 0;
- box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.8);
- }
-
- .vscode-tab-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- height: 30px;
- padding: 0 14px;
- box-sizing: border-box;
- border: none;
- background: transparent;
- border-radius: 15px;
- font-size: 13px;
- font-weight: 600;
- color: #64748b;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- text-decoration: none;
- cursor: pointer;
- position: relative;
- line-height: 1;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- outline: none;
- white-space: nowrap;
-
- .tab-icon {
- font-size: 16px;
- color: inherit;
- transition: color 0.15s ease;
- }
-
- .badge-count {
- background: #2563eb;
- color: #ffffff;
- font-size: 10px;
- font-weight: 700;
- padding: 1px 6px;
- border-radius: 10px;
- line-height: 1;
- }
-
- .dropdown-chevron {
- font-size: 16px;
- color: #94a3b8;
- margin-left: 2px;
- transition: transform 0.2s ease, color 0.15s ease;
-
- &.open {
- transform: rotate(180deg);
- color: #1e293b;
- }
- }
-
- &:hover:not(.active) {
- color: #0f172a;
- background: rgba(255, 255, 255, 0.6);
- }
-
- &.active {
- background: #ffffff;
- color: #2563eb;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.02);
-
- .tab-icon {
- color: #2563eb;
- }
- }
-
- &.video-replay-tab-btn {
- .video-indicator-dot {
- width: 7px;
- height: 7px;
- border-radius: 50%;
- background: #10b981;
- box-shadow: 0 0 6px #10b981;
- margin-left: -2px;
- flex-shrink: 0;
- }
-
- &.has-video {
- .tab-icon {
- color: #0284c7;
- }
- }
- }
- }
-
- .header-right-actions {
- margin-left: auto;
- display: flex;
- align-items: center;
-
- .btn-resume-task,
- .btn-stop-task {
- padding: 5px 10px;
- background: #2563eb;
- border: 1px solid #2563eb;
- border-radius: 16px;
- font-size: 13px;
- font-weight: 600;
- color: #ffffff;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 4px;
- transition: all 0.2s;
- box-shadow: 0 2px 4px rgba(37, 99, 235, 0.2);
-
- .material-symbols-outlined {
- font-size: 15px;
- color: inherit;
- }
-
- &:hover {
- background: #1d4ed8;
- border-color: #1d4ed8;
- box-shadow: 0 4px 8px rgba(37, 99, 235, 0.3);
- }
- }
-
- .btn-stop-task {
- background: #ef4444;
- border-color: #ef4444;
- box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2);
-
- &:hover {
- background: #dc2626;
- border-color: #dc2626;
- box-shadow: 0 4px 8px rgba(239, 68, 68, 0.3);
- }
- }
- }
-
-
- }
-
- .stream-floating-toolbar {
- position: absolute;
- top: 12px;
- right: 20px;
- height: 36px;
- display: inline-flex;
- align-items: center;
- gap: 8px;
- z-index: 40;
- pointer-events: auto;
-
- @media (max-width: 1150px) {
- top: 72px;
- }
-
- /* Model Info Chip: click opens the run info popover */
- .model-info-chip {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- height: 30px;
- padding: 0 11px;
- margin: 0;
- box-sizing: border-box;
- background: rgba(255, 255, 255, 0.72);
- backdrop-filter: blur(16px) saturate(180%);
- -webkit-backdrop-filter: blur(16px) saturate(180%);
- border: 1px solid rgba(226, 232, 240, 0.85);
- border-radius: 15px;
- cursor: pointer;
- user-select: none;
- appearance: none;
- -webkit-appearance: none;
-
- &:hover {
- background: rgba(255, 255, 255, 0.92);
- border-color: rgba(203, 213, 225, 0.95);
- }
-
- &.active {
- background: #eff6ff;
- border-color: #bfdbfe;
-
- .model-icon,
- .model-name {
- color: #1d4ed8;
- }
- }
-
- &:focus-visible {
- outline: 2px solid #93c5fd;
- outline-offset: 1px;
- }
- box-shadow: 0 2px 6px -1px rgba(15, 23, 42, 0.05), inset 0 1px 1px rgba(255, 255, 255, 0.9);
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-
- .model-icon {
- font-size: 15px;
- width: 15px;
- height: 15px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- color: #64748b;
- }
-
- .model-label {
- font-size: 12px;
- font-weight: 500;
- color: #64748b;
- line-height: 1;
- }
-
- .model-name {
- font-size: 12px;
- font-weight: 700;
- color: #1e293b;
- letter-spacing: 0.1px;
- line-height: 1;
- }
- }
-
- /* Screen Recording Action Button: Interactive Button */
- .btn-screen-recording {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- height: 30px;
- padding: 0 12px;
- box-sizing: border-box;
- background: rgba(255, 255, 255, 0.72);
- backdrop-filter: blur(16px) saturate(180%);
- -webkit-backdrop-filter: blur(16px) saturate(180%);
- border: 1px solid rgba(226, 232, 240, 0.85);
- border-radius: 15px;
- box-shadow: 0 2px 6px -1px rgba(15, 23, 42, 0.05), inset 0 1px 1px rgba(255, 255, 255, 0.9);
- cursor: pointer;
- transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
- user-select: none;
- outline: none;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-
- .video-icon {
- font-size: 15px;
- width: 15px;
- height: 15px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- color: #64748b;
- transition: transform 0.2s ease, color 0.2s ease;
- }
-
- .btn-text {
- font-size: 12px;
- font-weight: 600;
- color: #475569;
- letter-spacing: 0.1px;
- line-height: 1;
- transition: color 0.15s ease;
- }
-
- .video-pulse-dot {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: #ef4444;
- box-shadow: 0 0 6px #ef4444;
- animation: pulseRed 1.5s infinite;
- }
-
- &.recording {
- background: rgba(254, 242, 242, 0.9);
- border-color: rgba(248, 113, 113, 0.6);
- box-shadow: 0 2px 6px -1px rgba(220, 38, 38, 0.15), inset 0 1px 1px rgba(255, 255, 255, 0.9);
-
- .video-icon {
- color: #dc2626;
- }
-
- .btn-text {
- color: #b91c1c;
- }
- }
-
- &.processing {
- .video-icon {
- color: #2563eb;
- animation: spin 1s linear infinite;
- }
- }
-
- &:hover:not(.active) {
- background: rgba(255, 255, 255, 0.88);
- border-color: rgba(203, 213, 225, 0.95);
- box-shadow: 0 4px 12px -2px rgba(15, 23, 42, 0.08), inset 0 1px 1px rgba(255, 255, 255, 1);
- color: #0f172a;
-
- .video-icon {
- color: #1a73e8;
- transform: scale(1.08);
- }
-
- .btn-text {
- color: #0f172a;
- }
- }
-
- &:active {
- transform: translateY(0);
- box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
- }
-
- &.has-steps {
- background: rgba(240, 253, 250, 0.9);
- border-color: rgba(94, 234, 212, 0.7);
- box-shadow: 0 2px 6px -1px rgba(13, 148, 136, 0.12), inset 0 1px 1px rgba(255, 255, 255, 0.9);
-
- .steps-icon {
- color: #0d9488;
- }
-
- .btn-text {
- color: #0f766e;
- }
-
- &:hover:not(.active) {
- background: rgba(204, 251, 241, 0.95);
- border-color: rgba(45, 212, 191, 0.85);
-
- .steps-icon {
- color: #0f766e;
- }
- }
- }
-
- &.failed {
- background: rgba(254, 242, 242, 0.8);
- border-color: rgba(254, 202, 202, 0.9);
-
- .error-icon {
- color: #ef4444;
- }
-
- .btn-text {
- color: #dc2626;
- }
- }
-
- .steps-count-badge {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 1px 5px;
- border-radius: 10px;
- background: #0d9488;
- color: #ffffff;
- font-size: 10px;
- font-weight: 700;
- line-height: 1;
- margin-left: 2px;
- }
-
- &.active {
- background: rgba(26, 115, 232, 0.95);
- border-color: rgba(26, 115, 232, 0.95);
- box-shadow: 0 2px 8px rgba(26, 115, 232, 0.35);
-
- .video-icon, .btn-text {
- color: #ffffff;
- }
-
- .video-pulse-dot {
- background: #ffffff;
- box-shadow: 0 0 6px rgba(255, 255, 255, 0.9);
- }
-
- .steps-count-badge {
- background: #ffffff;
- color: #1a73e8;
- }
- }
- }
- }
-
- .stream-logs-content {
- flex: 1;
- overflow-y: auto;
- padding: 72px 24px 100px 24px;
- position: relative;
-
- .session-loading-state {
- min-height: 180px;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 9px;
- color: #64748b;
- font-size: 13px;
-
- .material-symbols-outlined {
- font-size: 18px;
- }
- }
- display: flex;
- flex-direction: column;
-
- .floating-model-badge {
- position: sticky;
- top: 0px;
- float: right;
- margin-top: -2px;
- margin-right: 4px;
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 3px 9px;
- background: rgba(255, 255, 255, 0.92);
- backdrop-filter: blur(12px);
- border: 1px solid rgba(99, 102, 241, 0.2);
- border-radius: 12px;
- box-shadow: 0 1px 4px rgba(99, 102, 241, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
- z-index: 35;
- cursor: default;
- transition: all 0.2s ease;
- user-select: none;
-
- &:hover {
- background: rgba(255, 255, 255, 1);
- border-color: rgba(99, 102, 241, 0.4);
- box-shadow: 0 2px 8px rgba(99, 102, 241, 0.15);
- transform: translateY(-1px);
-
- .model-icon {
- color: #4f46e5;
- transform: rotate(15deg) scale(1.05);
- }
- }
-
- .model-icon {
- font-size: 13px;
- color: #6366f1;
- transition: all 0.3s ease;
- }
-
- .model-name {
- font-size: 11px;
- font-weight: 600;
- color: #334155;
- letter-spacing: normal;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
- }
- }
-
- &::-webkit-scrollbar {
- width: 6px;
- }
- &::-webkit-scrollbar-track {
- background: transparent;
- }
- &::-webkit-scrollbar-thumb {
- background: rgba(0, 0, 0, 0.15);
- border-radius: 10px;
- }
- &::-webkit-scrollbar-thumb:hover {
- background: rgba(0, 0, 0, 0.25);
- }
-
- .empty-state {
- flex: 1 1 auto;
- min-height: 100%;
- width: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- text-align: center;
- box-sizing: border-box;
- padding: 30px;
- margin: auto 0;
-
- .empty-icon {
- margin-bottom: 12px;
- opacity: 0.8;
-
- .material-symbols-outlined {
- font-size: 40px;
- color: #94a3b8;
- }
- }
-
- h3 {
- margin: 0 0 6px 0;
- font-size: 12px;
- font-weight: 600;
- color: $text-primary;
- }
-
- p {
- margin: 0;
- font-size: 12px;
- color: $text-secondary;
- max-width: 320px;
- line-height: 1.5;
- }
- }
-
- .logs-list-container {
- display: flex;
- flex-direction: column;
-
- .planning-loader-container {
- display: flex;
- align-items: center;
- margin-top: 10px;
- margin-bottom: 10px;
- padding-left: 2px;
-
- .planning-loader {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- color: #475569;
- font-size: 13px;
- font-weight: 500;
- user-select: none;
-
- .spin-animation {
- font-size: 16px;
- width: 16px;
- height: 16px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- animation: spin-loader 1s linear infinite;
- color: #2563eb;
- flex-shrink: 0;
- }
-
- .loader-text {
- color: #475569;
- font-size: 13px;
- font-weight: 500;
- line-height: 1.4;
- }
- }
- }
- }
-
- .phase-container {
- display: flex;
- flex-direction: column;
- }
-
- .phase-header {
- font-size: 12px;
- font-weight: normal;
- line-height: 1.5;
- min-height: 20px;
- box-sizing: border-box;
- color: #71717a;
- margin-top: $uniform-gap * 2;
- margin-bottom: 8px;
- user-select: none;
- display: flex;
- align-items: center;
- gap: 4px;
-
- &.first-phase {
- margin-top: 0;
- }
-
- .phase-worked-time {
- color: #71717a;
- font-weight: normal;
- }
-
- .phase-token-usage {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- color: #8e8e93;
- cursor: default;
-
- .token-dot {
- color: #a1a1aa;
- font-weight: bold;
- }
-
- .token-text {
- color: #71717a;
- font-variant-numeric: tabular-nums;
- }
- }
- }
-
- .phase-body {
- display: flex;
- flex-direction: column;
- gap: $uniform-gap;
- padding-left: 12px;
- border-left: 1px solid rgba(0, 0, 0, 0.04);
- margin-left: 6px;
- position: relative;
- z-index: 1;
-
- .block-item {
- display: flex;
- flex-direction: column;
- gap: $uniform-gap;
-
- .llm-stream-body,
- .step-body {
- .thinking-stream-details {
- border-radius: 8px;
- padding: 0;
- overflow: visible;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
- border: 1px solid transparent;
- background: transparent;
- display: flex;
- flex-direction: column;
-
- .details-content-wrapper {
- display: grid;
- grid-template-rows: 1fr;
- transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- overflow: hidden;
-
- .details-content {
- min-height: 0;
- }
- }
-
-
- &.native-thinking-details {
- border-color: transparent;
- background: transparent;
- box-shadow: none;
-
- &:not(.collapsed) {
- background: transparent;
- }
-
- .details-summary {
- color: #71717a;
-
- &::after {
- color: #d4d4d8;
- }
-
- &:hover {
- color: #1f1f1f;
- }
- }
-
- &.is-running {
- .details-summary {
- background: linear-gradient(
- 90deg,
- #71717a 0%,
- #2563eb 25%,
- #7c3aed 50%,
- #0284c7 75%,
- #71717a 100%
- );
- background-size: 200% 100%;
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- animation: subtle-color-flow 2.8s linear infinite;
- font-weight: 500;
-
- .stream-status-dot {
- background: #3b82f6;
- box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.45);
- animation: pulse-stream-dot 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
- }
- }
-
- &.is-resetting {
- .details-summary {
- background: none !important;
- -webkit-background-clip: border-box !important;
- background-clip: border-box !important;
- -webkit-text-fill-color: #71717a !important;
- color: #71717a !important;
- animation: none !important;
- font-weight: normal !important;
-
- .stream-status-dot {
- background: #f59e0b;
- box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.45);
- animation: pulse-stream-amber 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
- }
- }
-
- .stream-status-dot {
- background: #d4d4d8;
-
- &.streaming {
- background: #3b82f6;
- box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.45);
- animation: pulse-stream-dot 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
-
- &.resetting {
- background: #f59e0b;
- box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.45);
- animation: pulse-stream-amber 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
- }
-
- .stream-text {
- color: #1f1f1f;
- }
- }
-
- &.raw-thinking-details {
- border-color: #dbeafe;
- background: #f8fafc;
-
- &:not(.collapsed) {
- background: #f8fafc;
- }
-
- .details-summary {
- color: #1e40af;
-
- &::after {
- color: #93c5fd;
- }
-
- &:hover {
- color: #1e3a8a;
- }
- }
-
- .stream-status-dot {
- background: #93c5fd;
-
- &.streaming {
- background: #2563eb;
- animation: pulse-stream-blue 1.5s infinite;
- }
- }
- }
-
- &:not(.collapsed) {
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
-
- .details-summary {
- margin-bottom: $uniform-gap;
- }
-
- .details-summary::after {
- transform: rotate(90deg);
- }
- }
-
- .details-summary {
- padding: 0;
- margin-bottom: 0;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', Arial, sans-serif;
- font-weight: normal;
- font-size: 12px;
- line-height: 1.5;
- min-height: 20px;
- box-sizing: border-box;
- letter-spacing: 0.5px;
- cursor: pointer;
- user-select: none;
- display: flex;
- align-items: center;
- gap: 8px;
- outline: none;
- list-style: none;
-
- &::-webkit-details-marker {
- display: none;
- }
-
- &::after {
- content: '❯';
- display: inline-block;
- font-size: 9px;
- margin-left: 6px;
- transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), color 0.2s ease;
- }
-
- &:hover {
- .stream-status-dot {
- transform: scale(1.25);
- background: #71717a;
- }
-
- &::after {
- transform: translateX(3px);
- color: #71717a;
- }
- }
- }
-
- .stream-status-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- display: inline-block;
- transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.2s ease;
- }
-
- .stream-box {
- max-height: 250px;
- overflow-y: auto;
- background: transparent;
- padding: 4px 0;
- box-sizing: border-box;
-
- &::-webkit-scrollbar {
- width: 6px;
- }
- &::-webkit-scrollbar-track {
- background: transparent;
- }
- &::-webkit-scrollbar-thumb {
- background: rgba(0, 0, 0, 0.08);
- border-radius: 999px;
- &:hover {
- background: rgba(0, 0, 0, 0.16);
- }
- }
- }
-
- .stream-text {
- font-family: inherit;
- font-size: 12.5px;
- line-height: 1.65;
- color: #1f1f1f;
- white-space: normal;
- word-break: break-word;
-
- strong {
- font-weight: 600;
- color: #111827;
- }
-
- div, p {
- font-size: 12.5px;
- line-height: 1.65;
- color: #1f1f1f;
- margin: 4px 0;
- }
-
- h1, h2, h3, h4, h5, h6 {
- font-size: 12.5px;
- font-weight: 600;
- color: #1f1f1f;
- margin-top: 8px;
- margin-bottom: 4px;
- }
-
- .inline-code {
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- font-size: 11px;
- background: #f4f4f5;
- color: #b91c1c;
- padding: 2px 6px;
- border-radius: 4px;
- }
-
- ul, ol {
- margin: 4px 0;
- padding-left: 16px;
- li {
- font-size: 12.5px;
- line-height: 1.65;
- color: #1f1f1f;
- margin: 4px 0;
- padding-left: 2px;
- }
- }
-
- .md-para-gap {
- height: 6px;
- }
- }
-
- &.collapsed {
- .details-content-wrapper {
- grid-template-rows: 0fr;
- display: none;
- }
-
- .details-summary {
- margin-bottom: 0;
- }
- }
- }
-
- .thinking-stream-flat-card {
- background: transparent;
- border: 1px solid transparent;
- border-radius: 8px;
- padding: 0;
- margin-bottom: 0;
- box-shadow: none;
-
- .report-header {
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', Arial, sans-serif;
- font-weight: normal;
- font-size: 12px;
- line-height: 1.5;
- min-height: 20px;
- box-sizing: border-box;
- letter-spacing: 0.5px;
- color: #71717a;
- margin-bottom: $uniform-gap;
- user-select: none;
- display: flex;
- align-items: center;
- gap: 8px;
-
- .stream-status-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- display: inline-block;
- background: #d4d4d8;
-
- &.streaming {
- background: #71717a;
- animation: pulse-stream-grey 1.5s infinite;
- }
- }
- }
-
- .report-content {
- font-size: 12.5px;
- line-height: 1.65;
- color: #1f1f1f;
- }
-
- .stream-box {
- max-height: 250px;
- overflow-y: auto;
- background: transparent;
- padding: 4px 0;
- box-sizing: border-box;
-
- &::-webkit-scrollbar {
- width: 6px;
- }
- &::-webkit-scrollbar-track {
- background: transparent;
- }
- &::-webkit-scrollbar-thumb {
- background: rgba(0, 0, 0, 0.08);
- border-radius: 999px;
- &:hover {
- background: rgba(0, 0, 0, 0.16);
- }
- }
- }
-
- .stream-text {
- font-family: inherit;
- font-size: 12.5px;
- line-height: 1.65;
- color: #1f1f1f;
- white-space: normal;
- word-break: break-word;
-
- strong {
- font-weight: 600;
- color: #111827;
- }
-
- div, p {
- font-size: 12.5px;
- line-height: 1.65;
- color: #1f1f1f;
- margin: 4px 0;
- }
-
- h1 {
- font-size: 14px;
- font-weight: 700;
- color: #0f172a;
- margin-top: 12px;
- margin-bottom: 6px;
- }
-
- h2 {
- font-size: 13.5px;
- font-weight: 700;
- color: #0f172a;
- margin-top: 10px;
- margin-bottom: 5px;
- }
-
- h3 {
- font-size: 13px;
- font-weight: 600;
- color: #1e293b;
- margin-top: 8px;
- margin-bottom: 4px;
- }
-
- h4, h5, h6 {
- font-size: 12.5px;
- font-weight: 600;
- color: #334155;
- margin-top: 6px;
- margin-bottom: 3px;
- }
-
- em {
- font-style: italic;
- color: inherit;
- }
-
- blockquote {
- margin: 6px 0;
- padding: 4px 10px;
- border-left: 3px solid #cbd5e1;
- color: #475569;
- background: rgba(241, 245, 249, 0.5);
- border-radius: 0 4px 4px 0;
- }
-
- .code-block {
- margin: 6px 0;
- padding: 8px 12px;
- background: #f8fafc;
- border: 1px solid #e2e8f0;
- border-radius: 6px;
- overflow-x: auto;
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- font-size: 11.5px;
- line-height: 1.5;
- color: #1e293b;
- }
-
- .md-hr {
- border: none;
- border-top: 1px solid #e2e8f0;
- margin: 10px 0;
- }
-
- .inline-code {
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- font-size: 11px;
- background: #f4f4f5;
- color: #b91c1c;
- padding: 2px 6px;
- border-radius: 4px;
- }
-
- ul, ol {
- margin: 4px 0;
- padding-left: 16px;
- li {
- font-size: 12.5px;
- line-height: 1.65;
- color: #1f1f1f;
- margin: 4px 0;
- padding-left: 2px;
-
- &.md-verify-item {
- color: #334155;
- }
-
- &.md-finding-item {
- color: #92400e;
- }
- }
- }
-
- .md-para-gap {
- height: 6px;
- }
- }
- }
-
- .checker-stream-flat-card {
- background: #f5f3ff;
- border: 1px solid #e9d5ff;
- border-radius: 8px;
- padding: 12px 16px;
- margin-bottom: 8px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.01);
-
- .report-header {
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', Arial, "Liberation Sans", sans-serif;
- font-weight: 700;
- font-size: 12px;
- line-height: 1.5;
- letter-spacing: 0.5px;
- color: #6b21a8;
- margin-bottom: 8px;
- user-select: none;
- display: flex;
- align-items: center;
- gap: 8px;
-
- .stream-status-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- display: inline-block;
- background: #c084fc;
-
- &.streaming {
- background: #a855f7;
- animation: pulse-stream-purple 1.5s infinite;
- }
- }
- }
-
- .report-content {
- font-size: 12px;
- line-height: 1.6;
- color: #27272a;
-
- .stream-text {
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- font-size: 11.5px;
- white-space: pre-wrap;
- word-break: break-all;
- color: #5b21b6;
- background: rgba(255, 255, 255, 0.5);
- padding: 8px;
- border-radius: 6px;
- border: 1px dashed rgba(168, 85, 247, 0.2);
- }
- }
-
- .checker-result-badge {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-weight: 700;
- font-size: 11.5px;
- padding: 4px 10px;
- border-radius: 6px;
- margin-bottom: 10px;
- border: 1px solid transparent;
-
- &.success {
- background: #ecfdf5;
- color: #065f46;
- border-color: #a7f3d0;
- }
-
- &.failed {
- background: #fef2f2;
- color: #991b1b;
- border-color: #fca5a5;
- }
- }
-
- .checker-reason {
- font-size: 12px;
- line-height: 1.6;
- color: #3f3f46;
- font-weight: 500;
- }
- }
- }
-
- .step-body {
- display: flex;
- flex-direction: column;
- gap: $uniform-gap;
-
- .step-raw-thinking-message {
- background: transparent;
- border: 1px solid transparent;
- border-radius: 8px;
- padding: 0;
- margin-bottom: 0;
- box-shadow: none;
-
- &.is-running {
- .report-header {
- background: linear-gradient(
- 90deg,
- #71717a 0%,
- #2563eb 25%,
- #7c3aed 50%,
- #0284c7 75%,
- #71717a 100%
- );
- background-size: 200% 100%;
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- animation: subtle-color-flow 2.8s linear infinite;
- font-weight: 500;
-
- .stream-status-dot {
- background: #3b82f6;
- box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.45);
- animation: pulse-stream-dot 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
- }
- }
-
- &.is-resetting {
- .report-header {
- background: none !important;
- -webkit-background-clip: border-box !important;
- background-clip: border-box !important;
- -webkit-text-fill-color: #71717a !important;
- color: #71717a !important;
- animation: none !important;
- font-weight: normal !important;
- margin-bottom: 0;
-
- .stream-status-dot {
- background: #f59e0b;
- box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.45);
- animation: pulse-stream-amber 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
- }
- }
-
- .report-header {
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', Arial, sans-serif;
- font-weight: normal;
- font-size: 12px;
- line-height: 1.5;
- min-height: 20px;
- box-sizing: border-box;
- letter-spacing: 0.5px;
- color: #71717a;
- margin-bottom: $uniform-gap;
- user-select: none;
- display: flex;
- align-items: center;
- gap: 8px;
-
- .header-label {
- line-height: 1.5;
- word-break: break-word;
- }
-
- .stream-status-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- display: inline-block;
- background: #d4d4d8;
-
- &.streaming {
- background: #3b82f6;
- box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.45);
- animation: pulse-stream-dot 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
-
- &.resetting {
- background: #f59e0b;
- box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.45);
- animation: pulse-stream-amber 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
- }
- }
-
- .report-content {
- font-size: 12.5px;
- line-height: 1.65;
- color: #1f1f1f;
-
- strong {
- font-weight: 600;
- color: #111827;
- }
-
- div, p {
- font-size: 12.5px;
- line-height: 1.65;
- color: #1f1f1f;
- margin: 4px 0;
- }
-
- h1, h2, h3, h4, h5, h6 {
- font-size: 12.5px;
- font-weight: 600;
- color: #1f1f1f;
- margin-top: 8px;
- margin-bottom: 4px;
- }
-
- .inline-code {
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- font-size: 11px;
- background: #f4f4f5;
- color: #b91c1c;
- padding: 2px 6px;
- border-radius: 4px;
- }
-
- ul, ol {
- margin: 4px 0;
- padding-left: 16px;
- li {
- font-size: 12.5px;
- line-height: 1.65;
- color: #1f1f1f;
- margin: 4px 0;
- padding-left: 2px;
- }
- }
-
- .md-para-gap {
- height: 6px;
- }
- }
- }
-
- .step-action-summary-card {
- background: #f4fbf7;
- border: 1px solid #d1fae5;
- border-radius: 8px;
- padding: 12px 16px;
- margin-bottom: 8px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.01);
-
- .report-header {
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', Arial, "Liberation Sans", sans-serif;
- font-weight: 700;
- font-size: 12px;
- line-height: 1.5;
- letter-spacing: 0.5px;
- color: #065f46;
- margin-bottom: 8px;
- user-select: none;
- }
-
- .report-content {
- font-size: 12px;
- line-height: 1.6;
- color: #27272a;
-
- strong {
- font-weight: 600;
- color: #09090b;
- }
-
- .inline-code {
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- font-size: 11px;
- background: #e6f4ea;
- color: #137333;
- padding: 2px 6px;
- border-radius: 4px;
- }
-
- ul, ol {
- margin: 2px 0;
- padding-left: 20px;
- li {
- margin-bottom: 2px;
- }
- }
-
- .md-para-gap {
- height: 4px;
- }
- }
- }
-
- .step-actions-container {
- display: flex;
- flex-direction: column;
- gap: $uniform-gap;
- margin-top: 0;
- margin-bottom: 0;
-
- .llm-retry-notes {
- display: flex;
- flex-direction: column;
- gap: $uniform-gap;
- padding: 0;
-
- .llm-retry-note {
- min-height: $stream-row-height;
- box-sizing: border-box;
- display: flex;
- align-items: center;
- gap: 8px;
- color: #71717a;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', Arial, sans-serif;
- font-size: 12px;
- line-height: 1.5;
- letter-spacing: 0.2px;
-
- .stream-status-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: #d4d4d8;
- flex: 0 0 auto;
-
- &.streaming {
- background: #3b82f6;
- box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.45);
- animation: pulse-stream-dot 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
-
- &.resetting {
- background: #f59e0b;
- box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.45);
- animation: pulse-stream-amber 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
- }
-
- .llm-retry-note-text {
- color: #71717a;
- display: flex;
- flex-direction: column;
- min-width: 0;
-
- .llm-retry-status-line,
- .llm-retry-guidance {
- display: block;
- }
-
- .llm-retry-guidance {
- color: #a1a1aa;
- }
- }
- }
- }
-
- .step-generic-tool {
- border: 1px solid transparent;
-
- &.is-video-attention:not(.is-running) {
- .tool-call-label .stream-status-dot {
- background: #f59e0b;
- box-shadow: none;
- animation: none;
- }
- }
-
- &.is-llm-error {
- .tool-call-label {
- display: flex;
- align-items: flex-start;
- color: #71717a;
- line-height: 1.5;
- user-select: text;
-
- .error-icon {
- font-size: 14px;
- color: #94a3b8;
- margin-top: 1px;
- margin-left: -3px;
- margin-right: -3px;
- flex-shrink: 0;
- }
-
- .tool-call-text {
- display: block;
- color: inherit;
- }
-
- .error-title-text {
- font-weight: 500;
- color: #71717a;
- margin-right: 4px;
- flex-shrink: 0;
- }
-
- .error-message-text {
- color: #71717a;
- word-break: break-word;
- }
- }
- }
-
- &.is-running {
- .tool-call-label {
- .stream-status-dot {
- background: #3b82f6;
- box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.45);
- animation: pulse-stream-dot 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
-
- .tool-call-text > span:first-child {
- background: linear-gradient(
- 90deg,
- #71717a 0%,
- #2563eb 25%,
- #7c3aed 50%,
- #0284c7 75%,
- #71717a 100%
- );
- background-size: 200% 100%;
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- animation: subtle-color-flow 2.8s linear infinite;
- font-weight: 500;
- }
- }
- }
-
- .tool-call-label {
- font-size: 12px;
- font-weight: normal;
- color: #71717a;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', Arial, sans-serif;
- letter-spacing: 0.5px;
- line-height: 1.5;
- min-height: 22px;
- box-sizing: border-box;
- display: inline-flex;
- align-items: center;
- gap: 8px;
- user-select: none;
- padding: 0;
-
- span:not(.tool-key-pill) {
- color: inherit;
- }
-
- .stream-status-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: #d4d4d8;
- display: inline-block;
- flex-shrink: 0;
-
- &.streaming {
- background: #3b82f6;
- box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.45);
- animation: pulse-stream-dot 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
- }
-
- .is-failed-dot {
- font-size: 13px;
- color: #94a3b8;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 8px;
- height: 8px;
- flex-shrink: 0;
- line-height: 1;
- }
-
- .tool-call-text {
- display: inline-flex;
- align-items: center;
- gap: 2px;
- color: inherit;
- }
-
- .tool-key-pill {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 2px 6px;
- border: none;
- background: transparent;
- font-size: 12px;
- font-weight: 500;
- color: #1a73e8;
- cursor: pointer;
- border-radius: 4px;
- transition: all 0.2s ease;
- vertical-align: middle;
- text-decoration: none !important;
-
- .pill-icon {
- font-size: 14px;
- color: inherit;
- }
-
- &:hover {
- color: #1557b0;
- background: rgba(26, 115, 232, 0.08);
- }
- }
-
- .video-status-detail {
- color: inherit;
- margin-left: 2px;
- }
- }
- }
-
- .step-llm-error-card {
- background: #fef2f2;
- border: 1px solid #fecaca;
- border-radius: 10px;
- padding: 10px 12px;
- margin-top: 0;
- margin-bottom: 0;
- margin-left: -19px;
- box-shadow: 0 4px 12px rgba(239, 68, 68, 0.03);
- width: fit-content;
- min-width: 260px;
- max-width: 480px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- transition: all 0.2s ease;
- position: relative;
-
- &:hover {
- border-color: #fca5a5;
- box-shadow: 0 4px 16px rgba(239, 68, 68, 0.08);
- }
-
- .error-card-header {
- display: flex;
- align-items: center;
- gap: 8px;
- border-bottom: 1px solid #fee2e2;
- padding-bottom: 6px;
-
- .error-icon {
- font-size: 18px;
- color: #ef4444;
- }
-
- .error-title {
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', Arial, sans-serif;
- font-weight: 700;
- font-size: 12px;
- color: #991b1b;
- }
- }
-
- .error-card-body {
- display: flex;
- flex-direction: column;
- gap: 6px;
-
- .error-detail-row {
- display: flex;
- font-size: 12px;
-
- .detail-value {
- color: #b91c1c;
- font-weight: 500;
- word-break: break-word;
- line-height: 1.4;
- }
- }
- }
- }
-
- .llm-failure-stack {
- width: 380px;
- max-width: 100%;
- margin-left: -19px;
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 10px;
-
- .step-android-action-card {
- width: 100%;
- margin-left: 0;
- box-sizing: border-box;
- }
-
- .llm-resume-row {
- min-height: 36px;
- display: flex;
- align-items: center;
- gap: 10px;
- }
-
- .btn-continue-task {
- min-height: 36px;
- padding: 7px 12px 7px 10px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 7px;
- box-sizing: border-box;
- border: 1px solid #1d4ed8;
- border-radius: 9px;
- background: #2563eb;
- color: #ffffff;
- font-family: inherit;
- font-size: 12px;
- font-weight: 700;
- line-height: 1;
- cursor: pointer;
- box-shadow: 0 2px 7px rgba(37, 99, 235, 0.22);
- transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
-
- .continue-icon {
- width: 22px;
- height: 22px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- margin: -2px 0 -2px -2px;
- border-radius: 50%;
- background: rgba(255, 255, 255, 0.16);
- font-size: 16px;
- }
-
- .continue-arrow {
- font-size: 15px;
- opacity: 0.72;
- transition: transform 0.18s ease, opacity 0.18s ease;
- }
-
- &:hover {
- background: #1d4ed8;
- border-color: #1e40af;
- box-shadow: 0 4px 11px rgba(37, 99, 235, 0.28);
- transform: translateY(-1px);
-
- .continue-arrow {
- opacity: 1;
- transform: translateX(2px);
- }
- }
-
- &:active {
- box-shadow: 0 1px 4px rgba(37, 99, 235, 0.2);
- transform: translateY(0) scale(0.98);
- }
-
- &:focus-visible {
- outline: 3px solid rgba(59, 130, 246, 0.25);
- outline-offset: 2px;
- }
- }
-
- .llm-resume-hint {
- color: #94a3b8;
- font-size: 11px;
- line-height: 1.35;
- white-space: nowrap;
- }
-
- @media (max-width: 560px) {
- .llm-resume-row {
- width: 100%;
- }
-
- .btn-continue-task {
- flex: 1;
- }
-
- .llm-resume-hint {
- display: none;
- }
- }
- }
-
- .step-android-action-card {
- background: #f8fafc;
- border: 1px solid #e2e8f0;
- border-radius: 10px;
- padding: 10px 12px;
- margin-top: 0;
- margin-bottom: 0;
- margin-left: -19px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
- width: 380px;
- max-width: 100%;
- display: flex;
- flex-direction: column;
- gap: 8px;
- transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
- position: relative;
- cursor: pointer;
- user-select: none;
-
- &.is-running {
- border-color: rgba(59, 130, 246, 0.35);
- box-shadow: 0 2px 10px rgba(59, 130, 246, 0.08);
- animation: subtle-card-glow 2.5s ease-in-out infinite alternate;
-
- .action-icon {
- color: #3b82f6;
- }
-
- .action-title {
- background: linear-gradient(
- 90deg,
- #0f172a 0%,
- #2563eb 30%,
- #7c3aed 50%,
- #0284c7 70%,
- #0f172a 100%
- );
- background-size: 200% 100%;
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- animation: subtle-color-flow 2.8s linear infinite;
- }
- }
-
- &:hover {
- border-color: #cbd5e1;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
- transform: translateY(-1px);
-
- .action-expand-icon {
- color: #1e293b;
- background: #e2e8f0;
- }
- }
-
- &.is-expanded {
- border-color: #94a3b8;
- background: #ffffff;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
-
- .action-expand-icon {
- color: #0f172a;
- background: #e2e8f0;
- }
- }
-
- &.is-failed {
- background: #f8fafc;
- border-color: #fca5a5;
-
- &.has-details {
- cursor: pointer;
- }
-
- &:hover {
- border-color: #f87171;
- }
-
- &.is-expanded {
- background: #ffffff;
- border-color: #f87171;
- }
- }
-
- &.task-report-card {
- background: #f0fdf4;
- border: 1px solid #bbf7d0;
-
- &.is-completed {
- background: #f0fdf4;
- border-color: #86efac;
-
- .report-icon {
- color: #16a34a;
- font-variation-settings: 'FILL' 1;
- }
-
- .action-title {
- color: #14532d;
- font-weight: 700;
- }
-
- .action-status-badge.is-completed {
- background: #dcfce7;
- color: #15803d;
- border: 1px solid #bbf7d0;
- }
- }
-
- &.is-failed {
- background: #fef2f2;
- border-color: #fecaca;
-
- .report-icon {
- color: #dc2626;
- font-variation-settings: 'FILL' 1;
- }
-
- .action-title {
- color: #7f1d1d;
- font-weight: 700;
- }
-
- .action-status-badge.is-failed {
- background: #fee2e2;
- color: #b91c1c;
- border: 1px solid #fca5a5;
- }
- }
-
- .report-markdown-body {
- font-size: 12px;
- line-height: 1.5;
- color: #1f2937;
- font-weight: 400;
- user-select: text;
- word-break: break-word;
-
- p {
- margin: 3px 0;
- }
-
- strong {
- font-weight: 600;
- color: #0f172a;
- }
-
- ul, ol {
- margin: 3px 0;
- padding-left: 18px;
- }
-
- code {
- font-family: 'Fira Code', monospace;
- background: #e2e8f0;
- color: #1f2937;
- padding: 1px 4px;
- border-radius: 4px;
- font-size: 11px;
- }
- }
- }
-
- &.has-no-details {
- padding: 8px 12px;
- gap: 0;
-
- .action-card-header {
- border-bottom: none;
- padding-bottom: 0;
- }
- }
-
- .action-card-header {
- display: flex;
- align-items: center;
- gap: 8px;
- border-bottom: 1px solid #f1f5f9;
- padding-bottom: 6px;
- width: 100%;
-
- .action-icon {
- font-size: 18px;
- color: #64748b;
-
- &.is-failed {
- color: #ef4444;
- }
-
- }
-
- .action-title {
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', Arial, "Liberation Sans", sans-serif;
- font-weight: 700;
- font-size: 12px;
- color: #0f172a;
- flex-grow: 1;
- }
-
- .action-agent-badge {
- display: inline-flex;
- align-items: center;
- font-size: 10px;
- font-weight: 600;
- padding: 1px 6px;
- border-radius: 4px;
- background: #f0fdf4;
- color: #166534;
- border: 1px solid #bbf7d0;
- letter-spacing: 0.2px;
- margin-right: 6px;
- white-space: nowrap;
-
- &.is-outputter {
- background: linear-gradient(135deg, rgba(245, 243, 255, 0.95) 0%, rgba(238, 242, 255, 0.95) 100%);
- color: #6d28d9;
- border-color: #c4b5fd;
- }
- }
-
- .action-status-badge {
- display: inline-flex;
- align-items: center;
- gap: 3px;
- font-size: 11px;
- font-weight: 600;
- padding: 1px 6px;
- border-radius: 4px;
- margin-left: auto;
- white-space: nowrap;
-
- .status-icon {
- font-size: 13px;
- }
-
- &.is-failed {
- background: #fef2f2;
- color: #dc2626;
- border: 1px solid #fee2e2;
-
- .status-icon {
- color: #ef4444;
- }
- }
-
- &.is-paused {
- background: #fff7ed;
- color: #c2410c;
- border: 1px solid #fed7aa;
-
- .status-icon {
- color: #f97316;
- }
- }
-
- }
-
- .action-expand-icon {
- font-size: 18px;
- color: #94a3b8;
- margin-left: 4px;
- padding: 2px;
- border-radius: 50%;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- }
-
- .action-card-body {
- display: flex;
- flex-direction: column;
- gap: 6px;
-
- .action-detail-row {
- display: flex;
- align-items: baseline;
- font-size: 12px;
-
- .detail-label {
- color: #64748b;
- font-weight: 500;
- width: 85px;
- flex-shrink: 0;
- }
-
- .detail-value {
- color: #334155;
- font-weight: 600;
- word-break: break-word;
-
- &.coordinate-badge {
- background: #e2e8f0;
- color: #475569;
- padding: 2px 6px;
- border-radius: 4px;
- font-family: 'Fira Code', monospace;
- font-size: 11px;
- }
-
- &.command-value {
- font-family: 'Fira Code', Consolas, Monaco, monospace;
- background: #f1f5f9;
- border: 1px solid #e2e8f0;
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 11px;
- font-weight: 500;
- color: #0f172a;
- white-space: pre-wrap;
- word-break: break-all;
-
- code {
- font-family: inherit;
- font-size: inherit;
- color: inherit;
- background: none;
- padding: 0;
- border: none;
- }
- }
- }
- }
-
- .failure-retry-details {
- margin-top: 4px;
- padding-top: 9px;
- border-top: 1px solid #fecaca;
- display: flex;
- flex-direction: column;
- gap: 7px;
- animation: fadeInDetails 0.2s ease-out;
-
- .failure-retry-title {
- color: #991b1b;
- font-size: 10px;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.03em;
- }
-
- .failure-retry-row {
- display: flex;
- justify-content: space-between;
- gap: 12px;
- color: #7f1d1d;
- font-size: 10.5px;
- line-height: 1.4;
-
- span:last-child {
- color: #64748b;
- text-align: right;
- }
- }
- }
- }
-
- .action-card-expanded-details {
- display: flex;
- flex-direction: column;
- gap: 10px;
- padding-top: 4px;
- animation: fadeInDetails 0.2s ease-out;
-
- .expanded-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 8px;
- background: #f8fafc;
- padding: 10px;
- border-radius: 8px;
- border: 1px solid #f1f5f9;
-
- .grid-item {
- display: flex;
- flex-direction: column;
- gap: 2px;
-
- &.full-width {
- grid-column: 1 / -1;
- }
-
- .grid-label {
- font-size: 10px;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: #64748b;
- }
-
- .grid-value {
- font-size: 12px;
- font-weight: 600;
- color: #1e293b;
- word-break: break-all;
-
- &.code-pill {
- font-family: 'Roboto Mono', 'Fira Code', Consolas, monospace;
- font-size: 11px;
- background: #e2e8f0;
- color: #334155;
- padding: 2px 6px;
- border-radius: 4px;
- display: inline-block;
- width: fit-content;
- }
-
- &.command-value {
- code {
- font-family: 'Roboto Mono', 'Fira Code', Consolas, monospace;
- font-size: 11px;
- background: #0f172a;
- color: #38bdf8;
- padding: 4px 8px;
- border-radius: 4px;
- display: block;
- word-break: break-all;
- white-space: pre-wrap;
- }
- }
- }
- }
- }
-
- .expanded-screenshots-container {
- display: flex;
- flex-direction: row;
- flex-wrap: nowrap;
- gap: 12px;
- margin-top: 4px;
- justify-content: flex-start;
- width: 100%;
-
- .screenshot-column {
- flex: 1 1 0;
- min-width: 0;
- max-width: 320px;
- display: flex;
- flex-direction: column;
- gap: 6px;
- align-items: center;
-
- .screenshot-label {
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', Arial, "Liberation Sans", sans-serif;
- font-size: 11.5px;
- font-weight: 500;
- color: #64748b;
- letter-spacing: normal;
- text-transform: none;
-
- }
-
- .image-wrapper {
- display: inline-flex;
- justify-content: center;
- align-items: center;
- max-width: 100%;
- border: 1px solid #e2e8f0;
- border-radius: 8px;
- background-color: #f8fafc;
- cursor: zoom-in;
- overflow: hidden;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
- transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
-
- &:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
- border-color: #cbd5e1;
- }
-
- .img-relative-box {
- position: relative;
- display: inline-block;
- max-width: 100%;
-
- img {
- display: block;
- max-width: 100%;
- max-height: 280px;
- width: auto;
- height: auto;
- margin: 0 auto;
- border-radius: 6px;
- }
-
- .overlay-layer {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- pointer-events: none;
- overflow: visible;
- }
- }
- }
- }
- }
-
- .expanded-error-banner {
- display: flex;
- align-items: center;
- gap: 8px;
- background: #fff1f2;
- border: 1px solid #fecdd3;
- color: #be123c;
- padding: 8px 12px;
- border-radius: 6px;
- font-size: 12px;
- font-weight: 500;
-
- .icon {
- font-size: 16px;
- color: #e11d48;
- }
- }
- }
- }
-
- .final-summary-card {
- padding: 20px 24px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- background: #ffffff;
- border: 1px solid rgba(0, 0, 0, 0.06);
- border-radius: 12px;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.01), 0 2px 4px -1px rgba(0, 0, 0, 0.006);
-
- .final-summary-header {
- font-weight: 500;
- font-size: 12px;
- color: #71717a;
- display: flex;
- align-items: center;
- gap: 6px;
- user-select: none;
- }
-
- .final-summary-body {
- font-size: 12px;
- line-height: 1.6;
- color: #27272a;
-
- strong {
- font-weight: 600;
- color: #09090b;
- }
-
- ul, ol {
- margin: 6px 0;
- padding-left: 20px;
- li {
- margin-bottom: 6px;
- }
- }
-
- div {
- margin-bottom: 4px;
- }
-
- .md-para-gap {
- height: 8px;
- }
- }
- }
- }
- }
- }
- }
- }
-}
-
-@keyframes spin-loader {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@keyframes pulse-green {
- 0% {
- box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
- }
- 70% {
- box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
- }
-}
-
-@keyframes pulse-stream-blue {
- 0% {
- box-shadow: 0 0 0 0 rgba(14, 165, 233, 0.4);
- }
- 70% {
- box-shadow: 0 0 0 6px rgba(14, 165, 233, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(14, 165, 233, 0);
- }
-}
-
-@keyframes pulse-stream-green {
- 0% {
- box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4);
- }
- 70% {
- box-shadow: 0 0 0 6px rgba(34, 197, 94, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
- }
-}
-
-@keyframes pulse-stream-purple {
- 0% {
- box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.4);
- }
- 70% {
- box-shadow: 0 0 0 6px rgba(168, 85, 247, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(168, 85, 247, 0);
- }
-}
-
-@keyframes pulse-stream-grey {
- 0% {
- box-shadow: 0 0 0 0 rgba(113, 113, 122, 0.4);
- }
- 70% {
- box-shadow: 0 0 0 6px rgba(113, 113, 122, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(113, 113, 122, 0);
- }
-}
-
-@keyframes pulse-black-dot {
- 0% {
- box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.4);
- }
- 70% {
- box-shadow: 0 0 0 5px rgba(0, 0, 0, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
- }
-}
-
-@keyframes pulse-black-dot-small {
- 0% {
- opacity: 1;
- transform: scale(1);
- }
- 50% {
- opacity: 0.4;
- transform: scale(0.8);
- }
- 100% {
- opacity: 1;
- transform: scale(1);
- }
-}
-
-@keyframes fadeInDetails {
- from {
- opacity: 0;
- transform: translateY(-4px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes dropdownFadeIn {
- from {
- opacity: 0;
- transform: translateY(-4px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
-}
-
-.image-modal-backdrop {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(15, 23, 42, 0.75);
- backdrop-filter: blur(4px);
- z-index: 1000;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 24px;
- box-sizing: border-box;
- animation: fadeInModal 0.2s ease-out;
-
- .image-modal-content {
- position: relative;
- max-width: calc(100% - 48px);
- max-height: calc(100% - 48px);
- background: #0f172a;
- border-radius: 12px;
- padding: 12px;
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- box-sizing: border-box;
-
- .image-modal-close {
- position: absolute;
- top: -12px;
- right: -12px;
- width: 32px;
- height: 32px;
- border-radius: 50%;
- background: #ef4444;
- color: white;
- border: 2px solid white;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
- transition: transform 0.15s ease, background-color 0.15s ease;
- z-index: 20;
-
- &:hover {
- transform: scale(1.1);
- background: #dc2626;
- }
-
- .material-symbols-outlined {
- font-size: 18px;
- }
- }
-
- .img-relative-box {
- position: relative;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- max-width: 100%;
- max-height: 100%;
-
- img {
- display: block;
- max-width: 100%;
- max-height: calc(100vh - 140px);
- width: auto;
- height: auto;
- object-fit: contain;
- border-radius: 6px;
- }
-
- .overlay-layer {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- pointer-events: none;
- overflow: visible;
- border-radius: 6px;
- }
- }
- }
-}
-
-@keyframes pulse-animation {
- 0% {
- transform: scale(0.5);
- opacity: 1;
- }
- 100% {
- transform: scale(1.8);
- opacity: 0;
- }
-}
-
-@keyframes fadeInModal {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
-}
-
-@keyframes subtle-color-flow {
- 0% {
- background-position: 0% 50%;
- }
- 100% {
- background-position: 200% 50%;
- }
-}
-
-@keyframes pulse-stream-dot {
- 0%, 100% {
- transform: scale(1);
- box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.45);
- }
- 50% {
- transform: scale(1.15);
- box-shadow: 0 0 0 4px rgba(59, 130, 246, 0);
- }
-}
-
-@keyframes pulse-stream-amber {
- 0%, 100% {
- transform: scale(1);
- box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.45);
- }
- 50% {
- transform: scale(1.15);
- box-shadow: 0 0 0 4px rgba(245, 158, 11, 0);
- }
-}
-
-.spinning {
- animation: spin 1.2s linear infinite;
-}
-
-
-@keyframes subtle-card-glow {
- 0% {
- border-color: rgba(59, 130, 246, 0.25);
- box-shadow: 0 2px 8px rgba(59, 130, 246, 0.05);
- }
- 100% {
- border-color: rgba(99, 102, 241, 0.45);
- box-shadow: 0 4px 14px rgba(99, 102, 241, 0.12);
- }
-}
-
-/* Outputter Post-Execution Synthesis Card */
-.outputter-synthesis-stream-card {
- margin: 16px 0 12px 0;
- border-radius: 12px;
- border: 1px solid rgba(196, 181, 253, 0.85);
- background: linear-gradient(135deg, rgba(245, 243, 255, 0.95) 0%, rgba(250, 245, 255, 0.98) 100%);
- box-shadow: 0 4px 16px rgba(124, 58, 237, 0.08), 0 1px 3px rgba(0, 0, 0, 0.03);
- overflow: hidden;
- animation: fadeInModal 0.25s ease-out;
-
- .synthesis-card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 16px;
- background: rgba(237, 233, 254, 0.6);
- border-bottom: 1px solid rgba(196, 181, 253, 0.7);
- gap: 12px;
- flex-wrap: wrap;
-
- .header-left {
- display: flex;
- align-items: center;
- gap: 10px;
-
- .icon-sparkle {
- font-size: 22px;
- color: #7c3aed;
- }
-
- .main-title {
- font-size: 14px;
- font-weight: 700;
- color: #4338ca;
- }
- }
-
- .header-right {
- .btn-open-note {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- padding: 5px 12px;
- background: linear-gradient(135deg, #6366f1 0%, #7c3aed 100%);
- color: #ffffff;
- border: none;
- border-radius: 8px;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.15s ease;
- box-shadow: 0 2px 6px rgba(99, 102, 241, 0.25);
-
- .material-symbols-outlined {
- font-size: 15px;
- }
-
- &:hover {
- background: linear-gradient(135deg, #4f46e5 0%, #6d28d9 100%);
- box-shadow: 0 4px 10px rgba(99, 102, 241, 0.35);
- transform: translateY(-1px);
- }
- }
- }
- }
-
- .synthesis-card-preview {
- padding: 14px 16px;
-
- .parsed-note-container {
- display: flex;
- flex-direction: column;
- gap: 10px;
-
- .parsed-note-title {
- font-size: 14px;
- font-weight: 700;
- color: #1e1b4b;
- margin: 0;
- }
-
- .parsed-milestones {
- display: flex;
- flex-direction: column;
- gap: 6px;
-
- .milestone-item {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- font-size: 12.5px;
- line-height: 1.45;
- color: #374151;
-
- .status-icon {
- font-size: 16px;
- color: #7c3aed;
- margin-top: 1px;
- flex-shrink: 0;
- }
-
- &.status-checked .status-icon {
- color: #10b981;
- }
-
- &.status-progress .status-icon {
- color: #f59e0b;
- }
- }
- }
-
- .parsed-other-lines {
- display: flex;
- flex-direction: column;
- gap: 4px;
-
- .other-line {
- margin: 0;
- font-size: 12.5px;
- line-height: 1.5;
- color: #4b5563;
-
- strong {
- color: #1e1b4b;
- }
-
- code {
- font-family: 'JetBrains Mono', monospace;
- background: rgba(255, 255, 255, 0.9);
- border: 1px solid #e5e7eb;
- padding: 1px 4px;
- border-radius: 4px;
- color: #6d28d9;
- font-size: 11.5px;
- }
- }
- }
- }
- }
-}
-
-/* Checker blocks share the step timeline, marked by a violet left border. */
-.agent-stream-container .stream-logs-content .phase-body .block-item.type-checker {
- border-left: 2px solid #ddd6fe;
- padding-left: 12px;
- margin-left: -14px;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', Arial, sans-serif;
-
- .check-index {
- flex: 0 0 14px;
- text-align: right;
- color: #a1a1aa;
- font-variant-numeric: tabular-nums;
- user-select: none;
- }
-
- .check-lane {
- display: flex;
- flex-direction: column;
- gap: $uniform-gap;
- margin-bottom: $uniform-gap;
-
- .check-header {
- display: flex;
- align-items: center;
- gap: 8px;
- min-height: 20px;
- font-size: 12px;
- line-height: 1.5;
- letter-spacing: 0.2px;
- color: #71717a;
- user-select: none;
-
- .stream-status-dot {
- flex: 0 0 auto;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- display: inline-block;
- background: #d4d4d8;
-
- &.streaming {
- background: #7c3aed;
- box-shadow: 0 0 0 0 rgba(124, 58, 237, 0.45);
- animation: pulse-stream-dot 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
- }
-
- .check-title {
- flex: 0 0 auto;
- font-weight: 500;
- color: #52525b;
- }
-
- .check-subject {
- flex: 1 1 auto;
- min-width: 0;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .check-status {
- flex: 0 0 auto;
- margin-left: auto;
- font-weight: 500;
- color: #52525b;
- white-space: nowrap;
- }
- }
-
- &.tone-passed .check-status { color: #15803d; }
- &.tone-failed .check-status { color: #b91c1c; }
- &.tone-inconclusive .check-status { color: #b45309; }
- &.tone-running .check-status { color: #6d28d9; }
- &.tone-muted { opacity: 0.6; }
-
- .check-items {
- list-style: none;
- margin: 0;
- padding: 0 0 0 16px;
- display: flex;
- flex-direction: column;
- gap: 2px;
-
- .check-item {
- display: flex;
- align-items: baseline;
- gap: 8px;
- font-size: 12px;
- line-height: 1.5;
- color: #3f3f46;
-
- .check-text {
- flex: 1 1 auto;
- min-width: 0;
- word-break: break-word;
- }
-
- .check-method {
- flex: 0 0 auto;
- color: #a1a1aa;
- white-space: nowrap;
- }
- }
- }
- }
-
- .check-result {
- display: flex;
- flex-direction: column;
- gap: 4px;
- margin-top: $uniform-gap;
- padding-left: 16px;
-
- .check-verdict {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- font-size: 12px;
- line-height: 1.5;
- color: #52525b;
-
- .check-index {
- line-height: 18px;
- }
-
- .verdict-icon {
- flex: 0 0 auto;
- font-size: 15px;
- line-height: 18px;
- color: #a1a1aa;
- }
-
- &.passed .verdict-icon { color: #16a34a; }
- &.failed .verdict-icon { color: #dc2626; }
- &.inconclusive .verdict-icon { color: #d97706; }
- &.superseded, &.unchecked { color: #a1a1aa; }
-
- .verdict-body {
- flex: 1 1 auto;
- min-width: 0;
- word-break: break-word;
- }
-
- .verdict-suggestion {
- display: block;
- color: #1d4ed8;
- }
- }
-
- .check-note {
- font-size: 12px;
- line-height: 1.5;
- color: #71717a;
- padding-left: 22px;
- word-break: break-word;
- }
- }
-}
-
-@media (min-width: 1151px) {
- @container stream-panel (max-width: 660px) {
- .agent-stream-container .stream-floating-toolbar {
- .model-info-chip {
- padding: 0 9px;
-
- .model-label {
- display: none;
- }
- }
-
- .btn-screen-recording {
- padding: 0 9px;
- gap: 0;
-
- .btn-text {
- display: none;
- }
- }
- }
- }
-}
-
-@media (max-width: 1150px) {
- @container stream-panel (max-width: 900px) {
- .agent-stream-container .vscode-nav-header .vscode-tab-btn {
- padding: 0 9px;
- gap: 0;
-
- .tab-label {
- display: none;
- }
- }
- }
-}
-
-/* ========================================================================= */
-/* Floating Dropdown Popover Menu (Angular CDK Connected Overlay Top Layer) */
-/* ========================================================================= */
-/* Floating Dropdown Popover Menu */
-.vscode-dropdown-menu {
- position: relative;
- width: 360px;
- box-sizing: border-box;
- max-width: calc(100vw - 24px);
- max-height: 480px;
- background: #ffffff;
- border: 1px solid #cbd5e1;
- border-radius: 12px;
- box-shadow: 0 16px 40px -4px rgba(15, 23, 42, 0.16), 0 4px 12px rgba(0, 0, 0, 0.06);
- z-index: 100;
- overflow-y: auto;
- padding: 14px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- animation: dropdownFadeIn 0.15s cubic-bezier(0.16, 1, 0.3, 1);
-
- .popover-title-header {
- display: flex;
- align-items: center;
- gap: 8px;
- padding-bottom: 8px;
- border-bottom: 1px solid #e2e8f0;
- font-size: 14px;
- font-weight: 700;
- color: #0f172a;
-
- .material-symbols-outlined {
- font-size: 18px;
- color: #2563eb;
- }
- }
-
- /* Run info popover: elapsed, tokens, context share, Pro tuning */
- &.run-info-dropdown {
- width: 288px;
- gap: 0;
- padding: 12px 14px 10px;
-
- .run-info-header {
- display: flex;
- align-items: center;
- gap: 7px;
- padding-bottom: 9px;
- border-bottom: 1px solid #e2e8f0;
-
- .material-symbols-outlined {
- font-size: 18px;
- color: #64748b;
-
- &.is-pro {
- color: #2563eb;
- }
- }
-
- .run-info-title {
- font-size: 13px;
- font-weight: 700;
- color: #0f172a;
- }
-
- .run-info-model {
- margin-left: auto;
- max-width: 150px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- font-size: 10.5px;
- color: #94a3b8;
- }
- }
-
- .run-info-row {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
- gap: 12px;
- padding: 9px 0;
- border-bottom: 1px solid #f1f5f9;
-
- &:last-child {
- border-bottom: none;
- }
-
- &.is-stacked {
- flex-direction: column;
- align-items: stretch;
- gap: 0;
- }
- }
-
- .run-info-line {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
- gap: 12px;
- }
-
- .run-info-label {
- font-size: 12px;
- font-weight: 500;
- color: #64748b;
- white-space: nowrap;
- }
-
- .run-info-value {
- font-size: 12.5px;
- font-weight: 600;
- color: #0f172a;
- text-align: right;
- font-variant-numeric: tabular-nums;
-
- &.is-muted {
- color: #cbd5e1;
- font-weight: 500;
- }
- }
-
- .run-info-sub {
- display: block;
- margin-top: 2px;
- font-size: 11px;
- font-weight: 400;
- color: #94a3b8;
- }
-
- .run-info-sub-inline {
- margin-left: 3px;
- font-size: 11px;
- font-weight: 400;
- color: #94a3b8;
- }
-
- .run-info-meter {
- height: 4px;
- margin-top: 7px;
- border-radius: 2px;
- background: #e2e8f0;
- overflow: hidden;
-
- .run-info-meter-fill {
- height: 100%;
- border-radius: 2px;
- background: #2563eb;
- transition: width 0.3s ease;
- }
-
- &.is-high .run-info-meter-fill {
- background: #f59e0b;
- }
-
- &.is-critical .run-info-meter-fill {
- background: #ef4444;
- }
- }
-
- .run-info-note {
- padding-top: 9px;
- font-size: 11.5px;
- color: #94a3b8;
- }
- }
-
- &.notes-dropdown {
- width: 380px;
- max-height: 520px;
-
- .notes-selector-container {
- display: flex;
- flex-direction: column;
- gap: 6px;
- margin-bottom: 4px;
-
- .notes-selector-list {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
-
- .note-selector-pill {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- padding: 4px 10px;
- border: 1px solid #e2e8f0;
- background: #ffffff;
- border-radius: 16px;
- font-size: 11.5px;
- font-weight: 500;
- color: #475569;
- cursor: pointer;
- transition: all 0.15s ease;
-
- .pill-icon {
- font-size: 14px;
- }
-
- &:hover {
- background: #f8fafc;
- border-color: #cbd5e1;
- color: #1e293b;
- }
-
- &.active {
- background: #eff6ff;
- border-color: #bfdbfe;
- color: #1d4ed8;
- font-weight: 600;
- }
- }
- }
- }
-
- .dropdown-notes-content {
- display: flex;
- flex-direction: column;
- gap: 10px;
- padding: 4px 2px;
-
- .note-title {
- font-size: 13.5px;
- font-weight: 700;
- color: #1f1f1f;
- margin: 0 0 4px 0;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', sans-serif;
- }
-
- .code-segment {
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- background-color: #f1f3f4;
- color: #1967d2;
- padding: 1px 5px;
- border-radius: 4px;
- font-size: 12px;
- margin: 0 2px;
- word-break: break-all;
- }
-
- .milestones-container {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
-
- .milestone-block {
- display: flex;
- flex-direction: column;
-
- &.checked {
- .milestone-title-row {
- .milestone-status-icon.checked {
- opacity: 0.7;
- }
- .milestone-text {
- color: #a1a1aa;
- }
- }
- }
-
- .milestone-title-row {
- display: flex;
- align-items: flex-start;
- gap: 8px;
-
- .milestone-status-icon {
- font-size: 18px;
- user-select: none;
- margin-top: 2px;
- font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
-
- &.checked, &.progress {
- color: #1a73e8;
- }
- &.unchecked {
- color: #cbd5e1;
- }
- }
-
- .milestone-text {
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', sans-serif;
- color: #1f1f1f;
- font-size: 13px;
- line-height: 1.5;
- word-break: break-word;
- flex: 1;
- }
- }
-
- .milestone-substeps {
- margin-top: 4px;
- display: flex;
- flex-direction: column;
- gap: 5px;
- padding-left: 26px;
-
- .substep-row {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', sans-serif;
- color: #1f1f1f;
- font-size: 12.5px;
- line-height: 1.5;
-
- &.checked {
- color: #a1a1aa;
- .check-icon.checked {
- opacity: 0.7;
- }
- }
-
- &.verify {
- color: #334155;
- gap: 8px;
- align-items: flex-start;
-
- .verify-badge {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- font-family: 'Google Sans Text', 'Inter', -apple-system, sans-serif;
- font-size: 10.5px;
- font-weight: 550;
- line-height: 1.2;
- letter-spacing: 0.2px;
- text-transform: lowercase;
- color: #475569;
- background: #f1f5f9;
- border: 1px solid #e2e8f0;
- border-radius: 4px;
- padding: 1.5px 6px;
- margin-top: 1.5px;
- user-select: none;
-
- &.assert {
- color: #0369a1;
- background: #f0f9ff;
- border-color: #e0f2fe;
- }
- }
-
- .line-text {
- color: #334155;
- font-size: 12.5px;
- line-height: 1.5;
- }
- }
-
- &.finding {
- color: #92400e;
- gap: 8px;
- align-items: flex-start;
-
- .finding-badge {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- font-family: 'Google Sans Text', 'Inter', -apple-system, sans-serif;
- font-size: 10.5px;
- font-weight: 550;
- line-height: 1.2;
- letter-spacing: 0.2px;
- text-transform: lowercase;
- color: #b45309;
- background: #fef3c7;
- border: 1px solid #fde68a;
- border-radius: 4px;
- padding: 1.5px 6px;
- margin-top: 1.5px;
- user-select: none;
- }
-
- .line-text {
- color: #92400e;
- font-size: 12.5px;
- line-height: 1.5;
- }
- }
-
- .check-icon {
- font-size: 16px;
- user-select: none;
- margin-top: 2px;
- font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
-
- &.checked, &.progress {
- color: #1a73e8;
- }
- &.unchecked {
- color: #cbd5e1;
- }
- }
-
- .bullet {
- color: #9ca3af;
- font-weight: bold;
- user-select: none;
- }
-
- .line-text {
- word-break: break-word;
- color: inherit;
- }
- }
- }
-
- .milestone-checks-container {
- margin-top: 6px;
- padding-left: 26px;
- display: flex;
- flex-direction: column;
- gap: 6px;
- }
- }
-
- .verification-card {
- background: #f8fafc;
- border: 1px solid #e2e8f0;
- border-radius: 6px;
- padding: 7px 10px;
- display: flex;
- flex-direction: column;
- gap: 3px;
-
- &.standalone {
- margin-top: 6px;
- margin-bottom: 6px;
- }
-
- .card-header {
- display: flex;
- align-items: center;
-
- .card-tag {
- font-family: 'Google Sans Text', 'Inter', -apple-system, sans-serif;
- font-size: 9.5px;
- font-weight: 600;
- line-height: 1;
- letter-spacing: 0.6px;
- text-transform: uppercase;
- color: #64748b;
-
- &.assert {
- color: #0284c7;
- }
- }
- }
-
- .card-body {
- font-family: inherit;
- font-size: 12px;
- line-height: 1.5;
- color: #334155;
- word-break: break-word;
-
- .bold-segment {
- font-weight: 600;
- color: #1e293b;
- }
-
- .code-segment {
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- font-size: 11px;
- background: #f1f5f9;
- color: #0f172a;
- padding: 1px 5px;
- border-radius: 4px;
- border: 1px solid #e2e8f0;
- }
- }
-
- &.finding {
- background: #fffbeb;
- border-color: #fef3c7;
-
- .card-header .card-tag {
- color: #b45309;
- }
-
- .card-body {
- color: #92400e;
- }
- }
- }
-
- .other-lines-container {
- display: flex;
- flex-direction: column;
- gap: 6px;
- margin-top: 6px;
- padding-top: 6px;
- border-top: 1px dashed #e2e8f0;
-
- .md-line {
- display: flex;
- align-items: flex-start;
- gap: 6px;
- font-size: 12.5px;
- color: #334155;
- line-height: 1.5;
-
- .line-text {
- word-break: break-word;
- color: inherit;
- }
- }
- }
- }
- }
-
- &::-webkit-scrollbar {
- width: 6px;
- }
- &::-webkit-scrollbar-thumb {
- background: #cbd5e1;
- border-radius: 4px;
- }
-
- .dropdown-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding-bottom: 8px;
- border-bottom: 1px solid #e2e8f0;
-
- .dropdown-title {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 13px;
- font-weight: 700;
- color: #0f172a;
-
- .icon-main {
- font-size: 18px;
- color: #2563eb;
- }
- }
- }
-
- .dropdown-section-label {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 11px;
- font-weight: 700;
- color: #475569;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- padding: 4px 2px 2px 2px;
-
- &.history-label {
- margin-top: 6px;
- }
-
- .icon-bolt {
- font-size: 15px;
- color: #d97706;
- }
- .icon-history {
- font-size: 15px;
- color: #64748b;
- }
- }
-
- .dropdown-empty-state {
- padding: 20px 14px;
- text-align: center;
- font-size: 12px;
- color: #94a3b8;
- background: #f8fafc;
- border: 1px dashed #e2e8f0;
- border-radius: 6px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 6px;
-
- .empty-icon {
- font-size: 22px;
- color: #94a3b8;
- }
- }
-
- .dropdown-history-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
-
- .dropdown-item-card {
- padding: 10px 12px;
- background: #f8fafc;
- border: 1px solid #e2e8f0;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.15s ease;
- display: flex;
- flex-direction: column;
- gap: 6px;
-
- &:hover {
- background: #f1f5f9;
- border-color: #cbd5e1;
- transform: translateY(-1px);
-
- .item-header .item-header-actions {
- &:has(.btn-delete-card) .item-time,
- &:has(.btn-delete-card) .item-time-divider,
- &:has(.btn-stop-card) .item-time,
- &:has(.btn-stop-card) .item-time-divider {
- opacity: 0;
- }
-
- .btn-delete-card,
- .btn-stop-card {
- opacity: 1;
- pointer-events: auto;
- }
- }
- }
-
- &.selected {
- background: #eff6ff;
- border-color: #93c5fd;
- box-shadow: 0 0 0 1px #2563eb;
-
- .item-goal {
- color: #1e3a8a;
- font-weight: 600;
- }
- }
-
- &.running {
- border-left: 3.5px solid #2563eb;
- }
-
- .item-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- min-width: 0;
-
- .task-badge {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
- font-size: 10.5px;
- font-weight: 700;
- padding: 2px 5px;
- border-radius: 4px;
- letter-spacing: 0.05em;
- border: 1px solid #e2e8f0;
- background: #fafafa;
- color: #64748b;
-
- &::before {
- content: '';
- width: 4px;
- height: 4px;
- border-radius: 50%;
- display: inline-block;
- margin-right: 4px;
- }
-
- &.running {
- background: #fafafa;
- color: #1f1f1f;
- border-color: #1f1f1f;
- &::before {
- background-color: #1f1f1f;
- animation: pulse-black-dot-small 1.5s infinite ease-in-out;
- }
- }
- &.paused {
- background: #fff7ed;
- color: #c2410c;
- border-color: #fdba74;
- &::before { background-color: #f97316; }
- }
- &.completed {
- background: #fafafa;
- color: #1f1f1f;
- border-color: #e2e8f0;
- &::before {
- background-color: #1f1f1f;
- }
- }
- &.pending {
- background: #fafafa;
- color: #64748b;
- border-color: #e2e8f0;
- &::before {
- background-color: #94a3b8;
- }
- }
- &.failed {
- background: #fafafa;
- color: #ef4444;
- border-color: #fee2e2;
- &::before {
- background-color: #ef4444;
- }
- }
- &.cancelled {
- background: #fafafa;
- color: #d97706;
- border-color: #fef3c7;
- &::before {
- background-color: #f59e0b;
- }
- }
-
- .status-dot {
- display: none;
- }
- }
-
- .item-header-actions {
- display: flex;
- align-items: center;
- gap: 4px;
- position: relative;
- min-width: 0;
- flex-shrink: 1;
- justify-content: flex-end;
-
- .item-device {
- display: inline-flex;
- align-items: center;
- gap: 2.5px;
- min-width: 0;
- flex-shrink: 1;
- font-size: 10.5px;
- color: #64748b;
- line-height: 1;
- cursor: default;
- transition: color 0.15s ease, opacity 0.15s ease;
-
- .device-icon {
- font-size: 12px;
- width: 12px;
- height: 12px;
- line-height: 12px;
- color: #64748b;
- opacity: 0.75;
- flex-shrink: 0;
- }
-
- .device-name {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- min-width: 0;
- font-family: inherit;
- letter-spacing: -0.15px;
- }
-
- &:hover {
- color: #334155;
- .device-icon {
- color: #334155;
- opacity: 1;
- }
- }
- }
-
- .item-time-divider {
- font-size: 10px;
- color: #94a3b8;
- line-height: 1;
- user-select: none;
- flex-shrink: 0;
- transition: opacity 0.15s ease;
- }
-
- .item-time {
- font-size: 11px;
- color: #64748b;
- white-space: nowrap;
- flex-shrink: 0;
- line-height: 1;
- transition: opacity 0.15s ease;
- }
-
- .btn-delete-card {
- position: absolute;
- right: 0;
- top: 50%;
- transform: translateY(-50%);
- width: 18px;
- height: 18px;
- padding: 0;
- border: none;
- background: transparent;
- border-radius: 3px;
- color: #94a3b8;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- opacity: 0;
- pointer-events: none;
- transition: all 0.15s ease;
-
- .material-symbols-outlined {
- font-size: 13px;
- line-height: 1;
- }
-
- &:hover {
- color: #ef4444;
- background: #fee2e2;
- }
- }
-
- .btn-stop-card {
- position: absolute;
- right: 0;
- top: 50%;
- transform: translateY(-50%);
- width: 18px;
- height: 18px;
- padding: 0;
- border: none;
- background: transparent;
- border-radius: 3px;
- color: #ef4444;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- opacity: 0;
- pointer-events: none;
- transition: all 0.15s ease;
-
- .material-symbols-outlined {
- font-size: 14px;
- line-height: 1;
- }
-
- &:hover {
- color: #dc2626;
- background: #fee2e2;
- transform: translateY(-50%) scale(1.1);
- }
- }
- }
- }
-
- .item-goal {
- font-size: 12.5px;
- color: #334155;
- line-height: 1.4;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
-
- .item-footer {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- font-size: 10.5px;
- color: #94a3b8;
-
- .item-id {
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- font-size: 10.5px;
- color: #94a3b8;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
- flex: 1;
- transition: color 0.15s ease;
-
- &:hover {
- color: #475569;
- }
- }
-
- .item-date {
- flex-shrink: 0;
- }
- }
- }
-
- .dropdown-note-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px 12px;
- border-radius: 6px;
- cursor: pointer;
- font-size: 13px;
- color: #334155;
- transition: background 0.15s ease;
-
- .note-icon {
- font-size: 18px;
- color: #64748b;
- }
-
- .note-key-text {
- flex: 1;
- font-weight: 500;
- }
-
- .check-icon {
- font-size: 18px;
- color: #2563eb;
- }
-
- &:hover {
- background: #f1f5f9;
- color: #0f172a;
- }
-
- &.selected {
- background: #eff6ff;
- color: #2563eb;
- font-weight: 600;
-
- .note-icon {
- color: #2563eb;
- }
- }
- }
-}
diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts
deleted file mode 100644
index 437237215..000000000
--- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts
+++ /dev/null
@@ -1,2234 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Component, ChangeDetectionStrategy, NgZone, signal, computed, effect, inject, untracked, DestroyRef, AfterViewInit } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
-import { RouterLink } from '@angular/router';
-import { HttpClient } from '@angular/common/http';
-import { AgentService, StartupProgressEvent } from '../../services/agent.service';
-import { Session, ModelInfo, SessionUsage } from '../../core/models/session.model';
-import { MarkdownSegment, MarkdownLine, NoteMilestone, ParsedNote } from '../../core/models/markdown.model';
-import { OverlayModule, ConnectedPosition } from '@angular/cdk/overlay';
-import { StepBlock, PhaseBlock, StepEvent, ActionParam, CheckerResult, StreamResetNotice, DEFAULT_STREAM_RESET_MESSAGE } from '../../core/models/stream.model';
-import {
- contextPercent,
- formatCompactTokens,
- formatElapsed,
- parseRunTuning,
- sessionElapsedSeconds,
- tuningLabel
-} from '../../utils/run-info.util';
-
-export const PLANNING_LOADER_PHRASES: string[] = [
- 'Planning next step...',
- 'Analyzing screen coordinates...',
- 'Consulting neural network...',
- 'Formulating tactical action plan...',
- 'Deciphering UI state & elements...',
- 'Synthesizing decision pathways...',
- 'Calibrating next move...',
- 'Aligning logical vectors...',
- 'Evaluating optimal sub-goals...',
- 'Gathering sensory inputs...',
- 'Computing next interaction...',
- 'Optimizing execution strategy...',
- 'Simulating probable outcomes...',
- 'Summoning AI intuition...',
- 'Brewing the next command...',
- 'Strategizing tactical moves...'
-];
-
-export interface StartupWorkItem extends StartupProgressEvent {
- isActive: boolean;
- elapsed: string;
-}
-
-interface StartupWorkStage {
- started: string;
- completed: string;
- completedMessage: string;
- /** Sub-steps that replace the live message while the stage is still running. */
- liveDetailStages?: string[];
- /** A later event whose message is a better completion line than the stage's own. */
- completedDetailStage?: string;
-}
-
-const STARTUP_WORK_STAGES: StartupWorkStage[] = [
- {
- started: 'device_check',
- completed: 'device_ready',
- completedMessage: 'Android device connected'
- },
- {
- started: 'uiautomator',
- completed: 'uiautomator_ready',
- completedMessage: 'UI hierarchy service is ready',
- // First task on a device installs / upgrades the accessibility helper
- // (a few seconds): say so instead of a generic "connecting".
- liveDetailStages: ['helper_install', 'helper_upgrade'],
- // "UI hierarchy source: Artemis accessibility helper v1.1.3" (or UIAutomator2).
- completedDetailStage: 'hierarchy_backend'
- },
- {
- started: 'environment',
- completed: 'environment_ready',
- completedMessage: 'Device environment is ready'
- }
-];
-
-function formatStartupElapsed(seconds: number): string {
- const safeSeconds = Math.max(0, seconds);
- return safeSeconds < 10
- ? `${safeSeconds.toFixed(1)}s`
- : `${Math.round(safeSeconds)}s`;
-}
-
-/**
- * Collapse noisy process-level startup events into the three device preparation
- * operations that are useful to someone watching a run.
- */
-export function buildStartupWorkItems(
- events: StartupProgressEvent[],
- nowSeconds: number,
- executionHasOutput: boolean,
- agentIsActive: boolean
-): StartupWorkItem[] {
- const byStage = new Map(events.map((event) => [event.stage, event]));
- const firstResponse = byStage.get('first_response');
-
- return STARTUP_WORK_STAGES.flatMap((stage, stageIndex) => {
- const started = byStage.get(stage.started);
- const explicitlyCompleted = byStage.get(stage.completed);
- if (!started && !explicitlyCompleted) return [];
-
- const nextStageStarted = STARTUP_WORK_STAGES
- .slice(stageIndex + 1)
- .map((nextStage) => byStage.get(nextStage.started) || byStage.get(nextStage.completed))
- .find((event): event is StartupProgressEvent => Boolean(event));
- const inferredCompletion = stage.started === 'environment'
- ? firstResponse
- : nextStageStarted;
- const completed = explicitlyCompleted || inferredCompletion;
- const isActive = !completed && !executionHasOutput && agentIsActive;
- const startTimestamp = started?.timestamp || explicitlyCompleted?.timestamp || nowSeconds;
- const endTimestamp = completed?.timestamp
- || (isActive ? nowSeconds : events[events.length - 1]?.timestamp || startTimestamp);
-
- const liveDetail = (stage.liveDetailStages || [])
- .map((detailStage) => byStage.get(detailStage))
- .filter((event): event is StartupProgressEvent => Boolean(event))
- .filter((event) => event.timestamp >= startTimestamp)
- .sort((a, b) => b.timestamp - a.timestamp)[0];
- const completedDetail = stage.completedDetailStage
- ? byStage.get(stage.completedDetailStage)
- : undefined;
- const message = completed
- ? (completedDetail?.message || explicitlyCompleted?.message || stage.completedMessage)
- : (liveDetail?.message || started!.message);
-
- return [{
- ...(explicitlyCompleted || started!),
- message,
- isActive,
- elapsed: formatStartupElapsed(endTimestamp - startTimestamp)
- }];
- });
-}
-
-import {
- parseNote,
- parseNoteLines,
- extractCheckerResult,
- renderMarkdownToHtml
-} from '../../utils/markdown-parser.util';
-
-import {
- getActionObject,
- isAndroidAction,
- getActionIcon,
- getActionTitle,
- getActionTargetText,
- getActionInputLabel,
- getActionInputText,
- getActionCoords,
- getActionBounds,
- getActionResourceId,
- getActionClass,
- isActionFailed,
- getActionErrorMessage,
- extractActionExtraParams,
- getStepPreImageUrl,
- getStepPostImageUrl,
- isReportStatusAction,
- getReportStatusValue,
- getReportStatusExplanation
-} from '../../utils/action-formatter.util';
-
-import {
- getToolArgs,
- isNoteTool,
- isVideoTool,
- getVideoAnalysisView,
- formatVideoTime,
- getVideoToolTarget,
- isDeviceActionTool,
- isFailureAnalyzerActionTool,
- getToolAgentName,
- shouldShowTool,
- getUniqueGenericTools,
- getToolKey,
- getToolDisplayLabel,
- isCompressionTool,
- isCompressionWaiting,
- getCompressionPhaseLabel,
- getToolIcon,
- getToolTitle,
- getToolTargetText,
- getToolInputLabel,
- getToolInputText,
- getToolCoords,
- getToolAnalysisText,
- getToolGenericDetails,
- isAdbCommandTool,
- getAdbCommandLine,
- getAdbCwd,
- getAdbTerminalId,
- isToolFailed,
- getToolErrorMessage,
- extractToolExtraParams,
- isHumanThinking,
- cleanErrorMessage
-} from '../../utils/tool-formatter.util';
-
-import { drawActionCoordinatesOnOverlay } from '../../utils/image-overlay.util';
-
-import {
- consolidateLogsToBlocks,
- isBackendSwitchNote,
- groupBlocksToPhases,
- extractBlockTokens,
- formatTokenCount,
- getSortedStepEvents,
- compileSessionSummary,
- computeSessionDuration,
- checkPlanningLoader
-} from '../../utils/stream-aggregator.util';
-
-export type { MarkdownSegment, MarkdownLine, NoteMilestone, ParsedNote, StreamResetNotice };
-
-@Component({
- selector: 'app-agent-stream',
- standalone: true,
- imports: [CommonModule, FormsModule, OverlayModule],
- templateUrl: './agent-stream.component.html',
- styleUrl: './agent-stream.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush
-})
-export class AgentStreamComponent implements AfterViewInit {
- public agentService = inject(AgentService);
- private http = inject(HttpClient);
- private destroyRef = inject(DestroyRef);
- private zone = inject(NgZone);
-
- // Auto-scroll state tracking
- public isUserAtBottom = true;
- private resizeObserver: ResizeObserver | null = null;
- private streamLogsContainer: HTMLElement | null = null;
- private autoScrollTimer: ReturnType | null = null;
- private autoScrollStreamBoxes = false;
- private onContainerScroll = () => {
- const container = this.streamLogsContainer;
- if (!container) return;
- const threshold = 150;
- const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
- this.isUserAtBottom = distanceToBottom <= threshold;
- };
-
- // Dynamic Planning Loader Game-style Phrases
- public currentPlanningText = signal(PLANNING_LOADER_PHRASES[0]);
- private planningIntervalId: any = null;
- private currentPhraseIndex = 0;
-
- // Top Nav Dropdown States
- public isTaskDropdownOpen = signal(false);
- public isNotesDropdownOpen = signal(false);
-
- // Run info popover (opened from the Flash / Pro chip): elapsed time, token
- // usage, executor context share and the Pro tuning the run started with.
- public isRunInfoOpen = signal(false);
- public runUsage = signal(null);
- private runInfoClock = signal(Date.now());
-
- public runInfoIsPro = computed(() => this.getModelClass(this.agentService.viewedModel()?.name) === 'is-pro');
-
- public runInfoElapsed = computed(() => {
- const session = this.agentService.currentSession();
- const running = this.agentService.isCurrentSessionRunning();
- return formatElapsed(sessionElapsedSeconds(session, running, this.runInfoClock()));
- });
-
- public runInfoTuning = computed(() => {
- const usage = this.runUsage();
- if (usage?.run_tuning && (usage.run_tuning.verification_level || usage.run_tuning.explorer_mode)) {
- return usage.run_tuning;
- }
- return parseRunTuning(this.agentService.currentSession()?.device_info);
- });
-
- public runInfoContextPercent = computed(() => {
- const usage = this.runUsage();
- if (!usage) return null;
- return contextPercent(usage.operator_context_tokens, usage.operator_context_window_tokens);
- });
-
- // CDK Connected Overlay Positioning Strategy
- public readonly dropdownPositions: ConnectedPosition[] = [
- {
- originX: 'end',
- originY: 'bottom',
- overlayX: 'end',
- overlayY: 'top',
- offsetY: 6,
- },
- {
- originX: 'end',
- originY: 'top',
- overlayX: 'end',
- overlayY: 'bottom',
- offsetY: -6,
- },
- {
- originX: 'start',
- originY: 'bottom',
- overlayX: 'start',
- overlayY: 'top',
- offsetY: 6,
- }
- ];
-
- // Zoom Modal State
- public selectedZoomImage = signal(null);
- public selectedZoomAction = signal(null);
-
- // Signals and properties for managing delayed collapsing of thought streams
- public collapsedStreams = signal>(new Set());
- private scheduledCollapses = new Set();
-
- // Signals and maps for typewriter effect simulation. All active typewriters
- // are driven by ONE requestAnimationFrame loop (run outside the Angular
- // zone) that batches every text advance into a single signal update.
- public typedTextsSignal = signal>({});
- private typingTargets = new Map();
- private typingRafId: number | null = null;
- private lastTypingTimestamp = 0;
- // ≈ legacy pace of 8 characters every 12ms interval tick.
- private static readonly TYPING_CHARS_PER_SECOND = 667;
- private static readonly TYPING_MIN_FRAME_MS = 28;
-
- // Active reset notices per block (displayed after typewriter rewind finishes)
- public activeBlockResets = signal>(new Map());
- public activeResetNotices = signal>(new Map());
- private rewindTargets = new Map();
- // Fast rewind pace (backspace speed ≈ 4x normal typing speed for snappy deletion)
- private static readonly REWIND_CHARS_PER_SECOND = 2600;
-
- // Rendered-markdown memo per template slot: returning the identical string
- // instance for unchanged text lets the [innerHTML] binding skip re-sanitizing
- // and re-parsing the whole fragment on every change-detection pass.
- private markdownHtmlCache = new Map();
- // Thinking-text extraction memo (includes the JSON-detection heuristics).
- private thinkingTextCache = new WeakMap();
- private deviceSerialCache = new WeakMap();
-
- // Set to track expanded and collapsed state of action cards
- public expandedActionCards = signal>(new Set());
- public collapsedActionCards = signal>(new Set());
- public retryClock = signal(Date.now());
-
- // Performance caches for parameter and event extractions
- private actionParamsCache = new WeakMap();
- private toolParamsCache = new WeakMap();
- private sortedEventsCache = new WeakMap();
-
- // Top Nav Task Queue Computed Properties
- public activeQueue = computed(() => {
- const list = this.agentService.sessions().filter((s) => {
- const status = this.getTaskStatus(s);
- return status === 'running' || status === 'paused' || status === 'pending';
- });
- return list.sort((a, b) => {
- const statusA = this.getTaskStatus(a);
- const statusB = this.getTaskStatus(b);
- const isRunA = statusA === 'running' || statusA === 'paused';
- const isRunB = statusB === 'running' || statusB === 'paused';
- if (isRunA && !isRunB) return -1;
- if (!isRunA && isRunB) return 1;
- return (a.start_time || 0) - (b.start_time || 0);
- });
- });
-
- public historyTasks = computed(() => {
- return this.agentService.sessions().filter((s) => {
- const status = this.getTaskStatus(s);
- return status === 'completed' || status === 'failed' || status === 'cancelled';
- });
- });
-
- // Top Nav Notes Computed Properties
- public currentNoteContent = computed(() => {
- const notes = this.agentService.currentNotes();
- const key = this.agentService.selectedNoteKey();
- return notes[key] || '';
- });
-
- public noteKeys = computed(() => {
- return Object.keys(this.agentService.currentNotes()).filter(key => key.toLowerCase().endsWith('.md'));
- });
-
-
- public outputterReport = computed(() => {
- const notes = this.agentService.currentNotes();
- return notes['output.md'] || null;
- });
-
- // Memoized so the Task Report card is not re-parsed (and its DOM rebuilt)
- // on every change-detection pass while streams are typing.
- public parsedOutputReport = computed(() => {
- const report = this.outputterReport();
- return report ? parseNote(report) : null;
- });
-
- public parsedNote = computed(() => {
- return this.getParsedNote(this.currentNoteContent());
- });
-
- // Stream Log Filtering & Aggregation Computed Properties
- public filteredLogs = computed(() => {
- const logs = this.agentService.sessionLogs();
- const currentSessionId = this.agentService.currentSessionId();
- if (!currentSessionId) return logs;
- return logs.filter((log: any) => {
- const logSessionId = log.session_id || log.data?.session_id;
- return !logSessionId || logSessionId === currentSessionId;
- });
- });
-
- public sessionSummary = computed(() => {
- return compileSessionSummary(this.filteredLogs());
- });
-
- public showPlanningLoader = computed(() => {
- const isRunning = this.agentService.agentStatus() === 'running';
- const currentSessionId = this.agentService.currentSessionId();
- const runningSessionId = this.agentService.runningSessionId();
- const isCurrentRunningSession = !currentSessionId || currentSessionId === runningSessionId;
- return checkPlanningLoader(this.filteredLogs(), isRunning, isCurrentRunningSession);
- });
-
- public isViewingPausedTask = computed(() => {
- // A live task_paused event and the polled runner status must agree before
- // recovery controls are shown. This prevents a stale failure trace from
- // offering Resume while the active task is still reported as running.
- if (!this.agentService.isPaused() || this.agentService.agentStatus() !== 'paused') return false;
-
- const currentSessionId = this.agentService.currentSessionId();
- const pausedSessionId = this.agentService.runningSessionId();
- return !currentSessionId || currentSessionId === pausedSessionId;
- });
-
- public sessionDuration = computed(() => {
- const currentSession = this.agentService.sessions().find(s => s.session_id === this.agentService.currentSessionId());
- return computeSessionDuration(currentSession?.start_time || 0, this.filteredLogs());
- });
-
- public consolidatedBlocks = computed(() => {
- return consolidateLogsToBlocks(this.filteredLogs());
- });
-
- public phases = computed(() => {
- const currentSession = this.agentService.sessions().find(s => s.session_id === this.agentService.currentSessionId());
- return groupBlocksToPhases(this.consolidatedBlocks(), currentSession?.start_time || 0);
- });
-
- public startupWorkItems = computed(() => {
- this.retryClock();
- const events = this.agentService.currentStartupProgress();
- const agentIsActive = ['running', 'paused'].includes(this.agentService.agentStatus());
- return buildStartupWorkItems(
- events,
- Date.now() / 1000,
- this.consolidatedBlocks().length > 0,
- agentIsActive
- );
- });
-
- public startupPreparationIsRunning = computed(() => {
- return this.startupWorkItems().some((item) => item.isActive);
- });
-
- public startupPreparationIsComplete = computed(() => {
- const items = this.startupWorkItems();
- return items.length > 0 && !items.some((item) => item.isActive);
- });
-
- public startupPreparationDuration = computed(() => {
- const items = this.startupWorkItems();
- if (items.length === 0) return 0;
-
- const events = this.agentService.currentStartupProgress();
- const startedAt = events[0]?.timestamp || items[0].timestamp;
- const completedAt = events.find((event) => event.stage === 'environment_ready')?.timestamp
- || events.find((event) => event.stage === 'first_response')?.timestamp;
- const endedAt = this.startupPreparationIsRunning()
- ? Date.now() / 1000
- : completedAt || Math.max(...items.map((item) => item.timestamp));
-
- return Math.max(1, Math.round(endedAt - startedAt));
- });
-
- private retryablePauseTrace = computed(() => {
- if (!this.isViewingPausedTask()) return null;
-
- const blocks = this.consolidatedBlocks();
- for (let blockIndex = blocks.length - 1; blockIndex >= 0; blockIndex--) {
- const tools = blocks[blockIndex]?.data?.generic_tools;
- if (!Array.isArray(tools)) continue;
-
- for (let toolIndex = tools.length - 1; toolIndex >= 0; toolIndex--) {
- const tool = tools[toolIndex];
- // Older persisted traces used the llm_pause name without adding the
- // newer payload.pause marker. Treat both contracts consistently.
- if (this.isDisplayableLLMFailure(tool)) {
- return tool;
- }
- }
- }
- return null;
- });
-
- public hasVisibleBlocks(phase: any): boolean {
- if (!phase || !phase.blocks) return false;
- return phase.blocks.some((b: any) => this.hasVisibleContent(b));
- }
-
- constructor() {
- // Run info popover: fetch usage when opened or when the viewed session
- // changes, and keep refreshing while that session is still running.
- effect((onCleanup) => {
- const open = this.isRunInfoOpen();
- const sessionId = this.agentService.currentSessionId();
- const running = this.agentService.isCurrentSessionRunning();
- if (!open || !sessionId) return;
- if (untracked(() => this.runUsage())?.session_id !== sessionId) {
- this.runUsage.set(null);
- }
- this.loadRunUsage(sessionId);
- if (!running) return;
- const usageInterval = setInterval(() => this.loadRunUsage(sessionId), 3000);
- onCleanup(() => clearInterval(usageInterval));
- });
- effect((onCleanup) => {
- if (!this.isRunInfoOpen() || !this.agentService.isCurrentSessionRunning()) return;
- this.runInfoClock.set(Date.now());
- const clockInterval = setInterval(() => this.runInfoClock.set(Date.now()), 1000);
- onCleanup(() => clearInterval(clockInterval));
- });
-
- // The retry clock only needs to tick while something on screen is timing
- // (startup progress, LLM retry countdowns). An idle page stays quiet.
- this.zone.runOutsideAngular(() => {
- const retryClockInterval = setInterval(() => {
- const status = this.agentService.agentStatus();
- if (status === 'running' || status === 'paused' || this.agentService.isRetrying()) {
- this.retryClock.set(Date.now());
- }
- }, 1000);
- this.destroyRef.onDestroy(() => clearInterval(retryClockInterval));
- });
-
- // Auto scroll stream box during active text streaming
- effect(() => {
- const logs = this.filteredLogs();
- const activeStream = logs.find(log => log.type === 'llm_stream' && !log.data?.isCompleted);
- if (activeStream) {
- this.scheduleAutoScroll(true);
- }
- });
-
- // Auto scroll main log list when new logs are added or typewriter updates
- effect(() => {
- this.consolidatedBlocks();
- this.typedTextsSignal();
- this.scheduleAutoScroll(false);
- });
-
- // Listen for stream reset events to trigger fast typewriter rewind
- effect(() => {
- const resetEvent = this.agentService.streamResetEvent();
- if (!resetEvent) return;
- untracked(() => {
- this.triggerStreamRewind(
- resetEvent.stream_execution_id || resetEvent.stream_exec_id,
- resetEvent.message || DEFAULT_STREAM_RESET_MESSAGE
- );
- });
- });
-
- // Typewriter drive logic: triggers typing when block appears or expands
- effect(() => {
- const blocks = this.consolidatedBlocks();
- blocks.forEach(block => {
- // Check blocks render their per-turn segments directly (no typewriter).
- if (block.type === 'checker') return;
- const blockId = block.id;
-
- // If the block is flagged as reset, ensure typewriter rewind is active or reset notice is set
- if (block.data?.isReset) {
- untracked(() => {
- const resetMsg = block.data.resetMessage || DEFAULT_STREAM_RESET_MESSAGE;
- const currentRecord = this.typedTextsSignal();
- const hasText = (currentRecord[blockId]?.length || 0) > 0 || (currentRecord[blockId + '-native']?.length || 0) > 0;
- if (hasText) {
- if (!this.rewindTargets.has(blockId) && !this.rewindTargets.has(blockId + '-native')) {
- this.triggerStreamRewind(block.data.execution_id, resetMsg);
- }
- } else {
- const activeResets = this.activeBlockResets().get(blockId) || [];
- if (activeResets.length === 0) {
- this.upsertBlockResetNotice(blockId, {
- id: block.data.execution_id || `${blockId}-text`,
- message: resetMsg,
- isWaiting: !block.data?.isCompleted,
- streamType: 'text'
- });
- }
- if (!this.activeResetNotices().has(blockId)) {
- this.activeResetNotices.update(m => {
- const next = new Map(m);
- next.set(blockId, resetMsg);
- return next;
- });
- }
- }
- });
- return;
- }
-
- const rawText = this.getRawThinking(block) || '';
- const nativeText = this.getNativeThinking(block) || '';
-
- untracked(() => {
- // Handle Raw Thinking (Work)
- const currentRecord = this.typedTextsSignal();
- if (rawText) {
- if (!(blockId in currentRecord)) {
- if (block.data?.isCompleted) {
- this.typedTextsSignal.update(r => ({ ...r, [blockId]: rawText }));
- } else {
- this.typedTextsSignal.update(r => ({ ...r, [blockId]: '' }));
- this.startTyping(blockId, rawText, false);
- }
- } else {
- const currentVal = currentRecord[blockId] || '';
- if (currentVal.length < rawText.length && !this.typingTargets.has(blockId)) {
- if (block.data?.isCompleted) {
- this.typedTextsSignal.update(r => ({ ...r, [blockId]: rawText }));
- } else {
- this.startTyping(blockId, rawText, false);
- }
- }
- }
- }
-
- // Handle Native Thinking (Thought)
- const nativeKey = blockId + '-native';
- if (nativeText) {
- const execId = block.data?.execution_id || block.data?.step_id || blockId;
- if (!(nativeKey in currentRecord)) {
- if (block.data?.isCompleted) {
- this.typedTextsSignal.update(r => ({ ...r, [nativeKey]: nativeText }));
- this.collapsedStreams.update(set => {
- const newSet = new Set(set);
- newSet.add(execId);
- return newSet;
- });
- } else {
- this.typedTextsSignal.update(r => ({ ...r, [nativeKey]: '' }));
- this.startTyping(nativeKey, nativeText, true, execId);
- }
- } else {
- const currentVal = currentRecord[nativeKey] || '';
- if (currentVal.length < nativeText.length && !this.typingTargets.has(nativeKey)) {
- if (block.data?.isCompleted) {
- this.typedTextsSignal.update(r => ({ ...r, [nativeKey]: nativeText }));
- this.collapsedStreams.update(set => {
- const newSet = new Set(set);
- newSet.add(execId);
- return newSet;
- });
- } else {
- this.startTyping(nativeKey, nativeText, true, execId);
- }
- }
- }
- }
- });
- });
- });
-
- // Reset typewriter and collapse states when session changes
- effect(() => {
- this.agentService.currentSessionId();
- untracked(() => {
- this.stopTypingLoop();
- this.typedTextsSignal.set({});
- this.activeResetNotices.set(new Map());
- this.activeBlockResets.set(new Map());
- this.markdownHtmlCache.clear();
- this.scheduledCollapses.clear();
- this.collapsedStreams.set(new Set());
- this.isUserAtBottom = true;
-
- setTimeout(() => {
- const container = document.querySelector('.stream-logs-content');
- if (container) {
- container.scrollTop = container.scrollHeight;
- }
- }, 100);
- });
- });
-
- // Periodically rotate loading text like game loading screen hints
- effect(() => {
- const isLoading = this.showPlanningLoader();
- if (isLoading) {
- if (!this.planningIntervalId) {
- this.currentPhraseIndex = Math.floor(Math.random() * PLANNING_LOADER_PHRASES.length);
- this.currentPlanningText.set(PLANNING_LOADER_PHRASES[this.currentPhraseIndex]);
-
- this.planningIntervalId = setInterval(() => {
- this.currentPhraseIndex = (this.currentPhraseIndex + 1) % PLANNING_LOADER_PHRASES.length;
- this.currentPlanningText.set(PLANNING_LOADER_PHRASES[this.currentPhraseIndex]);
- }, 2800);
- }
- } else {
- if (this.planningIntervalId) {
- clearInterval(this.planningIntervalId);
- this.planningIntervalId = null;
- }
- }
- });
-
- this.destroyRef.onDestroy(() => {
- if (this.planningIntervalId) {
- clearInterval(this.planningIntervalId);
- this.planningIntervalId = null;
- }
- if (this.resizeObserver) {
- this.resizeObserver.disconnect();
- this.resizeObserver = null;
- }
- this.stopTypingLoop();
- if (this.autoScrollTimer) {
- clearTimeout(this.autoScrollTimer);
- this.autoScrollTimer = null;
- }
- if (this.streamLogsContainer) {
- this.streamLogsContainer.removeEventListener('scroll', this.onContainerScroll);
- this.streamLogsContainer = null;
- }
- });
- }
-
- public ngAfterViewInit(): void {
- const container = document.querySelector('.stream-logs-content') as HTMLElement | null;
- this.streamLogsContainer = container;
- if (!container) return;
-
- // The scroll listener lives outside the Angular zone: tracking the
- // user's scroll position must not trigger change detection per frame.
- this.zone.runOutsideAngular(() => {
- container.addEventListener('scroll', this.onContainerScroll, { passive: true });
- });
-
- if (typeof ResizeObserver !== 'undefined') {
- this.resizeObserver = new ResizeObserver(() => {
- if (this.isUserAtBottom) {
- this.scheduleAutoScroll(false);
- }
- });
- this.resizeObserver.observe(container);
- }
- }
-
- /**
- * Coalesce every auto-scroll request into one deferred pass that batches all
- * DOM reads before all writes, instead of thrashing layout per update.
- */
- private scheduleAutoScroll(includeStreamBoxes: boolean): void {
- this.autoScrollStreamBoxes = this.autoScrollStreamBoxes || includeStreamBoxes;
- if (this.autoScrollTimer) return;
- this.zone.runOutsideAngular(() => {
- this.autoScrollTimer = setTimeout(() => {
- this.autoScrollTimer = null;
- const includeBoxes = this.autoScrollStreamBoxes;
- this.autoScrollStreamBoxes = false;
-
- const boxes = includeBoxes
- ? (Array.from(document.getElementsByClassName('stream-box')) as HTMLElement[])
- : [];
- const boxTargets = boxes.map((el) => el.scrollHeight);
- const container = this.streamLogsContainer;
- const containerTarget = container && this.isUserAtBottom ? container.scrollHeight : null;
-
- boxes.forEach((el, i) => { el.scrollTop = boxTargets[i]; });
- if (container && containerTarget !== null) {
- container.scrollTop = containerTarget;
- }
- }, 50);
- });
- }
-
- // Top Nav Action Methods
- public toggleTaskDropdown(event: Event): void {
- event.stopPropagation();
- this.isTaskDropdownOpen.update(v => !v);
- if (this.isTaskDropdownOpen()) {
- this.isNotesDropdownOpen.set(false);
- }
- }
-
- public toggleNotesDropdown(event: Event): void {
- event.stopPropagation();
- this.isNotesDropdownOpen.update((v) => !v);
- if (this.isNotesDropdownOpen()) {
- this.isTaskDropdownOpen.set(false);
- }
- }
-
- public openOutputterNote(event?: Event): void {
- if (event) event.stopPropagation();
- this.agentService.selectedNoteKey.set('output.md');
- this.isNotesDropdownOpen.set(true);
- this.isTaskDropdownOpen.set(false);
- }
-
- public closeAllDropdowns(): void {
- this.isTaskDropdownOpen.set(false);
- this.isNotesDropdownOpen.set(false);
- this.isRunInfoOpen.set(false);
- }
-
- public toggleRunInfo(event: Event): void {
- event.stopPropagation();
- this.isRunInfoOpen.update((v) => !v);
- if (this.isRunInfoOpen()) {
- this.isTaskDropdownOpen.set(false);
- this.isNotesDropdownOpen.set(false);
- }
- }
-
- private loadRunUsage(sessionId: string): void {
- this.agentService.getSessionUsage(sessionId).subscribe({
- next: (usage) => {
- if (this.agentService.currentSessionId() !== sessionId) return;
- this.runUsage.set(usage);
- },
- error: (err) => console.error('Failed to load session usage:', err)
- });
- }
-
- public formatRunElapsed(seconds: number): string {
- return formatElapsed(seconds);
- }
-
- public formatRunTokens(tokens: number | null | undefined): string {
- return formatCompactTokens(tokens);
- }
-
- public runTuningLabel(kind: 'verify' | 'explore', id: string | null | undefined): string {
- return tuningLabel(kind, id);
- }
-
- public getTaskStatus(session: Session): 'running' | 'paused' | 'completed' | 'pending' | 'failed' | 'cancelled' {
- if (session.status) {
- const s = session.status.toLowerCase();
- if (s === 'completed' || s === 'success' || s === 'failed' || s === 'cancelled') {
- return (s === 'success' ? 'completed' : s) as any;
- }
- if (s === 'running' || s === 'paused' || s === 'pending') {
- return s as any;
- }
- }
- if (session.session_id === this.agentService.runningSessionId() && (this.agentService.agentStatus() === 'running' || this.agentService.agentStatus() === 'paused')) {
- return this.agentService.agentStatus() as 'running' | 'paused';
- }
- return 'completed';
- }
-
- /**
- * Determine the device serial number for the session
- */
- public getDeviceSerial(session: Session): string | null {
- if (this.deviceSerialCache.has(session)) {
- return this.deviceSerialCache.get(session) ?? null;
- }
- let resolved: string | null = null;
- const serial = session.device_serial || session.device_id;
- if (serial && serial !== 'pending' && serial !== 'null' && serial !== 'undefined') {
- resolved = serial;
- } else if (session.device_info) {
- try {
- const info = typeof session.device_info === 'string' ? JSON.parse(session.device_info) : session.device_info;
- const s = info?.device_id || info?.device_serial;
- if (s && s !== 'pending' && s !== 'null' && s !== 'undefined') {
- resolved = s;
- }
- } catch {
- // ignore
- }
- }
- this.deviceSerialCache.set(session, resolved);
- return resolved;
- }
-
- public selectTask(sessionId: string, event?: Event): void {
- if (event) event.stopPropagation();
- this.agentService.selectSession(sessionId, true);
- this.isTaskDropdownOpen.set(false);
- }
-
- public stopTask(sessionId: string, event?: Event): void {
- if (event) event.stopPropagation();
- this.agentService.stopTask(sessionId, false);
- }
-
- public deleteTask(sessionId: string, event?: Event): void {
- if (event) event.stopPropagation();
- if (!confirm(`Are you sure you want to delete this task? This cannot be undone.`)) {
- return;
- }
- this.agentService.deleteSession(sessionId).subscribe({
- error: (err) => {
- console.error(`Failed to delete session ${sessionId}:`, err);
- }
- });
- }
-
- public selectNote(key: string, event?: Event): void {
- if (event) event.stopPropagation();
- this.agentService.selectedNoteKey.set(key);
- }
-
- public getParsedNote(content: string): ParsedNote {
- return parseNote(content);
- }
-
- public getParsedNoteLines(content: string): MarkdownLine[] {
- return parseNoteLines(content);
- }
-
- // Typewriter & Delayed Collapse
- private upsertBlockResetNotice(blockId: string, notice: StreamResetNotice): void {
- this.activeBlockResets.update(m => {
- const next = new Map(m);
- const list = [...(next.get(blockId) || [])];
- const existingIdx = list.findIndex(r => r.id === notice.id);
- if (existingIdx > -1) {
- list[existingIdx] = notice;
- } else {
- list.push(notice);
- }
- next.set(blockId, list);
- return next;
- });
- }
-
- private startTyping(key: string, targetText: string, isThinking: boolean = false, execId?: string) {
- const blockId = key.endsWith('-native') ? key.replace('-native', '') : key;
- // If there was an active rewind, cancel it, reset text to empty, and record its reset notice as completed waiting
- const activeRewind = this.rewindTargets.get(key);
- if (activeRewind) {
- this.rewindTargets.delete(key);
- this.typedTextsSignal.update(r => ({ ...r, [key]: '' }));
- this.upsertBlockResetNotice(blockId, {
- id: activeRewind.execId || `${blockId}-${isThinking ? 'thinking' : 'text'}`,
- message: activeRewind.message,
- isWaiting: false,
- streamType: isThinking ? 'thinking' : 'text'
- });
- }
-
- // Cancel any active rewind on this key
- this.rewindTargets.delete(key);
-
- // When retry stream starts outputting content, change waiting dot to normal dot (isWaiting: false)
- // The previous prompt sentence DOES NOT disappear.
- this.activeBlockResets.update(m => {
- const list = m.get(blockId);
- if (!list || list.length === 0) return m;
- const targetType = isThinking ? 'thinking' : 'text';
- const updated = list.map(r => r.streamType === targetType ? { ...r, isWaiting: false } : r);
- const next = new Map(m);
- next.set(blockId, updated);
- return next;
- });
-
- this.typingTargets.set(key, { blockId, isThinking, execId, fallbackText: targetText });
- this.ensureTypingLoop();
- }
-
- private ensureTypingLoop(): void {
- if (this.typingRafId !== null) return;
- this.lastTypingTimestamp = performance.now();
- this.zone.runOutsideAngular(() => {
- const step = (now: number) => {
- this.typingRafId = null;
- const elapsed = now - this.lastTypingTimestamp;
- if (elapsed >= AgentStreamComponent.TYPING_MIN_FRAME_MS) {
- this.lastTypingTimestamp = now;
- this.advanceTyping(elapsed);
- }
- if (this.typingTargets.size > 0 || this.rewindTargets.size > 0) {
- this.typingRafId = requestAnimationFrame(step);
- }
- };
- this.typingRafId = requestAnimationFrame(step);
- });
- }
-
- private stopTypingLoop(): void {
- this.typingTargets.clear();
- this.rewindTargets.clear();
- if (this.typingRafId !== null) {
- cancelAnimationFrame(this.typingRafId);
- this.typingRafId = null;
- }
- }
-
- /**
- * Advance every active typewriter by the elapsed wall time and publish all
- * changes as one signal update (one render pass per animation frame at most).
- */
- private advanceTyping(elapsedMs: number): void {
- const blocks = untracked(() => this.consolidatedBlocks());
- const currentRecord = untracked(() => this.typedTextsSignal());
- const chars = Math.max(
- 1,
- Math.round(AgentStreamComponent.TYPING_CHARS_PER_SECOND * elapsedMs / 1000)
- );
- const rewindChars = Math.max(
- 2,
- Math.round(AgentStreamComponent.REWIND_CHARS_PER_SECOND * elapsedMs / 1000)
- );
- const updates: Record = {};
- let hasUpdates = false;
-
- // 1. Advance forward typing targets
- for (const [key, typing] of this.typingTargets) {
- const block = blocks.find((b) => b.id === typing.blockId);
- let target = typing.fallbackText;
- if (block) {
- target = (typing.isThinking ? this.getNativeThinking(block) : this.getRawThinking(block))
- || typing.fallbackText;
- }
-
- const current = currentRecord[key] || '';
- if (current.length < target.length) {
- updates[key] = target.slice(0, Math.min(current.length + chars, target.length));
- hasUpdates = true;
- } else if (block?.data?.isCompleted) {
- this.typingTargets.delete(key);
- if (typing.isThinking && typing.execId) {
- this.triggerDelayedCollapse(typing.execId);
- }
- }
- }
-
- // 2. Advance fast rewind targets (backspacing for mid-stream resets)
- const completedRewinds: Array<{ blockId: string; execId?: string; isThinking: boolean; message: string }> = [];
- for (const [key, rewind] of this.rewindTargets) {
- const current = updates[key] !== undefined ? updates[key] : (currentRecord[key] || '');
- if (current.length > 0) {
- const nextLen = Math.max(0, current.length - rewindChars);
- updates[key] = current.slice(0, nextLen);
- hasUpdates = true;
- if (nextLen === 0) {
- this.rewindTargets.delete(key);
- completedRewinds.push({
- blockId: rewind.blockId,
- execId: rewind.execId,
- isThinking: rewind.isThinking,
- message: rewind.message
- });
- }
- } else {
- this.rewindTargets.delete(key);
- completedRewinds.push({
- blockId: rewind.blockId,
- execId: rewind.execId,
- isThinking: rewind.isThinking,
- message: rewind.message
- });
- }
- }
-
- if (completedRewinds.length > 0) {
- for (const item of completedRewinds) {
- this.upsertBlockResetNotice(item.blockId, {
- id: item.execId || `${item.blockId}-${item.isThinking ? 'thinking' : 'text'}`,
- message: item.message,
- isWaiting: true,
- streamType: item.isThinking ? 'thinking' : 'text'
- });
- }
- this.activeResetNotices.update((notices) => {
- const next = new Map(notices);
- for (const item of completedRewinds) {
- next.set(item.blockId, item.message);
- }
- return next;
- });
- }
-
- if (hasUpdates) {
- this.typedTextsSignal.update((r) => ({ ...r, ...updates }));
- }
- }
-
- /**
- * Triggers fast typewriter rewind on a stream execution when a mid-stream reset occurs.
- * Once characters rewind to zero, the block displays the graceful retry notification.
- */
- public triggerStreamRewind(execId: string | undefined, message: string): void {
- const blocks = untracked(() => this.consolidatedBlocks());
- const currentTexts = untracked(() => this.typedTextsSignal());
-
- const targetBlock = blocks.find(b =>
- b.data?.execution_id === execId
- || b.id === `stream-${execId}`
- || b.data?.step_id === execId
- || (Array.isArray(b.data?.stream_segments) && b.data.stream_segments.some((s: any) => s.execution_id === execId))
- ) || blocks.find(b => b.data?.isReset);
-
- if (!targetBlock) return;
- const blockId = targetBlock.id;
-
- const keysToCheck: Array<{ key: string; isThinking: boolean }> = [
- { key: blockId, isThinking: false },
- { key: blockId + '-native', isThinking: true }
- ];
-
- let startedRewind = false;
- for (const { key, isThinking } of keysToCheck) {
- this.typingTargets.delete(key);
-
- const currentText = currentTexts[key] || '';
- if (currentText.length > 0) {
- this.rewindTargets.set(key, {
- blockId,
- isThinking,
- execId,
- message
- });
- startedRewind = true;
- }
- }
-
- if (startedRewind) {
- this.ensureTypingLoop();
- } else {
- this.upsertBlockResetNotice(blockId, {
- id: execId || `${blockId}-text`,
- message,
- isWaiting: true,
- streamType: 'text'
- });
- this.activeResetNotices.update(notices => {
- const next = new Map(notices);
- next.set(blockId, message);
- return next;
- });
- }
- }
-
- public getBlockResets(block: any, isThinking: boolean = false): StreamResetNotice[] {
- if (!block) return [];
- const blockId = block.id;
- const targetStreamType = isThinking ? 'thinking' : 'text';
-
- // 1. From activeBlockResets signal
- const activeList = (this.activeBlockResets().get(blockId) || [])
- .filter(r => r.streamType === targetStreamType);
-
- // 2. From block.data.stream_resets (aggregated from logs)
- const blockDataResets = (Array.isArray(block.data?.stream_resets) ? block.data.stream_resets : [])
- .filter((r: any) => (r.streamType || 'text') === targetStreamType);
-
- // Fast path: if both lists are empty, check fallback or return empty array immediately
- if (activeList.length === 0 && blockDataResets.length === 0) {
- if (!isThinking && block.data?.isReset && block.data?.resetMessage) {
- return [{
- id: block.data.execution_id || blockId,
- message: block.data.resetMessage,
- isWaiting: !block.data?.isCompleted,
- streamType: 'text'
- }];
- }
- return [];
- }
-
- // Merge by id, giving precedence to activeList
- const mergedMap = new Map();
- for (const r of blockDataResets) {
- mergedMap.set(r.id, {
- id: r.id,
- message: r.message,
- isWaiting: block.data?.isCompleted ? false : (r.isWaiting ?? false),
- streamType: r.streamType || targetStreamType
- });
- }
- for (const r of activeList) {
- mergedMap.set(r.id, {
- ...r,
- isWaiting: block.data?.isCompleted ? false : r.isWaiting
- });
- }
-
- return Array.from(mergedMap.values());
- }
-
- public hasWaitingReset(block: any, isThinking: boolean = false): boolean {
- const resets = this.getBlockResets(block, isThinking);
- return resets.some(r => r.isWaiting);
- }
-
- public hasBlockResets(block: any, isThinking: boolean = false): boolean {
- return this.getBlockResets(block, isThinking).length > 0;
- }
-
- public formatResetMessage(message: string, isWaiting: boolean): string {
- if (!message) return '';
- if (isWaiting) return message;
- return message
- .replace(/[,.\s]*(?:Retrying automatically(?:\.{3}|\.\.\.)?)\s*$/i, '')
- .trim();
- }
-
- public getCheckerSegmentResetMessage(item: any): string {
- return item?.data?.resetMessage || DEFAULT_STREAM_RESET_MESSAGE;
- }
-
- public getResetNotice(block: any): string | null {
- if (!block) return null;
- const resets = this.getBlockResets(block, false);
- if (resets.length > 0) {
- const last = resets[resets.length - 1];
- return this.formatResetMessage(last.message, last.isWaiting);
- }
- const blockId = block.id;
- const active = this.activeResetNotices().get(blockId);
- if (active) return this.formatResetMessage(active, !block.data?.isCompleted);
- if (block.data?.isReset && block.data?.resetMessage) {
- return this.formatResetMessage(block.data.resetMessage, !block.data?.isCompleted);
- }
- return null;
- }
-
- public isStreamRewinding(key: string): boolean {
- return this.rewindTargets.has(key);
- }
-
- public isCheckerSegmentResetNotice(item: any): boolean {
- if (!item?.data?.isReset) return false;
- const text = item.data.text;
- return !text || text.trim().length === 0;
- }
-
- public getStreamDisplayText(key: string, fallback: string = ''): string {
- const typed = this.typedTextsSignal()[key];
- return typed !== undefined ? typed : fallback;
- }
-
- private triggerDelayedCollapse(execId: string) {
- if (!this.scheduledCollapses.has(execId)) {
- this.scheduledCollapses.add(execId);
- setTimeout(() => {
- this.collapsedStreams.update((set: Set) => {
- const newSet = new Set(set);
- newSet.add(execId);
- return newSet;
- });
- }, 1000);
- }
- }
-
- public onDetailsToggle(event: Event, execId: string): void {
- const details = event.target as HTMLDetailsElement;
- const isOpen = details.open;
- this.collapsedStreams.update((set: Set) => {
- const newSet = new Set(set);
- if (isOpen) {
- newSet.delete(execId);
- } else {
- newSet.add(execId);
- }
- return newSet;
- });
- }
-
- public toggleCollapse(execId: string): void {
- this.collapsedStreams.update((set: Set) => {
- const newSet = new Set(set);
- if (newSet.has(execId)) {
- newSet.delete(execId);
- } else {
- newSet.add(execId);
- }
- return newSet;
- });
- }
-
- // Android Action Delegation Methods
- public getActionObject(action: any): any {
- return getActionObject(action);
- }
-
- public isAndroidAction(action: any): boolean {
- return isAndroidAction(action);
- }
-
- public getActionIcon(action: any): string {
- return getActionIcon(action);
- }
-
- public getActionTitle(action: any): string {
- return getActionTitle(action);
- }
-
- public getActionTargetText(action: any): string {
- return getActionTargetText(action);
- }
-
- public getActionInputLabel(action: any): string {
- return getActionInputLabel(action);
- }
-
- public getActionInputText(action: any): string {
- return getActionInputText(action);
- }
-
- public getActionCoords(action: any): string {
- return getActionCoords(action);
- }
-
- public isActionFailed(action: any, stepData?: any): boolean {
- return isActionFailed(action, stepData);
- }
-
- public getActionErrorMessage(action: any, stepData?: any): string {
- return getActionErrorMessage(action, stepData);
- }
-
- public isActionExpanded(cardId: string, defaultExpanded: boolean = false): boolean {
- if (this.collapsedActionCards().has(cardId)) return false;
- if (this.expandedActionCards().has(cardId)) return true;
- return defaultExpanded;
- }
-
- public toggleActionExpanded(cardId: string, event?: Event, defaultExpanded: boolean = false): void {
- if (event) {
- event.stopPropagation();
- }
- const isCurrentlyExpanded = this.isActionExpanded(cardId, defaultExpanded);
- if (isCurrentlyExpanded) {
- this.expandedActionCards.update(set => {
- const newSet = new Set(set);
- newSet.delete(cardId);
- return newSet;
- });
- this.collapsedActionCards.update(set => {
- const newSet = new Set(set);
- newSet.add(cardId);
- return newSet;
- });
- } else {
- this.collapsedActionCards.update(set => {
- const newSet = new Set(set);
- newSet.delete(cardId);
- return newSet;
- });
- this.expandedActionCards.update(set => {
- const newSet = new Set(set);
- newSet.add(cardId);
- return newSet;
- });
- }
- }
-
- public getActionBounds(action: any): string {
- return getActionBounds(action);
- }
-
- public getActionResourceId(action: any): string {
- return getActionResourceId(action);
- }
-
- public getActionClass(action: any): string {
- return getActionClass(action);
- }
-
- public getActionExtraParams(action: any): ActionParam[] {
- return extractActionExtraParams(action, this.actionParamsCache);
- }
-
- public getToolExtraParams(toolData: any): ActionParam[] {
- return extractToolExtraParams(toolData, this.toolParamsCache);
- }
-
- public getStepPreImageUrl(stepData: any, actionData?: any): string | null {
- return getStepPreImageUrl(stepData, actionData);
- }
-
- public getStepPostImageUrl(stepData: any, actionData?: any): string | null {
- return getStepPostImageUrl(stepData, actionData);
- }
-
- // Zoom Modal & Overlay Methods
- public openImageModal(url: string, actionObj?: any, event?: Event): void {
- if (event) {
- event.stopPropagation();
- }
- this.selectedZoomImage.set(url);
- this.selectedZoomAction.set(actionObj || null);
- }
-
- public closeImageModal(event?: Event): void {
- if (event) {
- event.stopPropagation();
- }
- this.selectedZoomImage.set(null);
- this.selectedZoomAction.set(null);
- }
-
- public onImageLoad(img: HTMLImageElement, overlay: HTMLElement, actionObj: any): void {
- if (!img || !overlay) return;
- try {
- this.drawActionCoordinatesOnOverlay(img, overlay, actionObj);
- } catch (e) {
- console.warn('Failed to draw action overlay:', e);
- }
- }
-
- public drawActionCoordinatesOnOverlay(img: HTMLImageElement, overlay: HTMLElement, actionObj: any): void {
- drawActionCoordinatesOnOverlay(img, overlay, actionObj);
- }
-
- // Generic Tool Delegation Methods
-
- /**
- * First save_note call per note key, derived once per blocks change instead
- * of re-scanning every block for every rendered tool row.
- */
- private firstSaveNoteByKey = computed>(() => {
- const firstByKey = new Map();
- for (const block of this.consolidatedBlocks()) {
- if (block.type === 'step' && block.data.generic_tools) {
- for (const t of this.getUniqueGenericTools(block.data.generic_tools)) {
- if (t.name && t.name.toLowerCase() === 'save_note') {
- const key = this.getToolKey(t);
- if (key && !firstByKey.has(key)) {
- firstByKey.set(key, t);
- }
- }
- }
- }
- }
- return firstByKey;
- });
-
- public isFirstSaveNoteForKey(tool: any): boolean {
- if (!tool || !tool.name || tool.name.toLowerCase() !== 'save_note') {
- return false;
- }
- const key = this.getToolKey(tool);
- if (!key) return true;
-
- const firstCall = this.firstSaveNoteByKey().get(key);
- if (!firstCall) return true;
- return (tool.trace_id && firstCall.trace_id)
- ? tool.trace_id === firstCall.trace_id
- : tool === firstCall;
- }
-
- public getToolDisplayLabel(tool: any): string {
- return getToolDisplayLabel(tool, this.isFirstSaveNoteForKey(tool));
- }
-
- /** Short plain-language phase for a compress_history line (hover title). */
- public getCompressionPhaseTitle(tool: any): string | null {
- return isCompressionTool(tool) ? getCompressionPhaseLabel(tool) : null;
- }
-
- /**
- * A generic tool line pulses only while work is in flight. A compression
- * whose summary is ready but held back by the context gate is waiting, not
- * running, even though its trace status is still `running`.
- */
- public isToolLineRunning(item: any, block: any): boolean {
- return this.isItemRunning(item, block) && !isCompressionWaiting(item?.data);
- }
-
- public isNoteTool(tool: any): boolean {
- return isNoteTool(tool);
- }
-
- public isVideoTool(tool: any): boolean {
- return isVideoTool(tool);
- }
-
- public getVideoToolTarget(tool: any): string | null {
- return getVideoToolTarget(tool);
- }
-
- public getVideoAnalysisLabel(tool: any): string {
- return getVideoAnalysisView(tool)?.title || 'Analyzing screen recording';
- }
-
- public getVideoAnalysisRangeLabel(tool: any): string {
- const range = getVideoAnalysisView(tool)?.requestedRange;
- if (!range) return 'Screen recording';
- return `${formatVideoTime(range.start)}–${formatVideoTime(range.end)}`;
- }
-
- public getVideoAnalysisDetail(tool: any): string {
- const view = getVideoAnalysisView(tool);
- if (!view || view.totalCount <= 1) return '';
- if (view.outcome === 'running' || view.outcome === 'recovering') {
- return view.completedCount > 0
- ? `${view.completedCount}/${view.totalCount} segments saved`
- : '';
- }
- if (view.outcome === 'partial') {
- return `${view.completedCount}/${view.totalCount} segments saved`;
- }
- return '';
- }
-
- public isVideoAnalysisAttention(tool: any): boolean {
- const outcome = getVideoAnalysisView(tool)?.outcome;
- return outcome === 'partial' || outcome === 'failed';
- }
-
- public onVideoToolClick(toolData: any): void {
- const curSessionId = this.agentService.currentSessionId();
- if (curSessionId) {
- const start = getVideoAnalysisView(toolData)?.requestedRange?.start;
- this.agentService.openVideoPlayer(curSessionId, undefined, undefined, start);
- }
- }
-
- public getScreenRecordingButtonTitle(): string {
- if (this.agentService.isCurrentSessionRunning()) {
- return 'Task is currently running (Recording screen)';
- }
- if (this.agentService.currentSessionRecordingStatus() === 'processing') {
- return 'Preparing screen recording...';
- }
- if (this.agentService.currentSessionVideoUrl()) {
- return 'Play Screen Recording Video';
- }
- if (this.agentService.hasCurrentSessionStepFrames()) {
- return this.agentService.currentSessionRecordingStatus() === 'failed'
- ? 'Video recording failed — Click to replay step-by-step screenshots'
- : 'Play step-by-step screenshots replay';
- }
- if (this.agentService.currentSessionRecordingStatus() === 'failed') {
- return 'Screen recording generation failed';
- }
- return 'Screen Recording';
- }
-
- public isDeviceActionTool(tool: any): boolean {
- return isDeviceActionTool(tool);
- }
-
- public isFailureAnalyzerActionTool(tool: any): boolean {
- return isDeviceActionTool(tool);
- }
-
- public getToolAgentName(tool: any): string | null {
- return getToolAgentName(tool);
- }
-
- public shouldShowTool(tool: any, stepData?: any): boolean {
- return shouldShowTool(tool, stepData);
- }
-
- public getUniqueGenericTools(tools: any[] | undefined): any[] {
- return getUniqueGenericTools(tools);
- }
-
- public getSortedStepEvents(stepData: any): StepEvent[] {
- return getSortedStepEvents(stepData, this.sortedEventsCache);
- }
-
- /**
- * Memoized per block-data object: templates call these many times per
- * change-detection pass, and the raw-thinking path runs JSON-detection
- * heuristics over the full text.
- */
- private getThinkingTexts(data: any): { native: string | null; raw: string | null } {
- let entry = this.thinkingTextCache.get(data);
- if (!entry) {
- const nativeText = data.operator_native_thinking || (data.stream_type === 'thinking' ? data.text : null);
- const rawText = data.operator_raw_thinking || (data.stream_type === 'text' ? data.text : null);
- entry = {
- native: nativeText && nativeText.trim() ? nativeText : null,
- raw: rawText && rawText.trim() && this.isHumanThinking(rawText) ? rawText : null
- };
- this.thinkingTextCache.set(data, entry);
- }
- return entry;
- }
-
- public getNativeThinking(block: any): string | null {
- if (!block || !block.data) return null;
- return this.getThinkingTexts(block.data).native;
- }
-
- public getRawThinking(block: any): string | null {
- if (!block || !block.data) return null;
- return this.getThinkingTexts(block.data).raw;
- }
-
- // --- Check blocks (verifier lane) ----------------------------------------------
- public isCheckerBlock(block: any): boolean {
- return block?.type === 'checker';
- }
-
- public isCheckerPhase(phase: any): boolean {
- const blocks: any[] = Array.isArray(phase?.blocks) ? phase.blocks : [];
- return blocks.length > 0 && blocks.every((b) => this.isCheckerBlock(b));
- }
-
- public getCheckerPhaseLabel(block: any): string {
- switch (block?.data?.phase) {
- case 'final':
- return 'Final check';
- case 'outcome':
- return 'Result';
- default:
- return 'Check';
- }
- }
-
- public getCheckerVerdicts(block: any): any[] {
- return Array.isArray(block?.data?.verdicts) ? block.data.verdicts : [];
- }
-
- /** Declared check items; persisted attempts only carry verdicts, so derive them. */
- public getCheckerItems(block: any): any[] {
- const d = block?.data || {};
- if (Array.isArray(d.items) && d.items.length > 0) return d.items;
- return this.getCheckerVerdicts(block).map((v) => ({ kind: v.kind, text: v.item_text, when: v.when }));
- }
-
- /** How the item is judged, in plain words (verify = must pass, assert = recorded test result). */
- public getCheckMethodLabel(item: any, block: any): string {
- const parts: string[] = [item?.kind === 'assert' ? 'Test assertion' : 'Must pass'];
- if (item?.when === 'at_end' && block?.data?.phase !== 'final') parts.push('at the end');
- return parts.join(' · ');
- }
-
- /** Superseded / unchecked attempts collapse to their header line. */
- public isCheckerMuted(block: any): boolean {
- const status = block?.data?.status;
- return status === 'superseded' || status === 'unchecked';
- }
-
- /** Number of the check item a verdict belongs to (1-based, matches the list above the stream). */
- public getVerdictItemNumber(block: any, verdict: any, fallbackIndex: number): number {
- const idx = this.getCheckerItems(block).findIndex((i) => i.text === verdict?.item_text);
- return (idx > -1 ? idx : fallbackIndex) + 1;
- }
-
- public getVerdictStatusText(status: string): string {
- switch (status) {
- case 'passed':
- return 'Passed';
- case 'failed':
- return 'Failed';
- case 'inconclusive':
- return 'Could not be confirmed';
- case 'superseded':
- return 'Superseded by a newer check';
- case 'unchecked':
- return 'Not checked';
- default:
- return status || '';
- }
- }
-
- public hasCheckerResult(block: any): boolean {
- if (!this.isCheckerBlock(block) || this.isBlockRunning(block) || this.isCheckerMuted(block)) return false;
- if (block.data?.phase === 'outcome') return this.getCheckerNotes(block).length > 0;
- return this.getCheckerVerdicts(block).length > 0 || this.getCheckerNotes(block).length > 0;
- }
-
- /** Follow-ups not already visible in the verdict rows (the findings text repeats them). */
- public getCheckerNotes(block: any): string[] {
- const d = block?.data || {};
- if (d.phase === 'outcome') return Array.isArray(d.last_findings) ? d.last_findings : [];
- const notes: string[] = [];
- if (Array.isArray(d.unmet_subgoals)) {
- for (const text of d.unmet_subgoals) notes.push(`Not finished yet: ${text}`);
- }
- if (d.reverted) notes.push('The step was sent back to be redone.');
- if (d.error) notes.push(String(d.error));
- return notes;
- }
-
- // Streamed turns of a check: the last one is live while the block runs;
- // finished Thought turns fold unless the reader opens them.
- public openCheckSegments = signal>(new Set());
-
- public isCheckerSegmentRunning(block: any, itemIdx: number): boolean {
- if (!this.isBlockRunning(block)) return false;
- return itemIdx === this.getSortedStepEvents(block.data).length - 1;
- }
-
- public isCheckerSegmentCollapsed(block: any, itemIdx: number): boolean {
- if (this.isCheckerSegmentRunning(block, itemIdx)) return false;
- return !this.openCheckSegments().has(`${block.id}-${itemIdx}`);
- }
-
- public toggleCheckerSegment(block: any, itemIdx: number): void {
- const key = `${block.id}-${itemIdx}`;
- this.openCheckSegments.update((set) => {
- const next = new Set(set);
- if (next.has(key)) next.delete(key); else next.add(key);
- return next;
- });
- }
-
- public isCheckerTextVisible(item: any): boolean {
- if (item?.data?.isReset) return true;
- const text = item?.data?.text;
- return typeof text === 'string' && text.trim().length > 0 && this.isHumanThinking(text);
- }
-
- public getCheckerTone(block: any): string {
- const d = block?.data || {};
- if (d.phase === 'outcome') {
- return d.task_status === 'completed' ? 'passed' : (d.task_status === 'blocked' ? 'failed' : 'inconclusive');
- }
- if (d.isCompleted === false) return 'running';
- if (d.status === 'superseded' || d.status === 'unchecked') return 'muted';
- if (d.status === 'error') return 'inconclusive';
- const verdicts = this.getCheckerVerdicts(block);
- if (verdicts.some((v) => v.status === 'failed')) return 'failed';
- if (verdicts.some((v) => v.status === 'inconclusive')) return 'inconclusive';
- return 'passed';
- }
-
- public getCheckerStatusLabel(block: any): string {
- const d = block?.data || {};
- if (d.phase === 'outcome') {
- const t = d.tests || {};
- const label = d.task_status === 'completed' ? 'Goal completed'
- : (d.task_status === 'blocked' ? 'Blocked' : 'Partially completed');
- const counts = [
- [t.passed, 'passed'],
- [t.failed, 'failed'],
- [t.inconclusive, 'inconclusive'],
- [t.unchecked, 'not checked']
- ]
- .filter(([n]) => Number(n) > 0)
- .map(([n, word]) => `${n} ${word}`);
- return counts.length > 0 ? `${label} · ${counts.join(' · ')}` : label;
- }
- if (d.isCompleted === false) return 'Checking…';
- switch (d.status) {
- case 'superseded':
- return 'Superseded';
- case 'unchecked':
- return 'Not checked';
- case 'error':
- return 'No verdict';
- }
- const verdicts = this.getCheckerVerdicts(block);
- const failed = verdicts.filter((v) => v.status === 'failed').length;
- if (failed > 0) return `${failed} failed`;
- if (verdicts.some((v) => v.status === 'inconclusive')) return 'Inconclusive';
- return verdicts.length > 0 ? 'Passed' : 'Done';
- }
-
- public getVerdictIcon(status: string): string {
- switch (status) {
- case 'passed':
- return 'check_circle';
- case 'failed':
- return 'cancel';
- case 'inconclusive':
- return 'help';
- case 'superseded':
- return 'history';
- case 'unchecked':
- return 'radio_button_unchecked';
- default:
- return 'pending';
- }
- }
-
- public hasVisibleContent(block: any): boolean {
- if (!block) return false;
- if (block.type === 'checker') return true;
- const hasNative = Boolean(this.getNativeThinking(block));
- const hasRaw = Boolean(this.getRawThinking(block));
- const hasReset = this.hasBlockResets(block, false) || this.hasBlockResets(block, true);
- const hasVisibleTools = Boolean(block.data?.generic_tools) && block.data.generic_tools.some((t: any) =>
- this.shouldShowTool(t, block.data)
- || this.isDisplayableLLMFailure(t)
- || this.isLLMRetry(t)
- || this.isReportStatusAction(t)
- || this.isBackendSwitchNote(t)
- );
- const hasAndroidActions = Boolean(block.data?.action_taken) && (this.isAndroidAction(block.data.action_taken) || this.isReportStatusAction(block.data.action_taken));
- return hasNative || hasRaw || hasReset || hasVisibleTools || hasAndroidActions;
- }
-
- public isReportStatusAction(action: any): boolean {
- return isReportStatusAction(action);
- }
-
- public getReportStatusValue(action: any): string {
- return getReportStatusValue(action);
- }
-
- public getReportStatusExplanation(action: any): string {
- return getReportStatusExplanation(action);
- }
-
- public isHumanThinking(text: string | null): boolean {
- return isHumanThinking(text);
- }
-
- public isTerminalLLMFailure(tool: any): boolean {
- return !!tool && this.retryablePauseTrace() === tool;
- }
-
- public isDisplayableLLMFailure(tool: any): boolean {
- return tool?.type === 'llm_call'
- && tool?.status === 'failed'
- && (tool?.name === 'llm_pause' || tool?.payload?.pause === true);
- }
-
- /** Mid-run change of the UI-hierarchy source (helper <-> UIAutomator2). */
- public isBackendSwitchNote(tool: any): boolean {
- return isBackendSwitchNote(tool);
- }
-
- public getBackendSwitchMessage(tool: any): string {
- return String(tool?.payload?.message || 'UI hierarchy source changed');
- }
-
- public resumePausedTask(event: Event): void {
- event.stopPropagation();
- this.agentService.resumeTask();
- }
-
- public getLLMErrorText(tool: any): string {
- const noDetails = 'The AI provider did not return error details after the request failed.';
- if (!tool) return noDetails;
- const rawError = tool.payload?.error || tool.error;
- if (!rawError) return noDetails;
- const cleaned = this.cleanErrorMessage(rawError);
- return cleaned === 'Unknown error' ? noDetails : cleaned;
- }
-
- public isLLMRetry(tool: any): boolean {
- return tool?.type === 'llm_call'
- && tool?.status === 'retrying'
- && tool?.payload?.source === 'provider_sdk'
- && ['google', 'gemini'].includes(String(tool?.payload?.provider || '').toLowerCase());
- }
-
- public getLLMRetryEntries(tool: any): any[] {
- const retries = tool?.payload?.retries;
- return Array.isArray(retries) && retries.length > 0 ? retries : [tool?.payload || tool];
- }
-
- public getLLMFailureRetryEntries(tool: any): any[] {
- const retries = tool?.payload?.retries;
- if (!Array.isArray(retries)) return [];
- return retries.filter((retry: any) =>
- retry?.source === 'provider_sdk'
- && ['google', 'gemini'].includes(String(retry?.provider || '').toLowerCase())
- );
- }
-
- public hasLLMFailureRetryEntries(tool: any): boolean {
- return this.getLLMFailureRetryEntries(tool).length > 0;
- }
-
- public getLLMRetryEntryDelay(entry: any): string | null {
- const delay = Number(entry?.delay);
- if (!Number.isFinite(delay) || delay <= 0) return null;
- return `${delay.toFixed(2).replace(/\.00$/, '')}s`;
- }
-
- public getLLMRetryEntryWaited(entry: any): string {
- const delay = Number(entry?.delay);
- if (!Number.isFinite(delay) || delay <= 0) return '0s';
-
- const rawStartedAt = Number(entry?.scheduled_at ?? entry?.timestamp);
- if (!Number.isFinite(rawStartedAt) || rawStartedAt <= 0) {
- return this.formatLLMDuration(delay);
- }
- const startedAtMs = rawStartedAt < 1e11 ? rawStartedAt * 1000 : rawStartedAt;
- const elapsed = Math.min(delay, Math.max(0, (this.retryClock() - startedAtMs) / 1000));
- return this.formatLLMDuration(elapsed);
- }
-
- public isLLMRetryEntryWaiting(entry: any): boolean {
- const delay = Number(entry?.delay);
- const rawStartedAt = Number(entry?.scheduled_at ?? entry?.timestamp);
- if (!Number.isFinite(delay) || delay <= 0 || !Number.isFinite(rawStartedAt)) return false;
- const startedAtMs = rawStartedAt < 1e11 ? rawStartedAt * 1000 : rawStartedAt;
- return this.retryClock() < startedAtMs + delay * 1000;
- }
-
- public getLLMFailureWaited(tool: any): string | null {
- const waited = Number(tool?.payload?.waited_seconds);
- if (!Number.isFinite(waited) || waited < 0) return null;
- return this.formatLLMDuration(waited);
- }
-
- public formatLLMDuration(seconds: number): string {
- if (seconds < 0.05) return '0s';
- if (seconds < 10) return `${seconds.toFixed(1).replace(/\.0$/, '')}s`;
- return `${Math.round(seconds)}s`;
- }
-
- public cleanErrorMessage(rawError: any): string {
- return cleanErrorMessage(rawError);
- }
-
- public getToolKey(tool: any): string | null {
- return getToolKey(tool);
- }
-
- public getToolCallName(action: any): string {
- const act = this.getActionObject(action);
- if (!act) return 'Tool';
- const label = this.getToolDisplayLabel(act);
- const key = this.getToolKey(act);
- return key ? `${label}: ${key}` : label;
- }
-
- public getToolCallLabel(action: any): string {
- return this.getToolCallName(action);
- }
-
- public isToolFailed(tool: any): boolean {
- return isToolFailed(tool);
- }
-
- public getToolErrorMessage(tool: any): string {
- return getToolErrorMessage(tool);
- }
-
- public getToolArgs(tool: any): any {
- return getToolArgs(tool);
- }
-
- public getToolIcon(tool: any): string {
- return getToolIcon(tool);
- }
-
- public getToolTitle(tool: any): string {
- return getToolTitle(tool);
- }
-
- public getToolTargetText(tool: any): string {
- return getToolTargetText(tool);
- }
-
- public getToolInputLabel(tool: any): string {
- return getToolInputLabel(tool);
- }
-
- public getToolInputText(tool: any): string {
- return getToolInputText(tool);
- }
-
- public getToolCoords(tool: any): string {
- return getToolCoords(tool);
- }
-
- public getToolAnalysisText(tool: any): string {
- return getToolAnalysisText(tool);
- }
-
- public getToolGenericDetails(tool: any): string {
- return getToolGenericDetails(tool);
- }
-
- public isAdbCommandTool(tool: any): boolean {
- return isAdbCommandTool(tool);
- }
-
- public getAdbCommandLine(tool: any): string {
- return getAdbCommandLine(tool);
- }
-
- public getAdbCwd(tool: any): string {
- return getAdbCwd(tool);
- }
-
- public getAdbTerminalId(tool: any): string {
- return getAdbTerminalId(tool);
- }
-
- public onSessionChange(event: Event): void {
- const selectElement = event.target as HTMLSelectElement;
- const sessionId = selectElement.value;
- if (sessionId) {
- this.agentService.selectSession(sessionId, true);
- }
- }
-
- public isStepEvent(eventType: string): boolean {
- return eventType === 'step_recorded' || eventType === 'step_updated';
- }
-
- public getCheckerResult(text: string): CheckerResult | null {
- return extractCheckerResult(text);
- }
-
- public isShortContent(data: any): boolean {
- if (!data) return true;
- const str = typeof data === 'string' ? data : JSON.stringify(data);
- return str.length < 200;
- }
-
- public formatJson(data: any): string {
- if (typeof data === 'string') {
- try {
- return JSON.stringify(JSON.parse(data), null, 2);
- } catch {
- return data;
- }
- }
- return JSON.stringify(data, null, 2);
- }
-
- public getStatusLabel(status: string): string {
- switch (status) {
- case 'idle':
- return 'Idle';
- case 'running':
- return 'Running';
- case 'completed':
- return 'Completed';
- case 'offline':
- return 'Offline';
- default:
- return status.toUpperCase();
- }
- }
-
- /**
- * Render markdown with a per-slot memo. Returning the identical cached
- * string instance for unchanged text means the [innerHTML] binding sees the
- * same reference and skips DOM/sanitizer work entirely.
- */
- public renderMarkdown(text: string, cacheKey?: string): string {
- if (!text) return '';
- if (!cacheKey) {
- return renderMarkdownToHtml(text);
- }
- const cached = this.markdownHtmlCache.get(cacheKey);
- if (cached && cached.source === text) {
- return cached.html;
- }
- const html = renderMarkdownToHtml(text);
- this.markdownHtmlCache.set(cacheKey, { source: text, html });
- return html;
- }
-
- public onToolKeyClick(key: string): void {
- this.agentService.selectedNoteKey.set(key);
- this.agentService.activeTab.set('notes');
-
- const sessionId = this.agentService.currentSessionId();
- if (sessionId) {
- this.agentService.fetchNotes(sessionId);
- }
- }
-
- public trackSession(index: number, session: Session): string {
- return session.session_id;
- }
-
- public trackNoteKey(index: number, key: string): string {
- return key;
- }
-
- public trackMilestone(index: number, milestone: NoteMilestone): number {
- return milestone.index;
- }
-
- public trackMarkdownLine(index: number, line: MarkdownLine): number {
- return index;
- }
-
- public trackPhase(index: number, phase: any): string {
- return phase.id;
- }
-
- public trackBlock(index: number, block: any): string {
- return block.id;
- }
-
- public trackTool(index: number, tool: any): string {
- return tool.trace_id || index.toString();
- }
-
- public trackStepEvent(index: number, item: { type: string; data: any; timestamp?: number }): string {
- if (!item || !item.data) return index.toString();
- // A step has at most one thinking and one text event; keying them by type
- // alone keeps their DOM stable while the streamed text grows (keying by
- // text length would tear down and rebuild the node on every chunk).
- if (item.type === 'thinking' || item.type === 'text') {
- return item.type;
- }
- return item.type + '-' + (item.data.trace_id
- || item.data.timestamp || item.data.start_time || item.data.created_at || item.data.id
- || ((item.data.action || item.data.name || '') + '-' + index));
- }
-
- public trackParam(index: number, param: { key: string; value: string }): string {
- return param.key;
- }
-
- public getModelDisplayName(name: string | undefined | null): string {
- if (!name) return 'Flash';
- const lower = name.toLowerCase();
- if (lower.includes('pro')) return 'Pro';
- if (lower.includes('flash')) return 'Flash';
- return name.replace(/^artemis\s+/i, '');
- }
-
- public getModelIcon(name: string | undefined | null): string {
- if (!name) return 'bolt';
- const lower = name.toLowerCase();
- if (lower.includes('pro')) return 'diamond';
- if (lower.includes('flash')) return 'bolt';
- return 'smart_toy';
- }
-
- public getModelClass(name: string | undefined | null): string {
- if (!name) return 'is-flash';
- const lower = name.toLowerCase();
- if (lower.includes('pro')) return 'is-pro';
- if (lower.includes('flash')) return 'is-flash';
- return '';
- }
-
- public getArchitectureTooltip(model?: ModelInfo | null): string {
- if (!model) return 'Agent Architecture: ARTEMIS Flash (Reactive Fast Loop)';
- const name = this.getModelDisplayName(model.name);
- const isPro = name.toLowerCase().includes('pro');
- const archDesc = isPro
- ? 'ARTEMIS Pro (Multi-Agent Cognitive State Graph)'
- : 'ARTEMIS Flash (Reactive Fast Loop)';
- if (model.id) {
- return `Agent Architecture: ${archDesc} · LLM: ${model.id} (${model.provider || 'google'})`;
- }
- return `Agent Architecture: ${archDesc}`;
- }
-
- public formatTokenCount(tokens?: number): string {
- return formatTokenCount(tokens);
- }
-
- public getTokenTooltip(phase: PhaseBlock): string {
- if (!phase.tokens) return '';
- if (phase.promptTokens && phase.completionTokens) {
- return `Consumed: ${phase.tokens.toLocaleString()} tokens (${phase.promptTokens.toLocaleString()} in / ${phase.completionTokens.toLocaleString()} out)`;
- }
- return `Consumed: ${phase.tokens.toLocaleString()} tokens`;
- }
-
- public isCurrentSessionActive(): boolean {
- const status = this.agentService.agentStatus();
- if (status !== 'running') return false;
- const currentId = this.agentService.currentSessionId();
- const runningId = this.agentService.runningSessionId();
- if (currentId && runningId && currentId !== runningId) {
- return false;
- }
- const currentSession = this.agentService.sessions().find(s => s.session_id === currentId);
- if (currentSession && (currentSession.status === 'completed' || currentSession.status === 'failed' || currentSession.status === 'cancelled')) {
- return false;
- }
- return true;
- }
-
- public isItemRunning(item: any, block: any): boolean {
- if (!item || !item.data) return false;
- if (!this.isCurrentSessionActive()) return false;
- if (item.data.status === 'success' || item.data.status === 'failed' || item.data.status === 'cancelled') return false;
- if (this.isReportStatusAction(item.data) || this.isReportStatusAction(item.data?.action_taken)) return false;
- if (item.data.status === 'running') return true;
-
- // Check if session is currently running and this is the active trailing item
- const blocks = this.consolidatedBlocks();
- const lastBlock = blocks.length > 0 ? blocks[blocks.length - 1] : null;
- if (lastBlock && (lastBlock.id === block?.id || !block?.data?.isCompleted)) {
- const events = this.getSortedStepEvents(block.data);
- const lastEvent = events.length > 0 ? events[events.length - 1] : null;
- if (lastEvent && (lastEvent.data?.trace_id === item.data?.trace_id || lastEvent === item)) {
- return true;
- }
- }
- return false;
- }
-
- public isBlockRunning(block: any): boolean {
- if (!block || !block.data) return false;
- if (!this.isCurrentSessionActive()) return false;
- if (block.type === 'checker') return block.data.isCompleted === false;
- if (this.isReportStatusAction(block.data.action_taken) || block.data?.status === 'completed' || block.data?.status === 'failed' || block.data?.status === 'cancelled') return false;
- if (block.data.isCompleted === false) return true;
- const blocks = this.consolidatedBlocks();
- const lastBlock = blocks.length > 0 ? blocks[blocks.length - 1] : null;
- return lastBlock?.id === block.id;
- }
-}
diff --git a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html
deleted file mode 100644
index efd8882db..000000000
--- a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html
+++ /dev/null
@@ -1,380 +0,0 @@
-
-
-
-
-
-
-
-
-
- @if (agentService.activeTab() === 'tasks') {
-
-
- @if (activeQueue().length === 0) {
-
- No active or queued tasks.
-
- }
- @for (session of activeQueue(); track trackSession($index, session)) {
-
-
-
- {{ session.initial_goal || 'Unnamed Task' }}
-
-
-
- }
-
-
-
- history Past Runs & History ({{ historyTasks().length }})
-
-
- @if (historyTasks().length === 0) {
-
- No past tasks.
-
- }
- @for (session of historyTasks(); track trackSession($index, session)) {
-
-
-
- {{ session.initial_goal || 'Unnamed Task' }}
-
-
-
- }
-
-
- }
-
-
- @if (agentService.activeTab() === 'notes') {
-
-
- @if (noteKeys().length > 1) {
-
-
- @for (key of noteKeys(); track trackNoteKey($index, key)) {
-
- draft
- {{ key }}
-
- }
-
-
- }
- @if (noteKeys().length === 0) {
-
-
- description
-
-
No Notes & Plans
-
No notes or structured plans were recorded for this session.
-
- }
-
- @if (noteKeys().length > 0) {
-
-
- @if (parsedNote().title) {
-
{{ parsedNote().title }}
- }
-
- @if (parsedNote().milestones.length > 0) {
-
- @for (milestone of parsedNote().milestones; track trackMilestone($index, milestone)) {
-
-
-
- @switch (milestone.type) {
- @case ('checked') {
-
check_box
- }
- @case ('progress') {
-
indeterminate_check_box
- }
- @case ('unchecked') {
-
check_box_outline_blank
- }
- }
-
- @for (seg of milestone.segments; track seg) {
- {{ seg.text }}
- }
-
-
-
- @if (milestone.subSteps.length > 0) {
-
- @for (step of milestone.subSteps; track trackMarkdownLine($index, step)) {
-
2 ? (step.indent - 2) * 12 : 0)"
- >
- @switch (step.type) {
- @case ('checked') {
- check_box
- }
- @case ('progress') {
- indeterminate_check_box
- }
- @case ('unchecked') {
- check_box_outline_blank
- }
- @case ('list-item') {
- •
- }
- }
- @if (step.type !== 'empty') {
-
- @for (seg of step.segments; track seg) {
- {{ seg.text }}
- }
-
- }
-
- }
-
- }
-
- @if (milestone.checks.length > 0) {
-
- @for (check of milestone.checks; track trackMarkdownLine($index, check)) {
-
2 ? (check.indent - 2) * 12 : 0)"
- >
-
-
- @for (seg of check.segments; track seg) {
- {{ seg.text }}
- }
-
-
- }
-
- }
-
- }
-
- }
-
- @if (parsedNote().otherLines.length > 0) {
-
- @for (line of parsedNote().otherLines; track trackMarkdownLine($index, line)) {
- @if (line.type === 'verify' || line.type === 'finding') {
-
-
-
- @for (seg of line.segments; track seg) {
- {{ seg.text }}
- }
-
-
- } @else {
-
- @switch (line.type) {
- @case ('checked') {
- check_box
- }
- @case ('progress') {
- indeterminate_check_box
- }
- @case ('unchecked') {
- check_box_outline_blank
- }
- @case ('list-item') {
- •
- }
- }
- @if (line.type !== 'empty') {
-
- @for (seg of line.segments; track seg) {
- {{ seg.text }}
- }
-
- }
-
- }
- }
-
- }
-
- }
-
- }
-
-
-
diff --git a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss
deleted file mode 100644
index 2de9b953a..000000000
--- a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss
+++ /dev/null
@@ -1,1171 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-$border-color: rgba(0, 0, 0, 0.08);
-$text-primary: #1f1f1f;
-$text-secondary: #71717a;
-$text-muted: #a1a1aa;
-
-:host {
- display: flex;
- flex-direction: column;
- height: 100%;
- width: 100%;
- overflow: hidden;
-}
-
-.chat-interface-container {
- container-type: inline-size;
- display: flex;
- flex-direction: column;
- height: 100%;
- background: #ffffff;
- box-sizing: border-box;
-
- .chat-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- height: 60px;
- padding: 0 20px;
- border-bottom: 1px solid $border-color;
- background: #ffffff;
- box-sizing: border-box;
-
- .tab-selector-bar {
- display: flex;
- height: 36px;
- box-sizing: border-box;
- background: rgba(241, 245, 249, 0.72);
- backdrop-filter: blur(16px) saturate(180%);
- -webkit-backdrop-filter: blur(16px) saturate(180%);
- border: 1px solid rgba(226, 232, 240, 0.85);
- border-radius: 18px;
- padding: 3px;
- gap: 3px;
- align-items: center;
- flex-shrink: 0;
- box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.8);
-
- .tab-selector-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- height: 30px;
- padding: 0 14px;
- box-sizing: border-box;
- border: none;
- background: transparent;
- border-radius: 15px;
- font-size: 13px;
- font-weight: 600;
- color: #64748b;
- cursor: pointer;
- line-height: 1;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- white-space: nowrap;
-
- .tab-icon {
- font-size: 16px;
- width: 16px;
- height: 16px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- }
-
- &:hover:not(.active) {
- color: #0f172a;
- background: rgba(255, 255, 255, 0.7);
- }
-
- &.active {
- background: #ffffff;
- color: #2563eb;
- font-weight: 700;
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.02);
- }
- }
- }
-
- .header-right {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- flex: 1 1 0;
- min-width: max-content;
- flex-shrink: 0;
-
- .btn-resume-task {
- padding: 6px 14px;
- background: linear-gradient(135deg, rgba(37, 99, 235, 0.92), rgba(26, 115, 232, 0.85));
- backdrop-filter: blur(14px) saturate(180%);
- -webkit-backdrop-filter: blur(14px) saturate(180%);
- border: 1px solid rgba(255, 255, 255, 0.35);
- border-radius: 16px;
- font-size: 13.5px;
- font-weight: 600;
- color: #ffffff;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 5px;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- margin-right: 8px;
- box-shadow: 0 4px 14px rgba(37, 99, 235, 0.3), inset 0 1px 1px rgba(255, 255, 255, 0.45);
-
- .material-symbols-outlined {
- font-size: 16px;
- color: inherit;
- }
-
- &:hover {
- background: linear-gradient(135deg, rgba(37, 99, 235, 1), rgba(26, 115, 232, 0.95));
- border-color: rgba(255, 255, 255, 0.6);
- box-shadow: 0 6px 18px rgba(37, 99, 235, 0.42), inset 0 1px 1px rgba(255, 255, 255, 0.6);
- transform: translateY(-0.5px);
- }
-
- &:active {
- transform: scale(0.97);
- }
- }
-
- .btn-clear-history {
- width: 32px;
- height: 32px;
- padding: 0;
- box-sizing: border-box;
- background: rgba(255, 255, 255, 0.72);
- backdrop-filter: blur(16px) saturate(180%);
- -webkit-backdrop-filter: blur(16px) saturate(180%);
- border: 1px solid rgba(203, 213, 225, 0.85);
- border-radius: 50%;
- color: #64748b;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: 0 2px 6px -1px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.9);
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-
- .material-symbols-outlined {
- font-size: 16px;
- color: inherit;
- }
-
- &:hover:not(:disabled) {
- background: #fef2f2;
- color: #ef4444;
- border-color: #fca5a5;
- box-shadow: 0 3px 10px rgba(239, 68, 68, 0.15);
- transform: scale(1.05);
- }
-
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- }
- }
- }
-
- .chat-messages {
- flex: 1;
- overflow-y: auto;
- padding: 12px 12px 100px 12px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- background: #ffffff;
-
- &::-webkit-scrollbar {
- width: 5px;
- }
- &::-webkit-scrollbar-thumb {
- background: rgba(148, 163, 184, 0.3);
- border-radius: 4px;
- }
-
- .queue-section-header {
- font-size: 11.5px;
- font-weight: 700;
- color: #475569;
- margin: 6px 0 2px;
- display: flex;
- align-items: center;
- gap: 6px;
-
- .section-icon {
- font-size: 14px;
- color: inherit;
- }
- }
-
- .empty-section-placeholder {
- display: flex;
- align-items: center;
- justify-content: center;
- background: #f1f5f9;
- border: 1px dashed #cbd5e1;
- border-radius: 8px;
- padding: 12px;
- color: #64748b;
- font-size: 12px;
- text-align: center;
- }
-
- .history-section {
- margin-top: 12px;
- border-top: 1px solid rgba(226, 232, 240, 0.8);
- padding-top: 12px;
-
- &[open] .history-summary {
- margin-bottom: 8px;
- }
-
- .history-summary {
- font-size: 11.5px;
- font-weight: 700;
- color: #475569;
- cursor: pointer;
- outline: none;
- user-select: none;
- list-style: none;
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 4px 6px;
- margin-left: -6px;
- border-radius: 6px;
- transition: background 0.2s;
-
- .section-icon {
- font-size: 14px;
- color: inherit;
- }
-
- &::-webkit-details-marker {
- display: none;
- }
-
- &:hover {
- background: #f1f5f9;
- color: #1e293b;
- }
- }
-
- .history-list-wrapper {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- }
-
- .task-card {
- background: #ffffff;
- border: 1px solid rgba(226, 232, 240, 0.8);
- border-radius: 10px;
- padding: 10px 12px;
- display: flex;
- flex-direction: column;
- gap: 6px;
- cursor: pointer;
- position: relative;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.01);
-
- &:hover {
- transform: translateY(-1px);
- border-color: rgba(203, 213, 225, 1);
- box-shadow: 0 3px 8px rgba(148, 163, 184, 0.08);
-
- .task-card-header .task-card-header-actions {
- &:has(.btn-delete-card) .task-time,
- &:has(.btn-delete-card) .task-time-divider,
- &:has(.btn-stop-card) .task-time,
- &:has(.btn-stop-card) .task-time-divider {
- opacity: 0;
- }
-
- .btn-delete-card,
- .btn-stop-card {
- opacity: 1;
- pointer-events: auto;
- }
- }
- }
-
- &.selected {
- border-color: #3b82f6;
- box-shadow: 0 0 0 1px #3b82f6, 0 4px 12px rgba(59, 130, 246, 0.08);
- background: #ffffff;
- }
-
- &.running {
- border-color: #10b981;
- box-shadow: 0 0 0 1px #10b981, 0 4px 12px rgba(16, 185, 129, 0.08);
-
- &.selected {
- border-color: #3b82f6;
- box-shadow: 0 0 0 1px #3b82f6, 0 4px 12px rgba(59, 130, 246, 0.08);
- }
- }
-
- .task-card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 8px;
- min-width: 0;
-
- .task-badge {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
- font-size: 10.5px;
- font-weight: 700;
- padding: 2px 5px;
- border-radius: 4px;
- letter-spacing: 0.05em;
- border: 1px solid #e2e8f0;
- background: #fafafa;
- color: #64748b;
-
- &::before {
- content: '';
- width: 4px;
- height: 4px;
- border-radius: 50%;
- display: inline-block;
- margin-right: 4px;
- }
-
- &.running {
- background: #fafafa;
- color: #1f1f1f;
- border-color: #1f1f1f;
- &::before {
- background-color: #1f1f1f;
- animation: pulse-black-dot-small 1.5s infinite ease-in-out;
- }
- }
- &.paused {
- background: #fff7ed;
- color: #c2410c;
- border-color: #fdba74;
- &::before { background-color: #f97316; }
- }
- &.completed {
- background: #fafafa;
- color: #1f1f1f;
- border-color: #e2e8f0;
- &::before {
- background-color: #1f1f1f;
- }
- }
- &.pending {
- background: #fafafa;
- color: #64748b;
- border-color: #e2e8f0;
- &::before {
- background-color: #94a3b8;
- }
- }
- &.failed {
- background: #fafafa;
- color: #ef4444;
- border-color: #fee2e2;
- &::before {
- background-color: #ef4444;
- }
- }
- &.cancelled {
- background: #fafafa;
- color: #d97706;
- border-color: #fef3c7;
- &::before {
- background-color: #f59e0b;
- }
- }
-
- .status-dot {
- display: none;
- }
- }
-
- .task-card-header-actions {
- display: flex;
- align-items: center;
- gap: 4px;
- position: relative;
- min-width: 0;
- flex-shrink: 1;
- justify-content: flex-end;
-
- .task-device {
- display: inline-flex;
- align-items: center;
- gap: 2.5px;
- min-width: 0;
- flex-shrink: 1;
- font-size: 11px;
- color: #94a3b8;
- line-height: 1;
- cursor: default;
- transition: color 0.15s ease, opacity 0.15s ease;
-
- .device-icon {
- font-size: 12.5px;
- width: 12.5px;
- height: 12.5px;
- line-height: 12.5px;
- color: #94a3b8;
- opacity: 0.75;
- flex-shrink: 0;
- }
-
- .device-name {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- min-width: 0;
- font-family: inherit;
- letter-spacing: -0.15px;
- }
-
- &:hover {
- color: #64748b;
- .device-icon {
- color: #64748b;
- opacity: 1;
- }
- }
- }
-
- .task-time-divider {
- font-size: 10px;
- color: #cbd5e1;
- line-height: 1;
- user-select: none;
- flex-shrink: 0;
- transition: opacity 0.15s ease;
- }
-
- .task-time {
- font-size: 11.5px;
- color: #94a3b8;
- white-space: nowrap;
- flex-shrink: 0;
- line-height: 1;
- transition: opacity 0.15s ease;
- }
-
- .btn-delete-card {
- position: absolute;
- right: 0;
- top: 50%;
- transform: translateY(-50%);
- width: 20px;
- height: 20px;
- padding: 0;
- border: none;
- background: transparent;
- border-radius: 4px;
- color: #94a3b8;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- opacity: 0;
- pointer-events: none;
- transition: all 0.15s ease;
-
- .material-symbols-outlined {
- font-size: 14px;
- line-height: 1;
- }
-
- &:hover:not(:disabled) {
- color: #ef4444;
- background: #fee2e2;
- }
-
- &:disabled {
- opacity: 0.3;
- cursor: not-allowed;
- }
- }
-
- .btn-stop-card {
- position: absolute;
- right: 0;
- top: 50%;
- transform: translateY(-50%);
- width: 20px;
- height: 20px;
- padding: 0;
- border: none;
- background: transparent;
- border-radius: 4px;
- color: #ef4444;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- opacity: 0;
- pointer-events: none;
- transition: all 0.15s ease;
-
- .material-symbols-outlined {
- font-size: 15px;
- line-height: 1;
- }
-
- &:hover:not(:disabled) {
- color: #dc2626;
- background: #fee2e2;
- transform: translateY(-50%) scale(1.1);
- }
-
- &:disabled {
- opacity: 0.3;
- cursor: not-allowed;
- }
- }
- }
- }
-
- .task-goal {
- font-size: 12.5px;
- font-weight: 500;
- color: #1e293b;
- line-height: 1.4;
- word-break: break-word;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
-
- .task-card-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 12px;
- border-top: 1px solid rgba(241, 245, 249, 0.8);
- padding-top: 6px;
- font-size: 11.5px;
- color: #94a3b8;
-
- .task-id {
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- font-size: 11px;
- color: #94a3b8;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
- flex: 1;
- transition: color 0.15s ease;
-
- &:hover {
- color: #475569;
- }
- }
-
- .task-date {
- flex-shrink: 0;
- font-size: 11px;
- color: #94a3b8;
- }
- }
- }
- }
-
-
-
-
- // Notes tab content
- .notes-tab-content {
- display: flex;
- flex-direction: column;
- gap: 16px;
- height: 100%;
-
- .notes-selector-container {
- display: flex;
- flex-direction: column;
- gap: 8px;
-
- .selector-title {
- font-size: 11px;
- font-weight: 700;
- color: #94a3b8;
- letter-spacing: 0.05em;
- }
-
- .notes-selector-list {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
-
- .note-selector-pill {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 6px 12px;
- border: 1px solid #e2e8f0;
- background: #ffffff;
- border-radius: 20px;
- font-size: 13px;
- font-weight: 500;
- color: #475569;
- cursor: pointer;
- transition: all 0.15s ease;
-
- .pill-icon {
- font-size: 15px;
- }
-
- &:hover {
- background: #f8fafc;
- border-color: #cbd5e1;
- color: #1e293b;
- }
-
- &.active {
- background: #eff6ff;
- border-color: #bfdbfe;
- color: #1d4ed8;
- font-weight: 600;
- }
- }
- }
- }
-
- .empty-notes-container {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 48px 24px;
- margin: auto 0;
- text-align: center;
- user-select: none;
- animation: fadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
-
- .empty-notes-icon {
- width: 52px;
- height: 52px;
- border-radius: 50%;
- background: #f1f5f9;
- border: 1px solid #e2e8f0;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: 14px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
-
- .material-symbols-outlined {
- font-size: 26px;
- color: #94a3b8;
- }
- }
-
- .empty-notes-title {
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- font-size: 14px;
- font-weight: 600;
- color: #1e293b;
- margin: 0 0 6px 0;
- }
-
- .empty-notes-subtitle {
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- font-size: 12.5px;
- color: #64748b;
- line-height: 1.5;
- max-width: 240px;
- margin: 0;
- }
- }
-
- .note-viewer-cards {
- display: flex;
- flex-direction: column;
- flex: 1;
- padding: 16px 8px;
- overflow-y: auto;
-
- .note-title {
- font-size: 14px;
- font-weight: 700;
- color: #1f1f1f;
- margin-top: 0;
- margin-bottom: 20px;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', sans-serif;
- }
-
- .code-segment {
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- background-color: #f1f3f4;
- color: #1967d2;
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 14px;
- margin: 0 2px;
- word-break: break-all;
- font-weight: normal;
- }
-
- .checked {
- .code-segment {
- color: #a1a1aa;
- background-color: #f8f9fa;
- }
- }
-
- .milestones-container {
- display: flex;
- flex-direction: column;
- gap: 6px;
- margin-bottom: 24px;
- }
-
- .milestone-block {
- display: flex;
- flex-direction: column;
-
- &.checked {
- .milestone-title-row {
- .milestone-status-icon.checked {
- opacity: 0.7;
- }
- .milestone-text {
- color: #a1a1aa;
- }
- }
- }
-
- .milestone-title-row {
- display: flex;
- align-items: flex-start;
- gap: 10px;
-
- .milestone-status-icon {
- font-size: 20px;
- user-select: none;
- margin-top: 3px;
- font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
-
- &.checked {
- color: #1a73e8;
- }
-
- &.progress {
- color: #1a73e8;
- }
-
- &.unchecked {
- color: #cbd5e1;
- }
- }
-
- .milestone-text {
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', sans-serif;
- color: #1f1f1f;
- font-size: 14px;
- line-height: 1.8;
- font-weight: 400;
- word-break: break-word;
- flex: 1;
-
- span {
- color: inherit;
- }
-
- .bold-segment {
- font-weight: inherit;
- color: inherit;
- }
- }
- }
-
- .milestone-substeps {
- margin-top: 8px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- padding-left: 28px;
-
- .substep-row {
- display: flex;
- align-items: flex-start;
- gap: 10px;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', sans-serif;
- color: #1f1f1f;
- font-size: 14px;
- line-height: 1.8;
-
- span {
- color: inherit;
- }
-
- &.checked {
- color: #a1a1aa;
-
- .check-icon.checked {
- opacity: 0.7;
- }
- }
-
- .check-icon {
- font-size: 20px;
- user-select: none;
- margin-top: 3px;
- font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
-
- &.checked {
- color: #1a73e8;
- }
-
- &.progress {
- color: #1a73e8;
- }
-
- &.unchecked {
- color: #cbd5e1;
- }
- }
-
- .bullet {
- color: #9ca3af;
- font-weight: bold;
- user-select: none;
- padding-left: 2px;
- }
-
- .line-text {
- word-break: break-word;
- color: inherit;
-
- span {
- color: inherit;
- }
-
- .bold-segment {
- font-weight: inherit;
- color: inherit;
- }
- }
- }
- }
-
- .milestone-checks-container {
- margin-top: 8px;
- padding-left: 28px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- }
-
- .verification-card {
- background: #f8fafc;
- border: 1px solid #e2e8f0;
- border-radius: 6px;
- padding: 8px 12px;
- display: flex;
- flex-direction: column;
- gap: 4px;
-
- &.standalone {
- margin-top: 8px;
- margin-bottom: 8px;
- }
-
- .card-header {
- display: flex;
- align-items: center;
-
- .card-tag {
- font-family: 'Google Sans Text', 'Inter', -apple-system, sans-serif;
- font-size: 10.5px;
- font-weight: 600;
- line-height: 1;
- letter-spacing: 0.6px;
- text-transform: uppercase;
- color: #64748b;
-
- &.assert {
- color: #0284c7;
- }
- }
- }
-
- .card-body {
- font-family: inherit;
- font-size: 13.5px;
- line-height: 1.6;
- color: #334155;
- word-break: break-word;
-
- .bold-segment {
- font-weight: 600;
- color: #1e293b;
- }
-
- .code-segment {
- font-family: 'Fira Code', 'Roboto Mono', Consolas, monospace;
- font-size: 12px;
- background: #f1f5f9;
- color: #0f172a;
- padding: 1.5px 6px;
- border-radius: 4px;
- border: 1px solid #e2e8f0;
- }
- }
-
- &.finding {
- background: #fffbeb;
- border-color: #fef3c7;
-
- .card-header .card-tag {
- color: #b45309;
- }
-
- .card-body {
- color: #92400e;
- }
- }
- }
-
- .other-lines-container {
- display: flex;
- flex-direction: column;
- padding: 8px 4px;
-
- .md-line {
- display: flex;
- align-items: flex-start;
- gap: 10px;
- margin-bottom: 14px;
- min-height: 22px;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', sans-serif;
- color: #1f1f1f;
- font-size: 14px;
- line-height: 1.6;
-
- &.empty {
- height: 12px;
- }
-
- &.h1 {
- font-size: 14px;
- font-weight: 700;
- margin-top: 24px;
- margin-bottom: 12px;
- border-bottom: 1px solid #e5e7eb;
- padding-bottom: 6px;
- }
-
- &.h2 {
- font-size: 14px;
- font-weight: 700;
- margin-top: 20px;
- margin-bottom: 10px;
- }
-
- &.h3 {
- font-size: 14px;
- font-weight: 700;
- margin-top: 14px;
- margin-bottom: 8px;
- }
-
- &.checked {
- color: #a1a1aa;
-
- .check-icon {
- color: #1a73e8;
- opacity: 0.7;
- }
- }
-
- &.progress {
- .check-icon {
- color: #1a73e8;
- }
- }
-
- &.unchecked {
- .check-icon {
- color: #cbd5e1;
- }
- }
-
- &.list-item {
- .bullet {
- color: #9ca3af;
- font-weight: bold;
- user-select: none;
- }
- }
-
- span {
- color: inherit;
- }
-
- .check-icon {
- font-size: 20px;
- user-select: none;
- margin-top: 1px;
- font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
- }
-
- .line-text {
- word-break: break-word;
- color: inherit;
-
- span {
- color: inherit;
- }
-
- .bold-segment {
- font-weight: inherit;
- color: inherit;
- }
- }
- }
- }
- }
- }
-}
-
-@keyframes pulse {
- 0% { transform: scale(0.85); opacity: 0.6; }
- 50% { transform: scale(1.15); opacity: 1; }
- 100% { transform: scale(0.85); opacity: 0.6; }
-}
-
-@keyframes bannerSlideIn {
- from { transform: translateY(5px); opacity: 0; }
- to { transform: translateY(0); opacity: 1; }
-}
-
-@keyframes fadeIn {
- from { opacity: 0; transform: translateY(4px); }
- to { opacity: 1; transform: translateY(0); }
-}
-
-@keyframes pulse-black-dot-small {
- 0% {
- box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.4);
- }
- 70% {
- box-shadow: 0 0 0 4px rgba(0, 0, 0, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
- }
-}
-
-@keyframes pulse-blue-dot {
- 0% {
- box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.4);
- }
- 70% {
- box-shadow: 0 0 0 5px rgba(37, 99, 235, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(37, 99, 235, 0);
- }
-}
-
-@container (max-width: 380px) {
- .chat-header {
- padding: 0 8px;
- gap: 6px;
- height: 36px;
-
- .header-left {
- display: none;
- }
-
- .tab-selector-bar {
- padding: 2px;
- gap: 1px;
-
- .tab-selector-btn {
- padding: 5px 8px;
- font-size: 12px;
- gap: 4px;
-
- .tab-icon {
- font-size: 14px;
- }
- }
- }
-
- .header-right {
- .btn-clear-history {
- height: 24px;
- width: 30px;
- padding: 0;
- font-size: 12px;
- gap: 0;
- border-radius: 16px;
- box-sizing: border-box;
- display: inline-flex;
- align-items: center;
- justify-content: center;
-
- .material-symbols-outlined {
- font-size: 14px;
- line-height: 1;
- }
-
- .btn-text {
- display: none;
- }
- }
- }
- }
-}
-
-@container (max-width: 340px) {
- .chat-header {
- .tab-selector-bar {
- .tab-selector-btn {
- padding: 5px 6px;
- .tab-text {
- display: none;
- }
- }
- }
-
- .header-right {
- .btn-clear-history {
- width: 26px;
- }
- }
- }
-}
diff --git a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts b/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts
deleted file mode 100644
index 45b36a1ea..000000000
--- a/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts
+++ /dev/null
@@ -1,269 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Component, ChangeDetectionStrategy, inject, computed, signal } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
-import { AgentService } from '../../services/agent.service';
-import { Session } from '../../core/models/session.model';
-import { MarkdownSegment, MarkdownLine, NoteMilestone, ParsedNote } from '../../core/models/markdown.model';
-import { parseNote, parseNoteLines } from '../../utils/markdown-parser.util';
-
-export type { MarkdownSegment, MarkdownLine, NoteMilestone, ParsedNote };
-
-@Component({
- selector: 'app-chat-interface',
- standalone: true,
- imports: [CommonModule, FormsModule],
- templateUrl: './chat-interface.component.html',
- styleUrl: './chat-interface.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush
-})
-export class ChatInterfaceComponent {
- public agentService = inject(AgentService);
-
- public taskInput: string = '';
- // Signals so async completion handlers refresh this OnPush view.
- public isSubmitting = signal(false);
- public errorMessage = signal(null);
-
- // Device-serial resolution can require a JSON.parse of device_info; memoize
- // it per session object so template re-evaluation stays cheap.
- private deviceSerialCache = new WeakMap();
-
- /**
- * Filtered computed list of active tasks (running or pending) sorted by status and submission order
- */
- public activeQueue = computed(() => {
- const list = this.agentService.sessions().filter((s) => {
- const status = this.getTaskStatus(s);
- return status === 'running' || status === 'paused' || status === 'pending';
- });
- return list.sort((a, b) => {
- const statusA = this.getTaskStatus(a);
- const statusB = this.getTaskStatus(b);
- const isRunA = statusA === 'running' || statusA === 'paused';
- const isRunB = statusB === 'running' || statusB === 'paused';
- if (isRunA && !isRunB) return -1;
- if (!isRunA && isRunB) return 1;
- return (a.start_time || 0) - (b.start_time || 0); // stable FIFO order
- });
- });
-
- /**
- * Filtered computed list of historical/completed tasks (completed, failed, or cancelled)
- */
- public historyTasks = computed(() => {
- return this.agentService.sessions().filter((s) => {
- const status = this.getTaskStatus(s);
- return status === 'completed' || status === 'failed' || status === 'cancelled';
- });
- });
-
- /**
- * Submit a new task goal to the backend
- */
- public submitTask(): void {
- const goal = this.taskInput.trim();
- if (!goal) {
- return;
- }
-
- this.isSubmitting.set(true);
- this.errorMessage.set(null);
-
- this.agentService.runTask(goal).subscribe({
- next: (res) => {
- this.taskInput = '';
- this.isSubmitting.set(false);
- // Fetch status to refresh sessions list and select new session
- this.agentService.fetchStatus();
- },
- error: (err) => {
- console.error('Failed to submit task:', err);
- this.isSubmitting.set(false);
- this.errorMessage.set(err.error?.detail || 'The runner is busy. Please wait for the current task to finish.');
- // Auto-dismiss error banner after 5 seconds
- setTimeout(() => {
- this.errorMessage.set(null);
- }, 5000);
- }
- });
- }
-
- /**
- * Stop an individual task session
- */
- public stopTask(sessionId: string, event: MouseEvent): void {
- event.stopPropagation();
- this.isSubmitting.set(true);
- this.errorMessage.set(null);
- this.agentService.stopTask(sessionId, false);
- setTimeout(() => {
- this.isSubmitting.set(false);
- }, 400);
- }
-
- /**
- * Stop the currently running task and optionally clear the backend queue
- */
- public stopTasks(stopAll: boolean = false): void {
- this.isSubmitting.set(true);
- this.errorMessage.set(null);
- this.agentService.stopTask(stopAll);
- setTimeout(() => {
- this.isSubmitting.set(false);
- }, 400);
- }
-
- /**
- * Clear all database data/history to start fresh
- */
- public clearHistory(): void {
- if (!confirm('Are you sure you want to clear all tasks and history? This cannot be undone.')) {
- return;
- }
- this.isSubmitting.set(true);
- this.errorMessage.set(null);
- this.agentService.clearAllHistory().subscribe({
- next: () => {
- this.isSubmitting.set(false);
- },
- error: (err: any) => {
- console.error('Failed to clear history:', err);
- this.isSubmitting.set(false);
- this.errorMessage.set(err.error?.detail || 'Failed to clear history.');
- }
- });
- }
-
- /**
- * Delete an individual task / session
- */
- public deleteTask(sessionId: string, event: MouseEvent): void {
- event.stopPropagation();
- if (!confirm(`Are you sure you want to delete this task? This cannot be undone.`)) {
- return;
- }
- this.isSubmitting.set(true);
- this.errorMessage.set(null);
- this.agentService.deleteSession(sessionId).subscribe({
- next: () => {
- this.isSubmitting.set(false);
- },
- error: (err: any) => {
- console.error(`Failed to delete task ${sessionId}:`, err);
- this.isSubmitting.set(false);
- this.errorMessage.set(err.error?.detail || 'Failed to delete task.');
- }
- });
- }
-
- /**
- * Determine the current task execution status
- */
- public getTaskStatus(session: Session): 'running' | 'paused' | 'completed' | 'pending' | 'failed' | 'cancelled' {
- if (session.status) {
- const s = session.status.toLowerCase();
- if (s === 'completed' || s === 'success' || s === 'failed' || s === 'cancelled') {
- return (s === 'success' ? 'completed' : s) as any;
- }
- if (s === 'running' || s === 'paused' || s === 'pending') {
- return s as any;
- }
- }
- if (session.session_id === this.agentService.runningSessionId() && (this.agentService.agentStatus() === 'running' || this.agentService.agentStatus() === 'paused')) {
- return this.agentService.agentStatus() as 'running' | 'paused';
- }
- return 'completed';
- }
-
- /**
- * Determine the device serial number for the session
- */
- public getDeviceSerial(session: Session): string | null {
- if (this.deviceSerialCache.has(session)) {
- return this.deviceSerialCache.get(session) ?? null;
- }
- let resolved: string | null = null;
- const serial = session.device_serial || session.device_id;
- if (serial && serial !== 'pending' && serial !== 'null' && serial !== 'undefined') {
- resolved = serial;
- } else if (session.device_info) {
- try {
- const info = typeof session.device_info === 'string' ? JSON.parse(session.device_info) : session.device_info;
- const s = info?.device_id || info?.device_serial;
- if (s && s !== 'pending' && s !== 'null' && s !== 'undefined') {
- resolved = s;
- }
- } catch {
- // ignore
- }
- }
- this.deviceSerialCache.set(session, resolved);
- return resolved;
- }
-
- /**
- * Select a session in the UI to monitor its steps
- */
- public selectTask(sessionId: string): void {
- this.agentService.selectSession(sessionId, true);
- }
-
- // Notes Computed Properties
- public currentNoteContent = computed(() => {
- const notes = this.agentService.currentNotes();
- const key = this.agentService.selectedNoteKey();
- return notes[key] || '';
- });
-
- public noteKeys = computed(() => {
- return Object.keys(this.agentService.currentNotes()).filter(key => key.toLowerCase().endsWith('.md'));
- });
-
- public parsedNote = computed(() => {
- return this.getParsedNote(this.currentNoteContent());
- });
-
- public getParsedNote(content: string): ParsedNote {
- return parseNote(content);
- }
-
- public getParsedNoteLines(content: string): MarkdownLine[] {
- return parseNoteLines(content);
- }
-
- public selectNote(key: string): void {
- this.agentService.selectedNoteKey.set(key);
- }
-
- public trackSession(index: number, session: Session): string {
- return session.session_id;
- }
-
- public trackNoteKey(index: number, key: string): string {
- return key;
- }
-
- public trackMilestone(index: number, milestone: NoteMilestone): number {
- return milestone.index;
- }
-
- public trackMarkdownLine(index: number, line: MarkdownLine): number {
- return index;
- }
-}
diff --git a/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html b/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html
deleted file mode 100644
index 1c468f660..000000000
--- a/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html
+++ /dev/null
@@ -1,433 +0,0 @@
-
-
-
-@if (agentService.isVideoWindowOpen() && isTheaterMode()) {
-
-}
-
-@if (agentService.isVideoWindowOpen()) {
-
-
-
-
- @if (!agentService.isVideoMinimized()) {
-
-
- @if (agentService.recordingPlaybackStatus() === 'live') {
- @if (!liveStreamError()) {
-
-
-
-
-
- LIVE
-
-
- refresh
-
-
-
- }
- @if (liveStreamError()) {
-
-
- phonelink_off
-
-
Device Stream Connecting
-
Waiting for ADB device screen stream...
-
- refresh Reconnect
-
-
- }
- }
-
- @if (agentService.recordingPlaybackStatus() !== 'live') {
-
- @if (agentService.playerMode() === 'video' && (agentService.recordingPlaybackStatus() === 'processing' || agentService.isVideoLoading())) {
-
- progress_activity
- {{ agentService.recordingPlaybackMessage() || 'Preparing screen recording...' }}
-
- }
-
- @if (agentService.playerMode() === 'steps' && totalStepFrames() > 0) {
-
-
- @if (agentService.recordingPlaybackStatus() === 'failed' || videoLoadError()) {
-
- info
- Screen recording unavailable. Replaying step-by-step screenshots.
-
- }
-
-
-
- @if (isStepImageLoading()) {
-
- progress_activity
- Loading screenshot...
-
- }
-
- @if (stepImageError() && !isStepImageLoading()) {
-
- broken_image
- Screenshot preview unavailable for this step
-
- }
-
- @if (!stepImageError()) {
-
-
-
-
-
- }
-
- @if (currentStepFrame(); as frame) {
-
-
- Step {{ frame.stepNumber }}
-
- @if (frame.action) {
-
- {{ getStepActionIcon(frame.action) }}
-
- }
- {{ frame.actionText || frame.title }}
-
-
-
-
- @if (frame.postImageUrl) {
-
-
- Pre
-
-
- Post
-
-
- }
-
-
- {{ isCardPinned() ? 'push_pin' : 'keep' }}
-
-
-
- }
-
-
-
-
- }
-
- @if (agentService.playerMode() === 'video' && agentService.recordingPlaybackStatus() === 'ready' && !agentService.isVideoLoading() && agentService.activeVideoUrl(); as videoUrl) {
- @if (!videoLoadError()) {
-
-
-
- }
-
- @if (!videoLoadError()) {
-
- }
- }
-
- @if (!agentService.isVideoLoading() && (agentService.playerMode() === 'video' || totalStepFrames() === 0) && (agentService.recordingPlaybackStatus() === 'failed' || agentService.recordingPlaybackStatus() === 'unavailable' || videoLoadError()) && totalStepFrames() === 0) {
-
-
- videocam_off
-
-
- {{ agentService.recordingPlaybackStatus() === 'failed' || videoLoadError() ? 'Recording Playback Failed' : 'No Video Recording Available' }}
-
-
- {{ videoLoadError() ? 'The finalized recording could not be played.' : (agentService.recordingPlaybackMessage() || 'No screen recording was found for this task.') }}
-
- @if (agentService.recordingPlaybackStatus() === 'failed' || videoLoadError()) {
-
- refresh Retry
-
- }
-
- }
- }
-
- }
-
- }
diff --git a/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss b/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss
deleted file mode 100644
index ff7f82c87..000000000
--- a/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss
+++ /dev/null
@@ -1,987 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-.theater-backdrop {
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- background: rgba(15, 23, 42, 0.6);
- backdrop-filter: blur(4px);
- -webkit-backdrop-filter: blur(4px);
- z-index: 1040;
- animation: fadeIn 0.15s ease-out;
-}
-
-.floating-video-wrapper {
- position: fixed;
- width: 400px;
- max-width: calc(100vw - 32px);
- background: rgba(255, 255, 255, 0.84);
- backdrop-filter: blur(24px) saturate(180%);
- -webkit-backdrop-filter: blur(24px) saturate(180%);
- border: 1px solid rgba(255, 255, 255, 0.85);
- border-radius: 16px;
- box-shadow: 0 20px 50px -10px rgba(15, 23, 42, 0.22), 0 0 0 1px rgba(226, 232, 240, 0.6), inset 0 1px 1px rgba(255, 255, 255, 0.9);
- z-index: 1050;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
- animation: popIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
- user-select: none;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', system-ui, -apple-system, sans-serif;
-
- &.minimized {
- width: 280px;
- box-shadow: 0 10px 25px -5px rgba(15, 23, 42, 0.18);
-
- .window-header {
- border-bottom: none;
- }
- }
-
- &.theater {
- left: 50% !important;
- top: 50% !important;
- transform: translate(-50%, -50%);
- width: 85vw;
- max-width: 960px;
- height: auto;
- max-height: 88vh;
- box-shadow: 0 24px 60px -12px rgba(15, 23, 42, 0.35);
-
- .video-body {
- max-height: calc(88vh - 46px);
-
- .video-container {
- video {
- max-height: calc(88vh - 120px);
- }
- }
- }
- }
-
- /* --- Window Header (Draggable Handle) --- */
- .window-header {
- height: 44px;
- background: rgba(255, 255, 255, 0.75);
- backdrop-filter: blur(16px);
- border-bottom: 1px solid rgba(226, 232, 240, 0.75);
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 8px 0 12px;
- cursor: grab;
- gap: 8px;
- flex-shrink: 0;
-
- &:active {
- cursor: grabbing;
- }
-
- .header-left {
- display: flex;
- align-items: center;
- gap: 8px;
- overflow: hidden;
- flex: 1;
-
- .header-icon {
- font-size: 18px;
- color: #2563eb;
- flex-shrink: 0;
- }
-
- .header-title {
- font-size: 13px;
- font-weight: 600;
- color: #0f172a;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- letter-spacing: -0.2px;
- }
-
- .mode-switcher-group {
- display: flex;
- align-items: center;
- background: rgba(241, 245, 249, 0.65);
- backdrop-filter: blur(12px);
- border: 1px solid rgba(226, 232, 240, 0.8);
- border-radius: 8px;
- padding: 2px;
- margin-left: 4px;
- flex-shrink: 0;
-
- .mode-btn {
- border: none;
- background: transparent;
- color: #475569;
- font-size: 10px;
- font-weight: 700;
- padding: 2px 7px;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.15s ease;
- display: flex;
- align-items: center;
- gap: 4px;
-
- .pulse-dot-mini {
- width: 5px;
- height: 5px;
- border-radius: 50%;
- background: #ef4444;
- animation: pulseRed 1.2s infinite;
- }
-
- &:hover:not(:disabled) {
- color: #0f172a;
- background: rgba(255, 255, 255, 0.6);
- }
-
- &.active {
- background: #0284c7;
- color: #ffffff;
- font-weight: 700;
- box-shadow: 0 1px 3px rgba(2, 132, 199, 0.3);
- }
-
- &:disabled {
- opacity: 0.4;
- cursor: not-allowed;
- }
- }
- }
- }
-
- .header-actions {
- display: flex;
- align-items: center;
- gap: 4px;
- flex-shrink: 0;
-
- .mode-toggle-group {
- display: flex;
- align-items: center;
- background: rgba(241, 245, 249, 0.7);
- backdrop-filter: blur(12px);
- border: 1px solid rgba(226, 232, 240, 0.85);
- border-radius: 8px;
- padding: 2px;
- margin-right: 4px;
- gap: 2px;
-
- .mode-btn {
- display: inline-flex;
- align-items: center;
- gap: 3px;
- border: none;
- background: transparent;
- color: #475569;
- font-size: 11px;
- font-weight: 600;
- padding: 2px 7px;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.15s ease;
-
- .mode-icon {
- font-size: 13px;
- }
-
- &:hover:not(.active) {
- color: #0f172a;
- background: rgba(255, 255, 255, 0.8);
- }
-
- &.active {
- background: #0284c7;
- color: #ffffff;
- box-shadow: 0 1px 3px rgba(2, 132, 199, 0.3);
- }
- }
- }
-
- .speed-pill-group {
- display: flex;
- align-items: center;
- background: rgba(241, 245, 249, 0.65);
- backdrop-filter: blur(12px);
- border: 1px solid rgba(226, 232, 240, 0.8);
- border-radius: 8px;
- padding: 2px;
- margin-right: 4px;
-
- .speed-btn {
- border: none;
- background: transparent;
- color: #334155;
- font-size: 11px;
- font-weight: 700;
- padding: 2px 6px;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.15s ease;
-
- &:hover {
- color: #0f172a;
- background: #cbd5e1;
- }
-
- &.active {
- background: #0284c7;
- color: #ffffff;
- font-weight: 700;
- box-shadow: 0 1px 3px rgba(2, 132, 199, 0.3);
- }
- }
- }
-
- .header-btn {
- background: rgba(241, 245, 249, 0.75);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- border: 1px solid rgba(226, 232, 240, 0.85);
- color: #1e293b;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- border-radius: 7px;
- box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.8);
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-
- &:hover {
- background: rgba(255, 255, 255, 0.95);
- color: #0f172a;
- border-color: rgba(203, 213, 225, 0.95);
- transform: translateY(-0.5px);
- box-shadow: 0 2px 6px rgba(15, 23, 42, 0.08);
- }
-
- &.close-btn:hover {
- background: rgba(254, 226, 226, 0.95);
- color: #dc2626;
- border-color: rgba(252, 165, 165, 0.9);
- box-shadow: 0 2px 6px rgba(220, 38, 38, 0.15);
- }
-
- &:active {
- transform: scale(0.96);
- }
-
- .material-symbols-outlined {
- font-size: 17px;
- font-weight: 600;
- }
- }
- }
- }
-
- /* --- Video Body Area --- */
- .video-body {
- position: relative;
- background: #ffffff;
- display: flex;
- flex-direction: column;
- align-items: stretch;
- justify-content: flex-start;
- overflow: hidden;
-
- .video-loading-state {
- padding: 36px 20px;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 10px;
- background: #ffffff;
- width: 100%;
- box-sizing: border-box;
- color: #475569;
- font-size: 13px;
- font-weight: 500;
-
- .spinning-icon {
- font-size: 28px;
- color: #2563eb;
- animation: spin 1s linear infinite;
- }
- }
-
- .live-stream-container {
- position: relative;
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background: #ffffff;
- min-height: 260px;
- overflow: hidden;
-
- .live-screen-img {
- width: 100%;
- max-height: 520px;
- display: block;
- object-fit: contain;
- user-select: none;
- background: #ffffff;
- }
-
- .live-status-overlay {
- position: absolute;
- top: 10px;
- left: 12px;
- right: 12px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- pointer-events: none;
-
- .live-badge {
- display: flex;
- align-items: center;
- gap: 6px;
- background: #fef2f2;
- border: 1px solid #fca5a5;
- color: #dc2626;
- font-size: 10.5px;
- font-weight: 700;
- padding: 3px 9px;
- border-radius: 12px;
- letter-spacing: 0.5px;
- box-shadow: 0 1px 4px rgba(220, 38, 38, 0.15);
-
- .pulse-dot {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: #ef4444;
- animation: pulseRed 1.2s infinite;
- }
- }
-
- .live-refresh-btn {
- pointer-events: auto;
- background: rgba(255, 255, 255, 0.95);
- backdrop-filter: blur(6px);
- border: 1px solid #cbd5e1;
- color: #1e293b;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- border-radius: 50%;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
- transition: all 0.15s ease;
-
- &:hover {
- background: #f1f5f9;
- color: #0f172a;
- border-color: #94a3b8;
- transform: rotate(90deg);
- }
-
- .material-symbols-outlined {
- font-size: 16px;
- }
- }
- }
- }
-
- .step-replay-container {
- display: flex;
- flex-direction: column;
- background: #ffffff;
- position: relative;
- width: 100%;
- overflow: hidden;
-
- .step-replay-notice {
- display: flex;
- align-items: center;
- gap: 6px;
- background: #fffbeb;
- color: #b45309;
- padding: 6px 12px;
- font-size: 11.5px;
- font-weight: 500;
- border-bottom: 1px solid #fde68a;
-
- .notice-icon {
- font-size: 14px;
- color: #d97706;
- flex-shrink: 0;
- }
- }
-
- .step-image-viewport {
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
- background: #f8fafc;
- min-height: 280px;
- max-height: 520px;
- overflow: hidden;
- padding: 10px 0;
-
- .step-image-loading-state,
- .step-image-error-state {
- position: absolute;
- inset: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 8px;
- background: #f8fafc;
- color: #64748b;
- font-size: 12px;
- font-weight: 500;
- z-index: 5;
-
- .spinning-icon {
- font-size: 24px;
- color: #0284c7;
- animation: spin 1s linear infinite;
- }
-
- .broken-icon {
- font-size: 28px;
- color: #94a3b8;
- }
- }
-
- .step-image-frame-wrap {
- position: relative;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- max-width: 92%;
- max-height: 500px;
-
- .step-frame-img {
- max-width: 100%;
- max-height: 500px;
- width: auto;
- height: auto;
- object-fit: contain;
- display: block;
- user-select: none;
- cursor: pointer;
- border-radius: 8px;
- box-shadow: 0 4px 20px -2px rgba(15, 23, 42, 0.14), 0 0 0 1px rgba(226, 232, 240, 0.9);
- background: #ffffff;
- transition: opacity 0.15s ease;
-
- &:hover {
- opacity: 0.98;
- }
- }
-
- .overlay-layer {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- pointer-events: none;
- z-index: 8;
- }
- }
-
- &:hover .step-frame-badge {
- opacity: 1;
- transform: translateY(0);
- pointer-events: auto;
- }
-
- .step-frame-badge {
- position: absolute;
- bottom: 12px;
- left: 10px;
- right: 10px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- background: rgba(15, 23, 42, 0.88);
- backdrop-filter: blur(14px) saturate(180%);
- -webkit-backdrop-filter: blur(14px) saturate(180%);
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 10px;
- padding: 6px 10px;
- color: #ffffff;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
- opacity: 0;
- transform: translateY(8px);
- pointer-events: none;
- transition: opacity 0.22s cubic-bezier(0.16, 1, 0.3, 1), transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
- z-index: 15;
-
- &.visible {
- opacity: 1;
- transform: translateY(0);
- pointer-events: auto;
- }
-
- .badge-content-left {
- display: flex;
- align-items: center;
- gap: 7px;
- overflow: hidden;
- flex: 1;
-
- .badge-step-no {
- font-size: 10.5px;
- font-weight: 700;
- background: #0284c7;
- padding: 2px 7px;
- border-radius: 5px;
- letter-spacing: 0.3px;
- flex-shrink: 0;
- box-shadow: 0 1px 3px rgba(2, 132, 199, 0.4);
- }
-
- .badge-action-desc {
- font-size: 11.5px;
- font-weight: 500;
- color: #f8fafc;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: flex;
- align-items: center;
- gap: 4px;
-
- .action-type-icon {
- font-size: 15px;
- color: #38bdf8;
- flex-shrink: 0;
- }
- }
- }
-
- .badge-actions-right {
- display: flex;
- align-items: center;
- gap: 6px;
- flex-shrink: 0;
-
- .pre-post-toggle {
- display: inline-flex;
- align-items: center;
- background: rgba(255, 255, 255, 0.12);
- border: 1px solid rgba(255, 255, 255, 0.15);
- border-radius: 6px;
- padding: 2px;
- gap: 2px;
-
- .toggle-btn {
- border: none;
- background: transparent;
- color: rgba(255, 255, 255, 0.7);
- font-size: 10.5px;
- font-weight: 700;
- padding: 2px 7px;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.15s ease;
-
- &:hover {
- color: #ffffff;
- background: rgba(255, 255, 255, 0.2);
- }
-
- &.active {
- background: #0284c7;
- color: #ffffff;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
- }
- }
- }
-
- .pin-card-btn {
- background: transparent;
- border: none;
- color: rgba(255, 255, 255, 0.6);
- cursor: pointer;
- padding: 3px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- transition: all 0.15s ease;
-
- &:hover {
- color: #ffffff;
- background: rgba(255, 255, 255, 0.15);
- }
-
- &.pinned {
- color: #38bdf8;
- background: rgba(56, 189, 248, 0.2);
- }
-
- .material-symbols-outlined {
- font-size: 14px;
- }
- }
- }
- }
- }
- }
-
- .video-container {
- position: relative;
- width: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background: #ffffff;
-
- video {
- width: 100%;
- height: auto;
- max-height: 520px;
- display: block;
- object-fit: contain;
- cursor: pointer;
- background: #ffffff;
- }
- }
-
- .video-controls-footer {
- background: rgba(255, 255, 255, 0.84);
- backdrop-filter: blur(20px) saturate(180%);
- -webkit-backdrop-filter: blur(20px) saturate(180%);
- border-top: 1px solid rgba(226, 232, 240, 0.75);
- padding: 12px 14px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- flex-shrink: 0;
- box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.9);
-
- .scrubber-bar-container {
- position: relative;
- width: 100%;
- height: 8px;
- display: flex;
- align-items: center;
- cursor: pointer;
-
- .scrubber-range {
- position: absolute;
- width: 100%;
- height: 100%;
- opacity: 0;
- cursor: pointer;
- z-index: 5;
- margin: 0;
- }
-
- &::before {
- content: '';
- position: absolute;
- left: 0;
- right: 0;
- height: 5px;
- background: rgba(226, 232, 240, 0.85);
- border-radius: 3px;
- }
-
- .scrubber-progress-fill {
- position: absolute;
- left: 0;
- height: 5px;
- background: linear-gradient(90deg, #0284c7, #2563eb);
- border-radius: 3px;
- pointer-events: none;
- box-shadow: 0 0 8px rgba(2, 132, 199, 0.4);
- }
-
- .step-ticks-wrapper {
- position: absolute;
- left: 0;
- right: 0;
- height: 100%;
- pointer-events: none;
- display: flex;
- align-items: center;
- z-index: 6;
-
- .step-tick {
- position: absolute;
- width: 7px;
- height: 7px;
- border-radius: 50%;
- background: rgba(255, 255, 255, 0.9);
- border: 1.5px solid #94a3b8;
- transform: translate(-50%, 0);
- pointer-events: auto;
- cursor: pointer;
- transition: all 0.15s ease;
-
- &:hover {
- transform: translate(-50%, 0) scale(1.4);
- border-color: #0284c7;
- background: #ffffff;
- }
-
- &.passed {
- background: #0284c7;
- border-color: #ffffff;
- }
-
- &.active {
- background: #38bdf8;
- border-color: #ffffff;
- transform: translate(-50%, 0) scale(1.3);
- box-shadow: 0 0 6px rgba(56, 189, 248, 0.8);
- }
- }
- }
- }
-
- .controls-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- .controls-left, .controls-right {
- display: flex;
- align-items: center;
- gap: 8px;
- }
-
- .ctrl-btn {
- background: rgba(241, 245, 249, 0.75);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- border: 1px solid rgba(226, 232, 240, 0.85);
- box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.85);
- color: #0f172a;
- width: 32px;
- height: 32px;
- border-radius: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-
- &:hover {
- background: rgba(255, 255, 255, 0.95);
- border-color: rgba(191, 219, 254, 0.9);
- color: #0284c7;
- transform: translateY(-0.5px);
- box-shadow: 0 3px 8px rgba(15, 23, 42, 0.08);
- }
-
- &:active {
- transform: scale(0.96);
- }
-
- &.active {
- color: #0284c7;
- background: rgba(224, 242, 254, 0.85);
- border-color: rgba(56, 189, 248, 0.8);
- box-shadow: 0 2px 6px rgba(2, 132, 199, 0.15);
- }
-
- &.play-btn {
- background: linear-gradient(135deg, rgba(2, 132, 199, 0.92), rgba(37, 99, 235, 0.88));
- backdrop-filter: blur(14px) saturate(180%);
- -webkit-backdrop-filter: blur(14px) saturate(180%);
- border: 1px solid rgba(255, 255, 255, 0.35);
- color: #ffffff;
- width: 36px;
- height: 36px;
- border-radius: 50%;
- box-shadow: 0 4px 14px rgba(2, 132, 199, 0.35), inset 0 1px 1px rgba(255, 255, 255, 0.5);
-
- &:hover {
- background: linear-gradient(135deg, rgba(2, 132, 199, 1), rgba(37, 99, 235, 0.95));
- box-shadow: 0 6px 18px rgba(2, 132, 199, 0.45), inset 0 1px 1px rgba(255, 255, 255, 0.6);
- transform: translateY(-1px) scale(1.04);
- }
-
- &:active {
- transform: scale(0.96);
- }
-
- .material-symbols-outlined {
- font-size: 22px;
- color: #ffffff;
- }
- }
-
- &.speed-pill-btn {
- width: auto;
- min-width: 34px;
- padding: 0 7px;
- font-size: 11px;
- font-weight: 700;
- font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
- color: #334155;
- letter-spacing: -0.2px;
-
- &:hover {
- color: #0284c7;
- border-color: rgba(191, 219, 254, 0.9);
- }
- }
-
- .material-symbols-outlined {
- font-size: 18px;
- color: #1e293b;
- }
- }
-
- .time-display {
- font-size: 12px;
- font-weight: 700;
- font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
- color: #1e293b;
- margin-left: 4px;
- letter-spacing: 0.5px;
- }
- }
- }
-
- /* --- Clean, Minimal High-Contrast Empty State --- */
- .empty-video-fallback {
- background: #ffffff;
- padding: 36px 20px;
- display: flex;
- flex-direction: column;
- align-items: center;
- text-align: center;
- box-sizing: border-box;
- width: 100%;
-
- .fallback-icon-wrap {
- width: 48px;
- height: 48px;
- border-radius: 50%;
- background: #fef2f2;
- border: 1px solid #fee2e2;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: 12px;
-
- .fallback-icon {
- font-size: 24px;
- color: #ef4444;
- }
- }
-
- .fallback-title {
- margin: 0 0 6px 0;
- font-size: 13.5px;
- font-weight: 600;
- color: #0f172a;
- }
-
- .fallback-desc {
- margin: 0 0 12px 0;
- font-size: 12px;
- color: #64748b;
- line-height: 1.4;
- max-width: 240px;
- }
-
- .retry-btn {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- background: #0284c7;
- color: #ffffff;
- border: none;
- border-radius: 6px;
- padding: 5px 12px;
- font-size: 11.5px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.15s ease;
-
- &:hover {
- background: #0369a1;
- transform: translateY(-1px);
- }
-
- .material-symbols-outlined {
- font-size: 15px;
- }
- }
- }
- }
-}
-
-@keyframes popIn {
- from {
- opacity: 0;
- transform: scale(0.96) translateY(6px);
- }
- to {
- opacity: 1;
- transform: scale(1) translateY(0);
- }
-}
-
-@keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
-}
-
-@keyframes spin {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
-}
-
-@keyframes pulseRed {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.5; }
-}
-
-::ng-deep {
- .action-point-marker {
- z-index: 10;
- pointer-events: none;
- }
- @keyframes pulse-animation {
- 0% {
- transform: scale(0.6);
- opacity: 1;
- }
- 100% {
- transform: scale(2.2);
- opacity: 0;
- }
- }
-}
diff --git a/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts b/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts
deleted file mode 100644
index 6b140cec3..000000000
--- a/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts
+++ /dev/null
@@ -1,631 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {
- Component,
- ChangeDetectionStrategy,
- NgZone,
- inject,
- signal,
- computed,
- ElementRef,
- ViewChild,
- HostListener,
- effect,
- OnDestroy
-} from '@angular/core';
-
-import { FormsModule } from '@angular/forms';
-import { AgentService } from '../../services/agent.service';
-import { StepReplayFrame } from '../../core/models/stream.model';
-import { drawActionCoordinatesOnOverlay } from '../../utils/image-overlay.util';
-import { getActionIcon } from '../../utils/action-formatter.util';
-import { locateTimelineTime, sessionTimeToTimelineTime } from '../../utils/recording-timeline.util';
-
-@Component({
- selector: 'app-floating-video-player',
- standalone: true,
- imports: [FormsModule],
- templateUrl: './floating-video-player.component.html',
- styleUrl: './floating-video-player.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush
-})
-export class FloatingVideoPlayerComponent implements OnDestroy {
- public agentService = inject(AgentService);
- private zone = inject(NgZone);
-
- @ViewChild('videoRef') videoRef?: ElementRef;
- @ViewChild('stepImgRef') stepImgRef?: ElementRef;
- @ViewChild('stepOverlayRef') stepOverlayRef?: ElementRef;
-
- // Playback state signals
- public isPlaying = signal(false);
- public playbackRate = signal(1.0);
- public isLooping = signal(false);
- public currentTime = signal(0);
- public duration = signal(0);
- public isMuted = signal(false);
- public isTheaterMode = signal(false);
- public videoLoadError = signal(false);
- public liveStreamUrl = signal('/api/stream/device-live');
- public liveStreamError = signal(false);
- public activeSegmentIndex = signal(0);
- public currentVideoUrl = computed(() => {
- const segments = this.agentService.activeVideoSegments();
- return segments[this.activeSegmentIndex()]?.url || this.agentService.activeVideoUrl();
- });
- private pendingLocalTime: number | null = null;
- private pendingAutoplay = false;
- private pendingAbsoluteSeek: number | null = null;
-
- // Step Replay state signals
- public activeStepIndex = signal(0);
- public isStepPlaying = signal(false);
- public viewMode = signal<'pre' | 'post'>('pre'); // 'pre' = action preview with coords, 'post' = action result
- public isCardHovered = signal(false);
- public isCardPinned = signal(false);
- public isCardVisible = computed(() => this.isCardHovered() || this.isCardPinned());
- public isStepImageLoading = signal(false);
- public stepImageError = signal(false);
- private stepTimer: any = null;
-
- public stepFrames = computed(() => this.agentService.currentSessionStepFrames());
- public totalStepFrames = computed(() => this.stepFrames().length);
- public currentStepFrame = computed(() => {
- const frames = this.stepFrames();
- const idx = this.activeStepIndex();
- return frames[idx] || null;
- });
- public currentStepImageUrl = computed(() => {
- const frame = this.currentStepFrame();
- if (!frame) return '';
- if (this.viewMode() === 'post' && frame.postImageUrl) {
- return frame.postImageUrl;
- }
- return frame.preImageUrl || frame.imageUrl || '';
- });
- public stepProgressPercent = computed(() => {
- const total = this.totalStepFrames();
- if (total <= 1) return 0;
- return (this.activeStepIndex() / (total - 1)) * 100;
- });
-
- // Available speed options
- public speedOptions = [0.5, 1.0, 1.5, 2.0, 4.0];
-
- // Dragging and window positioning state
- public posX = signal(
- typeof window !== 'undefined' ? Math.max(20, window.innerWidth - 420) : 100
- );
- public posY = signal(
- typeof window !== 'undefined' ? Math.max(60, window.innerHeight - 700) : 100
- );
-
- private isDragging = false;
- private dragStartX = 0;
- private dragStartY = 0;
- private initialPosX = 0;
- private initialPosY = 0;
-
- constructor() {
- // Reset video load error whenever active video URL changes
- effect(
- () => {
- const url = this.agentService.activeVideoUrl();
- const segments = this.agentService.activeVideoSegments();
- this.videoLoadError.set(false);
- this.activeSegmentIndex.set(0);
- this.currentTime.set(0);
- this.duration.set(segments.reduce((sum, segment) => sum + segment.duration, 0));
- this.isPlaying.set(false);
- },
- { allowSignalWrites: true }
- );
-
- // Reset step playback state only when session actually changes
- let lastTrackedSessionId: string | null = null;
- effect(
- () => {
- const curSessionId = this.agentService.currentSessionId();
- const total = this.totalStepFrames();
- if (curSessionId !== lastTrackedSessionId) {
- lastTrackedSessionId = curSessionId;
- this.activeStepIndex.set(0);
- this.viewMode.set('pre');
- this.pauseStepPlay();
- } else if (total > 0 && this.activeStepIndex() >= total) {
- this.activeStepIndex.set(total - 1);
- }
- },
- { allowSignalWrites: true }
- );
-
- // Track step screenshot URL changes and reset loading/error state
- effect(() => {
- const url = this.currentStepImageUrl();
- if (url) {
- this.isStepImageLoading.set(true);
- this.stepImageError.set(false);
- } else {
- this.isStepImageLoading.set(false);
- }
- }, { allowSignalWrites: true });
-
- // Re-draw overlay whenever activeStepIndex or viewMode changes
- effect(() => {
- this.activeStepIndex();
- this.viewMode();
- setTimeout(() => this.drawStepOverlay(), 40);
- });
-
- effect(() => {
- const request = this.agentService.videoSeekRequest();
- if (!request) return;
- this.pendingAbsoluteSeek = request.seconds;
- if (this.agentService.recordingPlaybackStatus() !== 'ready') return;
- const video = this.videoRef?.nativeElement;
- if (video && video.readyState >= 1) {
- const target = this.pendingAbsoluteSeek;
- this.pendingAbsoluteSeek = null;
- this.seekToSessionTime(target, true);
- }
- });
-
- effect(() => {
- const request = this.agentService.stepSeekRequest();
- if (!request) return;
- this.seekStepFrame(request.index);
- });
- }
-
- public onLiveStreamError(): void {
- this.liveStreamError.set(true);
- }
-
- public retryLiveStream(): void {
- this.liveStreamError.set(false);
- this.liveStreamUrl.set(`/api/stream/device-live?t=${Date.now()}`);
- }
-
- /**
- * Format seconds into mm:ss format
- */
- public formatTime(seconds: number): string {
- if (isNaN(seconds) || seconds < 0) return '00:00';
- const mins = Math.floor(seconds / 60);
- const secs = Math.floor(seconds % 60);
- const mStr = mins < 10 ? '0' + mins : String(mins);
- const sStr = secs < 10 ? '0' + secs : String(secs);
- return `${mStr}:${sStr}`;
- }
-
- /**
- * Progress percentage for timeline scrubber (0 - 100)
- */
- public progressPercent = computed(() => {
- const dur = this.duration();
- if (dur <= 0) return 0;
- return Math.min(100, Math.max(0, (this.currentTime() / dur) * 100));
- });
-
- /**
- * Start dragging the window from header. The move/up listeners are attached
- * only for the duration of the drag and live outside the Angular zone, so
- * ordinary mouse movement over the page never touches change detection.
- */
- public startDrag(event: MouseEvent): void {
- if (this.isTheaterMode()) return;
- this.isDragging = true;
- this.dragStartX = event.clientX;
- this.dragStartY = event.clientY;
- this.initialPosX = this.posX();
- this.initialPosY = this.posY();
- event.preventDefault();
-
- this.zone.runOutsideAngular(() => {
- document.addEventListener('mousemove', this.onDrag);
- document.addEventListener('mouseup', this.stopDrag);
- });
- }
-
- private onDrag = (event: MouseEvent): void => {
- if (!this.isDragging) return;
- const deltaX = event.clientX - this.dragStartX;
- const deltaY = event.clientY - this.dragStartY;
-
- const minX = 10;
- const minY = 10;
- const maxX = Math.max(10, window.innerWidth - 240);
- const maxY = Math.max(10, window.innerHeight - 80);
-
- const targetX = Math.max(minX, Math.min(maxX, this.initialPosX + deltaX));
- const targetY = Math.max(minY, Math.min(maxY, this.initialPosY + deltaY));
-
- this.posX.set(targetX);
- this.posY.set(targetY);
- };
-
- private stopDrag = (): void => {
- this.isDragging = false;
- document.removeEventListener('mousemove', this.onDrag);
- document.removeEventListener('mouseup', this.stopDrag);
- };
-
- @HostListener('window:resize')
- public onResize(): void {
- // Keep window within viewport bounds if window resized
- const maxX = Math.max(10, window.innerWidth - 240);
- const maxY = Math.max(10, window.innerHeight - 80);
- if (this.posX() > maxX) this.posX.set(maxX);
- if (this.posY() > maxY) this.posY.set(maxY);
- }
-
- @HostListener('window:keydown', ['$event'])
- public onKeyDown(event: KeyboardEvent): void {
- if (!this.agentService.isVideoWindowOpen()) return;
-
- if (event.key === 'Escape') {
- if (this.isTheaterMode()) {
- this.isTheaterMode.set(false);
- } else {
- this.agentService.closeVideoPlayer();
- }
- }
- }
-
- /**
- * Video element event handlers
- */
- public onLoadedMetadata(): void {
- const v = this.videoRef?.nativeElement;
- if (v) {
- const segments = this.agentService.activeVideoSegments();
- if (!segments.length) {
- this.duration.set(v.duration || 0);
- }
- this.videoLoadError.set(false);
- v.playbackRate = this.playbackRate();
- v.loop = this.isLooping() && !segments.length;
- v.muted = this.isMuted();
- if (this.pendingAbsoluteSeek !== null) {
- const target = this.pendingAbsoluteSeek;
- this.pendingAbsoluteSeek = null;
- this.seekToSessionTime(target, true);
- // A seek into another finalized segment changes the video source. Let
- // that segment's metadata event apply pendingLocalTime.
- if (this.pendingLocalTime !== null) return;
- }
- if (this.pendingLocalTime !== null) {
- v.currentTime = Math.max(0, Math.min(v.duration || this.pendingLocalTime, this.pendingLocalTime));
- this.pendingLocalTime = null;
- }
- if (this.pendingAutoplay) {
- this.pendingAutoplay = false;
- v.play().catch(() => {});
- }
- if (this.agentService.consumeVideoAutoplay()) {
- this.isMuted.set(true);
- v.muted = true;
- v.play().catch(() => {
- this.isPlaying.set(false);
- });
- }
- }
- }
-
- public onTimeUpdate(): void {
- const v = this.videoRef?.nativeElement;
- if (v) {
- const segments = this.agentService.activeVideoSegments();
- const segment = segments[this.activeSegmentIndex()];
- this.currentTime.set((segment?.start || 0) + v.currentTime);
- if (!segments.length && v.duration && v.duration !== this.duration()) {
- this.duration.set(v.duration);
- }
- }
- }
-
- public onPlay(): void {
- this.isPlaying.set(true);
- }
-
- public onPause(): void {
- this.isPlaying.set(false);
- }
-
- public onEnded(): void {
- const segments = this.agentService.activeVideoSegments();
- if (segments.length && this.activeSegmentIndex() < segments.length - 1) {
- this.pendingLocalTime = 0;
- this.pendingAutoplay = true;
- this.activeSegmentIndex.update(index => index + 1);
- return;
- }
- if (segments.length && this.isLooping()) {
- this.seek(0, true);
- return;
- }
- this.isPlaying.set(false);
- }
-
- public onError(): void {
- this.videoLoadError.set(true);
- this.isPlaying.set(false);
- }
-
- /**
- * Playback actions
- */
- public togglePlay(): void {
- const v = this.videoRef?.nativeElement;
- if (!v) return;
- if (v.paused) {
- v.play().catch(() => {});
- } else {
- v.pause();
- }
- }
-
- public restart(): void {
- this.seek(0, true);
- }
-
- /**
- * Seek to a session-relative time (seconds since the task session started,
- * the axis step timestamps and analyzer ranges use). Manifest v2 segments
- * carry real session offsets, so the time lands in the right segment even
- * across scrcpy restart gaps; legacy playlists fall back to the timeline axis.
- */
- public seekToSessionTime(sessionSeconds: number, autoplay = false): void {
- const segments = this.agentService.activeVideoSegments();
- this.seek(sessionTimeToTimelineTime(segments, sessionSeconds), autoplay);
- }
-
- /** Seek on the back-to-back playlist axis used by the scrubber. */
- public seek(seconds: number, autoplay = false): void {
- const v = this.videoRef?.nativeElement;
- if (!v) return;
- const target = Math.max(0, Math.min(this.duration(), seconds));
- const segments = this.agentService.activeVideoSegments();
- const location = locateTimelineTime(segments, target);
- if (!location) {
- v.currentTime = target;
- if (autoplay) v.play().catch(() => {});
- return;
- }
- const { index, localTime } = location;
- if (index === this.activeSegmentIndex()) {
- v.currentTime = localTime;
- if (autoplay) v.play().catch(() => {});
- } else {
- this.pendingLocalTime = localTime;
- this.pendingAutoplay = autoplay || !v.paused;
- this.activeSegmentIndex.set(index);
- }
- this.currentTime.set(target);
- }
-
- public onScrub(event: Event): void {
- const input = event.target as HTMLInputElement;
- const targetTime = parseFloat(input.value);
- this.seek(targetTime);
- }
-
- public setSpeed(rate: number): void {
- this.playbackRate.set(rate);
- const v = this.videoRef?.nativeElement;
- if (v) {
- v.playbackRate = rate;
- }
- if (this.isStepPlaying()) {
- this.scheduleNextStepFrame();
- }
- }
-
- public toggleLoop(): void {
- const next = !this.isLooping();
- this.isLooping.set(next);
- const v = this.videoRef?.nativeElement;
- if (v) {
- v.loop = next && !this.agentService.activeVideoSegments().length;
- }
- }
-
- public toggleMute(): void {
- const next = !this.isMuted();
- this.isMuted.set(next);
- const v = this.videoRef?.nativeElement;
- if (v) {
- v.muted = next;
- }
- }
-
- public toggleTheater(): void {
- this.isTheaterMode.set(!this.isTheaterMode());
- }
-
- public toggleMinimize(): void {
- this.agentService.isVideoMinimized.set(!this.agentService.isVideoMinimized());
- }
-
- public openInNewTab(): void {
- const url = this.agentService.activeVideoUrl();
- if (url) {
- window.open(url, '_blank');
- }
- }
-
- /**
- * Step Replay Controls
- */
- public toggleStepPlay(): void {
- if (this.isStepPlaying()) {
- this.pauseStepPlay();
- } else {
- this.startStepPlay();
- }
- }
-
- public startStepPlay(): void {
- if (this.totalStepFrames() <= 0) return;
- if (this.activeStepIndex() >= this.totalStepFrames() - 1) {
- this.activeStepIndex.set(0);
- }
- this.isStepPlaying.set(true);
- this.scheduleNextStepFrame();
- }
-
- public pauseStepPlay(): void {
- this.isStepPlaying.set(false);
- if (this.stepTimer) {
- clearTimeout(this.stepTimer);
- this.stepTimer = null;
- }
- }
-
- private scheduleNextStepFrame(): void {
- if (this.stepTimer) clearTimeout(this.stepTimer);
- const delay = Math.max(300, 1500 / this.playbackRate());
- this.stepTimer = setTimeout(() => {
- if (!this.isStepPlaying()) return;
- if (this.activeStepIndex() < this.totalStepFrames() - 1) {
- this.activeStepIndex.update((i) => i + 1);
- this.scheduleNextStepFrame();
- } else {
- if (this.isLooping()) {
- this.activeStepIndex.set(0);
- this.scheduleNextStepFrame();
- } else {
- this.pauseStepPlay();
- }
- }
- }, delay);
- }
-
- public prevStepFrame(): void {
- this.pauseStepPlay();
- this.viewMode.set('pre');
- this.activeStepIndex.update((i) => Math.max(0, i - 1));
- }
-
- public nextStepFrame(): void {
- this.pauseStepPlay();
- this.viewMode.set('pre');
- this.activeStepIndex.update((i) => Math.min(this.totalStepFrames() - 1, i + 1));
- }
-
- public seekStepFrame(index: number): void {
- this.viewMode.set('pre');
- this.activeStepIndex.set(Math.max(0, Math.min(this.totalStepFrames() - 1, index)));
- }
-
- public onStepScrub(event: Event): void {
- const input = event.target as HTMLInputElement;
- this.seekStepFrame(parseInt(input.value, 10));
- }
-
- public restartStep(): void {
- this.seekStepFrame(0);
- this.startStepPlay();
- }
-
- public setStepViewMode(mode: 'pre' | 'post', event?: Event): void {
- if (event) event.stopPropagation();
- this.viewMode.set(mode);
- setTimeout(() => this.drawStepOverlay(), 40);
- }
-
- public toggleStepViewMode(event?: Event): void {
- if (event) event.stopPropagation();
- this.setStepViewMode(this.viewMode() === 'pre' ? 'post' : 'pre');
- }
-
- public onViewportMouseEnter(): void {
- this.isCardHovered.set(true);
- }
-
- public onViewportMouseLeave(): void {
- this.isCardHovered.set(false);
- }
-
- public toggleCardPin(event?: Event): void {
- if (event) event.stopPropagation();
- this.isCardPinned.update((v) => !v);
- }
-
- public cycleSpeed(): void {
- const current = this.playbackRate();
- const idx = this.speedOptions.indexOf(current);
- const nextIdx = (idx + 1) % this.speedOptions.length;
- this.setSpeed(this.speedOptions[nextIdx]);
- }
-
- public onStepImageLoad(): void {
- this.isStepImageLoading.set(false);
- this.stepImageError.set(false);
- this.drawStepOverlay();
- }
-
- public onStepImageError(): void {
- this.isStepImageLoading.set(false);
- this.stepImageError.set(true);
- }
-
- public drawStepOverlay(): void {
- const img = this.stepImgRef?.nativeElement;
- const overlay = this.stepOverlayRef?.nativeElement;
- if (!img || !overlay) return;
- overlay.innerHTML = '';
-
- // Only draw coordinate overlays when viewing the pre-action screenshot
- if (this.viewMode() === 'post') {
- return;
- }
-
- const frame = this.currentStepFrame();
- if (!frame || !frame.action) return;
-
- try {
- drawActionCoordinatesOnOverlay(img, overlay, frame.action);
- } catch (err) {
- console.warn('Failed to draw step coordinates overlay:', err);
- }
- }
-
- public getStepActionIcon(action: any): string {
- return getActionIcon(action);
- }
-
- public openImageInNewTab(): void {
- const url = this.currentStepImageUrl() || this.currentStepFrame()?.imageUrl;
- if (url) {
- window.open(url, '_blank');
- }
- }
-
- public trackStepFrame(index: number, frame: StepReplayFrame): string {
- return frame.stepId || String(index);
- }
-
- public ngOnDestroy(): void {
- if (this.stepTimer) {
- clearTimeout(this.stepTimer);
- this.stepTimer = null;
- }
- document.removeEventListener('mousemove', this.onDrag);
- document.removeEventListener('mouseup', this.stopDrag);
- }
-}
diff --git a/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html b/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html
deleted file mode 100644
index 44ec41208..000000000
--- a/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html
+++ /dev/null
@@ -1,130 +0,0 @@
-
-
-
-
-
-
-
-
- @if (agentService.sessionLogs().length === 0) {
-
-
📡
-
No Stream Activity Selected
-
Please select a running session from the dropdown above to view real-time log payloads.
-
- }
-
- @if (agentService.sessionLogs().length > 0) {
-
- @for (log of agentService.sessionLogs(); track log; let i = $index) {
-
-
-
- @if (isStepEvent(log.type)) {
-
-
- @if (log.data.summary) {
-
-
💬 Message to User / Summary:
-
{{ log.data.summary }}
-
- }
-
- @if (log.data.operator_native_thinking) {
-
- 🧠 Model Thinking Process (Click to expand)
- {{ log.data.operator_native_thinking }}
-
- }
-
- @if (log.data.operator_raw_thinking) {
-
- 📝 Raw Plan / Thoughts (Click to expand)
- {{ log.data.operator_raw_thinking }}
-
- }
-
- @if (log.data.action_taken) {
-
- 🛠️ Tool Call Details (Click to expand)
-
-
{{ formatJson(log.data.action_taken) }}
-
-
- }
-
- @if (log.data.last_execution_result) {
-
- ✓ Execution Result (Click to expand)
-
-
{{ formatJson(log.data.last_execution_result) }}
-
-
- }
-
- }
-
- @if (!isStepEvent(log.type)) {
-
-
- 📄 Raw Payload Data (Click to expand)
-
-
{{ formatJson(log.data) }}
-
-
-
- }
-
- }
-
- }
-
-
diff --git a/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss b/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss
deleted file mode 100644
index e1d13c873..000000000
--- a/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss
+++ /dev/null
@@ -1,406 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-$border-color: rgba(0, 0, 0, 0.08);
-$text-primary: #1f2937;
-$text-secondary: #4b5563;
-$text-muted: #9ca3af;
-$bg-main: #f9fafb;
-
-:host {
- display: flex;
- flex-direction: column;
- height: 100%;
- width: 100%;
- overflow: hidden;
-}
-
-.agent-stream-container {
- display: flex;
- flex-direction: column;
- height: 100%;
- background: rgba(255, 255, 255, 0.4);
- backdrop-filter: blur(16px);
- box-sizing: border-box;
-
- .stream-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 20px 24px;
- border-bottom: 1px solid $border-color;
- background: rgba(255, 255, 255, 0.6);
- flex-wrap: wrap;
- gap: 16px;
-
- .header-title {
- h2 {
- margin: 0 0 4px 0;
- font-size: 20px;
- font-weight: 700;
- color: $text-primary;
- letter-spacing: -0.5px;
- }
- .subtitle {
- margin: 0;
- font-size: 13px;
- color: $text-secondary;
- }
- }
-
- .session-selector-wrapper {
- display: flex;
- align-items: center;
- gap: 12px;
-
- label {
- font-size: 13.5px;
- font-weight: 600;
- color: $text-primary;
- }
-
- .session-dropdown {
- padding: 8px 12px;
- border: 1px solid rgba(0, 0, 0, 0.12);
- border-radius: 8px;
- font-size: 13.5px;
- background-color: #ffffff;
- outline: none;
- color: $text-primary;
- min-width: 250px;
- max-width: 400px;
- cursor: pointer;
- transition: border-color 0.2s;
-
- &:focus {
- border-color: #3b82f6;
- }
- }
-
- .btn-refresh {
- padding: 8px 14px;
- background: #ffffff;
- border: 1px solid rgba(0, 0, 0, 0.12);
- border-radius: 8px;
- font-size: 13px;
- font-weight: 600;
- color: $text-primary;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 6px;
- transition: background 0.2s, transform 0.1s;
-
- &:hover {
- background: #f3f4f6;
- }
-
- &:active {
- transform: scale(0.98);
- }
- }
- }
- }
-
- .stream-logs-content {
- flex: 1;
- overflow-y: auto;
- padding: 24px;
- background: rgba(249, 250, 251, 0.3);
-
- &::-webkit-scrollbar {
- width: 6px;
- }
- &::-webkit-scrollbar-track {
- background: transparent;
- }
- &::-webkit-scrollbar-thumb {
- background: rgba(0, 0, 0, 0.15);
- border-radius: 10px;
- }
- &::-webkit-scrollbar-thumb:hover {
- background: rgba(0, 0, 0, 0.25);
- }
-
- .empty-state {
- height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- text-align: center;
- padding: 40px;
-
- .empty-icon {
- font-size: 48px;
- margin-bottom: 16px;
- opacity: 0.8;
- }
-
- h3 {
- margin: 0 0 8px 0;
- font-size: 18px;
- font-weight: 600;
- color: $text-primary;
- }
-
- p {
- margin: 0;
- font-size: 14px;
- color: $text-secondary;
- max-width: 320px;
- line-height: 1.5;
- }
- }
-
- .logs-list {
- display: flex;
- flex-direction: column;
- gap: 16px;
-
- .log-card {
- background: #ffffff;
- border: 1px solid rgba(0, 0, 0, 0.06);
- border-radius: 12px;
- padding: 16px 20px;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.01), 0 2px 4px -1px rgba(0, 0, 0, 0.006);
- display: flex;
- flex-direction: column;
- gap: 12px;
- transition: transform 0.2s, box-shadow 0.2s;
-
- &:hover {
- transform: translateY(-1px);
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.02), 0 4px 6px -2px rgba(0, 0, 0, 0.01);
- }
-
- .log-card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- .badge-group {
- display: flex;
- align-items: center;
- gap: 8px;
-
- .index-badge {
- font-size: 11px;
- font-weight: 700;
- color: $text-muted;
- font-family: monospace;
- }
-
- .type-badge {
- font-size: 10px;
- font-weight: 800;
- text-transform: uppercase;
- padding: 2.5px 7px;
- border-radius: 9999px;
- letter-spacing: 0.5px;
- font-family: monospace;
-
- &.llm_stream {
- background: #e0f2fe;
- color: #0369a1;
- }
- &.trace_recorded {
- background: #f3e8ff;
- color: #6b21a8;
- }
- &.step_recorded {
- background: #ecfdf5;
- color: #047857;
- }
- &.step_updated {
- background: #fef3c7;
- color: #b45309;
- }
- &.background_tasks_updated {
- background: #fff1f2;
- color: #be123c;
- }
- }
- }
-
- .timestamp {
- font-size: 11.5px;
- color: $text-muted;
- font-family: monospace;
- }
- }
-
- .log-card-body {
- &.step-body {
- display: flex;
- flex-direction: column;
- gap: 16px;
-
- .step-summary-section {
- .section-title {
- font-size: 11px;
- font-weight: 700;
- color: $text-secondary;
- margin-bottom: 6px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
-
- .summary-bubble {
- background: #f1f5f9;
- border: 1px solid rgba(0, 0, 0, 0.05);
- border-left: 4px solid #475569;
- border-radius: 8px;
- padding: 12px 16px;
- font-size: 13.5px;
- line-height: 1.5;
- color: #1e293b;
- }
- }
-
- details {
- border: 1px solid rgba(0, 0, 0, 0.06);
- border-radius: 8px;
- background: #fafafa;
- overflow: hidden;
- transition: border-color 0.2s;
-
- &[open] {
- border-color: rgba(0, 0, 0, 0.12);
- .details-summary {
- border-bottom: 1px solid rgba(0, 0, 0, 0.06);
- background: rgba(0, 0, 0, 0.02);
- }
- }
-
- .details-summary {
- padding: 10px 14px;
- font-size: 12.5px;
- font-weight: 700;
- color: $text-secondary;
- cursor: pointer;
- user-select: none;
- outline: none;
- background: #ffffff;
- transition: background 0.2s;
-
- &:hover {
- background: rgba(0, 0, 0, 0.01);
- }
- }
-
- .details-content {
- padding: 14px;
- font-size: 13px;
- line-height: 1.6;
- color: #334155;
-
- &.thinking-text {
- background: #f0fdf4;
- color: #14532d;
- white-space: pre-wrap;
- font-family: inherit;
- }
-
- &.planning-text {
- background: #f8fafc;
- color: #334155;
- white-space: pre-wrap;
- font-family: inherit;
- }
- }
-
- &.thinking-details {
- border-left: 4px solid #16a34a;
- .details-summary {
- color: #16a34a;
- }
- }
-
- &.planning-details {
- border-left: 4px solid #4f46e5;
- .details-summary {
- color: #4f46e5;
- }
- }
-
- &.tool-details {
- border-left: 4px solid #2563eb;
- .details-summary {
- color: #2563eb;
- }
- }
-
- &.result-details {
- border-left: 4px solid #0891b2;
- .details-summary {
- color: #0891b2;
- }
- }
- }
- }
-
- &.raw-body {
- details {
- border: 1px solid rgba(0, 0, 0, 0.06);
- border-radius: 8px;
- overflow: hidden;
-
- .details-summary {
- padding: 10px 14px;
- font-size: 12px;
- font-weight: 600;
- color: $text-secondary;
- cursor: pointer;
- outline: none;
- background: #ffffff;
- }
-
- .details-content {
- padding: 12px;
- }
- }
- }
-
- .json-code {
- margin: 0;
- background: #1e293b;
- color: #f8fafc;
- padding: 14px 18px;
- border-radius: 8px;
- font-family: 'Fira Code', 'Roboto Mono', monospace;
- font-size: 12.5px;
- line-height: 1.5;
- overflow-x: auto;
- white-space: pre-wrap;
- word-break: break-all;
-
- &::-webkit-scrollbar {
- height: 4px;
- }
- &::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.2);
- border-radius: 4px;
- }
- &::-webkit-scrollbar-thumb:hover {
- background: rgba(255, 255, 255, 0.3);
- }
- }
- }
- }
- }
- }
-}
diff --git a/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts b/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts
deleted file mode 100644
index 025eadc83..000000000
--- a/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
-import { AgentService } from '../../services/agent.service';
-
-@Component({
- selector: 'app-legacy-agent-stream',
- standalone: true,
- imports: [CommonModule, FormsModule],
- templateUrl: './legacy-agent-stream.component.html',
- changeDetection: ChangeDetectionStrategy.Eager,
- styleUrl: './legacy-agent-stream.component.scss'
-})
-export class LegacyAgentStreamComponent {
- public agentService = inject(AgentService);
-
- /**
- * Handle dropdown session selection change
- */
- public onSessionChange(event: Event): void {
- const selectElement = event.target as HTMLSelectElement;
- const sessionId = selectElement.value;
- if (sessionId) {
- this.agentService.selectSession(sessionId, true);
- }
- }
-
- /**
- * Check if the event type represents a structured step record
- */
- public isStepEvent(eventType: string): boolean {
- return eventType === 'step_recorded' || eventType === 'step_updated';
- }
-
- /**
- * Determine if the content is short enough to remain open by default
- */
- public isShortContent(data: any): boolean {
- if (!data) return true;
- const str = typeof data === 'string' ? data : JSON.stringify(data);
- return str.length < 200;
- }
-
- /**
- * Format JSON data for display in HTML pre tag
- */
- public formatJson(data: any): string {
- if (typeof data === 'string') {
- try {
- return JSON.stringify(JSON.parse(data), null, 2);
- } catch (e) {
- return data;
- }
- }
- return JSON.stringify(data, null, 2);
- }
-}
diff --git a/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss b/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss
deleted file mode 100644
index 8a7541bf7..000000000
--- a/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss
+++ /dev/null
@@ -1,115 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-.floating-nav-switcher {
- position: fixed;
- top: 12px;
- left: 24px;
- z-index: 1100;
- height: 36px;
- box-sizing: border-box;
- display: inline-flex;
- align-items: center;
- background: rgba(255, 255, 255, 0.72);
- backdrop-filter: blur(16px) saturate(180%);
- -webkit-backdrop-filter: blur(16px) saturate(180%);
- border: 1px solid rgba(226, 232, 240, 0.85);
- border-radius: 18px;
- padding: 3px;
- gap: 3px;
- box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(0, 0, 0, 0.02), inset 0 1px 1px rgba(255, 255, 255, 0.9);
- user-select: none;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- transition: background 0.25s cubic-bezier(0.4, 0, 0.2, 1),
- box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1),
- border-color 0.25s cubic-bezier(0.4, 0, 0.2, 1);
-
- &:hover {
- background: rgba(255, 255, 255, 0.85);
- box-shadow: 0 4px 14px -2px rgba(15, 23, 42, 0.1), 0 2px 4px rgba(0, 0, 0, 0.03), inset 0 1px 1px rgba(255, 255, 255, 1);
- border-color: rgba(203, 213, 225, 0.9);
- }
-
- .nav-tab-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- height: 30px;
- padding: 0 14px;
- box-sizing: border-box;
- border-radius: 15px;
- font-size: 13px;
- font-weight: 600;
- color: #475569;
- text-decoration: none;
- cursor: pointer;
- line-height: 1;
- transition: background 0.2s cubic-bezier(0.4, 0, 0.2, 1),
- color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
- box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1),
- transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- outline: none;
- white-space: nowrap;
-
- .tab-icon {
- font-size: 16px;
- width: 16px;
- height: 16px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- color: #64748b;
- transition: color 0.15s ease, transform 0.15s ease;
- }
-
- .tab-label {
- color: #475569;
- transition: color 0.15s ease;
- }
-
- &:hover:not(.active) {
- background: rgba(255, 255, 255, 0.7);
- color: #0f172a;
-
- .tab-icon {
- color: #1a73e8;
- }
- .tab-label {
- color: #0f172a;
- }
- }
-
- &.active {
- background: rgba(26, 115, 232, 0.95) !important;
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- color: #ffffff !important;
- font-weight: 700;
- box-shadow: 0 2px 8px rgba(26, 115, 232, 0.35), inset 0 1px 1px rgba(255, 255, 255, 0.35);
-
- .tab-icon,
- .tab-label,
- span {
- color: #ffffff !important;
- }
- }
-
- &:active {
- transform: scale(0.97);
- }
- }
-}
diff --git a/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts b/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts
deleted file mode 100644
index 866f0ed12..000000000
--- a/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Component, ChangeDetectionStrategy } from '@angular/core';
-
-import { RouterLink, RouterLinkActive } from '@angular/router';
-
-@Component({
- selector: 'app-nav-switcher',
- standalone: true,
- imports: [RouterLink, RouterLinkActive],
- template: `
-
-
- add_task
- New / Home
-
-
- space_dashboard
- Workspace
-
-
- `,
- changeDetection: ChangeDetectionStrategy.Eager,
- styleUrls: ['./nav-switcher.component.scss']
-})
-export class NavSwitcherComponent {}
diff --git a/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts b/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts
deleted file mode 100644
index 015a1fbf8..000000000
--- a/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts
+++ /dev/null
@@ -1,334 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export interface AppReference {
- name: string;
- icon: string;
- pkg?: string;
- category?: string;
-}
-
-export type SuggestionCategory =
- | 'all'
- | 'flash'
- | 'pro'
- | 'cross_app'
- | 'monitor';
-
-export interface SmartSuggestion {
- id: string;
- title: string;
- description: string;
- goal: string;
- profile: 'flash' | 'pro';
- category: 'flash' | 'pro' | 'cross_app' | 'monitor';
- tag: string;
- apps: AppReference[];
- requiredPackages?: string[];
- matchMode?: 'any' | 'all';
- priority?: number;
-}
-
-/**
- * Recognized Android App Package Registry
- */
-export const APP_REGISTRY: Record = {
- // Google Suite & System
- 'com.google.android.apps.maps': { name: 'Maps', icon: 'explore', pkg: 'com.google.android.apps.maps', category: 'navigation' },
- 'com.google.android.gm': { name: 'Gmail', icon: 'mail', pkg: 'com.google.android.gm', category: 'productivity' },
- 'com.android.chrome': { name: 'Chrome', icon: 'public', pkg: 'com.android.chrome', category: 'browser' },
- 'com.google.android.youtube': { name: 'YouTube', icon: 'smart_display', pkg: 'com.google.android.youtube', category: 'entertainment' },
- 'com.android.settings': { name: 'Settings', icon: 'settings', pkg: 'com.android.settings', category: 'system' },
- 'com.google.android.deskclock': { name: 'Clock', icon: 'timer', pkg: 'com.google.android.deskclock', category: 'utility' },
- 'com.android.deskclock': { name: 'Clock', icon: 'timer', pkg: 'com.android.deskclock', category: 'utility' },
- 'com.google.android.calculator': { name: 'Calculator', icon: 'calculate', pkg: 'com.google.android.calculator', category: 'utility' },
- 'com.android.calculator2': { name: 'Calculator', icon: 'calculate', pkg: 'com.android.calculator2', category: 'utility' },
- 'com.google.android.apps.photos': { name: 'Photos', icon: 'photo_library', pkg: 'com.google.android.apps.photos', category: 'media' },
- 'com.google.android.calendar': { name: 'Calendar', icon: 'calendar_month', pkg: 'com.google.android.calendar', category: 'productivity' },
- 'com.google.android.keep': { name: 'Keep Notes', icon: 'note_alt', pkg: 'com.google.android.keep', category: 'productivity' },
- 'com.android.vending': { name: 'Play Store', icon: 'storefront', pkg: 'com.android.vending', category: 'tools' },
- 'com.google.android.apps.messaging': { name: 'Messages', icon: 'chat', pkg: 'com.google.android.apps.messaging', category: 'communication' },
-
- // Popular Ecosystem Apps
- 'com.tencent.mm': { name: 'WeChat', icon: 'forum', pkg: 'com.tencent.mm', category: 'social' },
- 'com.xingin.xhs': { name: 'Xiaohongshu', icon: 'auto_stories', pkg: 'com.xingin.xhs', category: 'social' },
- 'com.sankuai.meituan': { name: 'Meituan', icon: 'restaurant', pkg: 'com.sankuai.meituan', category: 'lifestyle' },
- 'com.dianping.v1': { name: 'Dianping', icon: 'star', pkg: 'com.dianping.v1', category: 'lifestyle' },
- 'tv.danmaku.bili': { name: 'Bilibili', icon: 'video_library', pkg: 'tv.danmaku.bili', category: 'entertainment' },
- 'com.eg.android.AlipayGphone': { name: 'Alipay', icon: 'account_balance_wallet', pkg: 'com.eg.android.AlipayGphone', category: 'finance' },
- 'com.netease.cloudmusic': { name: 'NetEase Music', icon: 'headphones', pkg: 'com.netease.cloudmusic', category: 'entertainment' },
- 'com.spotify.music': { name: 'Spotify', icon: 'music_note', pkg: 'com.spotify.music', category: 'entertainment' }
-};
-
-/**
- * Curated, clean task preset library (1~2 representative tasks per common app)
- */
-export const SMART_TASK_LIBRARY: SmartSuggestion[] = [
- // 1. Google Maps
- {
- id: 'maps_coffee',
- title: 'Find Specialty Coffee',
- description: 'Search nearby top-rated cafes in Google Maps',
- goal: 'Open Google Maps, search for top-rated specialty coffee shops nearby, and view the top result details.',
- profile: 'flash',
- category: 'flash',
- tag: 'Maps',
- apps: [{ name: 'Maps', icon: 'explore', pkg: 'com.google.android.apps.maps' }],
- requiredPackages: ['com.google.android.apps.maps'],
- priority: 95
- },
- {
- id: 'pro_commute_share',
- title: 'Commute ETA & Message Draft',
- description: 'Check transit time on Maps and draft arrival ETA in Messages',
- goal: 'Open Google Maps to check commute time to the International Airport, calculate arrival time, then open Messages and draft an ETA text message.',
- profile: 'pro',
- category: 'cross_app',
- tag: 'Maps + Messages',
- apps: [
- { name: 'Maps', icon: 'explore', pkg: 'com.google.android.apps.maps' },
- { name: 'Messages', icon: 'chat', pkg: 'com.google.android.apps.messaging' }
- ],
- requiredPackages: ['com.google.android.apps.maps', 'com.google.android.apps.messaging'],
- matchMode: 'all',
- priority: 92
- },
-
- // 2. Gmail
- {
- id: 'gmail_receipts',
- title: 'Search Order Receipts',
- description: 'Find recent flight or delivery confirmation emails in Gmail',
- goal: 'Open Gmail and search for recent flight or package delivery confirmation emails.',
- profile: 'flash',
- category: 'flash',
- tag: 'Gmail',
- apps: [{ name: 'Gmail', icon: 'mail', pkg: 'com.google.android.gm' }],
- requiredPackages: ['com.google.android.gm'],
- priority: 90
- },
- {
- id: 'pro_email_to_calendar',
- title: 'Email Itinerary to Calendar',
- description: 'Extract flight or event dates from Gmail and schedule in Calendar',
- goal: 'Open Gmail to find the latest event invitation or itinerary, extract dates and location, then open Google Calendar and create a corresponding calendar event.',
- profile: 'pro',
- category: 'cross_app',
- tag: 'Gmail + Calendar',
- apps: [
- { name: 'Gmail', icon: 'mail', pkg: 'com.google.android.gm' },
- { name: 'Calendar', icon: 'calendar_month', pkg: 'com.google.android.calendar' }
- ],
- requiredPackages: ['com.google.android.gm'],
- priority: 94
- },
-
- // 3. Chrome
- {
- id: 'chrome_research',
- title: 'Search AI News Breakthroughs',
- description: 'Search latest multimodal AI developments in Chrome browser',
- goal: 'Open Chrome browser and search for latest breakthroughs in multimodal mobile AI agents.',
- profile: 'flash',
- category: 'flash',
- tag: 'Chrome',
- apps: [{ name: 'Chrome', icon: 'public', pkg: 'com.android.chrome' }],
- requiredPackages: ['com.android.chrome'],
- priority: 88
- },
- {
- id: 'pro_research_keep',
- title: 'Product Research & Notes Note',
- description: 'Compare top 3 headphones on Chrome and record comparison in Keep',
- goal: 'Open Chrome, research top 3 noise-cancelling headphones comparing price and battery life, then write a structured comparison summary note in Keep Notes.',
- profile: 'pro',
- category: 'pro',
- tag: 'Chrome + Keep',
- apps: [
- { name: 'Chrome', icon: 'public', pkg: 'com.android.chrome' },
- { name: 'Keep Notes', icon: 'note_alt', pkg: 'com.google.android.keep' }
- ],
- requiredPackages: ['com.android.chrome'],
- priority: 91
- },
-
- // 4. YouTube
- {
- id: 'youtube_lofi',
- title: 'Play Lo-Fi Music Radio',
- description: 'Search and play a Lo-Fi hip hop live stream on YouTube',
- goal: 'Open YouTube, search for "Lofi hip hop beats relaxing radio" and tap on the live stream.',
- profile: 'flash',
- category: 'flash',
- tag: 'YouTube',
- apps: [{ name: 'YouTube', icon: 'smart_display', pkg: 'com.google.android.youtube' }],
- requiredPackages: ['com.google.android.youtube'],
- priority: 85
- },
-
- // 5. Settings
- {
- id: 'settings_display_wifi',
- title: 'Dark Mode & Wi-Fi Check',
- description: 'Toggle dark theme and verify network connection in Settings',
- goal: 'Open Settings app, navigate to Display settings, ensure Dark theme is enabled, and check Wi-Fi connection status.',
- profile: 'flash',
- category: 'flash',
- tag: 'Settings',
- apps: [{ name: 'Settings', icon: 'settings', pkg: 'com.android.settings' }],
- requiredPackages: ['com.android.settings'],
- priority: 87
- },
- {
- id: 'pro_settings_qa',
- title: 'Subsystem Health & Crash Probe',
- description: 'Traverse Settings submenus to verify screens and check for crash dialogs',
- goal: 'Explore Settings submenus (Network, Connected devices, Apps, Battery, Storage), verify each screen loads properly without ANR or crash dialogs, and summarize results.',
- profile: 'pro',
- category: 'monitor',
- tag: 'Settings QA',
- apps: [{ name: 'Settings', icon: 'settings', pkg: 'com.android.settings' }],
- requiredPackages: ['com.android.settings'],
- priority: 93
- },
-
- // 6. Clock
- {
- id: 'clock_timer',
- title: '25-Min Pomodoro Timer',
- description: 'Start a 25-minute focus countdown timer in Clock app',
- goal: 'Open Clock app, switch to Timer tab, set 25 minutes and start the countdown timer.',
- profile: 'flash',
- category: 'flash',
- tag: 'Clock',
- apps: [{ name: 'Clock', icon: 'timer', pkg: 'com.google.android.deskclock' }],
- requiredPackages: ['com.google.android.deskclock', 'com.android.deskclock'],
- priority: 86
- },
-
- // 7. Calculator
- {
- id: 'calc_gratuity',
- title: 'Split Bill & Calculate Tip',
- description: 'Calculate 18% gratuity on $186.40 for 3 people in Calculator',
- goal: 'Open Calculator and calculate 18% tip on a bill of $186.40, then divide by 3 people.',
- profile: 'flash',
- category: 'flash',
- tag: 'Calculator',
- apps: [{ name: 'Calculator', icon: 'calculate', pkg: 'com.google.android.calculator' }],
- requiredPackages: ['com.google.android.calculator', 'com.android.calculator2'],
- priority: 84
- },
-
- // 8. Photos
- {
- id: 'photos_inspect',
- title: 'Inspect Recent Screenshot',
- description: 'Open Google Photos and review the latest screenshot taken',
- goal: 'Open Google Photos and view the most recent screenshot in the screenshots album.',
- profile: 'flash',
- category: 'flash',
- tag: 'Photos',
- apps: [{ name: 'Photos', icon: 'photo_library', pkg: 'com.google.android.apps.photos' }],
- requiredPackages: ['com.google.android.apps.photos'],
- priority: 82
- },
-
- // 9. WeChat
- {
- id: 'wechat_browse',
- title: 'Check WeChat Messages',
- description: 'Open WeChat and view top recent chat conversations',
- goal: 'Open WeChat and view the top recent chat messages.',
- profile: 'flash',
- category: 'flash',
- tag: 'WeChat',
- apps: [{ name: 'WeChat', icon: 'forum', pkg: 'com.tencent.mm' }],
- requiredPackages: ['com.tencent.mm'],
- priority: 89
- },
- {
- id: 'pro_wechat_to_calendar',
- title: 'WeChat Notice to Calendar',
- description: 'Extract meeting notice from WeChat chat and add to Calendar',
- goal: 'Open WeChat, locate the latest meeting announcement or event message in the top chat, extract the time and topic, then open Calendar and schedule an event.',
- profile: 'pro',
- category: 'cross_app',
- tag: 'WeChat + Calendar',
- apps: [
- { name: 'WeChat', icon: 'forum', pkg: 'com.tencent.mm' },
- { name: 'Calendar', icon: 'calendar_month', pkg: 'com.google.android.calendar' }
- ],
- requiredPackages: ['com.tencent.mm'],
- priority: 93
- },
-
- // 10. Xiaohongshu
- {
- id: 'xhs_coffee_guide',
- title: 'RED Cafe Guide Search',
- description: 'Search trending specialty cafe reviews on Xiaohongshu',
- goal: 'Open Xiaohongshu, search for top-rated specialty coffee shops, and view the top post.',
- profile: 'flash',
- category: 'flash',
- tag: 'Xiaohongshu',
- apps: [{ name: 'Xiaohongshu', icon: 'auto_stories', pkg: 'com.xingin.xhs' }],
- requiredPackages: ['com.xingin.xhs'],
- priority: 87
- },
-
- // 11. Meituan / Dianping
- {
- id: 'meituan_ramen_search',
- title: 'Meituan Food Search',
- description: 'Search top-rated Ramen nearby on Meituan or Dianping',
- goal: 'Open Meituan or Dianping, search for top-rated Ramen nearby, and view top restaurant rating.',
- profile: 'flash',
- category: 'flash',
- tag: 'Meituan',
- apps: [{ name: 'Meituan', icon: 'restaurant', pkg: 'com.sankuai.meituan' }],
- requiredPackages: ['com.sankuai.meituan', 'com.dianping.v1'],
- priority: 86
- },
-
- // 12. Bilibili
- {
- id: 'bilibili_stream',
- title: 'Bilibili Tech Video',
- description: 'Search and play an AI Agent tutorial video on Bilibili',
- goal: 'Open Bilibili, search for "AI Agent Architecture", and play the top matching video.',
- profile: 'flash',
- category: 'flash',
- tag: 'Bilibili',
- apps: [{ name: 'Bilibili', icon: 'video_library', pkg: 'tv.danmaku.bili' }],
- requiredPackages: ['tv.danmaku.bili'],
- priority: 85
- },
-
- // 13. Play Store
- {
- id: 'pro_playstore_review',
- title: 'Play Store App Review Study',
- description: 'Compare top task management apps and ratings on Google Play',
- goal: 'Open Google Play Store, search for top rated task management apps, compare ratings and latest user reviews of the top 2 candidates, and record recommendations.',
- profile: 'pro',
- category: 'pro',
- tag: 'Play Store',
- apps: [{ name: 'Play Store', icon: 'storefront', pkg: 'com.android.vending' }],
- requiredPackages: ['com.android.vending'],
- priority: 88
- }
-];
diff --git a/apps/showcase_ui/src/app/core/models/markdown.model.ts b/apps/showcase_ui/src/app/core/models/markdown.model.ts
deleted file mode 100644
index 10b9adddc..000000000
--- a/apps/showcase_ui/src/app/core/models/markdown.model.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export interface MarkdownSegment {
- text: string;
- bold: boolean;
- code?: boolean;
-}
-
-export interface MarkdownLine {
- type: 'h1' | 'h2' | 'h3' | 'checked' | 'progress' | 'unchecked' | 'list-item' | 'verify' | 'assert' | 'finding' | 'text' | 'empty';
- segments: MarkdownSegment[];
- indent: number;
- checkKind?: 'verify' | 'assert' | 'finding';
- atEnd?: boolean;
-}
-
-export interface NoteMilestone {
- index: number;
- type: 'checked' | 'progress' | 'unchecked';
- segments: MarkdownSegment[];
- subSteps: MarkdownLine[];
- checks: MarkdownLine[];
-}
-
-export interface ParsedNote {
- title: string | null;
- milestones: NoteMilestone[];
- otherLines: MarkdownLine[];
-}
diff --git a/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts b/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts
deleted file mode 100644
index bb70e239b..000000000
--- a/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import {
- DEFAULT_EXPLORER_MODE,
- DEFAULT_VERIFICATION_LEVEL,
- EXPLORER_MODES,
- VERIFICATION_LEVELS,
- levelIndex,
- notchPercent
-} from './pro-tuning.model';
-
-describe('pro tuning ladders', () => {
- it('keeps every notch id and label unique within its ladder', () => {
- for (const ladder of [VERIFICATION_LEVELS, EXPLORER_MODES]) {
- expect(new Set(ladder.map((l) => l.id)).size).toBe(ladder.length);
- expect(new Set(ladder.map((l) => l.label)).size).toBe(ladder.length);
- }
- });
-
- it('mirrors the backend wire values', () => {
- expect(VERIFICATION_LEVELS.map((l) => l.id)).toEqual(['off', 'final', 'checkpoints', 'strict']);
- expect(EXPLORER_MODES.map((l) => l.id)).toEqual(['flash', 'pro', 'ultra']);
- expect(VERIFICATION_LEVELS.map((l) => l.id)).toContain(DEFAULT_VERIFICATION_LEVEL);
- expect(EXPLORER_MODES.map((l) => l.id)).toContain(DEFAULT_EXPLORER_MODE);
- });
-
- it('every level explains itself', () => {
- for (const level of [...VERIFICATION_LEVELS, ...EXPLORER_MODES]) {
- expect(level.tagline.length).toBeGreaterThan(10);
- expect(level.latency.length).toBeGreaterThan(0);
- expect(level.runs.length).toBeGreaterThan(0);
- expect(level.bestFor.length).toBeGreaterThan(10);
- }
- });
-
- it('resolves ids case-insensitively and falls back to the default', () => {
- expect(levelIndex(VERIFICATION_LEVELS, ' STRICT ', DEFAULT_VERIFICATION_LEVEL)).toBe(3);
- expect(levelIndex(VERIFICATION_LEVELS, 'nope', DEFAULT_VERIFICATION_LEVEL)).toBe(1);
- expect(levelIndex(EXPLORER_MODES, null, DEFAULT_EXPLORER_MODE)).toBe(0);
- });
-
- it('maps notches onto the track', () => {
- expect(notchPercent(0, 4)).toBe(0);
- expect(notchPercent(3, 4)).toBe(100);
- expect(notchPercent(1, 3)).toBe(50);
- expect(notchPercent(9, 3)).toBe(100);
- expect(notchPercent(0, 1)).toBe(0);
- });
-});
diff --git a/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts b/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts
deleted file mode 100644
index 9e57c3eef..000000000
--- a/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts
+++ /dev/null
@@ -1,165 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * Launcher options for `/api/run` (`verification_level`, `explorer_mode`).
- * Keep ids in sync with `VERIFICATION_LEVEL_PRESETS` in `artemis/config/agent.py`
- * and `EXPLORER_TIERS` in `artemis/agents/explorer/tiers.py`.
- */
-
-export type VerificationLevelId = 'off' | 'final' | 'checkpoints' | 'strict';
-export type ExplorerModeId = 'flash' | 'pro' | 'ultra';
-
-/** One notch on a tuning slider. */
-export interface TuningLevel {
- /** Wire value sent to the backend. */
- id: TId;
- /** Short name shown next to the slider title and as the hover card heading. */
- label: string;
- /** One-sentence summary shown in the hover card. */
- tagline: string;
- /** Plain-language time cost, e.g. "no extra time". */
- latency: string;
- /** Checks or searches performed at this level. */
- runs: string[];
- /** Checks or searches omitted at this level. */
- skips?: string[];
- /** When to pick this level. */
- bestFor: string;
-}
-
-export const VERIFICATION_LEVELS: readonly TuningLevel[] = [
- {
- id: 'off',
- label: 'Off',
- tagline: 'No checking. The run ends as soon as the task looks done.',
- latency: 'no extra time',
- runs: [
- 'Each step is treated as finished the moment it is carried out.',
- 'You get the full action trace, but no pass / fail verdict.'
- ],
- skips: ['Nothing is double-checked and nothing is retried.'],
- bestFor: 'Quick tries and demos, when you only want to watch what happens.'
- },
- {
- id: 'final',
- label: 'At the end',
- tagline: 'One check of the finished result against your goal. This is the default.',
- latency: 'adds about 20–60 s at the end',
- runs: [
- 'When the task finishes, the final screen, the step history and the device state are compared with what you asked for.',
- 'If the result does not match, the task goes back and tries to fix it, up to 3 times.'
- ],
- skips: ['Nothing is checked while the task is still running.'],
- bestFor: 'Everyday tasks: an honest pass / fail without slowing the run down.'
- },
- {
- id: 'checkpoints',
- label: 'Every step',
- tagline: 'Each step is checked as soon as it is done, plus the final check.',
- latency: 'a short check after each step, done in the background',
- runs: [
- 'Every step is checked right after it completes, using the screenshots from that moment.',
- 'If a step went wrong, it gets fixed before moving on (up to 2 tries per step).',
- 'A failed test condition is written down and the task keeps going.',
- 'The final check still runs at the end.'
- ],
- bestFor: 'Long tasks where one early mistake would spoil everything after it.'
- },
- {
- id: 'strict',
- label: 'Strict',
- tagline: 'Every step is checked, with more retries. The first failed test stops the run.',
- latency: 'slowest: more checks and more retries',
- runs: [
- 'Each check takes longer and gets more attempts: 4 fixes per step and 5 at the end.',
- 'The first failed test condition stops the run immediately, with the evidence attached.'
- ],
- bestFor: 'Release checks and regression runs, where a wrong pass is never acceptable.'
- }
-];
-
-export const EXPLORER_MODES: readonly TuningLevel[] = [
- {
- id: 'flash',
- label: 'Quick glance',
- tagline: 'Finds buttons and text on the screen in a single look.',
- latency: '1 look per search',
- runs: [
- 'Something on screen is asked for by name, icon or colour and its position comes back straight away.',
- 'Several things can be looked up at once.'
- ],
- skips: ['No zooming in and no second try.'],
- bestFor: 'Ordinary apps with clearly labelled buttons, icons and text.'
- },
- {
- id: 'pro',
- label: 'Second look',
- tagline: 'Takes up to 3 looks, thinking in between, before answering.',
- latency: 'up to 3 looks per search',
- runs: [
- 'The screen layout is read first, then the picture is searched.',
- 'If the first try misses, a different approach is tried within the 3 looks.'
- ],
- skips: ['Still no zooming into small areas, to keep searches short.'],
- bestFor: 'Things described by where they are ("the switch next to Wi-Fi") or with unclear labels.'
- },
- {
- id: 'ultra',
- label: 'Close-up',
- tagline: 'Zooms into parts of the screen and takes up to 8 looks.',
- latency: 'up to 8 looks per search (slowest)',
- runs: [
- 'Parts of the screen can be cropped and magnified to read tiny text and crowded layouts piece by piece.',
- 'Later looks reuse the earlier ones, so they cost less time than they sound.'
- ],
- bestFor: 'Crowded screens, tiny targets, charts and drawings, and checks where exact placement matters.'
- }
-];
-
-/** Per-run tuning sent with `/api/run` for the Pro profile. */
-export interface ProTuningOptions {
- verificationLevel?: VerificationLevelId | string;
- explorerMode?: ExplorerModeId | string;
-}
-
-/** Effective defaults reported by `GET /api/run/defaults`. */
-export interface ProTuningDefaults {
- verification_level?: string | null;
- explorer_mode?: string | null;
-}
-
-export const DEFAULT_VERIFICATION_LEVEL: VerificationLevelId = 'final';
-export const DEFAULT_EXPLORER_MODE: ExplorerModeId = 'flash';
-
-/** Index of a level id within its ladder; falls back to the default when unknown. */
-export function levelIndex(
- ladder: readonly TuningLevel[],
- id: string | null | undefined,
- fallback: TId
-): number {
- const wanted = String(id ?? '').trim().toLowerCase();
- const idx = ladder.findIndex((l) => l.id === wanted);
- if (idx >= 0) return idx;
- return Math.max(0, ladder.findIndex((l) => l.id === fallback));
-}
-
-/** Slider fill percentage for a notch index on a ladder of `count` notches. */
-export function notchPercent(index: number, count: number): number {
- if (count <= 1) return 0;
- const clamped = Math.min(Math.max(index, 0), count - 1);
- return (clamped / (count - 1)) * 100;
-}
diff --git a/apps/showcase_ui/src/app/core/models/session.model.ts b/apps/showcase_ui/src/app/core/models/session.model.ts
deleted file mode 100644
index ef322d1cf..000000000
--- a/apps/showcase_ui/src/app/core/models/session.model.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export interface ModelInfo {
- name: string;
- id: string;
- provider: string;
- architecture?: string;
-}
-
-export interface TaskQueueItem {
- session_id: string;
- goal: string;
- profile?: string;
- status: 'pending' | 'running' | 'paused' | 'completed' | 'failed' | 'cancelled';
- created_at?: number;
- start_time?: number;
- device_serial?: string | null;
- device_id?: string | null;
-}
-
-export interface Session {
- session_id: string;
- initial_goal: string;
- start_time: number;
- end_time?: number;
- status?: string;
- video_url?: string;
- recording_status?: 'recording' | 'finalizing' | 'processing' | 'ready' | 'failed' | 'unavailable';
- model_info?: ModelInfo;
- device_serial?: string | null;
- device_id?: string | null;
- device_info?: any;
-}
-
-export interface AgentStatusResponse {
- status: 'idle' | 'running' | 'paused' | 'completed' | 'offline';
- session_id?: string | null;
- goal?: string | null;
- pid?: number | null;
- queue?: (TaskQueueItem | string)[];
- background_tasks?: any[];
- model_info?: ModelInfo | null;
- paused_error?: string | null;
-}
-
-/** Per-run Pro tuning persisted with the session (`device_info.run_tuning`). */
-export interface SessionRunTuning {
- verification_level?: string | null;
- explorer_mode?: string | null;
-}
-
-/** `GET /api/sessions/{id}/usage`: session-wide LLM usage and the live executor context. */
-export interface SessionUsage {
- session_id: string;
- llm_calls: number;
- prompt_tokens: number;
- completion_tokens: number;
- total_tokens: number;
- cached_tokens: number;
- /** Prompt size of the latest Operator / Flash runner call; null before the first call. */
- operator_context_tokens: number | null;
- operator_context_window_tokens: number;
- operator_context_updated_at?: number | null;
- profile?: string | null;
- run_tuning?: SessionRunTuning | null;
-}
-
-export type AgentStatus = 'idle' | 'running' | 'completed' | 'offline' | string;
-
-export type TaskStatus = 'running' | 'paused' | 'completed' | 'pending' | 'failed' | 'cancelled';
-
diff --git a/apps/showcase_ui/src/app/core/models/stream.model.ts b/apps/showcase_ui/src/app/core/models/stream.model.ts
deleted file mode 100644
index 86645f094..000000000
--- a/apps/showcase_ui/src/app/core/models/stream.model.ts
+++ /dev/null
@@ -1,246 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export interface ActionParam {
- key: string;
- value: string;
-}
-
-export interface ActionExecution {
- id?: string;
- trace_id?: string;
- agent?: string;
- agent_name?: string;
- action: string;
- name?: string;
- args?: Record;
- params?: Record;
- payload?: any;
- pre_image_name?: string | null;
- post_image_name?: string | null;
- status?: 'success' | 'failed' | 'error' | 'running' | string;
- duration?: number;
- duration_ms?: number;
- error?: string | null;
- timestamp?: number;
-}
-
-export interface StepEvent {
- type: 'thinking' | 'text' | 'action' | 'tool';
- data: any;
- timestamp?: number;
-}
-
-export interface LLMStreamEventData {
- execution_id: string;
- step_id?: string;
- session_id?: string;
- text?: string;
- chunk?: string;
- stream_type?: 'thinking' | 'text' | string;
- isCompleted?: boolean;
- isReset?: boolean;
- resetMessage?: string;
-}
-
-export const DEFAULT_STREAM_RESET_MESSAGE =
- 'A request error occurred during output generation, typically caused by lower API priority. Retrying automatically...';
-
-export interface StreamResetNotice {
- id: string;
- message: string;
- isWaiting: boolean;
- streamType: string;
-}
-
-export interface LLMStreamResetEventData {
- stream_exec_id?: string;
- stream_execution_id?: string;
- step_id?: string;
- session_id?: string;
- action?: 'discard' | string;
- reason?: string;
- category?: string;
- error?: string;
- message?: string;
- retry_attempt?: number;
- timestamp?: number;
-}
-
-export interface StepItemData {
- step_id: string;
- step_type?: string;
- step_number: number;
- session_id: string;
- timestamp: number;
- operator_native_thinking?: string;
- operator_raw_thinking?: string;
- action_taken?: any;
- generic_tools?: any[];
- last_execution_result?: any;
- pre_image_name?: string;
- post_image_name?: string;
- extra_metadata?: any;
- token_usage?: {
- prompt_tokens?: number;
- completion_tokens?: number;
- total_tokens?: number;
- };
- total_tokens?: number;
- duration?: number;
-}
-
-export interface StepBlock {
- id: string;
- type: 'llm_stream' | 'step' | 'checker';
- timestamp: string;
- data: any;
-}
-
-export interface PhaseBlock {
- id: string;
- durationSeconds: number;
- tokens?: number;
- promptTokens?: number;
- completionTokens?: number;
- blocks: StepBlock[];
-}
-
-/** One check item's verdict as booked by the Checker (ledger record shape). */
-export interface CheckerVerdict {
- item_text: string;
- kind: 'verify' | 'assert' | string;
- status: 'passed' | 'failed' | 'inconclusive' | 'superseded' | 'unchecked' | string;
- evidence: string;
- suggestion?: string;
-}
-
-export interface CheckerCheckItem {
- kind: string;
- text: string;
- when?: string;
-}
-
-/**
- * One streamed LLM turn of a multi-turn agent (the Checker's tool loop):
- * the text of one execution id with the time its first chunk arrived, so the
- * timeline can interleave it with the tool calls that followed. Live sessions
- * build these from `llm_stream` chunks; historical sessions rebuild them from
- * the persisted transcript (`PersistedCheckerStream`).
- */
-export interface StreamSegment {
- execution_id: string;
- stream_type: 'thinking' | 'text';
- text: string;
- timestamp: string;
- isCompleted?: boolean;
- isReset?: boolean;
- resetMessage?: string;
-}
-
-/** One persisted segment of a Checker attempt's streamed reasoning (backend `check_streams.jsonl`). */
-export interface PersistedStreamSegment {
- execution_id: string;
- /** `thought` is the model's reasoning stream, `answer` its visible text. */
- role: 'thought' | 'answer' | string;
- /** Unix seconds of the segment's first chunk. */
- when: number;
- text: string;
-}
-
-/**
- * One Checker attempt's persisted transcript as served by
- * `GET /api/sessions/{id}/checks` (`streams[]`). Text is bounded server-side;
- * `truncated` says the middle was cut out.
- */
-export interface PersistedCheckerStream {
- attempt_id: string;
- checkpoint_id?: string;
- phase?: string;
- trace_id?: string | null;
- ts?: number;
- truncated?: boolean;
- dropped_chars?: number;
- segments: PersistedStreamSegment[];
-}
-
-/**
- * Data of a `checker` timeline block: one Checker attempt (a midway checkpoint
- * of a completed subgoal or the exit final review), or the run outcome
- * (`phase: 'outcome'`). Streamed reasoning and tool traces land in the same
- * `operator_*_thinking` / `generic_tools` fields as an Operator step so the
- * timeline renders them with one code path.
- */
-export interface CheckerBlockData {
- event?: string;
- attempt_id?: string;
- checkpoint_id?: string;
- phase: 'checkpoint' | 'final' | 'outcome' | string;
- subgoal_text?: string;
- status?: 'running' | 'done' | 'superseded' | 'unchecked' | 'error' | string;
- trace_id?: string | null;
- anchor_step_id?: string | null;
- items?: CheckerCheckItem[];
- verdicts?: CheckerVerdict[];
- findings?: string[];
- unmet_subgoals?: string[];
- reverted?: boolean;
- applicable?: boolean;
- repairs_used?: number;
- route?: string;
- error?: string;
- task_status?: 'completed' | 'partial' | 'blocked' | string;
- tests?: { passed: number; failed: number; inconclusive: number; unchecked: number };
- last_findings?: string[];
- started_at?: number;
- finished_at?: number;
- duration?: number;
- isCompleted?: boolean;
- generic_tools?: any[];
- /** Per-turn stream segments (live chunks or the persisted transcript); the flat fields below hold the joined text. */
- stream_segments?: StreamSegment[];
- operator_native_thinking?: string;
- operator_raw_thinking?: string;
- [key: string]: any;
-}
-
-/** @deprecated legacy JSON-in-stream checker verdict (pre-ledger); kept for the parser. */
-export interface CheckerResult {
- success: boolean;
- reason: string;
-}
-
-export interface StepReplayFrame {
- index: number;
- stepNumber: number;
- rawStepNumber?: number;
- stepId?: string;
- title: string;
- imageUrl: string;
- preImageUrl?: string | null;
- postImageUrl?: string | null;
- action?: any;
- actionType?: string;
- actionText?: string;
- targetText?: string;
- coords?: string;
- status?: 'dispatched' | 'failed' | string;
- isPost?: boolean;
- timestamp?: number;
- phaseId?: string;
- summary?: string;
-}
-
diff --git a/apps/showcase_ui/src/app/core/models/system.model.ts b/apps/showcase_ui/src/app/core/models/system.model.ts
deleted file mode 100644
index cf15406f0..000000000
--- a/apps/showcase_ui/src/app/core/models/system.model.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export type ProbeStatus = 'pass' | 'warn' | 'fail' | 'skipped';
-
-export type ProbeCategory = 'device' | 'auth' | 'toolchain' | 'runtime';
-
-export interface ProbeAction {
- action_type: 'command' | 'hint' | 'link';
- label: string;
- payload: string;
-}
-
-export interface DeviceInfo {
- serial: string;
- state: string;
- model: string | null;
- product: string | null;
- android_version: string | null;
- screen_resolution: string | null;
- is_locked: boolean | null;
- is_emulator: boolean;
- installed_packages?: string[];
-}
-
-export interface AdbServerEndpoint {
- host: string;
- port: number;
- socket: string;
- identity: string;
- mode: 'local' | 'remote';
- is_local_default: boolean;
-}
-
-export interface AdbServerDevice {
- serial: string;
- state: string;
- model: string | null;
- product: string | null;
-}
-
-export interface AdbServerStatus {
- endpoint: AdbServerEndpoint;
-}
-
-export interface AdbServerConnectionResult {
- success: boolean;
- message: string;
- endpoint: AdbServerEndpoint;
- devices: AdbServerDevice[];
- persisted?: boolean;
- persistence_error?: string | null;
- error_code?: string;
- output?: string;
-}
-
-export interface AdbServerConnectionResponse {
- connection_result: AdbServerConnectionResult;
- report?: SystemReadinessReport;
-}
-
-export interface ProbeResult {
- id: string;
- category: ProbeCategory;
- title: string;
- status: ProbeStatus;
- is_blocker: boolean;
- summary: string;
- description: string;
- metadata: Record;
- actions: ProbeAction[];
-}
-
-export interface SystemReadinessReport {
- overall_ready: boolean;
- blocker_count: number;
- passed_blocker_count: number;
- probes: ProbeResult[];
- active_device: DeviceInfo | null;
- os_type?: 'linux' | 'darwin' | 'windows' | string;
- timestamp: number;
-}
-
-export type EmulatorLaunchStage =
- | 'idle'
- | 'starting'
- | 'waiting_for_adb'
- | 'booting'
- | 'ready'
- | 'failed'
- | 'stopped';
-
-export interface EmulatorLaunchState {
- avd_name: string | null;
- status: EmulatorLaunchStage;
- pid: number | null;
- serial: string | null;
- stage_message: string;
- progress_percent: number;
- started_at: number | null;
- elapsed_seconds: number;
- error: string | null;
- logs: string[];
- can_retry: boolean;
-}
diff --git a/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts b/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts
deleted file mode 100644
index 2d22e2bf4..000000000
--- a/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Injectable } from '@angular/core';
-import {
- AppReference,
- SmartSuggestion,
- SuggestionCategory,
- APP_REGISTRY,
- SMART_TASK_LIBRARY
-} from '../data/smart-tasks.data';
-
-@Injectable({
- providedIn: 'root'
-})
-export class TaskRecommendationService {
- public readonly allTasks: SmartSuggestion[] = SMART_TASK_LIBRARY;
- public readonly appRegistry = APP_REGISTRY;
-
- /**
- * Check if task matches any package installed on device
- */
- public isSuggestionOnDevice(
- suggestion: SmartSuggestion,
- installedPackages: Set
- ): boolean {
- if (installedPackages.size === 0) return false;
- if (!suggestion.requiredPackages || suggestion.requiredPackages.length === 0) {
- return true;
- }
-
- if (suggestion.matchMode === 'all') {
- return suggestion.requiredPackages.every(pkg => installedPackages.has(pkg));
- }
- return suggestion.requiredPackages.some(pkg => installedPackages.has(pkg));
- }
-
- /**
- * Intelligently rank and filter tasks:
- * Prioritizes tasks matching apps detected on user's phone without exposing clutter.
- */
- public filterAndRankTasks(
- installedPackages: Set,
- category: SuggestionCategory,
- shuffleOffset: number = 0
- ): SmartSuggestion[] {
- const hasDevice = installedPackages.size > 0;
-
- // 1. Score tasks: prioritize tasks whose apps are on the user's phone
- const scored = this.allTasks.map(task => {
- const isMatched = this.isSuggestionOnDevice(task, installedPackages);
- let score = task.priority ?? 50;
-
- if (hasDevice) {
- if (isMatched) {
- score += 100;
- if (task.requiredPackages && task.requiredPackages.length > 1 && task.matchMode === 'all') {
- score += 30; // bonus for full multi-app synergy match
- }
- } else {
- score -= 40;
- }
- }
-
- return { task, isMatched, score };
- });
-
- // 2. Filter by category
- let filtered = scored.filter(item => {
- if (category === 'flash') {
- return item.task.profile === 'flash';
- }
- if (category === 'pro') {
- return item.task.profile === 'pro';
- }
- if (category === 'cross_app') {
- return item.task.category === 'cross_app';
- }
- if (category === 'monitor') {
- return item.task.category === 'monitor';
- }
- return true; // 'all'
- });
-
- // 3. Sort by score descending (matched apps first)
- filtered.sort((a, b) => b.score - a.score);
-
- let result = filtered.map(f => f.task);
-
- // 4. Apply shuffle rotation
- if (shuffleOffset > 0 && result.length > 0) {
- const shift = shuffleOffset % result.length;
- result = [...result.slice(shift), ...result.slice(0, shift)];
- }
-
- return result;
- }
-}
diff --git a/apps/showcase_ui/src/app/pages/home/home.component.html b/apps/showcase_ui/src/app/pages/home/home.component.html
deleted file mode 100644
index 060a01d25..000000000
--- a/apps/showcase_ui/src/app/pages/home/home.component.html
+++ /dev/null
@@ -1,2103 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- What would you like to do on your phone?
-
-
-
-
-
-
-
- @if (isReady()) {
- check_circle
- } @else {
- tune
- }
- System Setup & Prerequisites
-
-
-
- @if (!isReady()) {
- lock
- } @else {
- rocket_launch
- }
- Launch Autonomous Task
-
-
-
-
-
-
-
- @if (activeTab() === 'diagnostics') {
-
-
-
-
-
-
-
-
-
-
-
- @if (hasReadinessReport() && !isEnvironmentReady()) {
-
-
-
- bolt
-
-
-
-
Automated Dependency Setup
-
- Linux
- macOS
- Windows
-
-
-
Run in terminal to install ADB, FFmpeg, scrcpy, and project dependencies:
-
-
-
- {{ oneClickSetupCmd() }}
-
- {{ copiedId() === 'one_click_deps' ? 'check' : 'content_copy'
- }}
- {{ copiedId() === 'one_click_deps' ? 'Copied!' : 'Copy' }}
-
-
-
- }
-
-
-
-
-
-
-
- @if (pythonProbe()?.status === 'fail') {
-
-
-
- $
- uv python install 3.12
-
- {{ copiedId() === 'py_install' ? 'check' : 'content_copy'
- }}
- {{ copiedId() === 'py_install' ? 'Copied!' : 'Copy' }}
-
-
-
-
- }
-
-
-
-
-
- @if (adbProbe()?.status === 'fail') {
-
-
-
- $
- {{ adbInstallCmd() }}
-
- {{ copiedId() === 'env_adb_cmd' ? 'check' : 'content_copy'
- }}
- {{ copiedId() === 'env_adb_cmd' ? 'Copied!' : 'Copy' }}
-
-
-
-
- }
-
-
-
-
-
- @if (configProbe()?.status === 'fail') {
-
-
-
- $
- artemis init
-
- {{ copiedId() === 'cfg_init' ? 'check' : 'content_copy'
- }}
- {{ copiedId() === 'cfg_init' ? 'Copied!' : 'Copy' }}
-
-
-
-
- }
-
-
-
-
-
- @if (toolchainProbe()?.status !== 'pass') {
-
-
-
- $
- {{ toolchainInstallCmd() }}
-
- {{ copiedId() === 'install_toolchain' ? 'check' : 'content_copy'
- }}
- {{ copiedId() === 'install_toolchain' ? 'Copied!' : 'Copy' }}
-
-
-
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Google Gemini
- Recommended
-
-
Fast multimodal reasoning with free API key from Google AI Studio.
-
-
-
-
-
-
-
-
-
-
-
- tune
-
-
-
- Custom Configuration
-
-
Define models in config/artemis.jsonc & keys in .env
- (OpenAI, Claude, Ollama).
-
-
-
-
-
-
- @if (modelSetupMode() === 'gemini') {
-
-
-
-
-
-
-
-
document_scanner
-
- Optional: Google Cloud Vision OCR
-
-
-
-
- @if (isOcrConfigured()) {
-
- }
-
- {{ showOcrConfig() ? 'expand_less' : 'expand_more' }}
-
-
-
-
- @if (showOcrConfig()) {
-
-
- Provides screen text coordinate grounding to improve element detection and interaction accuracy on text-heavy apps.
-
-
-
-
-
- }
-
- } @else {
-
-
-
-
-
-
-
-
-
-
-
Default Model
-
-
- {{ modelConfigEnv()?.default_model?.provider || 'google' }}
- /
- {{ modelConfigEnv()?.default_model?.model || 'gemini-3.8-flash' }}
-
-
-
- @if (modelConfigEnv()?.default_model?.fallback) {
-
-
Fallback Model
-
- {{ modelConfigEnv()?.default_model?.fallback?.provider }}/{{
- modelConfigEnv()?.default_model?.fallback?.model }}
-
-
- }
-
-
-
-
- @if (showFullConfigFile()) {
-
-
{{ modelConfigEnv()?.config_content }}
-
- }
-
-
-
-
-
-
-
-
- @for (envItem of modelConfigEnv()?.env_vars || []; track envItem.name) {
-
-
- {{ envItem.name }}
-
-
- @if (envItem.is_set) {
-
- check_circle
- Configured
- @if (envItem.preview) {
- ({{ envItem.preview }})
- }
-
- } @else {
-
- radio_button_unchecked
- Not Set
-
- }
-
-
- }
-
-
-
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
- @if (isDeviceReady()) {
-
-
-
-
- phone_android
-
-
-
-
- {{ activeDevice()?.model || activeDevice()?.serial }}
-
-
- @if (activeDevice()?.is_emulator) {
- Virtual Device
- ·
- }
- {{ activeDevice()?.serial }}
- @if (activeDevice()?.screen_resolution) {
- ·
- {{ activeDevice()?.screen_resolution }}
- }
- @if (activeDevice()?.android_version) {
- ·
- Android {{ activeDevice()?.android_version }}
- }
- @if (isRemoteAdbServer()) {
- ·
-
- Remote via {{ adbServerStatus()?.endpoint?.host }}:{{ adbServerStatus()?.endpoint?.port }}
-
- }
-
-
-
-
- Connected
-
-
-
-
- @if (connectedDevices().length > 1) {
-
-
Select Active Device:
-
- @for (dev of connectedDevices(); track dev.serial) {
-
-
- phone_android
-
- {{ dev.model || dev.serial }}
- @if (dev.serial === activeDevice()?.serial) {
- ✓ Active
- }
-
- }
-
-
- }
-
- }
-
-
- @if (isEmulatorLaunching() || (!isDeviceReady() && adbProbe()?.summary === 'Device Booting')) {
-
-
-
-
-
-
-
= 35" [class.active]="emulatorLaunchState()?.status === 'starting'">
-
- @if (emulatorLaunchState()?.progress_percent! >= 35) {
- check
- } @else {
-
- }
-
-
1. Process Start
-
-
-
= 35">
-
-
= 65" [class.active]="emulatorLaunchState()?.status === 'waiting_for_adb'">
-
- @if (emulatorLaunchState()?.progress_percent! >= 65) {
- check
- } @else if (emulatorLaunchState()?.status === 'waiting_for_adb') {
-
- } @else {
- 2
- }
-
-
2. ADB Handshake
-
-
-
= 65">
-
-
-
- @if (emulatorLaunchState()?.status === 'ready') {
- check
- } @else if (emulatorLaunchState()?.status === 'booting' || adbProbe()?.summary === 'Device Booting') {
-
- } @else {
- 3
- }
-
-
3. Android OS Boot
-
-
-
-
-
-
- check_circle
-
-
4. Ready
-
-
-
-
-
-
-
- {{ emulatorLaunchState()?.progress_percent || 25 }}% Complete
-
- timer
- Elapsed: {{ emulatorLaunchState()?.elapsed_seconds || 0 }}s
-
-
-
-
-
-
- info
- Android virtualization cold boot usually takes 20-45 seconds. Live progress is actively tracked above—please do not click launch repeatedly.
-
-
-
- @if (showLaunchLogs()) {
-
-
-
@if (emulatorLaunchState()?.logs?.length) {@for (log of emulatorLaunchState()?.logs; track log) {{{ log }}
}} @else {Waiting for process output...
}
-
- }
-
-
- }
-
-
- @if (!isDeviceReady() && !isEmulatorLaunching() && emulatorLaunchState()?.status === 'failed') {
-
-
-
-
- @if (emulatorLaunchState()?.logs?.length) {
-
-
-
@for (log of emulatorLaunchState()?.logs; track log) {{{ log }}
}
-
- }
-
-
- @if (emulatorLaunchState()?.avd_name) {
-
- refresh
- Retry Launch ({{ emulatorLaunchState()?.avd_name }})
-
- }
-
- sync
- Restart ADB Server
-
-
- Dismiss
-
-
-
-
- }
-
-
- @if (showConnectionMethods() || (!isDeviceReady() && !isEmulatorLaunching() && adbProbe()?.summary !== 'Device Booting' && adbProbe()?.summary !== 'Device Unauthorized' && adbProbe()?.summary !== 'Device Locked' && adbProbe()?.summary !== 'Lock State Unknown' && adbProbe()?.status !== 'fail' && emulatorLaunchState()?.status !== 'failed')) {
-
-
-
-
-
-
- smartphone
-
-
-
- Android Emulator
-
-
- @if (installedAvds().length > 0) {
- {{ installedAvds().length }} local virtual device{{ installedAvds().length > 1 ? 's' : '' }} available
- } @else {
- Launch a local Android Virtual Device
- }
-
-
-
-
-
-
-
-
-
-
-
- usb
-
-
-
- USB Cable
-
-
Connect physical phone via USB
-
-
-
-
-
-
-
-
-
-
- wifi
-
-
-
- Wireless ADB
-
-
Connect over local Wi-Fi
-
-
-
-
-
-
-
-
-
-
- devices
-
-
-
- Remote Computer
-
-
Phone connected to another computer
-
-
-
-
-
-
-
-
- @if (activeAdbGuideTab() !== 'remote' && isRemoteAdbServer()) {
-
-
- hub
-
- Artemis is currently using
- {{ adbServerStatus()?.endpoint?.host }}:{{ adbServerStatus()?.endpoint?.port }} .
- Switch to local ADB before using a local emulator, USB cable, or Wireless ADB.
-
-
-
- sync
- {{ isSwitchingToLocalAdb() ? 'Switching...' : 'Use Local ADB' }}
-
-
- }
-
-
- @if (activeAdbGuideTab() === 'emulator') {
-
-
- @if (installedAvds().length > 0) {
-
-
-
-
- @for (avd of installedAvds(); track avd) {
-
-
-
-
-
- @if (launchingAvd() === avd || (isEmulatorLaunching() && emulatorLaunchState()?.avd_name === avd)) {
-
- {{ emulatorLaunchState()?.status === 'booting' ? 'Booting OS' : 'Starting' }} ({{ emulatorLaunchState()?.elapsed_seconds || 0 }}s)
- } @else {
- play_arrow
- Launch
- }
-
-
-
- }
-
-
- } @else {
-
-
-
-
- devices_off
-
-
-
-
No Android Virtual Devices (AVD) Detected
-
- Linux
- macOS
- Windows
-
-
-
Follow the step-by-step instructions below to install and start an emulator:
-
-
-
-
-
- desktop_windows
- Android Studio (GUI)
-
-
- terminal
- Command Line (CLI)
-
-
-
-
-
- @if (emulatorSetupMode() === 'studio') {
-
-
-
-
Install Android Studio
-
- Download for {{ effectiveOs() === 'darwin' ? 'macOS' : (effectiveOs() === 'windows' ? 'Windows' : 'Linux') }} from
developer.android.com/studio and install.
-
-
-
-
-
-
Open Device Manager
-
- On Studio welcome screen, open More Actions > Virtual Device Manager (or Tools > Device Manager ).
-
-
-
-
-
-
Create Virtual Device
-
- Click Create Device , choose Pixel 8 , select Android 14 (API 34) , and click Finish .
-
-
-
-
-
-
Launch Emulator
-
- Click ▶ Play in Device Manager. Artemis will detect and connect to it automatically!
-
-
-
- }
-
-
- @if (emulatorSetupMode() === 'cli') {
-
-
-
1
-
-
{{ emuHypervisorTitle() }}
-
{{ emuHypervisorDesc() }}
-
- {{ effectiveOs() === 'windows' ? 'PS>' : '$' }}
- {{ emuHypervisorCmd() }}
-
- {{ copiedId() === 'emu_kvm' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'emu_kvm' ? 'Copied!' : 'Copy' }}
-
-
-
-
-
-
-
2
-
-
Download Emulator & System Image (API 34)
-
Install Android 14 Google APIs system image using SDK Manager:
-
- {{ effectiveOs() === 'windows' ? 'PS>' : '$' }}
- {{ emuSdkInstallCmd() }}
-
- {{ copiedId() === 'emu_sdk' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'emu_sdk' ? 'Copied!' : 'Copy' }}
-
-
-
-
-
-
-
3
-
-
Create Virtual Device (Pixel 8 AVD)
-
Initialize a new Android Virtual Device named Pixel_8_API_34:
-
- {{ effectiveOs() === 'windows' ? 'PS>' : '$' }}
- {{ emuCreateAvdCmd() }}
-
- {{ copiedId() === 'emu_create' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'emu_create' ? 'Copied!' : 'Copy' }}
-
-
-
-
-
-
-
4
-
-
Start the Emulator
-
Launch the virtual device (or add -no-window for headless execution):
-
- {{ effectiveOs() === 'windows' ? 'PS>' : '$' }}
- emulator -avd Pixel_8_API_34 -no-boot-anim
-
- {{ copiedId() === 'emu_run' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'emu_run' ? 'Copied!' : 'Copy' }}
-
-
-
-
-
- }
-
- }
-
-
- }
-
-
- @if (activeAdbGuideTab() === 'usb') {
-
-
- cable
- Connect your Android phone via USB cable and enable USB Debugging:
-
-
-
-
-
-
Connect USB Cable
-
Plug your Android phone into this computer with a data cable.
-
-
-
-
-
Enable Developer Options
-
In phone Settings > About Phone , tap Build Number 7
- times.
-
-
-
-
-
Enable USB Debugging
-
In Settings > Developer Options , turn USB Debugging ON.
-
-
-
-
-
-
Authorize Computer
-
Check "Always allow" and tap Allow on the phone prompt.
-
-
-
-
- }
-
-
- @if (activeAdbGuideTab() === 'wifi') {
-
-
- wifi
- Connect your phone and computer on the same Wi-Fi network:
-
-
-
-
-
-
- @if (wifiConnectMessage()) {
-
- check_circle
- {{ wifiConnectMessage() }}
-
- }
- @if (wifiConnectError()) {
-
- error
- {{ wifiConnectError() }}
-
- }
-
-
-
help_outline
-
- Find IP & port in phone Settings > Developer Options > Wireless Debugging .
-
-
-
- }
-
-
- @if (activeAdbGuideTab() === 'remote') {
-
-
- hub
- Enter the host and port of an ADB server endpoint that this computer can access, so Artemis can discover and control a phone physically connected to another computer.
-
-
-
-
-
-
-
- Remember this endpoint for future Artemis launches
-
-
- @if (remoteAdbMessage()) {
-
- check_circle
- {{ remoteAdbMessage() }}
-
- }
- @if (remoteAdbError()) {
-
- error
- {{ remoteAdbError() }}
-
- }
-
- @if (remoteAdbDevices().length > 0) {
-
- @for (device of remoteAdbDevices(); track device.serial) {
-
-
phone_android
-
- {{ device.model || device.serial }}
- {{ device.serial }}
-
-
- {{ device.state === 'device' ? 'Ready' : device.state }}
-
-
- }
-
- }
-
- @if (remoteAdbProbeResult()?.success && !isProbedRemoteAdbActive()) {
-
- @if (isActivatingRemoteAdb()) {
- sync
- Connecting...
- } @else {
- check
- {{ remoteAdbHasReadyDevice() ? 'Use Connection' : 'Use Anyway' }}
- }
-
- }
-
-
-
- Artemis sends all ADB commands to this server instead of the default local server. Any phone visible to that server becomes available to Artemis.
-
-
- }
-
-
- }
-
-
- @if (adbProbe()?.status === 'warn' && (adbProbe()?.summary === 'Device Locked' || adbProbe()?.summary === 'Lock State Unknown')) {
-
-
-
lock
-
-
- {{ adbProbe()?.summary === 'Device Locked' ? 'Device Is Locked' : 'Unable to Verify Lock State' }}
-
-
- {{ adbProbe()?.description }}
-
-
-
- refresh
- {{ isRefreshingDiagnostics() ? 'Checking...' : 'Check Again' }}
-
-
-
-
- @if (connectedDevices().length > 1) {
-
-
Select Active Device:
-
- @for (dev of connectedDevices(); track dev.serial) {
-
-
- phone_android
-
- {{ dev.model || dev.serial }}
- @if (dev.serial === activeDevice()?.serial) {
- ✓ Active
- }
-
- }
-
-
- }
-
- }
-
-
- @if (adbProbe()?.status === 'warn' && adbProbe()?.summary === 'Device Unauthorized') {
-
-
-
screen_lock_portrait
-
-
USB Debugging Authorization Pending
-
- Your Android device was detected, but it requires authorization. Please unlock your phone screen and tap "Allow USB Debugging" .
-
-
-
- refresh
- {{ isRemoteAdbServer() ? 'Refresh Devices' : 'Restart ADB & Check' }}
-
-
-
- }
-
-
- @if (adbProbe()?.status === 'fail') {
-
-
-
error
-
- Android Platform Tools (adb) not found. Please install ADB on your operating system:
-
-
-
-
-
-
- Linux (Ubuntu / Debian)
- @if (effectiveOs() === 'linux') {
- Detected
- }
-
-
- $
- sudo apt-get install -y adb
-
- {{ copiedId() === 'linux_adb' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'linux_adb' ? 'Copied!' : 'Copy' }}
-
-
-
-
-
-
- macOS (Homebrew)
- @if (effectiveOs() === 'darwin') {
- Detected
- }
-
-
- $
- brew install android-platform-tools
-
- {{ copiedId() === 'mac_adb' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'mac_adb' ? 'Copied!' : 'Copy' }}
-
-
-
-
-
-
- Windows (WinGet)
- @if (effectiveOs() === 'windows') {
- Detected
- }
-
-
- >
- winget install Google.PlatformTools
-
- {{ copiedId() === 'win_adb' ? 'check' : 'content_copy' }}
- {{ copiedId() === 'win_adb' ? 'Copied!' : 'Copy' }}
-
-
-
-
-
- }
-
-
-
-
-
- @if (isReady()) {
-
- }
-
-
- }
-
-
-
-
- @if (activeTab() === 'launcher') {
-
-
-
- @if (hasReadinessReport() && !isReady()) {
-
- {{ adbProbe()?.summary === 'Device Locked' ? 'lock' : 'warning' }}
-
- {{ adbProbe()?.summary === 'Device Locked'
- ? 'Android device is locked. Unlock it and enter the home screen before running a task.'
- : 'Prerequisites incomplete: runtime, credentials, or Android device is not ready. Click here to open Setup Guide.' }}
-
- arrow_forward
-
- }
-
-
-
-
-
- @if (showIntentSuggestion()) {
-
-
- auto_awesome
-
- Continuous monitoring or complex task detected. Recommended: Pro Mode
-
-
-
- Switch to Pro
- arrow_forward
-
-
- }
-
-
-
-
-
-
-
-
-
- {{ selectedProfile() === 'flash' ? 'bolt' : 'psychology' }}
-
-
- @if (selectedProfile() === 'flash') {
-
- Flash
- ·
- Token-efficient, rapid 3–5s/step decisions straight from the live screen. Best for everyday tasks with a clear path, however long they run.
- (No plan, no result check, no ADB commands)
-
- } @else {
-
- Pro
- ·
- Plans first, checks its results and recovers when a tap is blocked. Handles 100+ step workflows and live phone monitoring (15–40s / step).
-
- }
-
-
-
-
- @if (errorMessage()) {
-
- error
- {{ errorMessage() }}
-
- }
-
-
-
-
-
-
-
- bolt
- Flash
-
-
- psychology
- Pro
-
-
-
-
-
- @if (taskGoal().length > 0) {
- {{ taskGoal().length }} chars
- }
-
-
-
- @if (isSubmitting()) {
- Starting...
- } @else {
- play_arrow Run Task
- }
-
-
-
-
-
-
- @if (selectedProfile() === 'pro') {
-
-
-
-
-
- @if (expectedOutput().trim()) {
-
- close
-
- }
-
-
-
-
-
-
-
-
- @for (slider of tuningSliders(); track slider.kind) {
-
-
- {{ slider.name }}
- {{ slider.level.label }}
-
-
- {{ slider.ends[0] }}
- {{ slider.ends[1] }}
-
-
-
-
-
- @for (lvl of slider.ladder; track lvl.id; let i = $index) {
-
- }
- @if (slider.index === slider.ladder.length - 1) {
-
-
-
- @for (px of ditherPixels; track $index) {
-
- }
-
- }
-
-
- @if (slider.index === slider.ladder.length - 1) {
-
- }
-
-
- @if (activeTuningTip() === slider.kind) {
-
-
-
- {{ slider.level.label }}
- {{ slider.level.latency }}
-
-
{{ slider.level.tagline }}
-
-
What happens
-
- @for (line of slider.level.runs; track $index) {
- {{ line }}
- }
-
-
- @if (slider.level.skips?.length) {
-
-
Not included
-
- @for (line of slider.level.skips; track $index) {
- {{ line }}
- }
-
-
- }
-
- Use it for
- {{ slider.level.bestFor }}
-
-
- }
-
-
- }
-
-
- }
-
-
-
-
-
-
-
-
- @for (item of filteredSuggestions(); track item.id) {
-
-
-
-
- @for (app of item.apps; track app.name) {
-
- {{ app.icon }}
-
- }
-
{{ getAppNamesDisplay(item.apps) }}
-
-
-
- {{ item.profile === 'pro' ? 'Pro' : 'Flash' }}
-
-
-
-
-
-
{{ item.title }}
-
{{ item.description }}
-
-
-
-
-
- }
-
-
-
-
-
- }
-
-
-
diff --git a/apps/showcase_ui/src/app/pages/home/home.component.scss b/apps/showcase_ui/src/app/pages/home/home.component.scss
deleted file mode 100644
index c1a5e68d6..000000000
--- a/apps/showcase_ui/src/app/pages/home/home.component.scss
+++ /dev/null
@@ -1,5168 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// =============================================================================
-// DESIGN SYSTEM TOKENS & TYPOGRAPHY (Aligned with Workspace Minimalism)
-// =============================================================================
-$font-sans: 'Google Sans Text', 'Google Sans', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
-$font-mono: 'JetBrains Mono', 'Fira Code', 'Roboto Mono', 'SFMono-Regular', Consolas, monospace;
-
-// Surfaces & Backgrounds
-$bg-base: #f8f9fa;
-$card-bg: #ffffff;
-$card-border: #e2e8f0;
-$card-border-hover: #cbd5e1;
-$card-shadow: 0 1px 3px rgba(0, 0, 0, 0.03), 0 4px 12px rgba(0, 0, 0, 0.02);
-
-// Brand & Semantic Colors
-$primary: #1a73e8;
-$primary-dark: #1557b0;
-$primary-light: #eff6ff;
-$primary-border: #bfdbfe;
-
-$emerald: #059669;
-$emerald-dark: #047857;
-$emerald-light: #ecfdf5;
-$emerald-border: #a7f3d0;
-
-$amber: #d97706;
-$amber-dark: #b45309;
-$amber-light: #fffbeb;
-$amber-border: #fde68a;
-
-$rose: #dc2626;
-$rose-dark: #b91c1c;
-$rose-light: #fef2f2;
-$rose-border: #fecaca;
-
-// Neutral Text Scale
-$text-main: #0f172a;
-$text-secondary: #475569;
-$text-muted: #64748b;
-$text-light: #94a3b8;
-
-// Shared OS switcher. It is rendered in both the dependency installer and
-// the empty-AVD guide, so the styling cannot be scoped to either card.
-.os-selector-pills {
- display: inline-flex;
- align-items: center;
- padding: 2px;
- gap: 2px;
- background: rgba(255, 255, 255, 0.65);
- border: 1px solid rgba(226, 232, 240, 0.85);
- border-radius: 8px;
- box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.8);
- backdrop-filter: blur(12px) saturate(180%);
- -webkit-backdrop-filter: blur(12px) saturate(180%);
-
- .os-pill {
- padding: 3px 9px;
- border: 0;
- border-radius: 6px;
- background: transparent;
- color: $text-secondary;
- font-family: inherit;
- font-size: 11px;
- font-weight: 600;
- line-height: 1.35;
- cursor: pointer;
- transition: color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
-
- &:hover {
- color: $text-main;
- background: rgba(255, 255, 255, 0.72);
- }
-
- &.active {
- color: #ffffff;
- background: $primary;
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12);
- }
-
- &:focus-visible {
- outline: 2px solid rgba($primary, 0.35);
- outline-offset: 1px;
- }
- }
-}
-
-// =============================================================================
-// FROSTED GLASS (GLASSMORPHISM) MIXINS
-// =============================================================================
-@mixin glass-surface($opacity: 0.72, $blur: 16px) {
- background: rgba(255, 255, 255, $opacity);
- backdrop-filter: blur($blur) saturate(180%);
- -webkit-backdrop-filter: blur($blur) saturate(180%);
-}
-
-@mixin glass-primary-btn {
- background: linear-gradient(135deg, rgba(26, 115, 232, 0.92) 0%, rgba(37, 99, 235, 0.85) 100%);
- backdrop-filter: blur(10px) saturate(180%);
- -webkit-backdrop-filter: blur(10px) saturate(180%);
- border: 1px solid rgba(255, 255, 255, 0.28);
- box-shadow: 0 4px 14px rgba(26, 115, 232, 0.32), inset 0 1px 1px rgba(255, 255, 255, 0.45);
- color: #ffffff !important;
- transition: background 0.2s cubic-bezier(0.4, 0, 0.2, 1),
- border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
- box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1),
- transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
- opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-
- span, .material-symbols-outlined, .run-icon, .action-icon {
- color: #ffffff !important;
- }
-
- &:hover:not(:disabled) {
- background: linear-gradient(135deg, rgba(26, 115, 232, 1) 0%, rgba(37, 99, 235, 0.95) 100%);
- box-shadow: 0 6px 20px rgba(26, 115, 232, 0.42), inset 0 1px 1px rgba(255, 255, 255, 0.6);
- transform: translateY(-1px);
- }
-
- &:active:not(:disabled) {
- transform: scale(0.97) translateY(0);
- box-shadow: 0 2px 8px rgba(26, 115, 232, 0.35);
- }
-
- &:disabled {
- background: rgba(148, 163, 184, 0.5);
- backdrop-filter: blur(10px);
- -webkit-backdrop-filter: blur(10px);
- border: 1px solid rgba(255, 255, 255, 0.2);
- box-shadow: none;
- opacity: 0.65;
- cursor: not-allowed;
- transform: none;
- }
-}
-
-@mixin glass-secondary-btn {
- background: rgba(255, 255, 255, 0.72);
- backdrop-filter: blur(10px) saturate(180%);
- -webkit-backdrop-filter: blur(10px) saturate(180%);
- border: 1px solid rgba(226, 232, 240, 0.88);
- box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.05), inset 0 1px 1px rgba(255, 255, 255, 0.95);
- color: $text-secondary;
- transition: background 0.2s cubic-bezier(0.4, 0, 0.2, 1),
- border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
- color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
- box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1),
- transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
- opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-
- &:hover:not(:disabled) {
- background: rgba(255, 255, 255, 0.92);
- border-color: rgba(191, 219, 254, 0.95);
- color: $primary;
- box-shadow: 0 4px 12px -2px rgba(15, 23, 42, 0.08), inset 0 1px 1px rgba(255, 255, 255, 1);
- transform: translateY(-0.5px);
-
- .material-symbols-outlined:not(.feedback-done) {
- color: $primary;
- }
- }
-
- &:active:not(:disabled) {
- transform: scale(0.97);
- }
-
- &:disabled {
- opacity: 0.55;
- cursor: not-allowed;
- transform: none;
- }
-}
-
-:host {
- display: block;
- min-height: 100vh;
- width: 100%;
- background-color: $bg-base;
- font-family: $font-sans;
- color: $text-main;
- box-sizing: border-box;
- -webkit-font-smoothing: antialiased;
-}
-
-*, *::before, *::after {
- box-sizing: border-box;
-}
-
-.home-container {
- position: relative;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- background-color: $bg-base;
- overflow-x: hidden;
-
- /* Dynamic Ambient Background with Intelligent Glow */
- .aurora-background {
- display: block;
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- pointer-events: none;
- z-index: 1;
- overflow: hidden;
- opacity: 0.9;
- transition: opacity 0.8s ease;
-
- /* The glow orbs are pre-softened radial gradients. A `filter: blur(90px+)`
- on top of them looked nearly identical but forced the GPU to re-blur
- half-megapixel layers on every frame of the float animations — the
- gradients below bake the same softness in for free. */
- .glow {
- position: absolute;
- border-radius: 50%;
- transform: translate3d(0, 0, 0);
- will-change: transform, opacity;
- pointer-events: none;
- }
-
- .glow-1 {
- top: -140px;
- left: 10%;
- width: 520px;
- height: 520px;
- background: radial-gradient(circle, rgba(59, 130, 246, 0.12) 0%, rgba(99, 102, 241, 0.04) 45%, transparent 82%);
- animation: floatGlow1 22s ease-in-out infinite alternate;
- }
-
- .glow-2 {
- top: -100px;
- right: 12%;
- width: 560px;
- height: 560px;
- background: radial-gradient(circle, rgba(147, 51, 234, 0.08) 0%, rgba(219, 39, 119, 0.03) 45%, transparent 82%);
- animation: floatGlow2 26s ease-in-out infinite alternate;
- }
-
- .glow-3 {
- top: 280px;
- left: 30%;
- width: 460px;
- height: 460px;
- background: radial-gradient(circle, rgba(6, 182, 212, 0.08) 0%, rgba(59, 130, 246, 0.03) 45%, transparent 82%);
- animation: floatGlow3 20s ease-in-out infinite alternate;
- }
-
- .glow-pending {
- top: -120px;
- left: 15%;
- right: 15%;
- margin: 0 auto;
- width: 720px;
- height: 520px;
- background: radial-gradient(circle, rgba(245, 158, 11, 0.09) 0%, rgba(217, 119, 6, 0.035) 38%, transparent 82%);
- opacity: 0;
- transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1);
- animation: readyPulseGlow 9s ease-in-out infinite alternate;
- pointer-events: none;
- }
-
- .glow-ready {
- top: -120px;
- left: 15%;
- right: 15%;
- margin: 0 auto;
- width: 700px;
- height: 520px;
- background: radial-gradient(circle, rgba(16, 185, 129, 0.14) 0%, rgba(6, 182, 212, 0.08) 38%, transparent 82%);
- opacity: 0;
- transition: opacity 1s cubic-bezier(0.4, 0, 0.2, 1);
- animation: readyPulseGlow 7s ease-in-out infinite alternate;
- pointer-events: none;
- }
-
- .glow-pro {
- top: -100px;
- left: 20%;
- right: 20%;
- margin: 0 auto;
- width: 680px;
- height: 520px;
- background: radial-gradient(circle, rgba(99, 102, 241, 0.18) 0%, rgba(147, 51, 234, 0.08) 38%, transparent 82%);
- opacity: 0;
- transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1);
- pointer-events: none;
- }
- }
-
- &.system-pending {
- .aurora-background {
- .glow-pending {
- opacity: 1;
- }
- .glow-1 {
- background: radial-gradient(circle, rgba(245, 158, 11, 0.07) 0%, rgba(99, 102, 241, 0.03) 55%, transparent 75%);
- }
- }
- }
-
- &.system-ready {
- .aurora-background {
- .glow-pending {
- opacity: 0;
- }
- .glow-ready {
- opacity: 1;
- }
- .glow-1 {
- background: radial-gradient(circle, rgba(16, 185, 129, 0.10) 0%, rgba(59, 130, 246, 0.05) 55%, transparent 75%);
- }
- }
- }
-
- &.pro-theme-active {
- .aurora-background {
- .glow-pro {
- opacity: 1;
- }
- .glow-ready {
- opacity: 0.35;
- }
- }
- }
-
- /* ========================================================================= */
- /* MAIN CONTENT LAYOUT */
- /* ========================================================================= */
- .home-content {
- position: relative;
- z-index: 5;
- max-width: 960px;
- width: 100%;
- margin: 0 auto;
- padding: 36px 24px 60px;
- display: flex;
- flex-direction: column;
- gap: 18px;
-
- /* Hero section */
- .hero-section {
- text-align: center;
- padding: 16px 0 6px;
- margin: 0;
-
- .hero-title {
- font-family: 'Outfit', 'Google Sans', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- font-size: 32px;
- font-weight: 600;
- letter-spacing: -0.035em;
- line-height: 1.2;
- color: #0f172a;
- margin: 0;
- }
-
- .hero-subtitle {
- font-size: 14.5px;
- color: #64748b;
- font-weight: 400;
- margin: 8px 0 0;
- letter-spacing: -0.01em;
- }
- }
-
- /* Top Mode Stepper Tabs Switcher (Frosted Glass Segmented Control) */
- /* Top Mode Stepper Tabs Switcher (Frosted Glass Segmented Control matching top-left nav) */
- .mode-tabs-container {
- display: flex;
- justify-content: center;
- margin-bottom: 6px;
-
- .mode-tabs {
- display: grid;
- grid-template-columns: 1fr 1fr;
- width: 100%;
- max-width: 520px;
- box-sizing: border-box;
- background: rgba(255, 255, 255, 0.72);
- backdrop-filter: blur(12px) saturate(180%);
- -webkit-backdrop-filter: blur(12px) saturate(180%);
- padding: 3px;
- border-radius: 9999px;
- border: 1px solid rgba(226, 232, 240, 0.85);
- gap: 3px;
- box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(0, 0, 0, 0.02), inset 0 1px 1px rgba(255, 255, 255, 0.9);
- user-select: none;
- font-family: 'Google Sans', 'Google Sans Text', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- transition: background 0.25s cubic-bezier(0.4, 0, 0.2, 1),
- box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1),
- border-color 0.25s cubic-bezier(0.4, 0, 0.2, 1);
-
- &:hover {
- background: rgba(255, 255, 255, 0.85);
- box-shadow: 0 4px 14px -2px rgba(15, 23, 42, 0.08), 0 2px 4px rgba(0, 0, 0, 0.02), inset 0 1px 1px rgba(255, 255, 255, 1);
- border-color: rgba(203, 213, 225, 0.9);
- }
-
- .mode-tab-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 7px;
- height: 32px;
- padding: 0 14px;
- box-sizing: border-box;
- border: none;
- background: transparent;
- border-radius: 9999px;
- cursor: pointer;
- font-family: inherit;
- font-size: 13px;
- font-weight: 600;
- color: #475569;
- line-height: 1;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- outline: none;
- white-space: nowrap;
- min-width: 0;
-
- @media (max-width: 560px) {
- white-space: normal;
- height: auto;
- min-height: 32px;
- padding: 6px 10px;
- gap: 5px;
- font-size: 12px;
- line-height: 1.2;
- text-align: center;
- }
-
- .tab-icon {
- font-size: 16px;
- width: 16px;
- height: 16px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- color: #64748b;
- flex-shrink: 0;
- transition: color 0.15s ease, transform 0.15s ease;
-
- &.icon-ready {
- color: #059669;
- }
-
- &.icon-locked {
- color: #94a3b8;
- font-size: 15px;
- }
- }
-
- .tab-label {
- color: #475569;
- min-width: 0;
- transition: color 0.15s ease;
- }
-
- &:hover:not(.active) {
- background: rgba(255, 255, 255, 0.7);
- color: #0f172a;
-
- .tab-icon {
- color: #1e293b;
-
- &.icon-ready {
- color: #059669;
- }
- }
- .tab-label {
- color: #0f172a;
- }
- }
-
- &.active {
- background: #ffffff !important;
- color: #0f172a !important;
- font-weight: 700;
- box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04), inset 0 1px 0 #ffffff;
-
- .tab-icon {
- color: #1a73e8;
-
- &.icon-ready {
- color: #059669;
- }
- }
-
- .tab-label {
- color: #0f172a !important;
- }
- }
-
- &:active {
- transform: scale(0.98);
- }
- }
- }
- }
-
- /* ======================================================================= */
- /* VIEW 1: PREREQUISITES & SYSTEM SETUP GUIDE */
- /* ======================================================================= */
- .setup-guide-view {
- display: flex;
- flex-direction: column;
- gap: 16px;
- animation: fadeIn 0.2s ease-out;
-
- /* Unified Step Card Container */
- .guide-card {
- background: $card-bg;
- border: 1px solid $card-border;
- border-radius: 14px;
- box-shadow: $card-shadow;
- overflow: hidden;
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
-
- &:hover {
- border-color: $card-border-hover;
- }
-
- .guide-card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 14px;
- padding: 16px 20px;
- border-bottom: 1px solid #f1f5f9;
- background: #ffffff;
-
- .step-badge {
- width: 32px;
- height: 32px;
- border-radius: 8px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- font-size: 13.5px;
- font-weight: 700;
- background: #f1f5f9;
- color: $text-secondary;
- flex-shrink: 0;
- transition: all 0.2s ease;
-
- span, .step-check-icon {
- font-size: 18px;
- font-weight: 700;
- color: inherit;
- }
-
- &.step-pass {
- background: $emerald-light;
- color: $emerald;
- }
- }
-
- .step-info {
- flex: 1;
- min-width: 0;
-
- .step-title-row {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 2px;
- flex-wrap: wrap;
-
- .step-title {
- margin: 0;
- font-size: 15px;
- font-weight: 600;
- color: $text-main;
- }
-
- .status-pill {
- font-size: 11px;
- font-weight: 600;
- padding: 2px 8px;
- border-radius: 12px;
- line-height: 1.3;
- height: 22px;
- display: inline-flex;
- align-items: center;
-
- &.pill-pass {
- background: $emerald-light;
- color: $emerald;
- border: 1px solid $emerald-border;
- }
-
- &.pill-warn {
- background: $amber-light;
- color: $amber-dark;
- border: 1px solid $amber-border;
- }
-
- &.pill-fail {
- background: $rose-light;
- color: $rose-dark;
- border: 1px solid $rose-border;
- }
- }
- }
-
- .step-desc {
- margin: 0;
- font-size: 12.5px;
- color: $text-secondary;
- line-height: 1.4;
- }
- }
-
- .btn-restart-adb,
- .btn-header-action {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- padding: 0 13px;
- height: 32px;
- @include glass-secondary-btn;
- border-radius: 8px;
- font-family: inherit;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- flex-shrink: 0;
-
- .material-symbols-outlined {
- font-size: 15px;
- color: $text-muted;
- transition: transform 0.2s ease, color 0.15s ease;
- }
-
- .feedback-done {
- color: $emerald !important;
- font-size: 16px;
- }
-
- .feedback-text {
- color: $emerald;
- font-weight: 600;
- }
-
- .spinning {
- animation: spin 1s linear infinite;
- }
- }
- }
-
- .guide-card-body {
- padding: 18px 20px;
- display: flex;
- flex-direction: column;
- gap: 14px;
- }
- }
-
- /* --------------------------------------------------------------------- */
- /* STEP 1: ONE-CLICK INSTALLER & ENVIRONMENT MATRIX */
- /* --------------------------------------------------------------------- */
- .one-click-install-bar {
- background: #f0f9ff;
- border: 1px solid #bae6fd;
- border-radius: 10px;
- padding: 12px 16px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 14px;
-
- .install-bar-left {
- display: flex;
- align-items: center;
- gap: 12px;
- min-width: 0;
-
- .install-bar-icon-box {
- width: 32px;
- height: 32px;
- border-radius: 8px;
- background: #0284c7;
- color: #ffffff;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-
- .bolt-icon {
- font-size: 18px;
- }
- }
-
- .install-bar-text {
- display: flex;
- flex-direction: column;
- gap: 2px;
- min-width: 0;
-
- .install-bar-title-row {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
-
- .install-bar-title {
- font-size: 13px;
- font-weight: 600;
- color: $text-main;
- }
-
- .os-selector-pills {
- display: inline-flex;
- align-items: center;
- background: rgba(255, 255, 255, 0.65);
- backdrop-filter: blur(12px) saturate(180%);
- -webkit-backdrop-filter: blur(12px) saturate(180%);
- border: 1px solid rgba(226, 232, 240, 0.85);
- border-radius: 8px;
- padding: 2px;
- gap: 2px;
- box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.8);
-
- .os-pill {
- border: none;
- background: transparent;
- font-size: 11px;
- font-weight: 600;
- color: $text-secondary;
- padding: 3px 9px;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.15s ease;
-
- &:hover {
- color: $text-main;
- background: rgba(255, 255, 255, 0.5);
- }
-
- &.active {
- background: rgba(26, 115, 232, 0.92);
- backdrop-filter: blur(10px);
- -webkit-backdrop-filter: blur(10px);
- color: #ffffff !important;
- box-shadow: 0 2px 6px rgba(26, 115, 232, 0.3), inset 0 1px 1px rgba(255, 255, 255, 0.35);
- }
- }
- }
- }
-
- .install-bar-desc {
- font-size: 12px;
- color: $text-secondary;
- line-height: 1.4;
- margin-top: 2px;
- }
- }
- }
-
- .code-box {
- margin: 0;
- flex-shrink: 0;
- }
-
- @media (max-width: 860px) {
- flex-direction: column;
- align-items: stretch;
- }
- }
-
- /* 4-Item 2x2 Symmetric Matrix Grid */
- .env-matrix-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 12px;
-
- @media (max-width: 640px) {
- grid-template-columns: 1fr;
- }
-
- .env-item-card {
- background: #ffffff;
- border: 1px solid $card-border;
- border-radius: 10px;
- padding: 12px 14px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- gap: 8px;
- transition: border-color 0.18s ease, box-shadow 0.18s ease;
- min-width: 0;
-
- &:hover {
- border-color: $card-border-hover;
- }
-
- .item-header {
- display: flex;
- align-items: center;
- gap: 10px;
- min-width: 0;
-
- .item-icon-box {
- width: 32px;
- height: 32px;
- border-radius: 8px;
- background: #f8fafc;
- border: 1px solid $card-border;
- color: $text-secondary;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- transition: all 0.15s ease;
-
- .item-icon {
- font-size: 17px;
- }
- }
-
- .item-title-group {
- display: flex;
- flex-direction: column;
- gap: 1px;
- flex: 1;
- min-width: 0;
-
- .item-name {
- font-size: 13px;
- font-weight: 600;
- color: $text-main;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .item-subtitle {
- font-size: 11.5px;
- color: $text-muted;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
-
- .item-status {
- display: flex;
- align-items: center;
- margin-left: auto;
- flex-shrink: 0;
-
- .status-dot {
- width: 7px;
- height: 7px;
- border-radius: 50%;
-
- &.dot-pass {
- background: #10b981;
- }
-
- &.dot-fail {
- background: #ef4444;
- }
-
- &.dot-warn {
- background: #f59e0b;
- }
- }
- }
- }
-
- .item-body {
- display: flex;
- flex-direction: column;
- gap: 4px;
- min-width: 0;
-
- .item-action-box {
- display: flex;
- flex-direction: column;
- gap: 4px;
- min-width: 0;
- }
- }
-
- &.item-fail {
- background: #fff8f8;
- border-color: $rose-border;
-
- .item-icon-box {
- background: $rose-light;
- color: $rose;
- border-color: $rose-border;
- }
- }
- }
- }
-
- /* --------------------------------------------------------------------- */
- /* STEP 2: AI MODEL & CREDENTIALS CONFIGURATION */
- /* --------------------------------------------------------------------- */
- .model-choice-cards-row {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
- gap: 12px;
-
- .choice-card {
- border: 1px solid $card-border;
- border-radius: 10px;
- padding: 14px 16px;
- background: #f8fafc;
- cursor: pointer;
- transition: all 0.18s ease;
- user-select: none;
-
- .choice-card-top {
- display: flex;
- align-items: flex-start;
- gap: 12px;
-
- .choice-radio {
- margin-top: 2px;
- flex-shrink: 0;
-
- .radio-dot {
- display: block;
- width: 16px;
- height: 16px;
- border-radius: 50%;
- border: 1.5px solid #cbd5e1;
- background: #ffffff;
- transition: all 0.15s ease;
-
- &.checked {
- border-color: $primary;
- background: $primary;
- box-shadow: inset 0 0 0 3px #ffffff;
- }
- }
- }
-
- .choice-icon-wrap {
- width: 34px;
- height: 34px;
- border-radius: 8px;
- background: #ffffff;
- border: 1px solid $card-border;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-
- .brand-svg {
- width: 18px;
- height: 18px;
- }
-
- &.icon-gear {
- color: $text-secondary;
-
- .material-symbols-outlined {
- font-size: 18px;
- }
- }
- }
-
- .choice-text {
- flex: 1;
- min-width: 0;
-
- .choice-title-row {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
-
- .choice-title {
- font-size: 13.5px;
- font-weight: 600;
- color: $text-main;
- }
-
- .tag-badge {
- font-size: 10.5px;
- font-weight: 600;
- padding: 1px 7px;
- border-radius: 10px;
- line-height: 1.3;
-
- &.tag-rec {
- background: $primary-light;
- color: $primary;
- border: 1px solid $primary-border;
- }
-
- &.tag-ready {
- background: $emerald-light;
- color: $emerald;
- border: 1px solid $emerald-border;
- }
- }
- }
-
- .choice-desc {
- margin: 3px 0 0 0;
- font-size: 12px;
- color: $text-secondary;
- line-height: 1.4;
-
- code {
- font-family: $font-mono;
- background: #ffffff;
- border: 1px solid $card-border;
- padding: 1px 4px;
- border-radius: 4px;
- color: $primary-dark;
- font-size: 11px;
- }
- }
- }
- }
-
- &:hover:not(.selected) {
- border-color: $card-border-hover;
- background: #ffffff;
- }
-
- &.selected {
- border-color: $primary;
- background: #ffffff;
- box-shadow: 0 0 0 1px $primary;
-
- .choice-icon-wrap {
- border-color: $primary-border;
-
- &.icon-gear {
- color: $primary;
- }
- }
- }
- }
- }
-
- .credentials-input-box {
- background: #f8fafc;
- border: 1px solid $card-border;
- border-radius: 10px;
- padding: 14px 16px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- animation: fadeIn 0.15s ease-out;
-
- &.ocr-box {
- background: #ffffff;
- border: 1px solid $card-border;
- }
-
- .input-box-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-wrap: wrap;
- gap: 8px;
-
- .header-label {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 12.5px;
- font-weight: 600;
- color: $text-main;
-
- .label-icon {
- font-size: 16px;
- color: $primary;
- }
- }
-
- .btn-header-link {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- font-size: 11.5px;
- font-weight: 600;
- color: $primary;
- text-decoration: none;
- padding: 3px 10px;
- height: 26px;
- background: rgba(239, 246, 255, 0.75);
- backdrop-filter: blur(10px);
- -webkit-backdrop-filter: blur(10px);
- border: 1px solid rgba(191, 219, 254, 0.85);
- border-radius: 6px;
- box-shadow: 0 1px 3px rgba(37, 99, 235, 0.06), inset 0 1px 1px rgba(255, 255, 255, 0.8);
- transition: all 0.15s ease;
-
- .material-symbols-outlined {
- font-size: 13px;
- color: $primary;
- }
-
- &:hover {
- background: rgba(219, 234, 254, 0.9);
- color: $primary-dark;
- border-color: $primary;
- box-shadow: 0 2px 6px rgba(37, 99, 235, 0.15);
- transform: translateY(-0.5px);
- }
- }
- }
-
- .unified-key-row {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
-
- .unified-key-wrapper {
- flex: 1 1 260px;
- min-width: 0;
- display: flex;
- align-items: center;
- background: rgba(255, 255, 255, 0.75);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- border: 1px solid $card-border;
- border-radius: 8px;
- padding: 0 10px;
- height: 36px;
- transition: border-color 0.15s ease, box-shadow 0.15s ease;
-
- &:focus-within {
- border-color: $primary;
- box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.12);
- }
-
- .key-prefix-icon {
- font-size: 16px;
- color: $text-light;
- margin-right: 6px;
- flex-shrink: 0;
- }
-
- .unified-key-input {
- flex: 1;
- min-width: 0;
- border: none;
- outline: none;
- font-family: $font-mono;
- font-size: 12.5px;
- color: $text-main;
- background: transparent;
- padding: 4px 0;
-
- &.is-masked {
- letter-spacing: 2px;
- font-weight: 700;
- font-size: 13px;
- }
-
- &::placeholder {
- color: $text-light;
- font-family: $font-sans;
- font-size: 12px;
- letter-spacing: normal;
- font-weight: normal;
- }
- }
-
- .btn-icon-action {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 26px;
- height: 26px;
- background: transparent;
- border: none;
- border-radius: 4px;
- color: $text-muted;
- cursor: pointer;
- transition: all 0.15s ease;
- flex-shrink: 0;
-
- .material-symbols-outlined {
- font-size: 15px;
- }
-
- .text-emerald {
- color: $emerald !important;
- }
-
- &:hover {
- background: rgba(241, 245, 249, 0.8);
- color: $text-main;
- }
- }
- }
-
- .btn-unified-save {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 5px;
- padding: 0 18px;
- height: 36px;
- @include glass-primary-btn;
- border-radius: 8px;
- font-family: inherit;
- font-size: 12.5px;
- font-weight: 600;
- cursor: pointer;
- white-space: nowrap;
- flex-shrink: 0;
-
- .material-symbols-outlined {
- font-size: 16px;
- }
-
- .spin {
- animation: spin 1s linear infinite;
- }
- }
-
- .btn-unified-test {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- padding: 0 15px;
- height: 36px;
- background: rgba(239, 246, 255, 0.7);
- backdrop-filter: blur(14px) saturate(180%);
- -webkit-backdrop-filter: blur(14px) saturate(180%);
- color: $primary;
- border: 1px solid rgba(191, 219, 254, 0.85);
- border-radius: 8px;
- box-shadow: 0 2px 6px -1px rgba(37, 99, 235, 0.08), inset 0 1px 1px rgba(255, 255, 255, 0.9);
- font-family: inherit;
- font-size: 12.5px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- white-space: nowrap;
- flex-shrink: 0;
-
- .material-symbols-outlined {
- font-size: 15px;
- color: $primary;
- }
-
- &:hover:not(:disabled) {
- background: rgba(219, 234, 254, 0.9);
- border-color: $primary;
- color: $primary-dark;
- box-shadow: 0 4px 12px -2px rgba(37, 99, 235, 0.15), inset 0 1px 1px rgba(255, 255, 255, 1);
- transform: translateY(-0.5px);
- }
-
- &:active:not(:disabled) {
- transform: scale(0.97);
- }
-
- &:disabled {
- opacity: 0.55;
- cursor: not-allowed;
- transform: none;
- }
-
- .spin {
- animation: spin 1s linear infinite;
- }
- }
- }
-
- .feedback-msg {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 6px 10px;
- border-radius: 6px;
- font-size: 12px;
- font-weight: 600;
-
- .material-symbols-outlined {
- font-size: 15px;
- }
-
- &.success {
- background: $emerald-light;
- color: $emerald-dark;
- border: 1px solid $emerald-border;
- }
-
- &.error {
- background: $rose-light;
- color: $rose-dark;
- border: 1px solid $rose-border;
- }
- }
- }
-
- .ocr-enhancement-card {
- background: #ffffff;
- border: 1px solid $card-border;
- border-radius: 10px;
- overflow: hidden;
- margin-top: 2px;
- transition: border-color 0.18s ease;
-
- .ocr-card-toggle-bar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 14px;
- background: #ffffff;
- cursor: pointer;
- user-select: none;
- transition: background 0.15s ease;
-
- &:hover {
- background: #f8fafc;
- }
-
- .toggle-bar-left {
- display: flex;
- align-items: center;
- gap: 10px;
-
- .icon-ocr {
- font-size: 18px;
- color: $primary;
- flex-shrink: 0;
- }
-
- .toggle-bar-titles {
- display: flex;
- align-items: center;
- gap: 8px;
-
- .ocr-main-title {
- font-size: 13px;
- font-weight: 600;
- color: $text-main;
- }
- }
- }
-
- .toggle-bar-right {
- display: flex;
- align-items: center;
- gap: 10px;
-
- .status-dot {
- width: 7px;
- height: 7px;
- border-radius: 50%;
-
- &.dot-pass {
- background: #10b981;
- }
- }
-
- .caret-icon {
- font-size: 18px;
- color: $text-muted;
- flex-shrink: 0;
- }
- }
- }
-
- .ocr-content-body {
- padding: 12px 14px;
- border-top: 1px solid #f1f5f9;
- display: flex;
- flex-direction: column;
- gap: 10px;
- background: #f8fafc;
- animation: fadeIn 0.15s ease-out;
-
- .ocr-help-desc {
- margin: 0;
- font-size: 12px;
- color: $text-secondary;
- line-height: 1.45;
- }
-
- .credentials-input-box.ocr-box {
- margin: 0;
- padding: 12px;
- background: #ffffff;
- border: 1px solid $card-border;
- border-radius: 8px;
- }
- }
- }
-
- .custom-config-inspector {
- display: flex;
- flex-direction: column;
- gap: 12px;
- animation: fadeIn 0.15s ease-out;
-
- .inspector-card {
- background: #ffffff;
- border: 1px solid $card-border;
- border-radius: 10px;
- overflow: hidden;
-
- .inspector-card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-wrap: wrap;
- gap: 8px;
- padding: 10px 14px;
- background: #ffffff;
- border-bottom: 1px solid #f1f5f9;
-
- .inspector-header-left {
- display: flex;
- align-items: center;
- gap: 8px;
-
- .header-icon {
- font-size: 18px;
- color: $primary;
- }
-
- .inspector-header-titles {
- display: flex;
- flex-direction: column;
-
- .inspector-title {
- font-size: 13px;
- font-weight: 600;
- color: $text-main;
- }
-
- .inspector-subtitle {
- font-size: 11.5px;
- color: $text-secondary;
-
- code {
- font-family: $font-mono;
- background: #f1f5f9;
- padding: 1px 4px;
- border-radius: 4px;
- color: $primary-dark;
- }
- }
- }
- }
-
- .inspector-header-actions {
- display: flex;
- align-items: center;
- gap: 6px;
- }
-
- .btn-file-action {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 0 10px;
- height: 28px;
- @include glass-secondary-btn;
- border-radius: 6px;
- font-family: inherit;
- font-size: 11.5px;
- font-weight: 600;
- cursor: pointer;
-
- .material-symbols-outlined {
- font-size: 13px;
- }
-
- &.btn-toggle-code {
- background: rgba(239, 246, 255, 0.75);
- color: $primary;
- border-color: rgba(191, 219, 254, 0.85);
-
- &:hover {
- background: rgba(219, 234, 254, 0.9);
- color: $primary-dark;
- border-color: $primary;
- }
- }
- }
- }
-
- .inspector-summary-row {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
- padding: 12px 14px;
- background: #ffffff;
-
- .summary-metric {
- display: flex;
- flex-direction: column;
- gap: 4px;
-
- .metric-label {
- font-size: 10.5px;
- font-weight: 700;
- color: $text-muted;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
-
- .metric-val-pill {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- padding: 4px 10px;
- background: #f8fafc;
- border: 1px solid $card-border;
- border-radius: 6px;
- font-family: $font-mono;
- font-size: 12px;
- color: $text-main;
-
- .provider-dot {
- width: 7px;
- height: 7px;
- border-radius: 50%;
- background: $primary;
- }
-
- .provider-name {
- font-weight: 700;
- color: $primary;
- }
-
- .model-divider {
- color: $text-light;
- }
-
- .model-name {
- font-weight: 600;
- color: $text-main;
- }
-
- &.secondary {
- background: #f8fafc;
- color: $text-secondary;
- font-size: 11.5px;
- }
- }
- }
- }
-
- .jsonc-viewer-box {
- border-top: 1px solid $card-border;
- background: #0f172a;
- max-height: 260px;
- overflow-y: auto;
- padding: 12px 14px;
-
- .jsonc-code {
- margin: 0;
- font-family: $font-mono;
- font-size: 11.5px;
- line-height: 1.5;
- color: #e2e8f0;
- white-space: pre-wrap;
- word-break: break-all;
- }
- }
-
- .env-keys-table {
- display: flex;
- flex-direction: column;
-
- .env-key-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-wrap: wrap;
- gap: 8px;
- padding: 8px 14px;
- border-bottom: 1px solid #f1f5f9;
- transition: background 0.12s ease;
-
- &:last-child {
- border-bottom: none;
- }
-
- &:hover {
- background: #f8fafc;
- }
-
- .env-name-col {
- display: flex;
- align-items: center;
- gap: 8px;
-
- .env-key-name {
- font-family: $font-mono;
- font-size: 12px;
- font-weight: 600;
- color: $text-main;
- background: #f1f5f9;
- padding: 2px 6px;
- border-radius: 4px;
- }
- }
-
- .env-status-col {
- display: flex;
- align-items: center;
-
- .badge-env-pass {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- font-size: 11px;
- font-weight: 600;
- color: $emerald;
- background: $emerald-light;
- border: 1px solid $emerald-border;
- padding: 2px 7px;
- border-radius: 6px;
-
- .material-symbols-outlined {
- font-size: 13px;
- }
-
- .key-preview {
- font-family: $font-mono;
- font-size: 10.5px;
- color: $emerald-dark;
- margin-left: 2px;
- }
- }
-
- .badge-env-unset {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- font-size: 11px;
- font-weight: 600;
- color: $text-light;
- background: #f8fafc;
- border: 1px solid $card-border;
- padding: 2px 7px;
- border-radius: 6px;
-
- .material-symbols-outlined {
- font-size: 13px;
- }
- }
- }
- }
- }
- }
- }
-
- /* --------------------------------------------------------------------- */
- /* STEP 3: ANDROID DEVICE & EMULATOR CONNECTION */
- /* --------------------------------------------------------------------- */
- .connected-device-panel {
- display: flex;
- flex-direction: column;
- gap: 10px;
-
- .device-hero-card {
- display: flex;
- align-items: center;
- gap: 16px;
- padding: 16px 20px;
- background: #ffffff;
- border: 1px solid #e2e8f0;
- border-radius: 12px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02), 0 4px 12px rgba(0, 0, 0, 0.02);
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-
- &:hover {
- border-color: #cbd5e1;
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04), 0 8px 18px rgba(0, 0, 0, 0.03);
- }
-
- .device-avatar {
- width: 44px;
- height: 44px;
- border-radius: 10px;
- background: #f1f5f9;
- border: 1px solid #e2e8f0;
- color: #334155;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- transition: all 0.2s ease;
-
- .device-hero-icon {
- font-size: 22px;
- color: #334155 !important;
- }
- }
-
- .device-hero-details {
- display: flex;
- flex-direction: column;
- gap: 4px;
- min-width: 0;
- flex: 1;
-
- .device-hero-name {
- font-size: 14.5px;
- font-weight: 600;
- color: $text-main;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- letter-spacing: -0.01em;
- }
-
- .device-hero-meta {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- color: $text-muted;
- flex-wrap: wrap;
-
- .meta-dot {
- color: $text-light;
- }
-
- .remote-endpoint-label {
- color: $primary;
- font-family: $font-mono;
- font-size: 11px;
- }
- }
- }
-
- .device-hero-status {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- font-weight: 500;
- color: $emerald-dark;
- margin-left: auto;
- flex-shrink: 0;
-
- .status-dot {
- width: 7px;
- height: 7px;
- border-radius: 50%;
- background: #10b981;
- box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
- animation: statusDotPulse 2s infinite cubic-bezier(0.4, 0, 0.6, 1);
- }
- }
-
- }
-
- .multi-device-switcher {
- display: flex;
- flex-direction: column;
- gap: 5px;
-
- .switcher-label {
- font-size: 11px;
- font-weight: 700;
- color: $text-muted;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
-
- .switcher-chips {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
-
- .device-chip-btn {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- padding: 0 10px;
- height: 30px;
- background: #ffffff;
- border: 1px solid $card-border;
- border-radius: 6px;
- font-family: inherit;
- font-size: 11.5px;
- font-weight: 600;
- color: $text-secondary;
- cursor: pointer;
- transition: all 0.15s ease;
-
- .chip-icon {
- font-size: 14px;
- }
-
- .active-check {
- color: $emerald;
- font-size: 10.5px;
- font-weight: 700;
- }
-
- &.selected {
- background: $emerald-light;
- border-color: $emerald;
- color: $emerald;
- }
-
- &:hover:not(.selected) {
- background: #f8fafc;
- border-color: $card-border-hover;
- }
- }
- }
- }
- }
-
- /* Live Emulator Boot Tracker & Diagnostic Card */
- .emulator-boot-tracker-container,
- .emulator-failed-container {
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-bottom: 8px;
- }
-
- .emulator-boot-tracker-card {
- display: flex;
- flex-direction: column;
- gap: 16px;
- padding: 20px;
- background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
- border: 1.5px solid rgba(37, 99, 235, 0.25);
- border-radius: 12px;
- box-shadow: 0 4px 16px rgba(37, 99, 235, 0.06);
-
- .tracker-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 16px;
- flex-wrap: wrap;
-
- .tracker-title-group {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- flex: 1;
- min-width: 260px;
-
- .tracker-icon-box {
- width: 36px;
- height: 36px;
- border-radius: 10px;
- background: rgba(37, 99, 235, 0.1);
- color: $primary;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-
- .spinning-icon {
- font-size: 20px;
- animation: spin 1.2s linear infinite;
- }
- }
-
- .tracker-titles {
- display: flex;
- flex-direction: column;
- gap: 4px;
-
- .tracker-main-heading {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
-
- .tracker-title {
- margin: 0;
- font-size: 15px;
- font-weight: 700;
- color: $text-main;
-
- .avd-highlight {
- color: $primary;
- font-family: 'JetBrains Mono', monospace, sans-serif;
- }
- }
-
- .stage-tag-badge {
- display: inline-flex;
- align-items: center;
- padding: 2px 8px;
- border-radius: 12px;
- background: rgba(37, 99, 235, 0.12);
- color: $primary;
- font-size: 11px;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
- }
-
- .tracker-subtitle {
- margin: 0;
- font-size: 13px;
- color: $text-secondary;
- line-height: 1.4;
- }
- }
- }
-
- .tracker-actions {
- display: flex;
- align-items: center;
- gap: 8px;
-
- .btn-toggle-logs {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 6px 13px;
- @include glass-secondary-btn;
- border-radius: 6px;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
-
- .material-symbols-outlined {
- font-size: 15px;
- }
-
- &.active {
- background: rgba(239, 246, 255, 0.9);
- color: $primary;
- border-color: rgba(37, 99, 235, 0.4);
- box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12);
- }
- }
-
- .btn-stop-emu {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- padding: 6px 13px;
- background: rgba(254, 242, 242, 0.75);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- border: 1px solid rgba(254, 202, 202, 0.85);
- border-radius: 6px;
- color: $rose;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- box-shadow: 0 1px 3px rgba(220, 38, 38, 0.06), inset 0 1px 1px rgba(255, 255, 255, 0.8);
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-
- .material-symbols-outlined {
- font-size: 15px;
- }
-
- &:hover {
- background: rgba(254, 226, 226, 0.95);
- border-color: $rose;
- box-shadow: 0 4px 12px -2px rgba(220, 38, 38, 0.15);
- transform: translateY(-0.5px);
- }
-
- &:active {
- transform: scale(0.97);
- }
- }
- }
- }
-
- /* Boot Stepper */
- .boot-stepper {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 14px;
- background: #ffffff;
- border: 1px solid $card-border;
- border-radius: 8px;
-
- .step-item {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 12px;
- font-weight: 600;
- color: $text-muted;
-
- .step-circle {
- width: 22px;
- height: 22px;
- border-radius: 50%;
- background: #f1f5f9;
- border: 1.5px solid $card-border;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 11px;
- font-weight: 700;
- color: $text-muted;
- transition: all 0.2s ease;
-
- .check-icon {
- font-size: 14px;
- color: #ffffff;
- }
-
- .spinner-tiny {
- width: 11px;
- height: 11px;
- border: 2px solid rgba(37, 99, 235, 0.3);
- border-top-color: $primary;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- }
- }
-
- .step-label {
- white-space: nowrap;
- }
-
- &.active {
- color: $primary;
-
- .step-circle {
- border-color: $primary;
- background: rgba(37, 99, 235, 0.1);
- color: $primary;
- }
- }
-
- &.done {
- color: $emerald;
-
- .step-circle {
- border-color: $emerald;
- background: $emerald;
- color: #ffffff;
- }
- }
- }
-
- .step-line {
- flex: 1;
- height: 2px;
- background: $card-border;
- margin: 0 10px;
- border-radius: 2px;
- transition: all 0.3s ease;
-
- &.done {
- background: $emerald;
- }
- }
- }
-
- /* Progress Bar */
- .progress-bar-container {
- display: flex;
- flex-direction: column;
- gap: 6px;
-
- .progress-bar-track {
- width: 100%;
- height: 8px;
- background: #e2e8f0;
- border-radius: 6px;
- overflow: hidden;
-
- .progress-bar-fill {
- height: 100%;
- background: linear-gradient(90deg, #3b82f6 0%, #10b981 100%);
- border-radius: 6px;
- transition: width 0.4s ease;
- }
- }
-
- .progress-bar-meta {
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: 12px;
- font-weight: 600;
-
- .progress-percent {
- color: $primary;
- }
-
- .progress-timer {
- display: flex;
- align-items: center;
- gap: 4px;
- color: $text-secondary;
-
- .timer-icon {
- font-size: 14px;
- }
- }
- }
- }
-
- /* Tip Banner */
- .boot-tip-banner {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px 12px;
- background: rgba(37, 99, 235, 0.05);
- border-left: 3px solid $primary;
- border-radius: 0 6px 6px 0;
- font-size: 12px;
- color: $text-secondary;
-
- .tip-icon {
- font-size: 16px;
- color: $primary;
- flex-shrink: 0;
- }
- }
-
- /* Logs Drawer */
- .boot-logs-drawer {
- display: flex;
- flex-direction: column;
- border: 1px solid #1e293b;
- border-radius: 8px;
- background: #0f172a;
- overflow: hidden;
-
- .logs-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 8px 12px;
- background: #1e293b;
- color: #94a3b8;
- font-size: 12px;
- font-weight: 600;
-
- .logs-header-left {
- display: flex;
- align-items: center;
- gap: 6px;
-
- .material-symbols-outlined {
- font-size: 14px;
- }
- }
-
- .btn-copy-logs {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 2px 8px;
- background: #334155;
- border: none;
- border-radius: 4px;
- color: #f8fafc;
- font-size: 11px;
- font-weight: 600;
- cursor: pointer;
-
- .material-symbols-outlined {
- font-size: 12px;
- }
-
- &:hover {
- background: #475569;
- }
- }
- }
-
- .logs-body {
- margin: 0;
- padding: 12px;
- max-height: 180px;
- overflow-y: auto;
- font-family: 'JetBrains Mono', monospace, sans-serif;
- font-size: 11px;
- line-height: 1.5;
- color: #cbd5e1;
- white-space: pre-wrap;
- word-break: break-all;
-
- .log-line {
- margin-bottom: 2px;
- }
-
- .log-empty {
- color: #64748b;
- font-style: italic;
- }
- }
- }
- }
-
- /* Failed Diagnostic Card */
- .emulator-failed-card {
- display: flex;
- flex-direction: column;
- gap: 14px;
- padding: 18px 20px;
- background: #fff8f8;
- border: 1.5px solid rgba(239, 68, 68, 0.3);
- border-radius: 12px;
- box-shadow: 0 4px 14px rgba(239, 68, 68, 0.06);
-
- .failed-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 12px;
-
- .failed-header-left {
- display: flex;
- align-items: flex-start;
- gap: 12px;
-
- .failed-icon-box {
- width: 34px;
- height: 34px;
- border-radius: 8px;
- background: rgba(239, 68, 68, 0.12);
- color: $rose;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-
- .failed-icon {
- font-size: 20px;
- }
- }
-
- .failed-titles {
- display: flex;
- flex-direction: column;
- gap: 2px;
-
- .failed-title {
- margin: 0;
- font-size: 15px;
- font-weight: 700;
- color: $rose;
- }
-
- .failed-desc {
- margin: 0;
- font-size: 13px;
- color: $text-main;
- line-height: 1.4;
- }
- }
- }
-
- .btn-dismiss-failed {
- background: transparent;
- border: none;
- color: $text-muted;
- cursor: pointer;
- padding: 4px;
- border-radius: 4px;
-
- &:hover {
- background: rgba(0, 0, 0, 0.05);
- color: $text-main;
- }
- }
- }
-
- .failed-logs-box {
- display: flex;
- flex-direction: column;
- background: #1e1e2e;
- border-radius: 8px;
- overflow: hidden;
-
- .logs-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 6px 12px;
- background: #282a36;
- color: #ff5555;
- font-size: 11px;
- font-weight: 600;
-
- .btn-copy-logs {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 2px 6px;
- background: #44475a;
- border: none;
- border-radius: 4px;
- color: #f8fafc;
- font-size: 10px;
- cursor: pointer;
- }
- }
-
- .failed-logs-pre {
- margin: 0;
- padding: 10px 12px;
- max-height: 120px;
- overflow-y: auto;
- font-family: 'JetBrains Mono', monospace;
- font-size: 11px;
- line-height: 1.4;
- color: #f8f8f2;
- white-space: pre-wrap;
- }
- }
-
- .failed-actions-bar {
- display: flex;
- align-items: center;
- gap: 10px;
- flex-wrap: wrap;
-
- .btn-retry-launch {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 6px 16px;
- @include glass-primary-btn;
- border-radius: 6px;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
-
- .material-symbols-outlined {
- font-size: 15px;
- }
- }
-
- .btn-restart-adb-sub {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 6px 14px;
- @include glass-secondary-btn;
- border-radius: 6px;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
-
- .material-symbols-outlined {
- font-size: 15px;
- }
- }
-
- .btn-dismiss-flat {
- background: transparent;
- border: none;
- color: $text-secondary;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- padding: 6px 10px;
-
- &:hover {
- text-decoration: underline;
- }
- }
- }
- }
-
- /* Foolproof Connection Guide Tabs */
- .foolproof-guide-container {
- display: flex;
- flex-direction: column;
- gap: 12px;
-
- .connection-methods-nav {
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 10px;
-
- @media (max-width: 1100px) {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-
- @media (max-width: 620px) {
- grid-template-columns: 1fr;
- }
-
- .method-nav-card {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 14px;
- background: #f8fafc;
- border: 1px solid $card-border;
- border-radius: 10px;
- cursor: pointer;
- text-align: left;
- transition: all 0.18s ease;
- outline: none;
- min-width: 0;
-
- .method-card-left {
- display: flex;
- align-items: center;
- gap: 10px;
- flex: 1;
- min-width: 0;
-
- .method-icon-box {
- width: 32px;
- height: 32px;
- border-radius: 8px;
- background: #ffffff;
- border: 1px solid $card-border;
- color: $text-secondary;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- transition: all 0.15s ease;
-
- .material-symbols-outlined {
- font-size: 17px;
- }
- }
-
- .method-info {
- display: flex;
- flex-direction: column;
- gap: 1px;
- min-width: 0;
- flex: 1;
-
- .method-title-row {
- display: flex;
- align-items: center;
-
- .method-title {
- font-size: 13px;
- font-weight: 600;
- color: $text-main;
- }
- }
-
- .method-desc {
- font-size: 11.5px;
- color: $text-muted;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
- }
- }
- }
-
- .method-card-right {
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- margin-left: 8px;
-
- .radio-indicator {
- width: 16px;
- height: 16px;
- border-radius: 50%;
- border: 1.5px solid #cbd5e1;
- background: #ffffff;
- transition: all 0.15s ease;
-
- &.checked {
- border-color: $primary;
- background: $primary;
- box-shadow: inset 0 0 0 3px #ffffff;
- }
- }
- }
-
- &.active {
- background: #ffffff;
- border-color: $primary;
- box-shadow: 0 0 0 1px $primary;
-
- .method-icon-box {
- background: $primary-light;
- color: $primary;
- border-color: $primary-border;
- }
-
- .method-title {
- color: $primary;
- }
- }
-
- &:hover:not(.active) {
- background: #ffffff;
- border-color: $card-border-hover;
- }
- }
- }
-
- .active-remote-notice {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- padding: 10px 12px;
- background: #fffbeb;
- border: 1px solid $amber-border;
- border-radius: 8px;
-
- .active-remote-copy {
- display: flex;
- align-items: center;
- gap: 8px;
- min-width: 0;
- color: #78350f;
- font-size: 11.5px;
- line-height: 1.45;
-
- .material-symbols-outlined {
- color: $amber;
- font-size: 17px;
- flex-shrink: 0;
- }
-
- strong {
- font-family: $font-mono;
- font-weight: 600;
- }
- }
-
- .btn-use-local-adb {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 5px;
- min-height: 32px;
- padding: 0 12px;
- @include glass-secondary-btn;
- border-radius: 6px;
- font-family: inherit;
- font-size: 11.5px;
- font-weight: 600;
- cursor: pointer;
- white-space: nowrap;
-
- .material-symbols-outlined {
- font-size: 15px;
- }
- }
-
- @media (max-width: 700px) {
- align-items: stretch;
- flex-direction: column;
-
- .btn-use-local-adb {
- width: 100%;
- }
- }
-
- }
-
- .method-details-panel {
- background: #ffffff;
- border: 1px solid $card-border;
- border-radius: 10px;
- padding: 14px 16px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- animation: fadeIn 0.15s ease-out;
-
- .avds-section {
- display: flex;
- flex-direction: column;
- gap: 10px;
-
- .section-header-bar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-wrap: wrap;
- gap: 6px;
- padding-bottom: 6px;
- border-bottom: 1px solid #f1f5f9;
-
- .section-title-group {
- display: flex;
- align-items: center;
- gap: 6px;
-
- .header-icon {
- font-size: 17px;
- color: $primary;
- }
-
- .section-title {
- font-size: 13px;
- font-weight: 600;
- color: $text-main;
- }
-
- .count-badge {
- font-size: 10.5px;
- font-weight: 600;
- padding: 1.5px 6px;
- border-radius: 6px;
- background: #f1f5f9;
- color: $text-secondary;
- }
- }
- }
-
- .avds-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
-
- .avd-card-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 8px 12px;
- background: #f8fafc;
- border: 1px solid $card-border;
- border-radius: 8px;
- gap: 12px;
- min-width: 0;
- transition: all 0.15s ease;
-
- &:hover {
- border-color: $card-border-hover;
- background: #ffffff;
- }
-
- .avd-item-main {
- display: flex;
- align-items: center;
- gap: 10px;
- flex: 1;
- min-width: 0;
-
- .avd-avatar {
- width: 32px;
- height: 32px;
- border-radius: 6px;
- background: #ffffff;
- border: 1px solid $card-border;
- color: $primary;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-
- .material-symbols-outlined {
- font-size: 17px;
- }
- }
-
- .avd-meta {
- display: flex;
- flex-direction: column;
- gap: 1px;
- flex: 1;
- min-width: 0;
-
- .avd-heading {
- display: flex;
- align-items: center;
- gap: 6px;
- min-width: 0;
-
- .avd-name {
- font-size: 13px;
- font-weight: 600;
- color: $text-main;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .avd-type-pill {
- font-size: 9.5px;
- font-weight: 600;
- padding: 1px 5px;
- border-radius: 4px;
- background: #ffffff;
- border: 1px solid $card-border;
- color: $text-muted;
- flex-shrink: 0;
- }
- }
- }
- }
-
- .avd-item-actions {
- display: flex;
- align-items: center;
- gap: 6px;
- flex-shrink: 0;
-
- .btn-avd-launch {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- padding: 0 15px;
- height: 30px;
- @include glass-primary-btn;
- border-radius: 6px;
- font-family: inherit;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
-
- .action-icon {
- font-size: 15px;
- }
-
- .spinner-icon {
- width: 13px;
- height: 13px;
- border: 2px solid rgba(255, 255, 255, 0.4);
- border-top-color: #ffffff;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- }
- }
- }
- }
- }
- }
-
- .emulator-setup-guide-container {
- display: flex;
- flex-direction: column;
- gap: 12px;
-
- .empty-avd-banner {
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-wrap: wrap;
- gap: 12px;
- padding: 12px 14px;
- background: #f8fafc;
- border: 1px solid $card-border;
- border-radius: 8px;
-
- .banner-left {
- display: flex;
- align-items: center;
- gap: 10px;
-
- .empty-avd-icon-box {
- width: 34px;
- height: 34px;
- border-radius: 6px;
- background: #ffffff;
- border: 1px solid $card-border;
- color: $text-muted;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-
- .material-symbols-outlined {
- font-size: 19px;
- }
- }
-
- .banner-titles {
- .banner-title-row {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
-
- .empty-title {
- margin: 0;
- font-size: 13px;
- font-weight: 600;
- color: $text-main;
- }
- }
-
- .empty-desc {
- margin: 2px 0 0;
- font-size: 12px;
- color: $text-secondary;
- }
- }
- }
-
- .mode-switch-pills {
- display: inline-flex;
- background: #ffffff;
- border: 1px solid $card-border;
- padding: 2px;
- border-radius: 6px;
- gap: 2px;
-
- .pill-btn {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 3px 8px;
- border-radius: 4px;
- border: none;
- background: transparent;
- color: $text-secondary;
- font-family: inherit;
- font-size: 11.5px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.15s ease;
-
- .pill-icon {
- font-size: 14px;
- }
-
- &:hover {
- color: $text-main;
- }
-
- &.active {
- background: $primary;
- color: #ffffff;
- }
- }
- }
- }
-
- .emu-flow-grid {
- .flow-step-card {
- .guide-link {
- color: $primary;
- font-weight: 600;
- text-decoration: none;
- &:hover {
- text-decoration: underline;
- }
- }
- }
- }
-
- .cli-steps-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
-
- .cli-step-item {
- display: flex;
- align-items: flex-start;
- gap: 10px;
- padding: 10px 12px;
- background: #f8fafc;
- border: 1px solid $card-border;
- border-radius: 8px;
-
- .step-badge-num {
- width: 22px;
- height: 22px;
- border-radius: 50%;
- background: $primary-light;
- color: $primary;
- font-size: 11.5px;
- font-weight: 700;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- margin-top: 1px;
- }
-
- .step-content {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 3px;
-
- .step-title {
- font-size: 12.5px;
- font-weight: 600;
- color: $text-main;
- }
-
- .step-text {
- margin: 0;
- font-size: 11.5px;
- color: $text-secondary;
- }
-
- .small-box {
- margin-top: 4px;
- }
- }
- }
- }
- }
-
- .usb-guide-intro,
- .wifi-guide-intro,
- .remote-guide-intro {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 12.5px;
- font-weight: 600;
- color: $text-secondary;
-
- .intro-icon {
- font-size: 17px;
- color: $primary;
- }
- }
-
- /* USB Steps Flow Grid */
- .steps-flow-grid {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 10px;
-
- @media (max-width: 900px) {
- grid-template-columns: repeat(2, 1fr);
- }
-
- @media (max-width: 520px) {
- grid-template-columns: 1fr;
- }
-
- .flow-step-card {
- background: #f8fafc;
- border: 1px solid $card-border;
- border-radius: 8px;
- padding: 12px 14px;
- display: flex;
- flex-direction: column;
- gap: 6px;
- min-width: 0;
- transition: all 0.15s ease;
-
- &:hover {
- border-color: $card-border-hover;
- background: #ffffff;
- }
-
- .step-card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- .step-number-badge {
- width: 20px;
- height: 20px;
- border-radius: 4px;
- background: $primary;
- color: #ffffff !important;
- font-size: 11px;
- font-weight: 700;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .step-card-icon {
- font-size: 17px;
- color: $text-light;
- }
- }
-
- .step-card-title {
- font-size: 12.5px;
- font-weight: 600;
- color: $text-main;
- }
-
- .step-card-desc {
- font-size: 11.5px;
- color: $text-muted;
- line-height: 1.4;
- }
- }
- }
-
- /* Interactive Wi-Fi Form Box */
- .wifi-interactive-box {
- background: #f8fafc;
- border: 1px solid $card-border;
- border-radius: 8px;
- padding: 12px 14px;
- display: flex;
- flex-direction: column;
- gap: 10px;
-
- .wifi-form-row {
- display: flex;
- align-items: flex-end;
- gap: 8px;
- flex-wrap: wrap;
-
- .wifi-input-col {
- display: flex;
- flex-direction: column;
- gap: 3px;
- min-width: 0;
-
- &.ip-col {
- flex: 1 1 180px;
- }
-
- &.port-col {
- flex: 0 1 90px;
- }
-
- .wifi-label {
- font-size: 11px;
- font-weight: 600;
- color: $text-muted;
- }
-
- .wifi-input-wrap {
- display: flex;
- align-items: center;
- background: #ffffff;
- border: 1px solid $card-border;
- border-radius: 6px;
- padding: 0 8px;
- height: 34px;
- transition: border-color 0.15s ease;
-
- &:focus-within {
- border-color: $primary;
- box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.1);
- }
-
- .wrap-icon {
- font-size: 15px;
- color: $text-light;
- margin-right: 5px;
- flex-shrink: 0;
- }
-
- .wifi-input {
- flex: 1;
- min-width: 0;
- border: none;
- outline: none;
- background: transparent;
- font-family: $font-mono;
- font-size: 12px;
- color: $text-main;
-
- &::placeholder {
- color: $text-light;
- font-family: $font-sans;
- font-size: 11.5px;
- }
- }
- }
- }
-
- .wifi-btn-col {
- flex-shrink: 0;
-
- .btn-wifi-connect {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- padding: 0 16px;
- height: 34px;
- @include glass-primary-btn;
- border-radius: 6px;
- font-family: inherit;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- white-space: nowrap;
-
- .material-symbols-outlined {
- font-size: 16px;
- }
-
- .spinning {
- animation: spin 1s linear infinite;
- }
- }
- }
- }
-
- .wifi-status-alert {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 6px 10px;
- border-radius: 6px;
- font-size: 11.5px;
- font-weight: 600;
-
- .material-symbols-outlined {
- font-size: 15px;
- }
-
- &.success {
- background: $emerald-light;
- color: $emerald-dark;
- border: 1px solid $emerald-border;
- }
-
- &.error {
- background: $rose-light;
- color: $rose-dark;
- border: 1px solid $rose-border;
- }
- }
- }
-
- .info-callout-box {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- padding: 8px 12px;
- background: $primary-light;
- border: 1px solid $primary-border;
- border-radius: 6px;
- font-size: 11.5px;
- color: $primary-dark;
- line-height: 1.45;
-
- .callout-icon {
- font-size: 16px;
- color: $primary;
- flex-shrink: 0;
- margin-top: 1px;
- }
- }
-
- &.remote-adb-panel {
- .remote-adb-interactive-box {
- display: flex;
- flex-direction: column;
- gap: 10px;
- padding: 12px 14px;
- background: #f8fafc;
- border: 1px solid $card-border;
- border-radius: 8px;
-
- .remote-adb-form-row {
- display: flex;
- align-items: flex-end;
- gap: 8px;
- flex-wrap: wrap;
-
- .remote-adb-input-col {
- display: flex;
- flex-direction: column;
- gap: 3px;
- min-width: 0;
-
- &.host-col {
- flex: 1 1 220px;
- }
-
- &.port-col {
- flex: 0 1 120px;
- }
-
- .remote-adb-label {
- color: $text-muted;
- font-size: 11px;
- font-weight: 600;
- }
-
- .remote-adb-input-wrap {
- display: flex;
- align-items: center;
- height: 36px;
- padding: 0 9px;
- background: #ffffff;
- border: 1px solid $card-border;
- border-radius: 6px;
- transition: border-color 0.15s ease, box-shadow 0.15s ease;
-
- &:focus-within {
- border-color: $primary;
- box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.1);
- }
-
- .wrap-icon {
- margin-right: 6px;
- color: $text-light;
- font-size: 15px;
- flex-shrink: 0;
- }
-
- .remote-adb-input {
- flex: 1;
- min-width: 0;
- border: 0;
- outline: 0;
- background: transparent;
- color: $text-main;
- font-family: $font-mono;
- font-size: 12px;
-
- &::placeholder {
- color: $text-light;
- }
- }
- }
- }
-
- .remote-adb-btn-col {
- flex-shrink: 0;
-
- .btn-remote-adb-connect {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 5px;
- height: 36px;
- padding: 0 16px;
- @include glass-primary-btn;
- border-radius: 6px;
- font-family: inherit;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- white-space: nowrap;
-
- .material-symbols-outlined {
- font-size: 16px;
- }
- }
- }
-
- @media (max-width: 640px) {
- align-items: stretch;
- flex-direction: column;
-
- .remote-adb-input-col,
- .remote-adb-input-col.host-col,
- .remote-adb-input-col.port-col,
- .remote-adb-btn-col {
- flex: 1 1 auto;
- width: 100%;
- }
-
- .remote-adb-btn-col .btn-remote-adb-connect {
- width: 100%;
- }
- }
- }
-
- .remember-remote-adb {
- display: inline-flex;
- align-items: center;
- align-self: flex-start;
- gap: 7px;
- color: $text-secondary;
- font-size: 11.5px;
- cursor: pointer;
-
- input {
- width: 15px;
- height: 15px;
- margin: 0;
- accent-color: $primary;
- }
- }
-
- .btn-use-tested-endpoint {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- align-self: flex-end;
- gap: 5px;
- min-width: 150px;
- height: 36px;
- padding: 0 16px;
- @include glass-primary-btn;
- border-radius: 6px;
- font-family: inherit;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
-
- .material-symbols-outlined {
- font-size: 16px;
- }
- }
-
- .remote-adb-status-alert {
- display: flex;
- align-items: flex-start;
- gap: 6px;
- padding: 7px 10px;
- border-radius: 6px;
- font-size: 11.5px;
- font-weight: 600;
- line-height: 1.4;
-
- .material-symbols-outlined {
- font-size: 15px;
- flex-shrink: 0;
- }
-
- &.success {
- color: $emerald-dark;
- background: $emerald-light;
- border: 1px solid $emerald-border;
- }
-
- &.error {
- color: $rose-dark;
- background: $rose-light;
- border: 1px solid $rose-border;
- }
- }
-
- .remote-device-results {
- display: flex;
- flex-direction: column;
- overflow: hidden;
- background: #ffffff;
- border: 1px solid $card-border;
- border-radius: 7px;
-
- .remote-device-row {
- display: flex;
- align-items: center;
- gap: 9px;
- min-width: 0;
- padding: 8px 10px;
- border-bottom: 1px solid #f1f5f9;
-
- &:last-child {
- border-bottom: 0;
- }
-
- > .material-symbols-outlined {
- color: $primary;
- font-size: 17px;
- }
-
- .remote-device-copy {
- display: flex;
- flex: 1;
- flex-direction: column;
- min-width: 0;
-
- strong {
- color: $text-main;
- font-size: 12px;
- font-weight: 600;
- }
-
- span {
- overflow: hidden;
- color: $text-muted;
- font-family: $font-mono;
- font-size: 10.5px;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- }
-
- .remote-device-state {
- color: $amber-dark;
- font-size: 10.5px;
- font-weight: 600;
- text-transform: capitalize;
-
- &.ready {
- color: $emerald-dark;
- }
- }
- }
- }
- }
-
- .remote-adb-explanation {
- margin: 0;
- color: $text-muted;
- font-size: 11.5px;
- line-height: 1.5;
- }
- }
- }
- }
-
- /* State C: Locked / Unauthorized Device Panels */
- .unauthorized-panel,
- .device-locked-panel {
- .unauthorized-card,
- .device-locked-card {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 12px 16px;
- background: $amber-light;
- border: 1px solid $amber-border;
- border-radius: 10px;
-
- .alert-icon {
- font-size: 24px;
- color: $amber;
- flex-shrink: 0;
- }
-
- .unauth-details,
- .locked-details {
- flex: 1;
- min-width: 0;
-
- .unauth-title,
- .locked-title {
- font-size: 13px;
- font-weight: 600;
- color: $amber-dark;
- margin-bottom: 2px;
- }
-
- .unauth-desc,
- .locked-desc {
- font-size: 11.5px;
- color: #78350f;
- line-height: 1.4;
- }
- }
-
- .btn-retry-auth,
- .btn-check-unlock {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 0 12px;
- height: 30px;
- background: rgba(254, 243, 199, 0.75);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- border: 1px solid rgba(253, 230, 138, 0.85);
- border-radius: 6px;
- box-shadow: 0 1px 3px rgba(217, 119, 6, 0.08), inset 0 1px 1px rgba(255, 255, 255, 0.8);
- font-family: inherit;
- font-size: 11.5px;
- font-weight: 600;
- color: $amber-dark;
- cursor: pointer;
- flex-shrink: 0;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-
- &:hover:not(:disabled) {
- background: rgba(253, 230, 138, 0.95);
- border-color: $amber;
- transform: translateY(-0.5px);
- box-shadow: 0 3px 8px rgba(217, 119, 6, 0.15);
- }
-
- &:active {
- transform: scale(0.97);
- }
-
- &:disabled {
- cursor: not-allowed;
- opacity: 0.65;
- }
- }
- }
- }
-
- /* State D: ADB Missing Panel */
- .adb-missing-panel {
- display: flex;
- flex-direction: column;
- gap: 10px;
-
- .error-msg-box {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 10px 12px;
- background: $rose-light;
- border: 1px solid $rose-border;
- border-radius: 8px;
- font-size: 12px;
- color: $rose-dark;
-
- .err-icon {
- font-size: 17px;
- flex-shrink: 0;
- }
- }
-
- .install-commands-row {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
- gap: 8px;
-
- .install-card {
- background: #f8fafc;
- border: 1px solid $card-border;
- border-radius: 8px;
- padding: 10px 12px;
- display: flex;
- flex-direction: column;
- gap: 6px;
- transition: all 0.15s ease;
-
- &.highlight-os {
- background: $primary-light;
- border-color: $primary-border;
- }
-
- .install-os {
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: 11.5px;
- font-weight: 600;
- color: $text-secondary;
-
- .detected-badge {
- font-size: 9.5px;
- font-weight: 700;
- color: $primary;
- background: #dbeafe;
- padding: 1px 5px;
- border-radius: 4px;
- text-transform: uppercase;
- }
- }
- }
- }
- }
-
- /* Ready Banner Footer: Consistent with guide-card design system */
- .ready-banner-footer {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- padding: 16px 20px;
- background: #ffffff;
- border: 1px solid $card-border;
- border-radius: 12px;
- box-shadow: $card-shadow;
- transition: all 0.2s ease;
-
- &:hover {
- border-color: $card-border-hover;
- }
-
- .ready-banner-left {
- display: flex;
- align-items: center;
- gap: 14px;
- min-width: 0;
- flex: 1;
-
- .step-badge {
- width: 32px;
- height: 32px;
- border-radius: 8px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- font-size: 13.5px;
- font-weight: 700;
- background: #f1f5f9;
- color: $text-secondary;
- flex-shrink: 0;
-
- .step-check-icon {
- font-size: 18px;
- font-weight: 700;
- color: inherit;
- }
-
- &.step-pass {
- background: $emerald-light;
- color: $emerald;
- }
- }
-
- .ready-text-group {
- display: flex;
- flex-direction: column;
- gap: 2px;
- min-width: 0;
-
- .ready-title {
- margin: 0;
- font-size: 15px;
- font-weight: 600;
- color: $text-main;
- letter-spacing: -0.01em;
- }
-
- .ready-desc {
- margin: 0;
- font-size: 12.5px;
- color: $text-secondary;
- line-height: 1.4;
- }
- }
- }
-
- .btn-proceed-launch {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 0 18px;
- height: 36px;
- @include glass-primary-btn;
- border-radius: 8px;
- font-family: inherit;
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
- white-space: nowrap;
- flex-shrink: 0;
-
- .rocket-icon {
- font-size: 16px;
- transition: transform 0.2s ease;
- }
-
- &:hover:not(:disabled) {
- .rocket-icon {
- transform: translateX(2px) translateY(-2px);
- }
- }
- }
-
- @media (max-width: 768px) {
- flex-direction: column;
- align-items: stretch;
-
- .btn-proceed-launch {
- justify-content: center;
- }
- }
- }
-
- }
-
- /* ======================================================================= */
- /* SHARED CODE BOX & TERMINAL SNIPPET STYLES */
- /* ======================================================================= */
- .code-box {
- display: inline-flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- padding: 0 6px 0 10px;
- height: 32px;
- background: #0f172a;
- color: #f8fafc;
- border-radius: 6px;
- font-family: $font-mono;
- font-size: 11.5px;
- min-width: 0;
- max-width: 100%;
-
- .terminal-prefix {
- color: #64748b;
- user-select: none;
- flex-shrink: 0;
- font-weight: 700;
- font-size: 11px;
- }
-
- .code-text, code {
- flex: 1;
- min-width: 0;
- color: #38bdf8 !important;
- font-family: inherit;
- background: transparent !important;
- border: none !important;
- padding: 0 !important;
- font-size: inherit;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .btn-copy-code {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 3px;
- padding: 0 8px;
- height: 22px;
- background: rgba(255, 255, 255, 0.14);
- backdrop-filter: blur(8px);
- -webkit-backdrop-filter: blur(8px);
- color: #ffffff !important;
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 5px;
- box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.2);
- font-family: $font-sans;
- font-size: 10.5px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.18s ease;
- flex-shrink: 0;
-
- .material-symbols-outlined, span {
- color: #ffffff !important;
- }
-
- .material-symbols-outlined {
- font-size: 12px;
- }
-
- &:hover {
- background: rgba(255, 255, 255, 0.26);
- border-color: rgba(255, 255, 255, 0.35);
- transform: translateY(-0.5px);
- }
-
- &:active {
- transform: scale(0.96);
- }
-
- &.copied {
- background: rgba(16, 185, 129, 0.35);
- backdrop-filter: blur(8px);
- border-color: rgba(16, 185, 129, 0.6);
- color: #34d399 !important;
- box-shadow: 0 0 10px rgba(16, 185, 129, 0.3), inset 0 1px 1px rgba(255, 255, 255, 0.4);
-
- .material-symbols-outlined, span {
- color: #34d399 !important;
- }
- }
- }
-
- &.small-box {
- height: 30px;
- font-size: 11.5px;
- padding: 0 6px 0 10px;
-
- .btn-copy-code {
- height: 22px;
- padding: 0 8px;
- font-size: 10.5px;
-
- .material-symbols-outlined {
- font-size: 12px;
- }
- }
- }
- }
-
- /* ======================================================================= */
- /* VIEW 2: TASK LAUNCHER (UNIFIED SCULPTURED COMPOSER SURFACE) */
- /* ======================================================================= */
- .task-launcher-view {
- display: flex;
- flex-direction: column;
- gap: 12px;
- animation: fadeIn 0.22s cubic-bezier(0.4, 0, 0.2, 1);
-
- .launcher-warning-bar {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 16px;
- background: #fffbeb;
- border: 1px solid #fde68a;
- border-radius: 12px;
- color: #92400e;
- font-size: 13px;
- cursor: pointer;
- transition: all 0.15s ease;
-
- .warn-icon {
- font-size: 18px;
- color: #d97706;
- flex-shrink: 0;
- }
-
- .arrow {
- font-size: 16px;
- margin-left: auto;
- }
-
- &:hover {
- background: #fef3c7;
- border-color: #fcd34d;
- }
- }
-
- /* Task composer */
- .composer-card {
- background: rgba(255, 255, 255, 0.88);
- backdrop-filter: blur(16px) saturate(190%);
- -webkit-backdrop-filter: blur(16px) saturate(190%);
- border: 1px solid rgba(255, 255, 255, 0.95);
- box-shadow: 0 20px 50px -12px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(0, 0, 0, 0.02), inset 0 1px 1px rgba(255, 255, 255, 0.95);
- border-radius: 20px;
- padding: 18px 20px 14px;
- transition: border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
- box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
- background 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- position: relative;
-
- &.is-pro-active {
- border-color: rgba(221, 214, 254, 0.95);
- box-shadow: 0 24px 60px -12px rgba(99, 102, 241, 0.16), 0 1px 3px rgba(0, 0, 0, 0.02), inset 0 1px 1px rgba(255, 255, 255, 0.95);
- }
-
- .intent-suggestion-strip {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- padding: 8px 12px;
- background: linear-gradient(135deg, rgba(238, 242, 255, 0.95) 0%, rgba(245, 243, 255, 0.95) 100%);
- border: 1px solid rgba(199, 210, 254, 0.85);
- border-radius: 10px;
- margin-bottom: 12px;
- animation: slideUpFade 0.22s cubic-bezier(0.4, 0, 0.2, 1);
-
- .suggestion-left {
- display: flex;
- align-items: center;
- gap: 8px;
- min-width: 0;
-
- .sparkle-icon {
- font-size: 17px;
- color: #6366f1;
- flex-shrink: 0;
- animation: sparklePulse 2.5s ease-in-out infinite alternate;
- }
-
- .suggestion-text {
- font-size: 12.5px;
- color: #3730a3;
- line-height: 1.4;
-
- strong {
- color: #1e1b4b;
- font-weight: 600;
- }
- }
- }
-
- .btn-switch-pro {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 4px 10px;
- border-radius: 6px;
- border: none;
- background: #6366f1;
- color: #ffffff;
- font-family: inherit;
- font-size: 11.5px;
- font-weight: 600;
- cursor: pointer;
- flex-shrink: 0;
- transition: all 0.18s cubic-bezier(0.4, 0, 0.2, 1);
- box-shadow: 0 1px 3px rgba(99, 102, 241, 0.25);
-
- .btn-arrow {
- font-size: 14px;
- transition: transform 0.15s ease;
- }
-
- &:hover {
- background: #4f46e5;
- box-shadow: 0 2px 6px rgba(99, 102, 241, 0.35);
- transform: translateY(-0.5px);
-
- .btn-arrow {
- transform: translateX(2px);
- }
- }
-
- &:active {
- transform: scale(0.96);
- }
- }
- }
-
- .composer-input-area {
- width: 100%;
-
- .composer-textarea {
- width: 100%;
- border: none;
- outline: none;
- background: transparent;
- padding: 0;
- margin: 0;
- font-family: inherit;
- font-size: 15.5px;
- line-height: 1.6;
- color: #0f172a;
- resize: vertical;
- min-height: 90px;
- letter-spacing: -0.01em;
-
- &::placeholder {
- color: #94a3b8;
- font-weight: 400;
- }
-
- &:disabled {
- color: #94a3b8;
- cursor: not-allowed;
- }
- }
- }
-
- /* Contextual Model Descriptor Strip (Flash & Pro) */
- .composer-model-descriptor {
- display: flex;
- align-items: center;
- gap: 9px;
- padding: 8px 12px;
- border-radius: 9px;
- background: rgba(248, 250, 252, 0.75);
- border: 1px solid rgba(241, 245, 249, 0.95);
- margin-top: 10px;
- margin-bottom: 12px;
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
-
- .descriptor-icon-wrap {
- width: 22px;
- height: 22px;
- border-radius: 6px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- background: rgba(26, 115, 232, 0.1);
- color: #1a73e8;
- flex-shrink: 0;
- transition: all 0.25s ease;
-
- .material-symbols-outlined {
- font-size: 14px;
- }
- }
-
- .descriptor-body {
- flex: 1;
- min-width: 0;
-
- .desc-line {
- margin: 0;
- font-size: 12.5px;
- line-height: 1.45;
- color: #475569;
- letter-spacing: -0.005em;
-
- .mode-name {
- font-weight: 700;
- color: #0f172a;
- }
-
- .bullet {
- color: #94a3b8;
- margin: 0 3px;
- }
-
- strong {
- color: #0f172a;
- font-weight: 600;
- }
-
- .limitation-tag {
- color: #64748b;
- font-size: 11.5px;
- margin-left: 3px;
- }
- }
- }
-
- &.is-pro {
- background: linear-gradient(135deg, rgba(245, 243, 255, 0.8) 0%, rgba(250, 245, 255, 0.8) 100%);
- border-color: rgba(237, 233, 254, 0.95);
-
- .descriptor-icon-wrap {
- background: rgba(124, 58, 237, 0.12);
- color: #7c3aed;
- }
-
- .descriptor-body .desc-line {
- color: #334155;
-
- .mode-name, strong {
- color: #1e1b4b;
- }
- }
- }
- }
-
- .error-banner {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 8px 12px;
- background: $rose-light;
- border: 1px solid $rose-border;
- border-radius: 8px;
- color: $rose-dark;
- font-size: 12px;
- margin-bottom: 10px;
-
- .error-icon {
- font-size: 16px;
- flex-shrink: 0;
- }
- }
-
- .composer-action-bar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding-top: 10px;
- border-top: 1px solid rgba(241, 245, 249, 0.95);
- gap: 12px;
- flex-wrap: wrap;
-
- .bar-left {
- display: flex;
- align-items: center;
-
- .model-toggle-capsule {
- display: inline-flex;
- align-items: center;
- background: rgba(241, 245, 249, 0.85);
- padding: 3px;
- border-radius: 18px;
- gap: 3px;
- border: 1px solid rgba(226, 232, 240, 0.88);
- width: 172px;
- box-sizing: border-box;
-
- .model-toggle-btn {
- flex: 1 1 0;
- width: 50%;
- height: 32px;
- padding: 0 10px;
- border-radius: 15px;
- border: none;
- background: transparent;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- outline: none;
- box-sizing: border-box;
- white-space: nowrap;
-
- .btn-icon {
- font-size: 16px;
- color: #64748b;
- flex-shrink: 0;
- transition: color 0.15s ease;
- }
-
- .btn-text {
- font-size: 13px;
- font-weight: 500;
- color: #475569;
- line-height: 1;
- }
-
- &:hover:not(.active) {
- background: rgba(255, 255, 255, 0.7);
-
- .btn-icon, .btn-text {
- color: #0f172a;
- }
- }
-
- &.active {
- background: #ffffff;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
-
- .btn-icon {
- color: #1a73e8;
- }
- .btn-text {
- color: #0f172a;
- font-weight: 600;
- }
- }
-
- &.btn-pro.active {
- background: linear-gradient(135deg, #6366f1 0%, #7c3aed 100%);
- box-shadow: 0 2px 8px rgba(99, 102, 241, 0.35);
-
- .btn-icon, .btn-text {
- color: #ffffff !important;
- }
- }
-
- &:active {
- transform: scale(0.97);
- }
- }
- }
- }
-
- .bar-right {
- display: flex;
- align-items: center;
- gap: 12px;
-
- .char-count {
- font-size: 11.5px;
- color: #94a3b8;
- font-variant-numeric: tabular-nums;
- }
-
- .btn-run-composer {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- height: 38px;
- padding: 0 20px;
- @include glass-primary-btn;
- border-radius: 12px;
- font-family: inherit;
- font-size: 13.5px;
- font-weight: 700;
- cursor: pointer;
- letter-spacing: -0.01em;
-
- &.btn-run-pro {
- background: linear-gradient(135deg, rgba(99, 102, 241, 0.92) 0%, rgba(124, 58, 237, 0.85) 100%);
- box-shadow: 0 4px 14px rgba(99, 102, 241, 0.32), inset 0 1px 1px rgba(255, 255, 255, 0.45);
-
- &:hover:not(:disabled) {
- background: linear-gradient(135deg, rgba(99, 102, 241, 1) 0%, rgba(124, 58, 237, 0.95) 100%);
- box-shadow: 0 6px 20px rgba(99, 102, 241, 0.42), inset 0 1px 1px rgba(255, 255, 255, 0.6);
- }
- }
-
- .run-icon {
- font-size: 18px;
- line-height: 1;
- transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
- }
-
- &:hover:not(:disabled) {
- .run-icon {
- transform: scale(1.15) rotate(15deg);
- }
- }
-
- .spinner {
- width: 14px;
- height: 14px;
- border: 2px solid rgba(255, 255, 255, 0.4);
- border-top-color: #ffffff;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- }
- }
- }
- }
- }
-
- /* Pro Mode Standalone Outputter Card (Minimalist Apple/Linear Style) */
- .pro-outputter-standalone-card {
- background: rgba(255, 255, 255, 0.88);
- backdrop-filter: blur(20px) saturate(180%);
- -webkit-backdrop-filter: blur(20px) saturate(180%);
- border: 1px solid rgba(226, 232, 240, 0.95);
- border-radius: 16px;
- padding: 12px 18px 14px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02), 0 8px 24px -6px rgba(15, 23, 42, 0.04);
- display: flex;
- flex-direction: column;
- gap: 8px;
- animation: slideUpFade 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-
- .outputter-card-header {
- display: flex;
- align-items: baseline;
- gap: 6px;
-
- .outputter-title {
- font-size: 13px;
- font-weight: 700;
- color: #0f172a;
- letter-spacing: -0.01em;
- }
-
- .outputter-optional-tag {
- font-size: 11.5px;
- font-weight: 500;
- color: #94a3b8;
- }
- }
-
- .outputter-input-wrapper {
- display: flex;
- align-items: center;
- background: #f8fafc;
- border: 1px solid #e2e8f0;
- border-radius: 10px;
- padding: 0 12px;
- height: 38px;
- transition: all 0.18s ease;
-
- &:focus-within {
- background: #ffffff;
- border-color: #7c3aed;
- box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
- }
-
- .outputter-standalone-input {
- flex: 1;
- border: none;
- outline: none;
- background: transparent;
- font-family: inherit;
- font-size: 13px;
- color: #0f172a;
-
- &::placeholder {
- color: #94a3b8;
- font-weight: 400;
- }
- }
-
- .btn-clear-standalone {
- border: none;
- background: transparent;
- color: #94a3b8;
- cursor: pointer;
- padding: 3px;
- border-radius: 4px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: color 0.15s ease, background 0.15s ease;
-
- &:hover {
- color: #475569;
- background: rgba(0, 0, 0, 0.05);
- }
-
- .material-symbols-outlined {
- font-size: 16px;
- }
- }
- }
- }
-
-
- /* Pro tuning sliders and hover cards */
- .pro-tuning-card {
- --tip-w: 320px;
- --thumb-w: 22px;
- --thumb-h: 14px;
- --col-gap: 28px;
- --accent: #6366f1;
- position: relative;
- background: rgba(255, 255, 255, 0.88);
- backdrop-filter: blur(20px) saturate(180%);
- -webkit-backdrop-filter: blur(20px) saturate(180%);
- border: 1px solid rgba(226, 232, 240, 0.95);
- border-radius: 16px;
- padding: 12px 18px 14px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02), 0 8px 24px -6px rgba(15, 23, 42, 0.04);
- display: flex;
- flex-direction: column;
- gap: 10px;
- animation: slideUpFade 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-
- &.tip-open {
- z-index: 30;
- }
-
- .tuning-card-header {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
- gap: 10px;
- min-height: 16px;
-
- .tuning-title {
- font-size: 13px;
- font-weight: 700;
- color: #0f172a;
- letter-spacing: -0.01em;
- }
-
- .btn-tuning-reset {
- border: none;
- background: transparent;
- padding: 0;
- font-family: inherit;
- font-size: 12px;
- font-weight: 500;
- color: #94a3b8;
- cursor: pointer;
- transition: color 0.15s ease;
-
- &:hover {
- color: #475569;
- }
-
- &:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 3px;
- border-radius: 3px;
- }
- }
- }
-
- .tuning-sliders {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 4px var(--col-gap);
- }
-
- .tuning-slider {
- display: flex;
- flex-direction: column;
- gap: 4px;
- min-width: 0;
- /* Hover card clamp range, relative to this slider's track. The left
- column may spill right across the gap; the right column may spill
- left. Overridden below when the columns stack. */
- --tip-min: 0px;
- --tip-max: calc(100% - var(--tip-w));
-
- &.verify {
- --tip-max: calc(200% + var(--col-gap) - var(--tip-w));
- }
-
- &.explore {
- --accent: #0ea5e9;
- --tip-min: calc(-100% - var(--col-gap));
- }
-
- .slider-head {
- display: flex;
- align-items: baseline;
- gap: 8px;
- font-size: 13px;
- line-height: 18px;
- color: #0f172a;
-
- .slider-name {
- font-weight: 400;
- color: #334155;
- }
-
- .slider-value {
- font-weight: 600;
- }
- }
-
- .slider-ends {
- display: flex;
- justify-content: space-between;
- font-size: 11.5px;
- line-height: 14px;
- color: #94a3b8;
- }
-
- .slider-track-wrap {
- position: relative;
- height: 26px;
- display: flex;
- align-items: center;
- }
-
- .slider-rail {
- position: relative;
- width: 100%;
- height: 6px;
- border-radius: 3px;
- background: #e2e8f0;
- }
-
- .slider-fill {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- width: calc(var(--pct) * (100% - var(--thumb-w)) + var(--thumb-w) / 2);
- border-radius: 3px;
- background: color-mix(in srgb, var(--accent) 24%, #e2e8f0);
- transition: width 0.15s ease;
- }
-
- .slider-notch {
- position: absolute;
- top: 50%;
- left: calc(var(--n) * (100% - var(--thumb-w)) + var(--thumb-w) / 2);
- width: 4px;
- height: 4px;
- border-radius: 50%;
- background: #94a3b8;
- transform: translate(-50%, -50%);
- transition: background 0.15s ease;
- z-index: 1;
-
- &.reached {
- background: rgba(255, 255, 255, 0.95);
- }
- }
-
- .slider-thumb {
- position: absolute;
- top: 50%;
- left: calc(var(--pct) * (100% - var(--thumb-w)) + var(--thumb-w) / 2);
- width: var(--thumb-w);
- height: var(--thumb-h);
- border-radius: 5px;
- background: #ffffff;
- border: 1px solid rgba(15, 23, 42, 0.1);
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 2px 6px rgba(15, 23, 42, 0.08);
- transform: translate(-50%, -50%);
- pointer-events: none;
- transition: left 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
- }
-
- /* End of track: a pixel front sweeps from the thumb to the left end,
- a solid fill follows it until the whole rail has changed colour,
- and the thumb emits one ripple. */
- .slider-solid {
- position: absolute;
- inset: 0;
- border-radius: 3px;
- background: var(--accent);
- transform-origin: right center;
- transform: scaleX(1);
- animation: solidSweep 1s cubic-bezier(0.4, 0, 0.2, 1) 0.2s both;
- pointer-events: none;
- }
-
- .slider-dither {
- position: absolute;
- inset: 0;
- display: block;
- overflow: hidden;
- border-radius: 3px;
- pointer-events: none;
-
- rect {
- fill: var(--accent);
- animation: ditherIn 0.3s ease-out both;
- }
- }
-
- .thumb-ripple {
- position: absolute;
- top: 50%;
- left: calc(var(--pct) * (100% - var(--thumb-w)) + var(--thumb-w) / 2);
- width: var(--thumb-w);
- height: var(--thumb-w);
- border-radius: 50%;
- border: 1.5px solid var(--accent);
- transform: translate(-50%, -50%) scale(0.6);
- opacity: 0;
- pointer-events: none;
- animation: thumbRipple 0.6s ease-out forwards;
- }
-
- &.dragging .slider-thumb {
- border-color: color-mix(in srgb, var(--accent) 45%, transparent);
- box-shadow:
- 0 1px 2px rgba(15, 23, 42, 0.12),
- 0 2px 8px rgba(15, 23, 42, 0.1),
- 0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent);
- }
-
- /* Native range input does drag, keyboard, and a11y; it is invisible
- but its thumb geometry matches the drawn thumb so hits line up. */
- .slider-input {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- margin: 0;
- opacity: 0;
- cursor: pointer;
- -webkit-appearance: none;
- appearance: none;
- background: transparent;
-
- &::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- width: var(--thumb-w);
- height: 26px;
- }
-
- &::-moz-range-thumb {
- width: var(--thumb-w);
- height: 26px;
- border: 0;
- }
- }
-
- .slider-input:focus-visible ~ .slider-thumb,
- &:has(.slider-input:focus-visible) .slider-thumb {
- box-shadow:
- 0 1px 2px rgba(15, 23, 42, 0.12),
- 0 0 0 2px #ffffff,
- 0 0 0 4px var(--accent);
- }
-
- .tuning-tip-arrow {
- position: absolute;
- bottom: calc(100% + 2px);
- left: calc(var(--pct) * (100% - var(--thumb-w)) + var(--thumb-w) / 2);
- width: 10px;
- height: 10px;
- background: #ffffff;
- border-right: 1px solid rgba(226, 232, 240, 1);
- border-bottom: 1px solid rgba(226, 232, 240, 1);
- transform: translate(-50%, 50%) rotate(45deg);
- z-index: 41;
- pointer-events: none;
- }
-
- .tuning-tip {
- position: absolute;
- bottom: calc(100% + 7px);
- left: clamp(
- var(--tip-min),
- calc(var(--pct) * (100% - var(--thumb-w)) + var(--thumb-w) / 2 - var(--tip-w) / 2),
- var(--tip-max)
- );
- width: var(--tip-w);
- max-width: calc(100vw - 32px);
- padding: 12px 14px;
- background: #ffffff;
- border: 1px solid rgba(226, 232, 240, 1);
- border-radius: 12px;
- box-shadow: 0 12px 32px -8px rgba(15, 23, 42, 0.18), 0 2px 6px rgba(15, 23, 42, 0.06);
- z-index: 40;
- pointer-events: none;
- animation: tipPop 0.16s ease-out;
- font-size: 12px;
- line-height: 1.5;
- color: #334155;
- display: flex;
- flex-direction: column;
- gap: 8px;
-
- .tip-heading {
- display: flex;
- flex-direction: column;
- gap: 1px;
-
- .tip-title {
- font-size: 13.5px;
- font-weight: 600;
- color: #0f172a;
- letter-spacing: -0.01em;
- }
-
- .tip-latency {
- font-size: 11.5px;
- color: #94a3b8;
- }
- }
-
- .tip-tagline {
- margin: 0;
- color: #0f172a;
- }
-
- .tip-section {
- display: flex;
- flex-direction: column;
- gap: 2px;
-
- ul {
- margin: 0;
- padding-left: 16px;
- display: flex;
- flex-direction: column;
- gap: 2px;
- }
-
- li::marker {
- color: #cbd5e1;
- }
-
- &.skips li {
- color: #64748b;
- }
- }
-
- .tip-section-title {
- font-size: 10.5px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.06em;
- color: #94a3b8;
- }
-
- .tip-best {
- display: flex;
- flex-direction: column;
- gap: 2px;
- color: #475569;
- }
- }
- }
-
- @media (max-width: 760px) {
- .tuning-sliders {
- grid-template-columns: minmax(0, 1fr);
- gap: 10px;
- }
-
- .tuning-slider {
- &.verify,
- &.explore {
- --tip-min: 0px;
- --tip-max: calc(100% - var(--tip-w));
- }
- }
- }
- }
-
- /* Smart Suggestions Hub (Device-Aware, Flash vs Pro Tailored) */
- .smart-suggestions-container {
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-top: 10px;
-
- .suggestions-header-bar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- flex-wrap: wrap;
-
- .header-left {
- display: flex;
- align-items: center;
- gap: 12px;
- flex-wrap: wrap;
-
- .section-label {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-family: inherit;
- font-size: 13px;
- font-weight: 700;
- color: #0f172a;
- letter-spacing: -0.01em;
-
- .section-icon {
- font-size: 17px;
- color: #6366f1;
- animation: sparklePulse 3s ease-in-out infinite alternate;
- }
-
- .device-context-pill {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 2px 8px;
- border-radius: 12px;
- background: rgba(16, 185, 129, 0.1);
- border: 1px solid rgba(16, 185, 129, 0.25);
- color: #059669;
- font-size: 11px;
- font-weight: 600;
- line-height: 1.3;
-
- .dev-icon {
- font-size: 13px;
- color: #059669;
- }
- }
- }
-
- .category-tabs-group {
- display: inline-flex;
- align-items: center;
- gap: 3px;
- background: rgba(255, 255, 255, 0.65);
- backdrop-filter: blur(16px);
- -webkit-backdrop-filter: blur(16px);
- padding: 3px;
- border-radius: 16px;
- border: 1px solid rgba(226, 232, 240, 0.85);
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
-
- .cat-tab-btn {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 4px 10px;
- border-radius: 12px;
- border: none;
- background: transparent;
- font-family: inherit;
- font-size: 11.5px;
- font-weight: 600;
- color: #64748b;
- cursor: pointer;
- transition: all 0.18s ease;
- white-space: nowrap;
-
- .tab-mini-icon {
- font-size: 13px;
- }
-
- &:hover:not(.active) {
- color: #0f172a;
- background: rgba(255, 255, 255, 0.6);
- }
-
- &.active {
- color: #0f172a;
- background: #ffffff;
- box-shadow: 0 2px 6px rgba(15, 23, 42, 0.08), inset 0 1px 0 #fff;
-
- &.cat-device {
- color: #059669;
- }
-
- &.cat-flash {
- color: #1a73e8;
- }
-
- &.cat-pro {
- color: #7c3aed;
- }
- }
- }
- }
- }
-
- .header-right {
- .btn-shuffle-suggestions {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 5px 12px;
- border-radius: 14px;
- background: rgba(255, 255, 255, 0.72);
- backdrop-filter: blur(12px);
- border: 1px solid rgba(226, 232, 240, 0.85);
- color: #64748b;
- font-family: inherit;
- font-size: 11.5px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-
- .shuffle-icon {
- font-size: 14px;
- transition: transform 0.2s ease;
- }
-
- &:hover {
- background: #ffffff;
- color: #0f172a;
- border-color: rgba(203, 213, 225, 0.9);
- transform: translateY(-1px);
- box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
-
- .shuffle-icon {
- transform: rotate(45deg);
- }
- }
-
- &:active {
- transform: scale(0.97);
- }
- }
- }
- }
-
- .suggestions-cards-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(285px, 1fr));
- gap: 12px;
-
- .suggestion-card {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- padding: 14px 16px;
- border-radius: 16px;
- background: rgba(255, 255, 255, 0.72);
- backdrop-filter: blur(10px) saturate(180%);
- -webkit-backdrop-filter: blur(10px) saturate(180%);
- border: 1px solid rgba(226, 232, 240, 0.85);
- box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.95);
- cursor: pointer;
- transition: background 0.22s cubic-bezier(0.4, 0, 0.2, 1),
- transform 0.22s cubic-bezier(0.4, 0, 0.2, 1),
- box-shadow 0.22s cubic-bezier(0.4, 0, 0.2, 1),
- border-color 0.22s cubic-bezier(0.4, 0, 0.2, 1);
- user-select: none;
- position: relative;
- overflow: hidden;
- min-height: 128px;
-
- &:hover {
- background: rgba(255, 255, 255, 0.95);
- transform: translateY(-2px);
- box-shadow: 0 8px 24px -4px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(0, 0, 0, 0.02), inset 0 1px 1px #ffffff;
- border-color: rgba(191, 219, 254, 0.95);
- }
-
- &.is-pro-card:hover {
- border-color: rgba(199, 210, 254, 0.95);
- box-shadow: 0 8px 24px -4px rgba(99, 102, 241, 0.12), inset 0 1px 1px #ffffff;
- }
-
- &:active {
- transform: scale(0.985);
- }
-
- .card-top-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
-
- .app-icons-cluster {
- display: flex;
- align-items: center;
- gap: 6px;
-
- .app-icon-badge {
- width: 26px;
- height: 26px;
- border-radius: 8px;
- background: #f1f5f9;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #475569;
- border: 1px solid rgba(226, 232, 240, 0.9);
- flex-shrink: 0;
-
- span {
- font-size: 14px;
- }
- }
-
- .app-names-label {
- font-size: 11.5px;
- font-weight: 600;
- color: #475569;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 140px;
- }
- }
-
- .mode-badge {
- padding: 2px 7px;
- border-radius: 8px;
- font-size: 10.5px;
- font-weight: 700;
- line-height: 1.2;
- flex-shrink: 0;
-
- &.mode-flash {
- background: rgba(26, 115, 232, 0.1);
- color: #1a73e8;
- border: 1px solid rgba(26, 115, 232, 0.2);
- }
-
- &.mode-pro {
- background: rgba(99, 102, 241, 0.12);
- color: #6366f1;
- border: 1px solid rgba(99, 102, 241, 0.25);
- }
- }
- }
-
- .card-body {
- margin: 8px 0 6px;
-
- .card-title {
- font-size: 13.5px;
- font-weight: 600;
- color: #0f172a;
- margin: 0 0 3px;
- line-height: 1.3;
- letter-spacing: -0.01em;
- }
-
- .card-desc {
- font-size: 12px;
- color: #64748b;
- margin: 0;
- line-height: 1.45;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
- }
-
-
- .card-footer-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 6px;
- margin-top: 6px;
- padding-top: 8px;
- border-top: 1px solid rgba(241, 245, 249, 0.85);
-
- .tag-pill {
- font-size: 11px;
- font-weight: 600;
- color: #64748b;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .use-prompt-action {
- display: inline-flex;
- align-items: center;
- gap: 3px;
- font-size: 11.5px;
- font-weight: 600;
- color: #1a73e8;
- opacity: 0.85;
- transition: all 0.15s ease;
- flex-shrink: 0;
-
- .action-arrow {
- font-size: 13px;
- transition: transform 0.15s ease;
- }
- }
- }
-
- &:hover .use-prompt-action {
- opacity: 1;
-
- .action-arrow {
- transform: translateX(2px);
- }
- }
- }
- }
- }
- }
-
- }
-}
-
-// =============================================================================
-// KEYFRAME ANIMATIONS
-// =============================================================================
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-
-@keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(3px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes slideUpFade {
- from {
- opacity: 0;
- transform: translateY(8px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes slideDownFade {
- from {
- opacity: 0;
- transform: translateY(-6px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes floatGlow1 {
- 0% {
- transform: translate(0, 0) scale(1);
- }
- 50% {
- transform: translate(35px, 45px) scale(1.06);
- }
- 100% {
- transform: translate(-25px, 25px) scale(0.96);
- }
-}
-
-@keyframes floatGlow2 {
- 0% {
- transform: translate(0, 0) scale(1);
- }
- 50% {
- transform: translate(-40px, 40px) scale(1.08);
- }
- 100% {
- transform: translate(20px, 60px) scale(0.94);
- }
-}
-
-@keyframes floatGlow3 {
- 0% {
- transform: translate(0, 0) scale(1);
- }
- 50% {
- transform: translate(25px, -30px) scale(1.04);
- }
- 100% {
- transform: translate(-30px, 15px) scale(0.97);
- }
-}
-
-/* Transform-only: animating opacity here overrode the .glow-pending/.glow-ready
- fade transitions and kept both state glows permanently visible. */
-@keyframes readyPulseGlow {
- 0% {
- transform: scale(0.97);
- }
- 50% {
- transform: scale(1.06);
- }
- 100% {
- transform: scale(0.98);
- }
-}
-
-@keyframes statusDotPulse {
- 0% {
- transform: scale(0.95);
- box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
- }
- 70% {
- transform: scale(1);
- box-shadow: 0 0 0 5px rgba(16, 185, 129, 0);
- }
- 100% {
- transform: scale(0.95);
- box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
- }
-}
-
-@keyframes sparklePulse {
- 0% {
- transform: scale(0.92) rotate(0deg);
- opacity: 0.8;
- }
- 50% {
- transform: scale(1.12) rotate(8deg);
- opacity: 1;
- }
- 100% {
- transform: scale(0.92) rotate(0deg);
- opacity: 0.8;
- }
-}
-
-/* Respect the user's motion preference: freeze the purely decorative ambient
- animations (aurora drift, pulses, sparkles). Functional spinners keep going. */
-@media (prefers-reduced-motion: reduce) {
- .home-container .aurora-background .glow,
- .sparkle-icon,
- .status-dot,
- .slider-dither rect,
- .slider-solid,
- .thumb-ripple {
- animation: none !important;
- }
-}
-
-@keyframes ditherIn {
- from {
- opacity: 0;
- }
-}
-
-@keyframes solidSweep {
- from {
- transform: scaleX(0);
- }
- to {
- transform: scaleX(1);
- }
-}
-
-@keyframes thumbRipple {
- 0% {
- opacity: 0.7;
- transform: translate(-50%, -50%) scale(0.6);
- }
- 100% {
- opacity: 0;
- transform: translate(-50%, -50%) scale(2.6);
- }
-}
-
-@keyframes tipPop {
- from {
- opacity: 0;
- transform: translateY(6px) scale(0.96);
- }
- to {
- opacity: 1;
- transform: translateY(0) scale(1);
- }
-}
diff --git a/apps/showcase_ui/src/app/pages/home/home.component.ts b/apps/showcase_ui/src/app/pages/home/home.component.ts
deleted file mode 100644
index e189b9ce5..000000000
--- a/apps/showcase_ui/src/app/pages/home/home.component.ts
+++ /dev/null
@@ -1,1111 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Component, signal, computed, effect, inject, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-import { Router, RouterLink } from '@angular/router';
-import { AgentService } from '../../services/agent.service';
-import { SystemService } from '../../services/system.service';
-import {
- AdbServerConnectionResult,
- AdbServerDevice,
- DeviceInfo,
- ProbeResult
-} from '../../core/models/system.model';
-import {
- AppReference,
- SmartSuggestion,
- SuggestionCategory
-} from '../../core/data/smart-tasks.data';
-import { TaskRecommendationService } from '../../core/services/task-recommendation.service';
-import {
- DEFAULT_EXPLORER_MODE,
- DEFAULT_VERIFICATION_LEVEL,
- EXPLORER_MODES,
- ExplorerModeId,
- TuningLevel,
- VERIFICATION_LEVELS,
- VerificationLevelId,
- levelIndex,
- notchPercent
-} from '../../core/models/pro-tuning.model';
-
-export type TuningKind = 'verify' | 'explore';
-
-/** One 2x2 px square of the "maxed out" dither texture drawn over a slider rail. */
-export interface DitherPixel {
- /** Horizontal position as a percentage of the rail width. */
- x: number;
- /** Row offset in px (rail is 6 px tall, three 2 px rows). */
- y: number;
- /** Resting opacity; squares near the thumb are stronger. */
- opacity: number;
- /** Animation delay in ms so the texture spreads leftwards from the thumb. */
- delay: number;
-}
-
-/**
- * Seeded pixels keep the slider texture stable across renders.
- * Delays increase with distance from the thumb to animate from right to left.
- */
-function buildDitherPixels(count = 260, seed = 7): DitherPixel[] {
- let state = seed >>> 0;
- const rand = (): number => {
- state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
- return state / 0x100000000;
- };
- const pixels: DitherPixel[] = [];
- for (let i = 0; i < count; i++) {
- const x = rand() * 100;
- const y = Math.floor(rand() * 3) * 2;
- pixels.push({
- x: Math.round(x * 10) / 10,
- y,
- opacity: Math.round((0.35 + 0.4 * rand()) * 100) / 100,
- // 8 ms per percent: the front takes ~0.8 s to reach the left end.
- delay: Math.round((100 - x) * 8)
- });
- }
- return pixels;
-}
-
-/** Shared view model for the verification and screen-reading sliders. */
-export interface TuningSliderVm {
- kind: TuningKind;
- name: string;
- /** One-word meaning of each end of the track, e.g. ["Off", "Strict"]. */
- ends: readonly [string, string];
- ladder: readonly TuningLevel[];
- index: number;
- level: TuningLevel;
- /** 0..1 position of the thumb along the track. */
- fraction: number;
-}
-
-export type { AppReference, SmartSuggestion, SuggestionCategory };
-
-type AdbGuideTab = 'emulator' | 'usb' | 'wifi' | 'remote';
-
-
-@Component({
- selector: 'app-home',
- standalone: true,
- imports: [FormsModule],
- templateUrl: './home.component.html',
- changeDetection: ChangeDetectionStrategy.Eager,
- styleUrl: './home.component.scss'
-})
-export class HomeComponent implements OnInit, OnDestroy {
- public agentService = inject(AgentService);
- public systemService = inject(SystemService);
- public taskRecService = inject(TaskRecommendationService);
- private router = inject(Router);
-
- // High-level navigation mode: 'diagnostics' (System Setup Guide) vs 'launcher' (Task Execution)
- public activeTab = signal<'diagnostics' | 'launcher'>('launcher');
-
- // Interactive guide sub-tab inside the ADB section
- public activeAdbGuideTab = signal('emulator');
- public emulatorSetupMode = signal<'studio' | 'cli'>('studio');
-
- // Interactive guide tab for LLM / OCR credentials: 'gemini' | 'ocr'
- public modelSetupMode = signal<'gemini' | 'custom'>('gemini');
- public showOcrConfig = signal(false);
- public showFullConfigFile = signal(false);
-
- // Model & Environment configuration from backend
- public modelConfigEnv = computed(() => this.systemService.modelConfigEnv());
-
- // Google Gemini API Key State
- public geminiKeyInput = signal('');
- public showGeminiKey = signal(false);
- public isSavingGeminiKey = signal(false);
- public isTestingGeminiKey = signal(false);
- public geminiSaveMessage = signal(null);
- public geminiSaveError = signal(null);
- public isGeminiKeyEdited = signal(false);
-
- // Vision OCR API Key State
- public ocrKeyInput = signal('');
- public showOcrKey = signal(false);
- public isSavingOcrKey = signal(false);
- public isTestingOcrKey = signal(false);
- public ocrSaveMessage = signal(null);
- public ocrSaveError = signal(null);
- public isOcrKeyEdited = signal(false);
-
- // Clipboard copy state tracker for interactive feedback
- public copiedId = signal(null);
-
- // Diagnostic re-check state
- public isRefreshingDiagnostics = signal(false);
-
- // Wireless ADB Interactive connection signals
- public wifiHost = signal('192.168.1.100');
- public wifiPort = signal('5555');
- public isConnectingWifi = signal(false);
- public wifiConnectMessage = signal(null);
- public wifiConnectError = signal(null);
- public adbRestartFeedback = signal(null);
- public showConnectionMethods = signal(false);
-
- // ADB server endpoint connection state
- public remoteAdbHost = signal('127.0.0.1');
- public remoteAdbPort = signal('5038');
- public rememberRemoteAdb = signal(true);
- public isConnectingRemoteAdb = signal(false);
- public isActivatingRemoteAdb = signal(false);
- public isSwitchingToLocalAdb = signal(false);
- public remoteAdbMessage = signal(null);
- public remoteAdbError = signal(null);
- public remoteAdbDevices = signal([]);
- public remoteAdbProbeResult = signal(null);
- public adbServerStatus = computed(() => this.systemService.adbServerStatus());
- public isRemoteAdbServer = computed(() => this.systemService.isRemoteAdbServer());
- public remoteAdbHasReadyDevice = computed(() =>
- this.remoteAdbDevices().some(device => device.state === 'device')
- );
- public isProbedRemoteAdbActive = computed(() => {
- const tested = this.remoteAdbProbeResult()?.endpoint;
- const active = this.adbServerStatus()?.endpoint;
- return !!tested && !!active && tested.identity === active.identity;
- });
-
- public wifiCommand = computed(() => {
- const h = this.wifiHost().trim() || '';
- const p = this.wifiPort().trim() || '5555';
- return `adb connect ${h}:${p}`;
- });
-
- // Task execution parameters
- public selectedProfile = signal<'flash' | 'pro'>('flash');
- public taskGoal = signal('');
- public isSubmitting = signal(false);
- public errorMessage = signal(null);
-
- // Pro Mode Outputter & Structured Output Configuration
- public expectedOutput = signal('');
- public enableOutputter = signal(true);
- public showOutputterDrawer = signal(false);
-
- // Slider indexes map to the API ids in VERIFICATION_LEVELS / EXPLORER_MODES.
- public readonly verificationLevels = VERIFICATION_LEVELS;
- public readonly explorerModes = EXPLORER_MODES;
- public verificationIndex = signal(
- levelIndex(VERIFICATION_LEVELS, DEFAULT_VERIFICATION_LEVEL, DEFAULT_VERIFICATION_LEVEL)
- );
- public explorerIndex = signal(
- levelIndex(EXPLORER_MODES, DEFAULT_EXPLORER_MODE, DEFAULT_EXPLORER_MODE)
- );
- /** Effective defaults from the backend config (`GET /api/run/defaults`). */
- private tuningDefaults = signal<{ verification: VerificationLevelId; explorer: ExplorerModeId }>({
- verification: DEFAULT_VERIFICATION_LEVEL,
- explorer: DEFAULT_EXPLORER_MODE
- });
- /** True once the user moved a slider; backend defaults then stop overriding it. */
- private tuningTouched = signal(false);
- /** Which slider's hover card is open (while hovering, dragging, or focused). */
- public activeTuningTip = signal(null);
- /** Pixel cloud drawn over a rail once its slider reaches the last notch. */
- public readonly ditherPixels: readonly DitherPixel[] = buildDitherPixels();
-
- public verificationLevel = computed>(
- () => VERIFICATION_LEVELS[this.verificationIndex()]
- );
- public explorerMode = computed>(
- () => EXPLORER_MODES[this.explorerIndex()]
- );
- public isTuningDefault = computed(() => {
- const d = this.tuningDefaults();
- return this.verificationLevel().id === d.verification && this.explorerMode().id === d.explorer;
- });
- public tuningSliders = computed(() => {
- const vi = this.verificationIndex();
- const ei = this.explorerIndex();
- return [
- {
- kind: 'verify',
- name: 'Result check',
- ends: ['Off', 'Strict'],
- ladder: VERIFICATION_LEVELS,
- index: vi,
- level: VERIFICATION_LEVELS[vi],
- fraction: notchPercent(vi, VERIFICATION_LEVELS.length) / 100
- },
- {
- kind: 'explore',
- name: 'Screen reading',
- ends: ['Faster', 'Sharper'],
- ladder: EXPLORER_MODES,
- index: ei,
- level: EXPLORER_MODES[ei],
- fraction: notchPercent(ei, EXPLORER_MODES.length) / 100
- }
- ];
- });
-
- public notchFraction(index: number, count: number): number {
- return notchPercent(index, count) / 100;
- }
-
- public setTuningIndex(kind: TuningKind, raw: number | string): void {
- const idx = Math.round(Number(raw));
- if (!Number.isFinite(idx)) return;
- const ladder = kind === 'verify' ? VERIFICATION_LEVELS : EXPLORER_MODES;
- const clamped = Math.min(Math.max(idx, 0), ladder.length - 1);
- this.tuningTouched.set(true);
- if (kind === 'verify') {
- this.verificationIndex.set(clamped);
- } else {
- this.explorerIndex.set(clamped);
- }
- // Keyboard nudges and drags should keep the explanation visible.
- this.activeTuningTip.set(kind);
- }
-
- public showTuningTip(kind: TuningKind): void {
- this.activeTuningTip.set(kind);
- }
-
- public hideTuningTip(kind: TuningKind): void {
- if (this.activeTuningTip() === kind) {
- this.activeTuningTip.set(null);
- }
- }
-
- public resetTuning(): void {
- const d = this.tuningDefaults();
- this.verificationIndex.set(levelIndex(VERIFICATION_LEVELS, d.verification, DEFAULT_VERIFICATION_LEVEL));
- this.explorerIndex.set(levelIndex(EXPLORER_MODES, d.explorer, DEFAULT_EXPLORER_MODE));
- this.tuningTouched.set(false);
- }
-
- /** Pull the effective config defaults so the sliders start where artemis.jsonc is. */
- private loadProTuningDefaults(): void {
- this.agentService.getProTuningDefaults().subscribe({
- next: (res) => {
- const vIdx = levelIndex(VERIFICATION_LEVELS, res?.verification_level, DEFAULT_VERIFICATION_LEVEL);
- const eIdx = levelIndex(EXPLORER_MODES, res?.explorer_mode, DEFAULT_EXPLORER_MODE);
- this.tuningDefaults.set({
- verification: VERIFICATION_LEVELS[vIdx].id,
- explorer: EXPLORER_MODES[eIdx].id
- });
- if (!this.tuningTouched()) {
- this.verificationIndex.set(vIdx);
- this.explorerIndex.set(eIdx);
- }
- },
- // Defaults are a convenience; the built-in ladder defaults already apply.
- error: () => undefined
- });
- }
-
- public toggleOutputterDrawer(): void {
- this.showOutputterDrawer.update((v) => !v);
- }
-
- public applyOutputPreset(preset: string): void {
- if (this.expectedOutput() === preset) {
- this.expectedOutput.set('');
- } else {
- this.expectedOutput.set(preset);
- this.showOutputterDrawer.set(true);
- }
- }
-
- // Smart intent detection for model recommendation
- public isIntentSuggestingPro = computed(() => {
- const text = this.taskGoal().toLowerCase();
- if (!text.trim()) return false;
- const keywords = [
- 'monitor', 'polling', 'poll', 'wait until', 'loop', 'keep watching',
- 'crash', 'logcat', 'troubleshoot', 'diagnose', 'debug', 'investigate',
- 'compare', 'extract', 'summarize', 'report',
- '监控', '轮询', '等待', '一直', '直到', '崩溃', '闪退', '排查', '分析日志', '对比', '总结'
- ];
- return keywords.some(k => text.includes(k));
- });
-
- public showIntentSuggestion = computed(() => {
- return this.isIntentSuggestingPro() && this.selectedProfile() === 'flash';
- });
-
- // Computed helper states delegating to SystemService
- public isReady = computed(() => this.systemService.isReady());
- public hasReadinessReport = computed(() => this.systemService.hasReadinessReport());
- public isLoading = computed(() => this.systemService.isLoading());
- public isRestartingAdb = computed(() => this.systemService.isRestartingAdb());
- public launchingAvd = computed(() => this.systemService.launchingAvd());
- public emulatorLaunchState = computed(() => this.systemService.emulatorLaunchState());
- public isEmulatorLaunching = computed(() => this.systemService.isEmulatorLaunching());
- public showLaunchLogs = signal(false);
-
- // Probes
- public pythonProbe = computed(() => this.systemService.pythonProbe());
- public configProbe = computed(() => this.systemService.configProbe());
- public adbProbe = computed(() => this.systemService.adbProbe());
- public llmProbe = computed(() => this.systemService.llmProbe());
- public geminiProbe = computed(() => this.systemService.geminiProbe());
- public ocrProbe = computed(() => this.systemService.ocrProbe());
- public toolchainProbe = computed(() => this.systemService.toolchainProbe());
-
- // Step-level readiness
- public isEnvironmentReady = computed(() => this.systemService.isEnvironmentReady());
- public isCredentialsReady = computed(() => this.systemService.isCredentialsReady());
- public isSkipCredentialsCheck = computed(() => this.systemService.isSkipCredentialsCheck());
- public isDeviceReady = computed(() => this.systemService.isDeviceReady());
-
- // Device information
- public activeDevice = computed(() => this.systemService.activeDevice());
- public connectedDevices = computed(() => this.systemService.connectedDevices());
- public installedAvds = computed(() => this.systemService.installedAvds());
- public emulatorPath = computed(() => this.systemService.emulatorPath());
- public isEmulatorInPath = computed(() => this.systemService.isEmulatorInPath());
- public totalStepCount = computed(() => this.systemService.totalStepCount());
- public passedStepCount = computed(() => this.systemService.passedStepCount());
- public blockerCount = computed(() => this.systemService.blockerCount());
- public passedBlockerCount = computed(() => this.systemService.passedBlockerCount());
-
- // Configured LLM providers from probe metadata
- public configuredLlmProviders = computed(() => {
- const meta = this.llmProbe()?.metadata;
- if (meta && Array.isArray(meta['providers'])) {
- return meta['providers'];
- }
- return [];
- });
-
- // Multi-OS detection & active OS selection
- public selectedOs = signal<'linux' | 'darwin' | 'windows' | null>(null);
- public effectiveOs = computed<'linux' | 'darwin' | 'windows'>(() => {
- return this.selectedOs() || this.systemService.osType();
- });
-
- public oneClickSetupCmd = computed(() => {
- const os = this.effectiveOs();
- if (os === 'windows') {
- return 'powershell -ExecutionPolicy Bypass -File scripts/install_deps.ps1';
- }
- return 'bash scripts/install_deps.sh';
- });
-
- public adbInstallCmd = computed(() => {
- const os = this.effectiveOs();
- if (os === 'windows') {
- return 'winget install Google.PlatformTools';
- }
- if (os === 'darwin') {
- return 'brew install android-platform-tools';
- }
- return 'sudo apt-get install -y adb';
- });
-
- public toolchainInstallCmd = computed(() => {
- const os = this.effectiveOs();
- if (os === 'windows') {
- return 'winget install Gyan.FFmpeg Genymobile.scrcpy';
- }
- if (os === 'darwin') {
- return 'brew install ffmpeg scrcpy';
- }
- return 'sudo apt-get install -y ffmpeg scrcpy';
- });
-
- public emuHypervisorTitle = computed(() => {
- const os = this.effectiveOs();
- if (os === 'windows') return 'Enable Windows Hypervisor Platform (WHPX)';
- if (os === 'darwin') return 'Verify macOS Hypervisor / Install Tools';
- return 'Enable KVM Hardware Acceleration (Linux)';
- });
-
- public emuHypervisorDesc = computed(() => {
- const os = this.effectiveOs();
- if (os === 'windows') return 'Enable Windows Hypervisor Platform in PowerShell (Run as Administrator):';
- if (os === 'darwin') return 'macOS uses native Hypervisor.framework. Install SDK tools via Homebrew (or Studio):';
- return 'Ensure virtualization permissions are granted to your user account:';
- });
-
- public emuHypervisorCmd = computed(() => {
- const os = this.effectiveOs();
- if (os === 'windows') return 'Enable-WindowsOptionalFeature -Online -FeatureName HypervisorPlatform';
- if (os === 'darwin') return 'brew install --cask android-commandlinetools';
- return 'sudo apt-get install -y qemu-kvm libvirt-daemon-system && sudo adduser $USER kvm';
- });
-
- public emuSdkInstallCmd = computed(() => {
- const os = this.effectiveOs();
- if (os === 'darwin') {
- return 'sdkmanager --install "system-images;android-34;google_apis;arm64-v8a" "emulator" "platform-tools"';
- }
- return 'sdkmanager --install "system-images;android-34;google_apis;x86_64" "emulator" "platform-tools"';
- });
-
- public emuCreateAvdCmd = computed(() => {
- const os = this.effectiveOs();
- if (os === 'darwin') {
- return 'avdmanager create avd -n Pixel_8_API_34 -k "system-images;android-34;google_apis;arm64-v8a" --device "pixel_8"';
- }
- return 'avdmanager create avd -n Pixel_8_API_34 -k "system-images;android-34;google_apis;x86_64" --device "pixel_8"';
- });
-
- // Flag indicating whether Google Cloud Vision OCR is configured
- public isOcrConfigured = computed(() => {
- const meta = this.ocrProbe()?.metadata;
- return meta?.['configured'] === true;
- });
-
- public currentApiKey = computed(() => this.systemService.currentApiKey());
- public apiKeysMap = computed>(() => this.systemService.apiKeysMap());
-
- public savedGeminiKey = computed(() => {
- const keys = this.apiKeysMap();
- return keys['google'] || this.currentApiKey() || '';
- });
-
- public isGeminiModified = computed(() => {
- return this.geminiKeyInput().trim() !== this.savedGeminiKey().trim();
- });
-
- public savedOcrKey = computed(() => {
- const keys = this.apiKeysMap();
- return keys['ocr'] || '';
- });
-
- public isOcrModified = computed(() => {
- return this.ocrKeyInput().trim() !== this.savedOcrKey().trim();
- });
-
- // Rich Smart Suggestions Library (Device-Aware, Flash vs Pro Tailored)
- public readonly allSuggestions = this.taskRecService.allTasks;
-
- // Suggestion category filter & shuffle state
- public selectedCategory = signal('all');
- public shuffleOffset = signal(0);
-
- // Set of installed package strings from active device
- public installedPackages = computed>(() => {
- const pkgs = this.activeDevice()?.installed_packages;
- if (pkgs && Array.isArray(pkgs)) {
- return new Set(pkgs);
- }
- return new Set();
- });
-
- public filteredSuggestions = computed(() => {
- return this.taskRecService.filterAndRankTasks(
- this.installedPackages(),
- this.selectedCategory(),
- this.shuffleOffset()
- );
- });
-
-
-
-
- private focusListener = () => {
- // Silently re-check environment when user returns to the browser tab
- this.systemService.fetchReadiness().subscribe();
- };
-
- constructor() {
- effect(() => {
- const keys = this.systemService.apiKeysMap();
- const current = this.systemService.currentApiKey();
- const googleKey = keys['google'] || current || '';
- const ocrKey = keys['ocr'] || '';
- if (!this.isGeminiKeyEdited()) {
- this.geminiKeyInput.set(googleKey);
- }
- if (!this.isOcrKeyEdited()) {
- this.ocrKeyInput.set(ocrKey);
- }
- });
- }
-
- ngOnInit(): void {
-
- this.loadProTuningDefaults();
- // Initial fetch of system readiness & model configuration
- this.systemService.fetchReadiness().subscribe();
- this.systemService.fetchModelConfigEnv().subscribe();
- this.systemService.fetchAdbServerStatus().subscribe({
- next: status => {
- if (status.endpoint.mode === 'remote') {
- this.remoteAdbHost.set(status.endpoint.host);
- this.remoteAdbPort.set(String(status.endpoint.port));
- this.activeAdbGuideTab.set('remote');
- }
- },
- error: () => {}
- });
-
- window.addEventListener('focus', this.focusListener);
- }
-
- ngOnDestroy(): void {
- window.removeEventListener('focus', this.focusListener);
- }
-
- public setTab(tab: 'diagnostics' | 'launcher'): void {
- this.activeTab.set(tab);
- if (tab === 'diagnostics') {
- this.systemService.fetchReadiness().subscribe();
- this.systemService.fetchModelConfigEnv().subscribe();
- }
- }
-
- public setSelectedOs(os: 'linux' | 'darwin' | 'windows'): void {
- this.selectedOs.set(os);
- }
-
- public setAdbGuideTab(tab: AdbGuideTab): void {
- this.activeAdbGuideTab.set(tab);
- this.remoteAdbError.set(null);
- this.remoteAdbMessage.set(null);
- }
-
- public setEmulatorSetupMode(mode: 'studio' | 'cli'): void {
- this.emulatorSetupMode.set(mode);
- }
-
- public setModelSetupMode(mode: 'gemini' | 'custom'): void {
- this.modelSetupMode.set(mode);
- if (mode === 'custom') {
- this.systemService.setSkipCredentialsCheck(true);
- this.systemService.fetchModelConfigEnv().subscribe();
- } else {
- this.systemService.setSkipCredentialsCheck(false);
- }
- }
-
- public toggleFullConfigFile(): void {
- this.showFullConfigFile.update(v => !v);
- }
-
- public toggleGeminiKeyVisibility(): void {
- this.showGeminiKey.update(v => !v);
- }
-
- public toggleOcrKeyVisibility(): void {
- this.showOcrKey.update(v => !v);
- }
-
- public toggleOcrConfig(): void {
- this.showOcrConfig.update(v => !v);
- }
-
- public onGeminiKeyChange(val: string): void {
- this.geminiKeyInput.set(val);
- this.isGeminiKeyEdited.set(true);
- this.geminiSaveError.set(null);
- this.geminiSaveMessage.set(null);
- }
-
- public onOcrKeyChange(val: string): void {
- this.ocrKeyInput.set(val);
- this.isOcrKeyEdited.set(true);
- this.ocrSaveError.set(null);
- this.ocrSaveMessage.set(null);
- }
-
- public saveGeminiKey(): void {
- const key = this.geminiKeyInput().trim();
- if (!key) return;
- this.isSavingGeminiKey.set(true);
- this.geminiSaveError.set(null);
- this.geminiSaveMessage.set(null);
-
- this.systemService.updateApiKey('google', key, true).subscribe({
- next: (res) => {
- this.isSavingGeminiKey.set(false);
- this.isGeminiKeyEdited.set(false);
- this.geminiSaveMessage.set(res?.message || '✓ Gemini API key verified & saved successfully.');
- setTimeout(() => this.geminiSaveMessage.set(null), 5000);
- },
- error: (err) => {
- this.isSavingGeminiKey.set(false);
- this.geminiSaveError.set(err?.error?.detail || err?.message || 'Failed to update Gemini API key.');
- }
- });
- }
-
- public clearGeminiKey(): void {
- this.geminiKeyInput.set('');
- this.isGeminiKeyEdited.set(false);
- this.geminiSaveError.set(null);
- this.geminiSaveMessage.set(null);
-
- if (this.savedGeminiKey().trim()) {
- this.isSavingGeminiKey.set(true);
- this.systemService.updateApiKey('google', '', true).subscribe({
- next: (res) => {
- this.isSavingGeminiKey.set(false);
- this.geminiSaveMessage.set(res?.message || '✓ Gemini API key cleared.');
- setTimeout(() => this.geminiSaveMessage.set(null), 5000);
- },
- error: (err) => {
- this.isSavingGeminiKey.set(false);
- this.geminiSaveError.set(err?.error?.detail || err?.message || 'Failed to clear Gemini API key.');
- }
- });
- } else {
- this.geminiSaveMessage.set('✓ Gemini API key cleared.');
- setTimeout(() => this.geminiSaveMessage.set(null), 3000);
- }
- }
-
- public saveOcrKey(): void {
- const key = this.ocrKeyInput().trim();
- if (!key) return;
- this.isSavingOcrKey.set(true);
- this.ocrSaveError.set(null);
- this.ocrSaveMessage.set(null);
-
- this.systemService.updateApiKey('ocr', key, true).subscribe({
- next: (res) => {
- this.isSavingOcrKey.set(false);
- this.isOcrKeyEdited.set(false);
- this.ocrSaveMessage.set(res?.message || '✓ Vision OCR API key verified & saved.');
- setTimeout(() => this.ocrSaveMessage.set(null), 5000);
- },
- error: (err) => {
- this.isSavingOcrKey.set(false);
- this.ocrSaveError.set(err?.error?.detail || err?.message || 'Failed to update Vision OCR key.');
- }
- });
- }
-
- public clearOcrKey(): void {
- this.ocrKeyInput.set('');
- this.isOcrKeyEdited.set(false);
- this.ocrSaveError.set(null);
- this.ocrSaveMessage.set(null);
-
- if (this.savedOcrKey().trim()) {
- this.isSavingOcrKey.set(true);
- this.systemService.updateApiKey('ocr', '', true).subscribe({
- next: (res) => {
- this.isSavingOcrKey.set(false);
- this.ocrSaveMessage.set(res?.message || '✓ Vision OCR API key cleared.');
- setTimeout(() => this.ocrSaveMessage.set(null), 5000);
- },
- error: (err) => {
- this.isSavingOcrKey.set(false);
- this.ocrSaveError.set(err?.error?.detail || err?.message || 'Failed to clear Vision OCR key.');
- }
- });
- } else {
- this.ocrSaveMessage.set('✓ Vision OCR API key cleared.');
- setTimeout(() => this.ocrSaveMessage.set(null), 3000);
- }
- }
-
- public testGeminiKey(): void {
- const key = this.geminiKeyInput().trim();
- if (!key) return;
- this.isTestingGeminiKey.set(true);
- this.geminiSaveError.set(null);
- this.geminiSaveMessage.set(null);
-
- this.systemService.testApiKey('google', key).subscribe({
- next: (res) => {
- this.isTestingGeminiKey.set(false);
- if (res?.valid) {
- this.geminiSaveMessage.set(res?.message || '✓ Gemini API key is valid!');
- } else {
- this.geminiSaveError.set(res?.message || 'Gemini API key verification failed.');
- }
- setTimeout(() => this.geminiSaveMessage.set(null), 5000);
- },
- error: (err) => {
- this.isTestingGeminiKey.set(false);
- this.geminiSaveError.set(err?.error?.detail || err?.message || 'Gemini API key test failed.');
- }
- });
- }
-
- public testOcrKey(): void {
- const key = this.ocrKeyInput().trim();
- if (!key) return;
- this.isTestingOcrKey.set(true);
- this.ocrSaveError.set(null);
- this.ocrSaveMessage.set(null);
-
- this.systemService.testApiKey('ocr', key).subscribe({
- next: (res) => {
- this.isTestingOcrKey.set(false);
- if (res?.valid) {
- this.ocrSaveMessage.set(res?.message || '✓ Vision OCR API key is valid!');
- } else {
- this.ocrSaveError.set(res?.message || 'Vision OCR API key verification failed.');
- }
- setTimeout(() => this.ocrSaveMessage.set(null), 5000);
- },
- error: (err) => {
- this.isTestingOcrKey.set(false);
- this.ocrSaveError.set(err?.error?.detail || err?.message || 'Vision OCR API key test failed.');
- }
- });
- }
-
- public getProviderDisplayName(tab: string): string {
- switch (tab) {
- case 'gemini': return 'Gemini';
- case 'ocr': return 'Vision OCR';
- default: return tab;
- }
- }
-
- public getProviderEnvVar(tab: string): string {
- switch (tab) {
- case 'gemini': return 'GEMINI_API_KEY';
- case 'ocr': return 'GOOGLE_VISION_API_KEY';
- default: return 'API_KEY';
- }
- }
-
- public getProviderHint(tab: string): string {
- switch (tab) {
- case 'gemini':
- return 'For a quick start, Google Gemini provides a free API key. Artemis also supports other models (OpenAI, Claude, OpenRouter, etc.)—you can configure your own API keys directly in .env or your environment.';
- case 'ocr':
- return 'Google Cloud Vision API key for on-screen OCR text detection and UI grounding.';
- default:
- return 'Configure your API key or use environment definitions.';
- }
- }
-
- public skipCredentialsCheck(): void {
- this.systemService.skipCredentialsCheck();
- }
-
- public getApiKeyPlaceholder(tab: string): string {
- switch (tab) {
- case 'gemini': return 'Enter Gemini API Key (e.g. AIzaSy...)';
- case 'ocr': return 'Enter Vision OCR API Key (e.g. AIzaSy...)';
- default: return 'Enter API Key...';
- }
- }
-
- public isTabProviderActive(tab: string): boolean {
- if (tab === 'ocr') {
- return this.isOcrConfigured();
- }
- const targetProvider = tab === 'gemini' ? 'google' : tab;
- const providers = this.configuredLlmProviders();
- return providers.some(p => p.provider === targetProvider);
- }
-
- public refreshReadiness(): void {
- this.isRefreshingDiagnostics.set(true);
- this.systemService.fetchReadiness(false, true).subscribe({
- next: () => {
- this.systemService.fetchModelConfigEnv().subscribe({
- next: () => {
- setTimeout(() => this.isRefreshingDiagnostics.set(false), 450);
- },
- error: () => this.isRefreshingDiagnostics.set(false)
- });
- },
- error: () => this.isRefreshingDiagnostics.set(false)
- });
- }
-
- public restartAdbServer(): void {
- this.adbRestartFeedback.set(null);
- this.systemService.restartAdb().subscribe({
- next: (res) => {
- this.adbRestartFeedback.set(
- res?.restart_result?.skipped ? 'Devices Refreshed ✓' : 'ADB Refreshed ✓'
- );
- setTimeout(() => this.adbRestartFeedback.set(null), 2500);
- },
- error: () => {
- this.adbRestartFeedback.set('Restart Failed');
- setTimeout(() => this.adbRestartFeedback.set(null), 3000);
- }
- });
- }
-
- public connectWifiDevice(): void {
- if (this.isRemoteAdbServer()) {
- this.wifiConnectError.set('Switch to local ADB before connecting a Wireless ADB device.');
- return;
- }
- const host = this.wifiHost().trim();
- const portStr = this.wifiPort().trim() || '5555';
- const port = parseInt(portStr, 10) || 5555;
-
- if (!host) {
- this.wifiConnectError.set('Please enter a valid IP address.');
- return;
- }
-
- this.isConnectingWifi.set(true);
- this.wifiConnectError.set(null);
- this.wifiConnectMessage.set(null);
-
- this.systemService.connectWirelessAdb(host, port).subscribe({
- next: (res) => {
- this.isConnectingWifi.set(false);
- const cr = res?.connect_result;
- if (cr?.success) {
- this.wifiConnectMessage.set(`Connected to ${host}:${port}!`);
- setTimeout(() => this.wifiConnectMessage.set(null), 4000);
- } else {
- this.wifiConnectError.set(cr?.message || 'Connection failed. Please check phone IP & Wi-Fi.');
- }
- },
- error: (err) => {
- this.isConnectingWifi.set(false);
- this.wifiConnectError.set(err?.error?.detail || 'Failed to connect. Please check adb connection.');
- }
- });
- }
-
- public connectRemoteAdbServer(): void {
- const host = this.remoteAdbHost().trim();
- const port = Number(this.remoteAdbPort().trim());
-
- if (!host) {
- this.remoteAdbError.set('Enter the host name or IP address of the ADB server.');
- return;
- }
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
- this.remoteAdbError.set('Enter a port between 1 and 65535.');
- return;
- }
-
- this.isConnectingRemoteAdb.set(true);
- this.remoteAdbError.set(null);
- this.remoteAdbMessage.set(null);
- this.remoteAdbDevices.set([]);
- this.remoteAdbProbeResult.set(null);
-
- this.systemService.probeAdbServer(host, port).subscribe({
- next: response => {
- this.isConnectingRemoteAdb.set(false);
- const result = response.connection_result;
- if (result.success) {
- this.remoteAdbProbeResult.set(result);
- this.remoteAdbDevices.set(result.devices || []);
- this.remoteAdbMessage.set(result.message);
- } else {
- this.remoteAdbError.set(result.message);
- }
- },
- error: error => {
- this.isConnectingRemoteAdb.set(false);
- this.remoteAdbError.set(
- error?.error?.detail || 'Unable to test the ADB server endpoint.'
- );
- }
- });
- }
-
- public activateRemoteAdbServer(): void {
- const tested = this.remoteAdbProbeResult();
- if (!tested?.success) {
- this.remoteAdbError.set('Test the endpoint before using it.');
- return;
- }
-
- this.isActivatingRemoteAdb.set(true);
- this.remoteAdbError.set(null);
- this.systemService.connectAdbServer(
- tested.endpoint.host,
- tested.endpoint.port,
- this.rememberRemoteAdb()
- ).subscribe({
- next: response => {
- this.isActivatingRemoteAdb.set(false);
- const result = response.connection_result;
- if (result.success) {
- this.remoteAdbProbeResult.set(result);
- this.remoteAdbDevices.set(result.devices || []);
- this.remoteAdbMessage.set(result.message);
- } else {
- this.remoteAdbError.set(result.message);
- }
- },
- error: error => {
- this.isActivatingRemoteAdb.set(false);
- this.remoteAdbError.set(
- error?.error?.detail || 'Unable to use the ADB server endpoint.'
- );
- }
- });
- }
-
- public updateRemoteAdbHost(value: string): void {
- this.remoteAdbHost.set(value);
- this.clearRemoteAdbProbe();
- }
-
- public updateRemoteAdbPort(value: string): void {
- this.remoteAdbPort.set(value);
- this.clearRemoteAdbProbe();
- }
-
- private clearRemoteAdbProbe(): void {
- this.remoteAdbProbeResult.set(null);
- this.remoteAdbDevices.set([]);
- this.remoteAdbMessage.set(null);
- this.remoteAdbError.set(null);
- }
-
- public switchToLocalAdbServer(): void {
- this.isSwitchingToLocalAdb.set(true);
- this.remoteAdbError.set(null);
- this.systemService.useLocalAdbServer(true).subscribe({
- next: response => {
- this.isSwitchingToLocalAdb.set(false);
- this.remoteAdbDevices.set([]);
- this.remoteAdbMessage.set(response.connection_result.message);
- },
- error: error => {
- this.isSwitchingToLocalAdb.set(false);
- this.remoteAdbError.set(
- error?.error?.detail || 'Unable to switch back to the local ADB server.'
- );
- }
- });
- }
-
- public toggleConnectionMethods(): void {
- this.showConnectionMethods.update(value => !value);
- }
-
- public launchAvdEmulator(avdName: string): void {
- if (this.isRemoteAdbServer()) {
- return;
- }
- this.systemService.launchEmulator(avdName).subscribe();
- }
-
- public toggleLaunchLogs(): void {
- this.showLaunchLogs.update(v => !v);
- }
-
- public stopEmulator(): void {
- this.systemService.stopEmulator().subscribe();
- }
-
- public dismissEmulatorStatus(): void {
- this.systemService.dismissEmulatorStatus().subscribe();
- }
-
- public selectTargetDevice(serial: string): void {
- this.systemService.selectDevice(serial).subscribe();
- }
-
- public getEmulatorCommand(avdName: string): string {
- const p = this.emulatorPath();
- const cmd = this.isEmulatorInPath() ? 'emulator' : (p || '~/Android/Sdk/emulator/emulator');
- return `${cmd} -avd ${avdName}`;
- }
-
- public copyToClipboard(text: string, id: string): void {
- if (navigator.clipboard) {
- navigator.clipboard.writeText(text).then(() => {
- this.copiedId.set(id);
- setTimeout(() => {
- if (this.copiedId() === id) {
- this.copiedId.set(null);
- }
- }, 2000);
- });
- }
- }
-
- public setProfile(profile: 'flash' | 'pro'): void {
- this.selectedProfile.set(profile);
- }
-
- public setCategory(cat: SuggestionCategory): void {
- this.selectedCategory.set(cat);
- }
-
- public shuffleSuggestions(): void {
- this.shuffleOffset.update(v => v + 3);
- }
-
- public getAppNamesDisplay(apps: AppReference[]): string {
- return apps.map(a => a.name).join(' + ');
- }
-
- public applySuggestion(item: SmartSuggestion): void {
- this.taskGoal.set(item.goal);
- this.selectedProfile.set(item.profile);
- this.errorMessage.set(null);
- }
-
- public applyQuickPrompt(promptGoal: string, profile?: 'flash' | 'pro'): void {
- this.taskGoal.set(promptGoal);
- if (profile) {
- this.selectedProfile.set(profile);
- }
- this.errorMessage.set(null);
- }
-
-
- public proceedToLauncher(): void {
- this.activeTab.set('launcher');
- }
-
- public runTask(): void {
- const goal = this.taskGoal().trim();
- if (!goal) {
- this.errorMessage.set('Please enter a task goal before running.');
- return;
- }
-
- if (!this.isReady()) {
- this.errorMessage.set('System prerequisites are not satisfied. Please review System Setup first.');
- this.activeTab.set('diagnostics');
- return;
- }
-
- this.isSubmitting.set(true);
- this.errorMessage.set(null);
-
- this.agentService
- .runTask(
- goal,
- this.selectedProfile(),
- this.selectedProfile() === 'pro' && this.expectedOutput().trim()
- ? this.expectedOutput().trim()
- : undefined,
- this.selectedProfile() === 'pro' ? this.enableOutputter() : undefined,
- this.selectedProfile() === 'pro'
- ? { verificationLevel: this.verificationLevel().id, explorerMode: this.explorerMode().id }
- : undefined
- )
- .subscribe({
- next: () => {
- this.isSubmitting.set(false);
- this.router.navigate(['/workspace']);
- },
- error: (err) => {
- console.error('Failed to submit task from home page:', err);
- this.isSubmitting.set(false);
- this.errorMessage.set(
- err?.error?.detail || 'Failed to submit task. Please check server connection.'
- );
- }
- });
- }
-}
diff --git a/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html b/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html
deleted file mode 100644
index c15a889fb..000000000
--- a/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss b/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss
deleted file mode 100644
index 9a01ecf60..000000000
--- a/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss
+++ /dev/null
@@ -1,151 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-$bg-base: #e2e5ea;
-$glass-bg: rgba(255, 255, 255, 0.45);
-$glass-border: rgba(255, 255, 255, 0.6);
-
-.workspace-container {
- position: relative;
- display: flex;
- height: 100vh;
- width: 100vw;
- background-color: $bg-base;
- overflow: hidden;
-
- &.is-resizing {
- cursor: col-resize;
- user-select: none;
- }
-
- /* --- Dynamic Aurora Background --- */
- .aurora-background {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 0;
- overflow: hidden;
- background: linear-gradient(135deg, #e2e5ea 0%, #cfd6df 100%);
-
- .glow {
- position: absolute;
- border-radius: 50%;
- filter: blur(90px);
- opacity: 0.5;
- animation: aurora-drift 22s infinite alternate ease-in-out;
- }
-
- .glow-1 {
- width: 55vw;
- height: 55vh;
- background: rgba(94, 114, 228, 0.25);
- top: -15%;
- left: -5%;
- animation-delay: 0s;
- }
-
- .glow-2 {
- width: 45vw;
- height: 45vh;
- background: rgba(45, 206, 137, 0.2);
- bottom: -10%;
- left: 30%;
- animation-delay: -5s;
- }
-
- .glow-3 {
- width: 50vw;
- height: 60vh;
- background: rgba(206, 104, 172, 0.15);
- top: 20%;
- right: -10%;
- animation-delay: -10s;
- }
- }
-
- /* --- Glassmorphism Panel --- */
- .glass-panel {
- position: relative;
- z-index: 1;
- background: $glass-bg;
- backdrop-filter: blur(24px) saturate(120%);
- -webkit-backdrop-filter: blur(24px) saturate(120%);
- border: 1px solid $glass-border;
- box-shadow: 0 8px 32px rgba(31, 38, 135, 0.05);
- }
-
- .left-panel {
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- background: rgba(255, 255, 255, 0.35);
- border-right: 1px solid rgba(255, 255, 255, 0.8);
- border-left: none;
- border-top: none;
- border-bottom: none;
- }
-
- .right-panel {
- min-width: 300px;
- flex-shrink: 0;
- padding: 0;
- border: none;
- background-color: #ffffff;
- box-shadow: -6px 0 24px rgba(0, 0, 0, 0.03);
- height: 100%;
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
- z-index: 2;
- }
-
- .resizer {
- width: 12px;
- cursor: col-resize;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 10;
- transition: background 0.2s ease;
- margin: 0 -6px;
-
- &:hover, .workspace-container.is-resizing & {
- background: rgba(255, 255, 255, 0.15);
-
- .resizer-handle {
- background: rgba(94, 114, 228, 0.8);
- height: 60px;
- }
- }
-
- .resizer-handle {
- width: 4px;
- height: 40px;
- background: rgba(0, 0, 0, 0.15);
- border-radius: 4px;
- transition: all 0.2s ease;
- }
- }
-}
-
-@keyframes aurora-drift {
- 0% { transform: translate(0, 0) scale(1); }
- 50% { transform: translate(5%, 3%) scale(1.05); }
- 100% { transform: translate(-2%, -5%) scale(0.95); }
-}
diff --git a/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts b/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts
deleted file mode 100644
index 9152ce6c7..000000000
--- a/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Component, HostListener, ChangeDetectionStrategy } from '@angular/core';
-
-import { LegacyAgentStreamComponent } from '../../components/legacy-agent-stream/legacy-agent-stream.component';
-import { ChatInterfaceComponent } from '../../components/chat-interface/chat-interface.component';
-
-@Component({
- selector: 'app-legacy-workspace',
- standalone: true,
- imports: [LegacyAgentStreamComponent, ChatInterfaceComponent],
- templateUrl: './legacy-workspace.component.html',
- changeDetection: ChangeDetectionStrategy.Eager,
- styleUrl: './legacy-workspace.component.scss'
-})
-export class LegacyWorkspaceComponent {
- public rightPanelWidth = typeof window !== 'undefined' ? Math.round(window.innerWidth / 3) : 450;
- public isDragging = false;
-
- public onDragStart(event: MouseEvent): void {
- this.isDragging = true;
- event.preventDefault();
- }
-
- @HostListener('document:mousemove', ['$event'])
- public onMouseMove(event: MouseEvent): void {
- if (!this.isDragging) {
- return;
- }
-
- const newWidth = window.innerWidth - event.clientX;
- const minWidth = 300;
- const maxWidth = window.innerWidth - 300;
-
- if (newWidth >= minWidth && newWidth <= maxWidth) {
- this.rightPanelWidth = newWidth;
- }
- }
-
- @HostListener('document:mouseup')
- public onMouseUp(): void {
- this.isDragging = false;
- }
-}
diff --git a/apps/showcase_ui/src/app/pages/workspace/workspace.component.html b/apps/showcase_ui/src/app/pages/workspace/workspace.component.html
deleted file mode 100644
index a44cd84e3..000000000
--- a/apps/showcase_ui/src/app/pages/workspace/workspace.component.html
+++ /dev/null
@@ -1,201 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- @if (errorMessage()) {
-
- error
- {{ errorMessage() }}
-
- close
-
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Start a new task...
-
-
-
-
-
-
-
-
-
-
-
- @if (taskInput.trim() && !isSubmitting()) {
-
- close
-
- }
-
-
- @if (isTaskRunning()) {
-
- stop
-
- }
-
-
-
- @if (isSubmitting()) {
- sync
- }
- @if (!isSubmitting()) {
- arrow_upward
- }
-
-
-
-
-
-
-
-
-
- @if (isTaskRunning()) {
-
- stop
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss b/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss
deleted file mode 100644
index 28535e473..000000000
--- a/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss
+++ /dev/null
@@ -1,885 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-$bg-base: #e2e5ea;
-$font-sans: 'Google Sans Text', 'Google Sans', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
-$primary: #0f172a;
-$primary-hover: #1e293b;
-$rose: #ef4444;
-$rose-dark: #dc2626;
-
-:host {
- display: block;
- height: 100vh;
- width: 100vw;
- overflow: hidden;
- font-family: $font-sans;
-}
-
-.workspace-container {
- position: relative;
- display: flex;
- height: 100vh;
- width: 100vw;
- background-color: $bg-base;
- overflow: hidden;
-
- &.is-resizing {
- cursor: col-resize;
- user-select: none;
- }
-
- /* --- Glassmorphism Panel --- */
- .glass-panel {
- position: relative;
- z-index: 1;
- background: #f4fafb;
- border: none;
- box-shadow: none;
- }
-
- .left-panel {
- flex: 1 0 500px;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- background: #f4fafb;
- border-right: 1px solid #e2e8f0;
- border-left: none;
- border-top: none;
- border-bottom: none;
- min-width: 450px;
- height: 100%;
- }
-
- .right-panel {
- flex: 0 1 auto;
- min-width: 250px;
- max-width: 100%;
- padding: 0;
- border: none;
- background-color: #ffffff;
- box-shadow: -6px 0 24px rgba(0, 0, 0, 0.03);
- height: 100%;
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
- z-index: 2;
- }
-
- .resizer {
- width: 12px;
- cursor: col-resize;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 10;
- transition: background 0.2s ease;
- margin: 0 -6px;
-
- &:hover, .workspace-container.is-resizing & {
- background: rgba(255, 255, 255, 0.15);
-
- .resizer-handle {
- background: rgba(94, 114, 228, 0.8);
- height: 60px;
- }
- }
-
- .resizer-handle {
- width: 4px;
- height: 40px;
- background: rgba(0, 0, 0, 0.15);
- border-radius: 4px;
- transition: background 0.2s ease, height 0.2s ease;
- }
- }
-
- /* ========================================================================= */
- /* BOTTOM FLOATING COMMAND BAR (Proximity Reveal & Minimalist Glassmorphism) */
- /* ========================================================================= */
- .workspace-floating-bar-wrapper {
- position: absolute;
- bottom: 18px;
- left: 50%;
- transform: translateX(-50%);
- width: 100%;
- max-width: min(680px, calc(100% - 36px));
- z-index: 50;
- display: flex;
- flex-direction: column;
- align-items: center;
- pointer-events: none;
- transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
-
- /* Floating Toast / Error Notification */
- .floating-dock-toast {
- pointer-events: auto;
- margin-bottom: 8px;
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 7px 14px;
- border-radius: 12px;
- font-size: 12.5px;
- font-weight: 500;
- box-shadow: 0 8px 24px -4px rgba(239, 68, 68, 0.15);
- animation: slideUpFade 0.2s ease-out;
- max-width: 90%;
-
- &.error {
- background: rgba(254, 242, 242, 0.88);
- backdrop-filter: blur(20px) saturate(180%);
- -webkit-backdrop-filter: blur(20px) saturate(180%);
- border: 1px solid rgba(254, 202, 202, 0.9);
- color: #991b1b;
-
- .toast-icon {
- font-size: 16px;
- color: #ef4444;
- flex-shrink: 0;
- }
-
- .toast-text {
- flex: 1;
- line-height: 1.4;
- }
-
- .btn-toast-close {
- background: transparent;
- border: none;
- padding: 2px;
- cursor: pointer;
- color: #991b1b;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- opacity: 0.7;
- transition: opacity 0.15s ease;
-
- &:hover {
- opacity: 1;
- background: rgba(0, 0, 0, 0.05);
- }
-
- .material-symbols-outlined {
- font-size: 15px;
- }
- }
- }
- }
-
- /* Dock Row: Flex container holding Main Card + Standalone Stop Button */
- .floating-dock-row {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- width: 100%;
- pointer-events: none;
- transition: gap 0.32s cubic-bezier(0.16, 1, 0.3, 1);
- }
-
- /* Standalone Floating Stop Button (Separated from Capsule) */
- .standalone-stop-btn {
- pointer-events: auto;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 34px;
- height: 34px;
- min-width: 34px;
- border-radius: 50%;
- border: 1px solid rgba(255, 255, 255, 0.85);
- background: rgba(239, 68, 68, 0.9);
- backdrop-filter: blur(24px) saturate(190%) brightness(105%);
- -webkit-backdrop-filter: blur(24px) saturate(190%) brightness(105%);
- color: #ffffff;
- box-shadow: 0 6px 18px -2px rgba(239, 68, 68, 0.4), inset 0 1px 1.5px rgba(255, 255, 255, 0.7);
- cursor: pointer;
- outline: none;
- flex-shrink: 0;
- transition: opacity 0.28s cubic-bezier(0.16, 1, 0.3, 1),
- transform 0.28s cubic-bezier(0.16, 1, 0.3, 1),
- width 0.28s cubic-bezier(0.16, 1, 0.3, 1),
- min-width 0.28s cubic-bezier(0.16, 1, 0.3, 1),
- margin 0.28s cubic-bezier(0.16, 1, 0.3, 1),
- background 0.2s ease,
- box-shadow 0.2s ease;
- animation: popIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
-
- .material-symbols-outlined {
- font-size: 17px;
- line-height: 1;
- }
-
- &:hover:not(:disabled) {
- background: #dc2626;
- transform: scale(1.1);
- box-shadow: 0 8px 22px rgba(239, 68, 68, 0.55), inset 0 1px 1.5px rgba(255, 255, 255, 0.9);
- }
-
- &:active:not(:disabled) {
- transform: scale(0.93);
- }
-
- &:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
- }
-
- /* Floating Dock Card Container */
- .floating-dock-card {
- pointer-events: auto;
- position: relative;
- width: 100%;
- border-radius: 18px;
- box-sizing: border-box;
- overflow: hidden;
- transition: max-width 0.32s cubic-bezier(0.16, 1, 0.3, 1),
- width 0.32s cubic-bezier(0.16, 1, 0.3, 1),
- border-radius 0.28s cubic-bezier(0.16, 1, 0.3, 1),
- box-shadow 0.28s ease,
- background 0.28s ease,
- transform 0.28s cubic-bezier(0.16, 1, 0.3, 1),
- border-color 0.28s ease;
- }
-
- /* ========================================================================= */
- /* AMBIENT LIQUID WAVE COLOR BLOCKS (Soft Diffused Aurora Glow Waves) */
- /* ========================================================================= */
- .dock-wave-container {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 100%;
- pointer-events: none;
- border-radius: inherit;
- overflow: hidden;
- z-index: 0;
- opacity: 0.78;
- transition: opacity 0.35s ease, filter 0.35s ease;
-
- /* Flash Mode Color Stops (Soft Electric Blue, Violet, Amber, Cyan) */
- .wave-stop-1 { stop-color: #3b82f6; stop-opacity: 0.65; }
- .wave-stop-2 { stop-color: #8b5cf6; stop-opacity: 0.7; }
- .wave-stop-3 { stop-color: #f59e0b; stop-opacity: 0.65; }
- .wave-stop-4 { stop-color: #06b6d4; stop-opacity: 0.6; }
-
- /* Pro Mode Color Stops (Soft Cosmic Nebula / Amethyst / Fuchsia) */
- &.is-pro {
- .wave-stop-1 { stop-color: #7c3aed; stop-opacity: 0.7; }
- .wave-stop-2 { stop-color: #ec4899; stop-opacity: 0.75; }
- .wave-stop-3 { stop-color: #6366f1; stop-opacity: 0.65; }
- .wave-stop-4 { stop-color: #a855f7; stop-opacity: 0.7; }
- }
-
- /* Deep Ambient Aurora Glow Diffusion */
- .wave-glow-ambient {
- position: absolute;
- bottom: -8px;
- left: 5%;
- right: 5%;
- height: 30px;
- border-radius: 50%;
- background: radial-gradient(ellipse at center, rgba(99, 102, 241, 0.4) 0%, rgba(245, 158, 11, 0.2) 50%, transparent 80%);
- filter: blur(16px);
- opacity: 0.7;
- animation: glowBreathe 5s ease-in-out infinite alternate;
- }
-
- &.is-pro .wave-glow-ambient {
- background: radial-gradient(ellipse at center, rgba(168, 85, 247, 0.45) 0%, rgba(236, 72, 153, 0.25) 50%, transparent 80%);
- }
-
- /* Wave Tracks Wrapper. The soft-liquid look comes from blurring each
- wave individually: blurring the WRAPPER forced the GPU to re-run the
- gaussian pass every animation frame (child transforms invalidate the
- parent's filtered layer), while per-wave blur over transform-only
- animations is rasterized once and then just composited. */
- .wave-tracks-wrapper {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 22px;
- overflow: hidden;
- mask-image: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.5) 60%, transparent 100%);
- -webkit-mask-image: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.5) 60%, transparent 100%);
-
- .liquid-wave {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 200%;
- height: 100%;
- will-change: transform;
- filter: blur(8px); /* Silky liquid softness, applied per cached wave layer */
-
- &.wave-back {
- opacity: 0.45;
- animation: waveSlide 12s ease-in-out infinite alternate;
- }
-
- &.wave-mid {
- opacity: 0.6;
- animation: waveSlideReverse 9s ease-in-out infinite alternate;
- }
-
- &.wave-front {
- opacity: 0.75;
- animation: waveSlide 6.5s ease-in-out infinite alternate;
- }
- }
- }
- }
-
- /* ======================================================================= */
- /* 1. DORMANT / RESTING CAPSULE STATE (Subtle Frosted Peek) */
- /* ======================================================================= */
- &.is-dormant {
- .floating-dock-card {
- max-width: 160px;
- width: 160px;
- margin: 0;
- background: rgba(255, 255, 255, 0.65);
- backdrop-filter: blur(26px) saturate(190%) brightness(104%);
- -webkit-backdrop-filter: blur(26px) saturate(190%) brightness(104%);
- border: 1px solid rgba(255, 255, 255, 0.85);
- border-radius: 17px;
- box-shadow: 0 6px 18px -2px rgba(15, 23, 42, 0.07), inset 0 1px 1.5px rgba(255, 255, 255, 0.95), inset 0 -1px 1px rgba(255, 255, 255, 0.3);
- cursor: pointer;
- opacity: 0.92;
- transform: translateY(2px);
-
- .dock-wave-container {
- opacity: 0.78;
- .wave-tracks-wrapper {
- height: 12px;
- }
- }
-
- &:hover {
- opacity: 1;
- transform: translateY(0);
- background: rgba(255, 255, 255, 0.82);
- border-color: rgba(255, 255, 255, 1);
- box-shadow: 0 8px 24px -2px rgba(15, 23, 42, 0.12), inset 0 1px 1.5px rgba(255, 255, 255, 1);
-
- .dock-wave-container {
- opacity: 0.95;
- filter: drop-shadow(0 0 6px rgba(99, 102, 241, 0.35));
- }
- }
-
- .capsule-peek-view {
- display: flex;
- opacity: 1;
- }
-
- .expanded-card-content {
- display: none;
- opacity: 0;
- }
- }
- }
-
- /* Capsule Peek View Layout */
- .capsule-peek-view {
- position: relative;
- z-index: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 6px 14px;
- min-height: 34px;
- box-sizing: border-box;
- user-select: none;
-
- .capsule-placeholder {
- color: #475569;
- font-size: 12px;
- font-weight: 500;
- letter-spacing: 0.1px;
- white-space: nowrap;
- text-align: center;
- }
- }
-
- /* ======================================================================= */
- /* 2. EXPANDED / ACTIVE STATE (Rich Frosted Command Box) */
- /* ======================================================================= */
- &.is-expanded {
- .floating-dock-row {
- gap: 0;
- }
-
- .standalone-stop-btn {
- opacity: 0;
- width: 0;
- min-width: 0;
- margin: 0;
- padding: 0;
- border-width: 0;
- transform: scale(0.5);
- pointer-events: none;
- }
-
- .floating-dock-card {
- max-width: 100%;
- width: 100%;
- background: rgba(255, 255, 255, 0.68);
- backdrop-filter: blur(32px) saturate(210%) brightness(105%);
- -webkit-backdrop-filter: blur(32px) saturate(210%) brightness(105%);
- border: 1px solid rgba(255, 255, 255, 0.82);
- box-shadow: 0 16px 44px -8px rgba(15, 23, 42, 0.12), 0 2px 8px rgba(0, 0, 0, 0.02), inset 0 1.5px 2px rgba(255, 255, 255, 0.95), inset 0 -1px 2px rgba(255, 255, 255, 0.3);
- opacity: 1;
- transform: translateY(0);
-
- .dock-wave-container {
- opacity: 0.88;
- .wave-tracks-wrapper {
- height: 22px;
- }
- }
-
- .capsule-peek-view {
- display: none;
- opacity: 0;
- }
-
- .expanded-card-content {
- display: block;
- opacity: 1;
- animation: cardFadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
- }
- }
- }
-
- /* Focused State Glow */
- &.is-focused .floating-dock-card {
- background: rgba(255, 255, 255, 0.78);
- backdrop-filter: blur(36px) saturate(220%) brightness(106%);
- -webkit-backdrop-filter: blur(36px) saturate(220%) brightness(106%);
- border-color: rgba(59, 130, 246, 0.55);
- box-shadow: 0 20px 48px -6px rgba(37, 99, 235, 0.16), 0 0 0 2.5px rgba(59, 130, 246, 0.18), inset 0 1.5px 2px rgba(255, 255, 255, 1);
-
- .dock-wave-container {
- opacity: 1;
- filter: drop-shadow(0 0 8px rgba(59, 130, 246, 0.35));
- }
- }
-
- /* Expanded Content Rows */
- .expanded-card-content {
- position: relative;
- z-index: 1;
- display: flex;
- flex-direction: column;
- width: 100%;
-
- .card-main-input-row {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px 10px 3px 14px;
- min-height: 48px;
- box-sizing: border-box;
-
- .leading-status-icon {
- width: 14px;
- height: 14px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-
- .pulse-indicator-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: #ef4444;
- box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.6);
- animation: pulseDot 1.4s infinite;
- }
- }
-
- .input-content-area {
- flex: 1;
- min-width: 0;
- display: flex;
- align-items: center;
-
- .dock-textarea {
- width: 100%;
- border: none;
- outline: none;
- background: transparent;
- resize: none;
- font-family: inherit;
- font-size: 13.5px;
- font-weight: 450;
- line-height: 1.5;
- color: #0f172a;
- padding: 3px 0;
- max-height: 120px;
- box-sizing: border-box;
- display: block;
-
- &::placeholder {
- color: #64748b;
- font-size: 12.5px;
- font-weight: 400;
- transition: color 0.15s ease;
- }
-
- &:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
- }
- }
-
- .dock-circle-btn {
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- border: none;
- cursor: pointer;
- outline: none;
- flex-shrink: 0;
- transition: background 0.18s cubic-bezier(0.16, 1, 0.3, 1),
- color 0.18s cubic-bezier(0.16, 1, 0.3, 1),
- transform 0.18s cubic-bezier(0.16, 1, 0.3, 1),
- box-shadow 0.18s cubic-bezier(0.16, 1, 0.3, 1),
- border-color 0.18s cubic-bezier(0.16, 1, 0.3, 1);
-
- .material-symbols-outlined {
- font-size: 18px;
- line-height: 1;
- }
-
- .spinning-icon {
- font-size: 17px;
- animation: spin 1s linear infinite;
- }
-
- /* Clear Button */
- &.btn-clear {
- width: 26px;
- height: 26px;
- background: rgba(255, 255, 255, 0.45);
- backdrop-filter: blur(8px);
- border: 1px solid rgba(255, 255, 255, 0.6);
- color: #64748b;
-
- &:hover {
- background: rgba(255, 255, 255, 0.75);
- color: #0f172a;
- }
- }
-
- /* Stop Button (Frosted Red) */
- &.btn-stop {
- width: 36px;
- height: 36px;
- background: rgba(239, 68, 68, 0.9);
- backdrop-filter: blur(8px);
- color: #ffffff;
- border: 1px solid rgba(255, 255, 255, 0.35);
- box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
- animation: popIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
-
- &:hover:not(:disabled) {
- background: #dc2626;
- transform: scale(1.05);
- box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
- }
-
- &:active:not(:disabled) {
- transform: scale(0.95);
- }
-
- &:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
- }
-
- /* Send Button */
- &.btn-send {
- width: 36px;
- height: 36px;
- background: rgba(15, 23, 42, 0.88);
- backdrop-filter: blur(12px);
- border: 1px solid rgba(255, 255, 255, 0.25);
- color: #ffffff;
- box-shadow: 0 4px 14px rgba(15, 23, 42, 0.2), inset 0 1px 1px rgba(255, 255, 255, 0.25);
-
- &:disabled {
- background: rgba(226, 232, 240, 0.6);
- color: #94a3b8;
- cursor: not-allowed;
- box-shadow: none;
- border-color: rgba(255, 255, 255, 0.5);
- transform: none;
- }
-
- &.has-content:not(:disabled) {
- background: linear-gradient(135deg, rgba(30, 41, 59, 0.95), rgba(15, 23, 42, 0.98));
- box-shadow: 0 4px 14px rgba(15, 23, 42, 0.25), inset 0 1px 1px rgba(255, 255, 255, 0.3);
-
- &:hover {
- background: linear-gradient(135deg, #2563eb, #1d4ed8);
- transform: scale(1.05);
- box-shadow: 0 6px 18px rgba(37, 99, 235, 0.35);
- }
- }
-
- &:active:not(:disabled) {
- transform: scale(0.95);
- }
- }
- }
- }
-
- /* Model Profile Selector & Status Footer Row */
- .card-footer-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 3px 14px 8px 14px;
- box-sizing: border-box;
-
- .footer-left-group {
- display: flex;
- align-items: center;
- gap: 8px;
-
- /* Segmented Model Toggle with Frosted Glass */
- .model-profile-toggle {
- display: inline-flex;
- align-items: center;
- background: rgba(255, 255, 255, 0.45);
- backdrop-filter: blur(14px);
- -webkit-backdrop-filter: blur(14px);
- border: 1px solid rgba(255, 255, 255, 0.65);
- box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.8), 0 1px 3px rgba(0, 0, 0, 0.03);
- border-radius: 8px;
- padding: 2px;
- gap: 2px;
-
- .profile-toggle-pill {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 3.5px;
- width: 62px;
- min-width: 62px;
- height: 22px;
- border-radius: 6px;
- border: none;
- background: transparent;
- color: #64748b;
- font-family: inherit;
- font-size: 11px;
- font-weight: 550;
- cursor: pointer;
- transition: color 0.16s cubic-bezier(0.16, 1, 0.3, 1),
- background 0.16s cubic-bezier(0.16, 1, 0.3, 1),
- box-shadow 0.16s cubic-bezier(0.16, 1, 0.3, 1);
- outline: none;
- user-select: none;
- box-sizing: border-box;
-
- .pill-icon {
- font-size: 13px;
- line-height: 1;
- flex-shrink: 0;
- }
-
- .pill-label {
- line-height: 1;
- }
-
- &:hover:not(.active) {
- color: #1e293b;
- background: rgba(255, 255, 255, 0.65);
- }
-
- &.active {
- background: rgba(255, 255, 255, 0.92);
- backdrop-filter: blur(8px);
- color: #0f172a;
- font-weight: 700;
- box-shadow: 0 1.5px 4px rgba(0, 0, 0, 0.08), inset 0 1px 1.5px rgba(255, 255, 255, 1);
-
- .icon-flash {
- color: #d97706;
- }
-
- .icon-pro {
- color: #7c3aed;
- }
- }
- }
- }
- }
-
- .footer-right-status {
- .running-status-pill {
- display: inline-flex;
- align-items: center;
- gap: 4.5px;
- font-size: 10.5px;
- font-weight: 600;
- color: #dc2626;
- background: rgba(254, 226, 226, 0.7);
- padding: 2px 7px;
- border-radius: 10px;
-
- .running-pulse-dot {
- width: 5.5px;
- height: 5.5px;
- border-radius: 50%;
- background: #ef4444;
- animation: pulseDot 1.2s infinite;
- }
- }
- }
- }
- }
- }
-
- /* When viewport shrinks to approximately 2/3 of standard desktop width (<= 1150px), hide right panel and leave only agent-stream */
- @media (max-width: 1150px) {
- .right-panel, .resizer {
- display: none !important;
- }
-
- .left-panel {
- border-right: none !important;
- width: 100% !important;
- flex: 1 1 100% !important;
- min-width: 0 !important;
- }
- }
-}
-
-/* Animations */
-@keyframes popIn {
- 0% {
- opacity: 0;
- transform: scale(0.8);
- }
- 100% {
- opacity: 1;
- transform: scale(1);
- }
-}
-
-@keyframes spin {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
-}
-
-@keyframes slideUpFade {
- from {
- opacity: 0;
- transform: translateY(8px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes cardFadeIn {
- from {
- opacity: 0;
- transform: translateY(4px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
-}
-
-@keyframes pulseDot {
- 0% {
- transform: scale(0.95);
- box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
- }
- 70% {
- transform: scale(1);
- box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
- }
- 100% {
- transform: scale(0.95);
- box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
- }
-}
-
-/* Fluid Wave Horizontal Slide (Seamless loop) */
-@keyframes waveSlide {
- 0% {
- transform: translate3d(0, 0, 0);
- }
- 100% {
- transform: translate3d(-50%, 0, 0);
- }
-}
-
-@keyframes waveSlideReverse {
- 0% {
- transform: translate3d(-50%, 0, 0);
- }
- 100% {
- transform: translate3d(0, 0, 0);
- }
-}
-
-/* Ambient Radial Glow Breathing */
-@keyframes glowBreathe {
- 0% {
- opacity: 0.55;
- transform: scaleX(0.92);
- }
- 100% {
- opacity: 0.95;
- transform: scaleX(1.08);
- }
-}
-
-/* Respect the user's motion preference: freeze the decorative liquid waves
- and pulses while keeping the frosted dock visuals intact. */
-@media (prefers-reduced-motion: reduce) {
- .liquid-wave,
- .wave-glow-ambient,
- .pulse-indicator-dot,
- .running-pulse-dot {
- animation: none !important;
- }
-}
diff --git a/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts b/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts
deleted file mode 100644
index 2ecb521f6..000000000
--- a/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts
+++ /dev/null
@@ -1,323 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Component, ChangeDetectionStrategy, NgZone, DestroyRef, inject, computed, signal, ViewChild, ElementRef, OnInit } from '@angular/core';
-
-import { FormsModule } from '@angular/forms';
-import { AgentStreamComponent } from '../../components/agent-stream/agent-stream.component';
-import { ChatInterfaceComponent } from '../../components/chat-interface/chat-interface.component';
-import { FloatingVideoPlayerComponent } from '../../components/floating-video-player/floating-video-player.component';
-import { AgentService } from '../../services/agent.service';
-
-@Component({
- selector: 'app-workspace',
- standalone: true,
- imports: [
- FormsModule,
- AgentStreamComponent,
- ChatInterfaceComponent,
- FloatingVideoPlayerComponent
-],
- templateUrl: './workspace.component.html',
- styleUrl: './workspace.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush
-})
-export class WorkspaceComponent implements OnInit {
- public agentService = inject(AgentService);
- private zone = inject(NgZone);
- private destroyRef = inject(DestroyRef);
-
- // Default right panel width to 1/3 of the screen (or 450px as fallback)
- public rightPanelWidth = signal(
- typeof window !== 'undefined' ? Math.round(window.innerWidth / 3) : 450
- );
- public isDragging = signal(false);
- private dragWidthRafId: number | null = null;
- private pendingDragWidth = 0;
-
- // Floating Command Bar State. taskInput is backed by a signal so computed
- // expressions (isBarExpanded) genuinely track it under OnPush.
- private taskInputSignal = signal('');
- public get taskInput(): string { return this.taskInputSignal(); }
- public set taskInput(value: string) { this.taskInputSignal.set(value); }
- public isSubmitting = signal(false);
- public errorMessage = signal(null);
- public selectedProfile = signal<'flash' | 'pro'>('flash');
-
- // Expand States (Signals for 0-latency reactivity)
- public isHoveringCard = signal(false);
- public isInputFocused = signal(false);
-
- @ViewChild('dockInput') public dockInputRef?: ElementRef;
-
- ngOnInit(): void {
- if (typeof localStorage !== 'undefined') {
- const saved = localStorage.getItem('artemis_selected_profile');
- if (saved === 'flash' || saved === 'pro') {
- this.selectedProfile.set(saved);
- }
- }
-
- // The global ⌘K/Ctrl+K shortcut is registered outside the Angular zone so
- // ordinary typing never schedules an extra change-detection pass.
- this.zone.runOutsideAngular(() => {
- window.addEventListener('keydown', this.onGlobalKeyDown);
- });
- this.destroyRef.onDestroy(() => {
- window.removeEventListener('keydown', this.onGlobalKeyDown);
- this.detachDragListeners();
- });
- }
-
- /**
- * Set agent architecture profile ('flash' vs 'pro')
- */
- public setProfile(profile: 'flash' | 'pro', event?: MouseEvent): void {
- if (event) {
- event.stopPropagation();
- }
- this.selectedProfile.set(profile);
- if (typeof localStorage !== 'undefined') {
- localStorage.setItem('artemis_selected_profile', profile);
- }
- }
-
- /**
- * Computed boolean whether the currently viewed task is actively running or paused.
- * Only displays the stop/cancel button when inspecting an active task.
- */
- public isTaskRunning = computed(() => {
- return this.agentService.isCurrentSessionRunning();
- });
-
- /**
- * Dynamic tooltip and label indicating which task will be stopped
- */
- public stopButtonTitle = computed(() => {
- const session = this.agentService.currentSession();
- if (session?.initial_goal) {
- const truncated = session.initial_goal.length > 45
- ? session.initial_goal.substring(0, 42) + '...'
- : session.initial_goal;
- return `Stop current task: "${truncated}"`;
- }
- const curId = this.agentService.currentSessionId();
- if (curId) {
- return `Stop current task (${curId})`;
- }
- return 'Stop current running task';
- });
-
- /**
- * Computed boolean whether the command bar should be in its expanded state:
- * - Mouse is hovering directly on the command card
- * - Input textarea is focused
- * - User has entered task text (drafting)
- */
- public isBarExpanded = computed(() => {
- return (
- this.isHoveringCard() ||
- this.isInputFocused() ||
- this.taskInput.trim().length > 0
- );
- });
-
- /**
- * Focus input handler
- */
- public onInputFocus(): void {
- this.isInputFocused.set(true);
- }
-
- /**
- * Blur input handler
- */
- public onInputBlur(): void {
- this.isInputFocused.set(false);
- }
-
- /**
- * Click on the resting capsule or dock card to focus textarea
- */
- public onCardClick(event: MouseEvent): void {
- const target = event.target as HTMLElement;
- // Don't steal focus if clicking action buttons or textarea directly
- if (target.closest('button') || target.tagName.toLowerCase() === 'textarea') {
- return;
- }
- this.focusInput();
- }
-
- /**
- * Focus textarea programmatically
- */
- public focusInput(): void {
- setTimeout(() => {
- this.dockInputRef?.nativeElement?.focus();
- }, 30);
- }
-
- /**
- * Clear typed task input
- */
- public clearInput(event?: MouseEvent): void {
- if (event) {
- event.stopPropagation();
- }
- this.taskInput = '';
- if (this.dockInputRef?.nativeElement) {
- this.dockInputRef.nativeElement.style.height = 'auto';
- this.dockInputRef.nativeElement.focus();
- }
- }
-
- /**
- * Auto-resize textarea as user types multi-line tasks
- */
- public onTextareaInput(event: Event): void {
- const textarea = event.target as HTMLTextAreaElement;
- if (!textarea) return;
- textarea.style.height = 'auto';
- const newHeight = Math.min(Math.max(textarea.scrollHeight, 24), 120);
- textarea.style.height = `${newHeight}px`;
- }
-
- /**
- * Global keyboard shortcut (⌘K or Ctrl+K) to focus input bar from anywhere
- */
- private onGlobalKeyDown = (event: KeyboardEvent): void => {
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
- event.preventDefault();
- this.focusInput();
- }
- };
-
- /**
- * Handle enter key in floating dock textarea
- */
- public onKeyDown(event: KeyboardEvent): void {
- if (event.key === 'Enter' && !event.shiftKey) {
- event.preventDefault();
- this.submitTask();
- }
- }
-
- /**
- * Submit a new task instruction
- */
- public submitTask(): void {
- const goal = this.taskInput.trim();
- if (!goal || this.isSubmitting()) {
- return;
- }
-
- this.isSubmitting.set(true);
- this.errorMessage.set(null);
-
- if (this.dockInputRef?.nativeElement) {
- this.dockInputRef.nativeElement.blur();
- }
- this.isInputFocused.set(false);
-
- this.agentService.runTask(goal, this.selectedProfile()).subscribe({
- next: (res) => {
- this.taskInput = '';
- if (this.dockInputRef?.nativeElement) {
- this.dockInputRef.nativeElement.style.height = 'auto';
- }
- this.isSubmitting.set(false);
- this.agentService.fetchStatus();
- },
- error: (err) => {
- console.error('Failed to submit task:', err);
- this.isSubmitting.set(false);
- this.errorMessage.set(err.error?.detail || 'The runner is busy. Please wait for current task to finish.');
- setTimeout(() => {
- this.errorMessage.set(null);
- }, 5000);
- }
- });
- }
-
- /**
- * Stop currently viewed task
- */
- public stopTask(event?: MouseEvent): void {
- if (event) {
- event.stopPropagation();
- }
- if (!this.isTaskRunning()) {
- return;
- }
- const targetSessionId = this.agentService.currentSessionId();
- this.isSubmitting.set(true);
- this.errorMessage.set(null);
- this.agentService.stopTask(targetSessionId, false);
- setTimeout(() => {
- this.isSubmitting.set(false);
- }, 400);
- }
-
- /**
- * Handle mouse down on resizer bar to start dragging. The move/up listeners
- * are attached only for the duration of the drag and run outside the Angular
- * zone: idle mouse movement over the workspace never triggers change
- * detection, and drag updates are coalesced to one per animation frame.
- */
- public onDragStart(event: MouseEvent): void {
- this.isDragging.set(true);
- event.preventDefault();
- this.zone.runOutsideAngular(() => {
- document.addEventListener('mousemove', this.onMouseMove);
- document.addEventListener('mouseup', this.onMouseUp);
- });
- }
-
- private onMouseMove = (event: MouseEvent): void => {
- if (!this.isDragging()) {
- return;
- }
-
- const newWidth = window.innerWidth - event.clientX;
- const minWidth = 250;
- const maxWidth = window.innerWidth - 300;
-
- // Apply boundary limits to prevent panels from shrinking too much
- if (newWidth >= minWidth && newWidth <= maxWidth) {
- this.pendingDragWidth = newWidth;
- if (this.dragWidthRafId === null) {
- this.dragWidthRafId = requestAnimationFrame(() => {
- this.dragWidthRafId = null;
- this.rightPanelWidth.set(this.pendingDragWidth);
- });
- }
- }
- };
-
- private onMouseUp = (): void => {
- this.isDragging.set(false);
- this.detachDragListeners();
- };
-
- private detachDragListeners(): void {
- document.removeEventListener('mousemove', this.onMouseMove);
- document.removeEventListener('mouseup', this.onMouseUp);
- if (this.dragWidthRafId !== null) {
- cancelAnimationFrame(this.dragWidthRafId);
- this.dragWidthRafId = null;
- }
- }
-}
diff --git a/apps/showcase_ui/src/app/services/agent.service.spec.ts b/apps/showcase_ui/src/app/services/agent.service.spec.ts
deleted file mode 100644
index c17e334d4..000000000
--- a/apps/showcase_ui/src/app/services/agent.service.spec.ts
+++ /dev/null
@@ -1,418 +0,0 @@
-import { signal, computed } from '@angular/core';
-import { of } from 'rxjs';
-
-import { AgentService } from './agent.service';
-
-describe('AgentService live LLM retry timeline', () => {
- function createServiceWithoutPolling(): AgentService {
- const service = Object.create(AgentService.prototype) as AgentService;
- service.sessionLogs = signal([]);
- service.isSessionContentLoading = signal(false);
- service.startupProgressBySession = signal({});
- (service as any).pendingStartupProgress = signal([]);
- (service as any).sessionLoadGeneration = 0;
- (service as any).sessionSnapshotRequestId = 0;
- (service as any).sessionSnapshotAppliedId = 0;
- (service as any).pendingSnapshotRequests = new Set();
- return service;
- }
-
- it('orders startup milestones and replaces duplicate stages', () => {
- const service = createServiceWithoutPolling();
-
- (service as any).appendStartupProgress({
- session_id: 'session-1',
- stage: 'device',
- message: 'Checking device',
- timestamp: 102
- }, 'session-1');
- (service as any).appendStartupProgress({
- session_id: 'session-1',
- stage: 'queued',
- message: 'Task queued',
- timestamp: 100
- }, 'session-1');
- (service as any).appendStartupProgress({
- session_id: 'session-1',
- stage: 'device',
- message: 'Device connected',
- timestamp: 103
- }, 'session-1');
-
- const events = service.startupProgressBySession()['session-1'];
- expect(events.map((event) => event.stage)).toEqual(['queued', 'device']);
- expect(events[1].message).toBe('Device connected');
- });
-
- it('normalizes a live retry event into the historical trace contract', () => {
- const service = createServiceWithoutPolling();
- const event = {
- trace_id: 'retry-1',
- step_id: 'step-1',
- timestamp: 100,
- error: '503 high demand',
- delay: 1.5,
- provider: 'google',
- source: 'provider_sdk',
- recoverable: true,
- request_id: 'request-1',
- scheduled_at: 100
- };
-
- (service as any).appendLiveLLMRetryTrace(event, 'session-1');
-
- const [log] = service.sessionLogs();
- expect(log.type).toBe('trace_recorded');
- expect(log.data).toEqual(jasmine.objectContaining({
- trace_id: 'retry-1',
- session_id: 'session-1',
- step_id: 'step-1',
- type: 'llm_call',
- name: 'llm_retry',
- status: 'retrying'
- }));
- expect(log.data.payload).toEqual(jasmine.objectContaining({
- error: '503 high demand',
- delay: 1.5,
- provider: 'google',
- source: 'provider_sdk',
- request_id: 'request-1'
- }));
- });
-
- it('upserts duplicate live retry events instead of adding another row', () => {
- const service = createServiceWithoutPolling();
- const event = {
- trace_id: 'retry-1',
- timestamp: 100,
- delay: 1.5,
- provider: 'google',
- source: 'provider_sdk',
- request_id: 'request-1',
- scheduled_at: 100
- };
-
- (service as any).appendLiveLLMRetryTrace(event, 'session-1');
- (service as any).appendLiveLLMRetryTrace({ ...event, error: 'updated 503' }, 'session-1');
-
- expect(service.sessionLogs().length).toBe(1);
- expect(service.sessionLogs()[0].data.payload.error).toBe('updated 503');
- });
-
- it('replaces an old history snapshot while preserving live retry events', () => {
- const service = createServiceWithoutPolling();
- (service as any).currentSessionId = signal('session-1');
- (service as any).http = {
- get: () => of([{
- step_id: 'step-1',
- timestamp: 100,
- generic_tools: [{ trace_id: 'persisted-retry', name: 'llm_retry' }]
- }])
- };
- service.sessionLogs.set([
- { type: 'step_updated', history_snapshot: true, data: { step_id: 'old-step' } },
- { type: 'trace_recorded', data: { trace_id: 'live-retry', name: 'llm_retry' } }
- ]);
- service.isSessionContentLoading.set(true);
-
- (service as any).backfillSessionSteps('session-1');
-
- const logs = service.sessionLogs();
- expect(logs.length).toBe(2);
- expect(logs[0].history_snapshot).toBeTrue();
- expect(logs[0].data.step_id).toBe('step-1');
- expect(logs[1].data.trace_id).toBe('live-retry');
- expect(service.isSessionContentLoading()).toBeFalse();
- });
-
- it('attaches the persisted checker transcript to the attempt it belongs to', () => {
- const service = createServiceWithoutPolling();
- (service as any).currentSessionId = signal('session-1');
- (service as any).http = {
- get: () => of({
- records: [
- { attempt_id: 'abc#1', checkpoint_id: 'abc', subgoal_text: 'Create the alarm', trace_id: 't-1', item_text: 'alarm exists', kind: 'verify', status: 'passed', evidence: 'seen', ts: 10 },
- { attempt_id: 'final#1', checkpoint_id: 'final', trace_id: 't-2', item_text: 'alarm exists', kind: 'verify', status: 'passed', evidence: 'seen', ts: 20 }
- ],
- streams: [
- {
- attempt_id: 'abc#1',
- trace_id: 't-1',
- ts: 11,
- segments: [
- { execution_id: 'e-1', role: 'thought', when: 8, text: 'Looking' },
- { execution_id: 'e-1', role: 'answer', when: 9, text: 'Seen it' }
- ]
- }
- ],
- run_outcome: null
- })
- };
- service.sessionLogs.set([{ type: 'checker_event', checks_snapshot: true, data: { attempt_id: 'stale' } }]);
-
- service.fetchChecks('session-1');
-
- const logs = service.sessionLogs();
- expect(logs.map((l) => l.data.attempt_id)).toEqual(['abc#1', 'final#1']);
- expect(logs[0].data.stream_segments).toEqual([
- { execution_id: 'e-1', stream_type: 'thinking', text: 'Looking', timestamp: new Date(8000).toISOString(), isCompleted: true },
- { execution_id: 'e-1', stream_type: 'text', text: 'Seen it', timestamp: new Date(9000).toISOString(), isCompleted: true }
- ]);
- expect('stream_segments' in logs[1].data).toBeFalse();
- });
-
- it('follows a just-started task even when status polling saw it first', () => {
- const service = createServiceWithoutPolling();
- (service as any).http = {
- post: () => of({ tasks: [{ session_id: 'new-session' }] })
- };
- service.agentStatus = signal('running');
- service.runningSessionId = signal('new-session');
- service.userPinnedSessionId = signal(null);
- (service as any).sessions = signal([{
- session_id: 'new-session',
- status: 'running'
- }]);
- const selectSpy = spyOn(service, 'selectSession');
-
- service.runTask('test goal').subscribe();
-
- expect(selectSpy).toHaveBeenCalledWith('new-session', false);
- });
-
- it('keeps the paused state when the backend says there is nothing to resume', () => {
- const service = createServiceWithoutPolling();
- (service as any).http = { post: () => of({ status: 'not_paused' }) };
- (service as any).rawSessions = signal([{ session_id: 'session-1', status: 'paused' }]);
- (service as any).pendingQueue = signal([]);
- service.agentStatus = signal('paused');
- service.runningSessionId = signal('session-1');
- service.isPaused = signal(true);
- service.pausedError = signal('503 unavailable');
- const statusSpy = spyOn(service, 'fetchStatus');
-
- service.resumeTask();
-
- expect(service.agentStatus()).toBe('paused');
- expect(service.isPaused()).toBeTrue();
- expect(statusSpy).toHaveBeenCalled();
- });
-
- it('moves the active session to running only after resume succeeds', () => {
- const service = createServiceWithoutPolling();
- (service as any).http = { post: () => of({ status: 'resumed' }) };
- (service as any).rawSessions = signal([{ session_id: 'session-1', status: 'paused' }]);
- (service as any).pendingQueue = signal([]);
- service.agentStatus = signal('paused');
- service.runningSessionId = signal('session-1');
- service.isPaused = signal(true);
- service.pausedError = signal('503 unavailable');
- spyOn(service, 'fetchStatus');
-
- service.resumeTask();
-
- expect(service.agentStatus()).toBe('running');
- expect(service.isPaused()).toBeFalse();
- expect((service as any).rawSessions()[0].status).toBe('running');
- });
-});
-
-describe('AgentService recording finalization lifecycle', () => {
- function createVideoService(response: any): AgentService {
- const service = Object.create(AgentService.prototype) as AgentService;
- (service as any).http = { get: () => of(response) };
- (service as any).rawSessions = signal([
- { session_id: 'session-1', status: 'completed', recording_status: 'recording' }
- ]);
- (service as any).activeVideoSessionId = 'session-1';
- (service as any).videoRequestGeneration = 1;
- (service as any).videoRetryTimer = null;
- (service as any).videoWaitStartedAt = Date.now();
- service.activeVideoUrl = signal(null);
- service.activeVideoSegments = signal([]);
- service.isVideoLoading = signal(false);
- service.recordingPlaybackStatus = signal('idle');
- service.recordingPlaybackMessage = signal('');
- service.shouldAutoplayVideo = signal(true);
- service.playerMode = signal<'video' | 'steps'>('video');
- (service as any).hasCurrentSessionStepFrames = computed(() => false);
- (service as any).currentSessionStepFrames = computed(() => []);
- return service;
- }
-
- it('keeps unfinished media out of the video element and schedules another readiness check', () => {
- const service = createVideoService({
- session_id: 'session-1',
- status: 'processing',
- has_video: false,
- video_url: null,
- retry_after_ms: 750
- });
- const retrySpy = spyOn(service, 'scheduleVideoRetry');
-
- (service as any).requestSessionVideo('session-1', 1);
-
- expect(service.recordingPlaybackStatus()).toBe('processing');
- expect(service.isVideoLoading()).toBeTrue();
- expect(service.activeVideoUrl()).toBeNull();
- expect(retrySpy).toHaveBeenCalledWith('session-1', 1, 750);
- });
-
- it('publishes finalized media and preserves the automatic replay request', () => {
- const service = createVideoService({
- session_id: 'session-1',
- status: 'ready',
- has_video: true,
- video_url: '/videos/recording.mp4?v=1',
- video_segments: [
- { url: '/videos/recording.mp4?v=1', start: 0, duration: 5, width: 1080, height: 1920 }
- ]
- });
-
- (service as any).requestSessionVideo('session-1', 1);
-
- expect(service.recordingPlaybackStatus()).toBe('ready');
- expect(service.isVideoLoading()).toBeFalse();
- expect(service.activeVideoUrl()).toBe('/videos/recording.mp4?v=1');
- expect(service.activeVideoSegments().length).toBe(1);
- expect(service.shouldAutoplayVideo()).toBeTrue();
- });
-
- it('stops polling and exposes a terminal recording failure', () => {
- const service = createVideoService({
- session_id: 'session-1',
- status: 'failed',
- has_video: false,
- video_url: null,
- message: 'ffmpeg failed'
- });
-
- (service as any).requestSessionVideo('session-1', 1);
-
- expect(service.recordingPlaybackStatus()).toBe('failed');
- expect(service.isVideoLoading()).toBeFalse();
- expect(service.recordingPlaybackMessage()).toBe('ffmpeg failed');
- });
-});
-
-describe('AgentService video analysis seeking', () => {
- it('publishes repeatable, clamped seek requests for the floating player', () => {
- const service = Object.create(AgentService.prototype) as AgentService;
- service.videoSeekRequest = signal<{ seconds: number; requestId: number } | null>(null);
- (service as any).videoSeekRequestId = 0;
-
- service.requestVideoSeek(-5);
- const first = service.videoSeekRequest();
- service.requestVideoSeek(0);
- const second = service.videoSeekRequest();
-
- expect(first?.seconds).toBe(0);
- expect(second?.seconds).toBe(0);
- expect(second?.requestId).toBeGreaterThan(first?.requestId || 0);
- });
-});
-
-describe('AgentService task cancellation and active session tracking', () => {
- it('computes isCurrentSessionRunning true only when viewing an active running/paused session', () => {
- const service = Object.create(AgentService.prototype) as any;
- service.currentSessionId = signal(null);
- service.rawSessions = signal([]);
- service.activeTasks = signal([]);
- service.pendingQueue = signal([]);
- service.agentStatus = signal('idle');
- service.runningSessionId = signal(null);
- service.runningGoal = signal(null);
- service.activeSessionTracking = new Map();
-
- service.currentSession = computed(() => {
- const curId = service.currentSessionId();
- if (!curId) return null;
- return service.sessions().find((s: any) => s.session_id === curId) || null;
- });
-
- service.sessions = computed(() => {
- return service.rawSessions();
- });
-
- service.isCurrentSessionRunning = computed(() => {
- const curId = service.currentSessionId();
- if (!curId) return false;
- const session = service.currentSession();
- if (session) {
- return session.status === 'running' || session.status === 'paused';
- }
- const isActiveStatus = service.agentStatus() === 'running' || service.agentStatus() === 'paused';
- if (isActiveStatus && service.runningSessionId() === curId) {
- return true;
- }
- if (service.activeTasks().some((at: any) => at.session_id === curId)) {
- return true;
- }
- return false;
- });
-
- // 1. No session selected -> false
- expect(service.isCurrentSessionRunning()).toBeFalse();
-
- // 2. Completed session selected -> false
- service.rawSessions.set([
- { session_id: 'sess-completed', status: 'completed', initial_goal: 'Done', start_time: 100 },
- { session_id: 'sess-running', status: 'running', initial_goal: 'Running', start_time: 101 }
- ]);
- service.currentSessionId.set('sess-completed');
- expect(service.isCurrentSessionRunning()).toBeFalse();
-
- // 3. Active running session selected -> true
- service.currentSessionId.set('sess-running');
- expect(service.isCurrentSessionRunning()).toBeTrue();
-
- // 4. Paused session selected -> true
- service.rawSessions.set([
- { session_id: 'sess-paused', status: 'paused', initial_goal: 'Paused', start_time: 102 }
- ]);
- service.currentSessionId.set('sess-paused');
- expect(service.isCurrentSessionRunning()).toBeTrue();
- });
-
- it('stops a specific session by passing its session_id', () => {
- const service = Object.create(AgentService.prototype) as AgentService;
- service.currentSessionId = signal('sess-2');
- service.runningSessionId = signal('sess-1');
- service.runningGoal = signal('Goal 1');
- service.agentStatus = signal('running');
- service.isPaused = signal(false);
- service.pausedError = signal(null);
- service.isRetrying = signal(false);
- service.sessionLogs = signal([]);
- service.activeTasks = signal([{ session_id: 'sess-1' }, { session_id: 'sess-2' }]);
- (service as any).pendingQueue = signal([]);
- (service as any).rawSessions = signal([
- { session_id: 'sess-1', status: 'running', initial_goal: 'Goal 1' },
- { session_id: 'sess-2', status: 'running', initial_goal: 'Goal 2' }
- ]);
- service.sessions = computed(() => (service as any).rawSessions());
-
- let postedUrl = '';
- let postedPayload: any = null;
- (service as any).http = {
- post: (url: string, payload: any) => {
- postedUrl = url;
- postedPayload = payload;
- return of({});
- }
- };
- (service as any).setSessionStatus = (sid: string, st: string) => {};
- (service as any).fetchStatus = () => {};
- (service as any).fetchSessions = () => {};
-
- // Stop sess-2 specifically
- service.stopTask('sess-2', false);
-
- expect(postedUrl).toContain('session_id=sess-2');
- expect(postedPayload?.session_id).toBe('sess-2');
- // Because sess-1 is still running, agentStatus should NOT be reset to idle
- expect(service.agentStatus()).toBe('running');
- // activeTasks should have filtered out sess-2
- expect(service.activeTasks().map((at: any) => at.session_id)).toEqual(['sess-1']);
- });
-});
diff --git a/apps/showcase_ui/src/app/services/agent.service.ts b/apps/showcase_ui/src/app/services/agent.service.ts
deleted file mode 100644
index b063c99b8..000000000
--- a/apps/showcase_ui/src/app/services/agent.service.ts
+++ /dev/null
@@ -1,2012 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Injectable, signal, inject, computed, DestroyRef, NgZone } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
-import { Observable } from 'rxjs';
-
-import { Session, ModelInfo, TaskQueueItem, AgentStatusResponse, SessionUsage } from '../core/models/session.model';
-import { ProTuningDefaults, ProTuningOptions } from '../core/models/pro-tuning.model';
-import { StepItemData, StepReplayFrame, LLMStreamResetEventData, StreamResetNotice, DEFAULT_STREAM_RESET_MESSAGE, PersistedCheckerStream, StreamSegment } from '../core/models/stream.model';
-import { extractStepReplayFrames } from '../utils/action-formatter.util';
-import { persistedStreamToSegments } from '../utils/stream-aggregator.util';
-export type { Session, ModelInfo, TaskQueueItem, AgentStatusResponse, StepItemData, StepReplayFrame, LLMStreamResetEventData, StreamResetNotice };
-
-const SESSION_CACHE_KEY = 'artemis.sessions.v1';
-
-export interface VideoSegment {
- url: string;
- /** Back-to-back playlist timeline start (seconds); gaps between segments are not represented. */
- start: number;
- duration: number;
- width: number;
- height: number;
- /** Session-relative first-frame offset in milliseconds (manifest v2). */
- offset_ms?: number;
- duration_ms?: number;
-}
-
-export type RecordingPlaybackStatus =
- | 'idle'
- | 'live'
- | 'processing'
- | 'ready'
- | 'failed'
- | 'unavailable';
-
-export interface StartupProgressEvent {
- session_id?: string;
- stage: string;
- message: string;
- timestamp: number;
-}
-
-interface SessionVideoResponse {
- session_id: string;
- status?: 'processing' | 'ready' | 'failed' | 'unavailable';
- has_video: boolean;
- video_url: string | null;
- video_segments?: VideoSegment[];
- retry_after_ms?: number;
- message?: string;
-}
-
-@Injectable({
- providedIn: 'root'
-})
-export class AgentService {
- private http = inject(HttpClient);
- private zone = inject(NgZone);
- private activePauseCardKey: string | null = null;
-
- // Signals to expose state to components
- private rawSessions = signal([]);
- private pendingQueue = signal([]);
- public activeTasks = signal([]);
- // Persistent tracking of active/pending sessions across polling boundaries
- private activeSessionTracking = new Map();
-
- public sessions = computed(() => {
- const raw = this.rawSessions();
- const pending = this.pendingQueue();
- const status = this.agentStatus();
- const goal = this.runningGoal();
- const runId = this.runningSessionId();
- const activeList = this.activeTasks();
-
- const sessionMap = new Map();
- // Lookup maps keep the per-session merge O(1) instead of scanning the
- // pending/active lists multiple times for every raw session.
- const pendingById = new Map(pending.map((p) => [p.session_id, p]));
- const activeById = new Map(activeList.map((at) => [at.session_id, at]));
-
- // 1. First add raw sessions from DB
- raw.forEach((s) => {
- const activeMatch = activeById.get(s.session_id);
- const pendingMatch = pendingById.get(s.session_id);
- const isCurrentActive = ((status === 'running' || status === 'paused') && runId === s.session_id)
- || !!activeMatch;
- const isTerminal = s.status === 'completed' || s.status === 'success' || s.status === 'failed' || s.status === 'cancelled';
- let sStatus = s.status;
- if (!isTerminal) {
- const isPending = !!pendingMatch && !isCurrentActive;
- sStatus = isCurrentActive ? (status === 'paused' ? 'paused' : 'running') : (isPending ? 'pending' : s.status);
- } else {
- sStatus = (s.status === 'success') ? 'completed' : s.status;
- }
- let serial = s.device_serial || s.device_id || activeMatch?.device_id || pendingMatch?.device_serial || null;
- if (!serial && s.device_info) {
- try {
- const info = typeof s.device_info === 'string' ? JSON.parse(s.device_info) : s.device_info;
- serial = info?.device_id || info?.device_serial || null;
- } catch {
- // ignore
- }
- }
- const finalSession: Session = {
- ...s,
- status: sStatus,
- device_serial: serial,
- model_info: isCurrentActive && this.activeModel() ? this.activeModel()! : s.model_info
- };
- sessionMap.set(s.session_id, finalSession);
- if (isTerminal) {
- this.activeSessionTracking.delete(s.session_id);
- } else if (sStatus === 'running' || sStatus === 'paused' || sStatus === 'pending') {
- this.activeSessionTracking.set(s.session_id, finalSession);
- }
- });
-
- // 2. Add pending queue sessions if not yet in raw sessions
- pending.forEach((p) => {
- if (!sessionMap.has(p.session_id)) {
- const activeMatch = activeById.get(p.session_id);
- const isCurrentRunning = ((status === 'running' || status === 'paused') && runId === p.session_id)
- || !!activeMatch;
- const finalPending: Session = {
- ...p,
- status: isCurrentRunning ? (status === 'paused' ? 'paused' : 'running') : (p.status || 'pending'),
- device_serial: p.device_serial || p.device_id || activeMatch?.device_id || null
- };
- sessionMap.set(p.session_id, finalPending);
- this.activeSessionTracking.set(p.session_id, finalPending);
- }
- });
-
- // 3. Ensure all currently active running sessions are present (multi-device & external runs)
- if (activeList.length > 0) {
- activeList.forEach((at) => {
- const sid = at.session_id || `active-${at.device_id}`;
- const existing = sessionMap.get(sid);
- if (!existing) {
- const newSession: Session = {
- session_id: sid,
- initial_goal: at.goal || goal || '',
- start_time: at.acquired_at ? (new Date(at.acquired_at).getTime() / 1000) : (Date.now() / 1000),
- status: 'running',
- model_info: this.activeModel() || undefined,
- device_serial: at.device_id || null
- };
- sessionMap.set(sid, newSession);
- this.activeSessionTracking.set(sid, newSession);
- } else if (existing.status === 'pending') {
- const updated: Session = {
- ...existing,
- status: 'running',
- device_serial: at.device_id || existing.device_serial
- };
- sessionMap.set(sid, updated);
- this.activeSessionTracking.set(sid, updated);
- }
- });
- } else if ((status === 'running' || status === 'paused') && runId && !sessionMap.has(runId)) {
- const activeSession: Session = {
- session_id: runId,
- initial_goal: goal || '',
- start_time: Date.now() / 1000,
- status,
- model_info: this.activeModel() || undefined
- };
- sessionMap.set(runId, activeSession);
- this.activeSessionTracking.set(runId, activeSession);
- }
-
- // 4. Bridge transient queue-to-running transition gaps
- this.activeSessionTracking.forEach((ts, sid) => {
- if (!sessionMap.has(sid)) {
- sessionMap.set(sid, {
- ...ts,
- status: 'running'
- });
- }
- });
-
- return Array.from(sessionMap.values()).sort((a, b) => b.start_time - a.start_time);
- });
-
- public currentSessionId = signal(null);
- public sessionLogs = signal([]); // Dynamic array of all raw events received
- public isSessionContentLoading = signal(false);
- public agentStatus = signal('idle'); // Status of the agent runner process
- public runningSessionId = signal(null);
- public runningGoal = signal(null);
- public isPaused = signal(false);
- public pausedError = signal(null);
- public isRetrying = signal(false);
- public retryMessage = signal(null);
- /** Architecture / LLM of the task the status poll reports as running. */
- public activeModel = signal<{ name: string; id: string; provider: string } | null>(null);
- public userPinnedSessionId = signal(null); // Pinned session if user explicitly selected a non-running task
- public startupProgressBySession = signal>({});
- private pendingStartupProgress = signal([]);
-
- public currentStartupProgress = computed(() => {
- const sessionId = this.currentSessionId();
- if (sessionId) {
- const normalized = String(sessionId).trim().toLowerCase();
- const bySession = this.startupProgressBySession();
- const direct = bySession[sessionId];
- if (direct?.length) return direct;
- const matched = Object.entries(bySession).find(
- ([key]) => key.trim().toLowerCase() === normalized
- );
- if (matched?.[1]?.length) return matched[1];
- }
- return this.pendingStartupProgress();
- });
-
- // Notes and Tab States
- public currentNotes = signal>({});
- public selectedNoteKey = signal('task_plan.md');
- public activeTab = signal<'tasks' | 'notes'>('tasks');
-
- // Video Replay Floating Window States
- public isVideoWindowOpen = signal(false);
- public isVideoMinimized = signal(false);
- public activeVideoUrl = signal(null);
- public activeVideoSegments = signal([]);
- public activeVideoTitle = signal('');
- public isVideoLoading = signal(false);
- public recordingPlaybackStatus = signal('idle');
- public recordingPlaybackMessage = signal('');
- public shouldAutoplayVideo = signal(false);
- public videoSeekRequest = signal<{ seconds: number; requestId: number } | null>(null);
- public stepSeekRequest = signal<{ index: number; requestId: number } | null>(null);
- public playerMode = signal<'video' | 'steps'>('video');
- public streamResetEvent = signal(null);
- private activeVideoSessionId: string | null = null;
- private videoSeekRequestId = 0;
- private stepSeekRequestId = 0;
- private videoRequestGeneration = 0;
- private videoRetryTimer: ReturnType | null = null;
- private videoWaitStartedAt = 0;
-
- /**
- * Step logs only. Streaming text chunks cannot change replay frames, so the
- * custom equality keeps downstream frame extraction from re-running on every
- * llm_stream update of the sessionLogs signal.
- */
- private stepLogsForReplay = computed(
- () => this.sessionLogs().filter(
- (log) => log && (log.type === 'step_updated' || log.type === 'step_recorded')
- ),
- { equal: (a, b) => a.length === b.length && a.every((log, i) => log === b[i]) }
- );
-
- /**
- * Computed step replay frames from current session logs
- */
- public currentSessionStepFrames = computed(() => {
- return extractStepReplayFrames(this.stepLogsForReplay());
- });
-
- public hasCurrentSessionStepFrames = computed(() => {
- return this.currentSessionStepFrames().length > 0;
- });
-
- public setPlayerMode(mode: 'video' | 'steps'): void {
- this.playerMode.set(mode);
- }
-
- public togglePlayerMode(): void {
- this.playerMode.update((mode) => (mode === 'video' ? 'steps' : 'video'));
- }
-
- public requestStepSeek(index: number): void {
- this.stepSeekRequestId++;
- this.stepSeekRequest.set({ index, requestId: this.stepSeekRequestId });
- }
-
- /**
- * Clear user-pinned selection so subsequent runs automatically follow active runner
- */
- public clearUserPinnedSession(): void {
- this.userPinnedSessionId.set(null);
- }
-
- /**
- * Computed active/viewed session
- */
- public currentSession = computed(() => {
- const curId = this.currentSessionId();
- if (!curId) return null;
- return this.sessions().find(s => s.session_id === curId) || null;
- });
-
- /**
- * Architecture / LLM of the session being viewed.
- *
- * Each session row carries its own `model_info` (resolved by the backend),
- * so a Pro session stays "Pro" while a Flash task runs elsewhere. Only a
- * session that has not reached the list yet falls back to the running model.
- */
- public viewedModel = computed(() => {
- const current = this.currentSession();
- if (current?.model_info) return current.model_info;
- return this.activeModel();
- });
-
- /**
- * Computed whether the currently viewed session is actively running or paused.
- * Returns false when viewing completed/failed/cancelled tasks, or when no session is selected.
- */
- public isCurrentSessionRunning = computed(() => {
- const curId = this.currentSessionId();
- if (!curId) return false;
- const session = this.currentSession();
- if (session) {
- return session.status === 'running' || session.status === 'paused';
- }
- // Fallback if session was just submitted/selected but not yet merged into sessions list
- const isActiveStatus = this.agentStatus() === 'running' || this.agentStatus() === 'paused';
- if (isActiveStatus && this.runningSessionId() === curId) {
- return true;
- }
- if (this.activeTasks().some((at) => at.session_id === curId)) {
- return true;
- }
- return false;
- });
-
- /**
- * Computed video URL for the currently viewed session
- */
- public currentSessionVideoUrl = computed(() => {
- const curId = this.currentSessionId();
- if (!curId) return null;
- const session = this.sessions().find(s => s.session_id === curId);
- return session?.video_url || null;
- });
-
- public currentSessionRecordingStatus = computed(() => {
- const session = this.currentSession();
- const status = session?.recording_status;
- return status === 'recording' || status === 'finalizing' || status === 'processing'
- ? 'processing'
- : status;
- });
-
- private eventSource: EventSource | null = null;
- private statusInterval: any = null;
- private sessionLoadGeneration = 0;
- private sessionSnapshotRequestId = 0;
- private sessionSnapshotAppliedId = 0;
- private pendingSnapshotRequests = new Set();
-
- // Streaming chunks are coalesced into one signal update per short window so a
- // chatty LLM stream cannot force a full recompute pass per SSE chunk.
- private pendingStreamChunks = new Map();
- private streamFlushTimer: ReturnType | null = null;
- private lastQueueSignature: string | null = null;
- private lastActiveTasksSignature: string | null = null;
- private lastPersistedSessionsJson: string | null = null;
- private onVisibilityChange = () => {
- if (typeof document !== 'undefined' && !document.hidden) {
- this.fetchStatus();
- this.fetchSessions();
- }
- };
-
- constructor() {
- this.restoreSessionsCache();
- this.fetchSessions();
- this.startStatusPolling();
- this.ensureLiveStream();
- if (typeof document !== 'undefined') {
- document.addEventListener('visibilitychange', this.onVisibilityChange);
- }
- inject(DestroyRef).onDestroy(() => this.destroy());
- }
-
- /**
- * Check if any task is currently running
- */
- public isRunningTask = computed(() => {
- if (this.agentStatus() === 'running' || this.agentStatus() === 'paused') return true;
- return this.sessions().some(s => s.status === 'running' || s.status === 'paused');
- });
-
- /**
- * Effective Pro tuning defaults (verification level, explorer mode) from the
- * backend agent config, so the launcher sliders start where the config is.
- */
- public getProTuningDefaults(): Observable {
- return this.http.get('/api/run/defaults');
- }
-
- /** Session-wide token totals, live executor context size and the run's tuning. */
- public getSessionUsage(sessionId: string): Observable {
- return this.http.get(`/api/sessions/${encodeURIComponent(sessionId)}/usage`);
- }
-
- /**
- * Run a new task by submitting to backend queue
- */
- public runTask(
- goal: string,
- profile: string = 'flash',
- expectedOutput?: string,
- enableOutputter?: boolean,
- proTuning?: ProTuningOptions
- ): Observable {
- return new Observable((obs) => {
- const submittedEvent: StartupProgressEvent = {
- stage: 'submitting',
- message: 'Submitting the task',
- timestamp: Date.now() / 1000
- };
- this.pendingStartupProgress.set([submittedEvent]);
- const payload: any = { goal, profile };
- if (expectedOutput && expectedOutput.trim()) {
- payload.expected_output = expectedOutput.trim();
- }
- if (enableOutputter !== undefined) {
- payload.enable_outputter = enableOutputter;
- }
- if (proTuning?.verificationLevel) {
- payload.verification_level = proTuning.verificationLevel;
- }
- if (proTuning?.explorerMode) {
- payload.explorer_mode = proTuning.explorerMode;
- }
- this.clearUserPinnedSession();
- this.http.post('/api/run', payload).subscribe({
- next: (res) => {
- if (res && res.tasks && res.tasks.length > 0) {
- const newSessionId = res.tasks[0].session_id;
- if (newSessionId) {
- this.appendStartupProgress(
- { ...submittedEvent, session_id: newSessionId },
- newSessionId
- );
- this.pendingStartupProgress.set([]);
- const isCurrentlyRunning =
- this.agentStatus() === 'running' ||
- this.sessions().some((s) => s.status === 'running');
- const activeSessionId = this.runningSessionId()
- || this.sessions().find((s) => s.status === 'running' || s.status === 'paused')?.session_id;
- // The status poll can observe the new runner before /api/run
- // returns. In that case it is still the task we just submitted,
- // not an older task that should remain selected.
- if (!isCurrentlyRunning || activeSessionId === newSessionId) {
- this.selectSession(newSessionId, false);
- }
- }
- }
- this.pendingStartupProgress.set([]);
- obs.next(res);
- obs.complete();
- },
- error: (err) => {
- this.pendingStartupProgress.set([]);
- obs.error(err);
- }
- });
- });
- }
-
- /**
- * Stop the currently viewed task (or specified task), and optionally clear queue.
- * Supports stopping any task across any ingress/platform (Web, CLI, MCP, SDK).
- */
- public stopTask(targetOrStopAll?: string | boolean | null, stopAll: boolean = false): void {
- let targetSessionId: string | null = null;
- let effectiveStopAll = stopAll;
-
- if (typeof targetOrStopAll === 'boolean') {
- effectiveStopAll = targetOrStopAll;
- } else if (typeof targetOrStopAll === 'string' && targetOrStopAll.trim()) {
- targetSessionId = targetOrStopAll.trim();
- } else {
- // Default to currently viewed session, or currently running session
- targetSessionId = this.currentSessionId() || this.runningSessionId() || null;
- }
-
- if (!effectiveStopAll && !targetSessionId) {
- targetSessionId = this.sessions().find(
- (session) => session.status === 'running' || session.status === 'paused'
- )?.session_id || null;
- }
-
- // Keep the task's terminal state stable while the stop request and the
- // persisted session update cross the network. Clearing the global runner
- // state first used to make the session merger infer "completed" from the
- // still-stale DB row, causing a brief completed -> cancelled flicker.
- if (targetSessionId) {
- this.setSessionStatus(targetSessionId, 'cancelled');
- }
-
- // Determine if other active sessions are still executing across multiple devices
- const otherRunningSessions = this.sessions().filter(
- (s) => (s.status === 'running' || s.status === 'paused') && s.session_id !== targetSessionId
- );
-
- // Apply optimistic updates: only set idle if effectiveStopAll is true or no other tasks are running
- if (effectiveStopAll || otherRunningSessions.length === 0) {
- this.agentStatus.set('idle');
- this.runningSessionId.set(null);
- this.runningGoal.set(null);
- } else if (this.runningSessionId() === targetSessionId) {
- this.runningSessionId.set(otherRunningSessions[0].session_id);
- this.runningGoal.set(otherRunningSessions[0].initial_goal || null);
- }
-
- this.isPaused.set(false);
- this.pausedError.set(null);
- this.isRetrying.set(false);
- this.invalidateStatusSignatures();
- if (effectiveStopAll) {
- this.pendingQueue.set([]);
- }
-
- // Mark active streaming / pending logs as finished if viewing target session
- if (!targetSessionId || this.currentSessionId() === targetSessionId) {
- this.flushStreamChunks();
- this.sessionLogs.update((logs) => {
- if (!logs.some((l) => l.type === 'llm_stream' && !l.data?.isCompleted)) {
- return logs;
- }
- return logs.map((l) => {
- if (l.type === 'llm_stream' && !l.data?.isCompleted) {
- return { ...l, data: { ...l.data, isCompleted: true } };
- }
- return l;
- });
- });
- }
-
- // Optimistically remove from activeTasks
- if (targetSessionId) {
- this.activeTasks.update((list) => list.filter((at) => at.session_id !== targetSessionId));
- }
-
- let url = `/api/stop?all=${effectiveStopAll}`;
- if (!effectiveStopAll && targetSessionId) {
- url += `&session_id=${encodeURIComponent(targetSessionId)}`;
- }
-
- const payload: any = { all: effectiveStopAll };
- if (targetSessionId) {
- payload.session_id = targetSessionId;
- }
-
- this.http.post(url, payload).subscribe({
- next: () => {
- this.fetchStatus();
- this.fetchSessions();
- },
- error: (err) => {
- console.error('Failed to stop task:', err);
- this.fetchSessions();
- this.fetchStatus();
- }
- });
- }
-
- /**
- * Resume paused task
- */
- public resumeTask(): void {
- this.http.post<{ status?: string }>('/api/resume', {}).subscribe({
- next: (response) => {
- if (response?.status !== 'resumed') {
- // A stale recovery control must not optimistically change the task
- // back to running when the backend says there is nothing to resume.
- this.fetchStatus();
- return;
- }
- const resumedSessionId = this.runningSessionId();
- this.isPaused.set(false);
- this.pausedError.set(null);
- this.agentStatus.set('running');
- this.setSessionStatus(resumedSessionId, 'running');
- this.fetchStatus();
- },
- error: (err) => {
- console.error('Failed to resume task:', err);
- }
- });
- }
-
- /**
- * Fetch all past and active sessions from the backend
- */
- public fetchSessions(): void {
- this.http.get('/api/sessions').subscribe({
- next: (data) => {
- this.rawSessions.set(data);
- this.persistSessionsCache(data);
- // On initial load, if nothing is selected, not pinned, not running, and sessions exist, select latest
- if (!this.currentSessionId() && !this.userPinnedSessionId() && this.agentStatus() !== 'running' && data.length > 0) {
- this.selectSession(data[0].session_id, false);
- }
- },
- error: (err) => {
- console.error('Failed to fetch sessions from backend:', err);
- }
- });
- }
-
- /**
- * Delete an individual session / task
- */
- public deleteSession(sessionId: string): Observable {
- // 1. Optimistically update local session state immediately
- this.invalidateStatusSignatures();
- this.rawSessions.update((list) => list.filter((s) => s.session_id !== sessionId));
- this.persistSessionsCache(this.rawSessions());
- this.pendingQueue.update((list) => list.filter((s) => s.session_id !== sessionId));
-
- if (this.userPinnedSessionId() === sessionId) {
- this.userPinnedSessionId.set(null);
- }
-
- if (this.currentSessionId() === sessionId) {
- this.selectSession('', false);
- if (this.isVideoWindowOpen()) {
- this.closeVideoPlayer();
- }
- const runningId = this.runningSessionId();
- if (runningId && this.agentStatus() === 'running') {
- this.selectSession(runningId, false);
- }
- }
-
- // 2. Send API request
- return new Observable((obs) => {
- this.http.post(`/api/sessions/${sessionId}/delete`, {}).subscribe({
- next: (res) => {
- this.fetchSessions();
- this.fetchStatus();
- obs.next(res);
- obs.complete();
- },
- error: (err) => {
- console.error(`Failed to delete session ${sessionId}:`, err);
- this.fetchSessions();
- this.fetchStatus();
- obs.error(err);
- }
- });
- });
- }
-
- /**
- * Clear all sessions, tasks, and history
- */
- public clearAllHistory(): Observable {
- // 1. Optimistically clear all local session state immediately
- this.invalidateStatusSignatures();
- this.userPinnedSessionId.set(null);
- this.rawSessions.set([]);
- this.clearSessionsCache();
- this.pendingQueue.set([]);
- this.selectSession('', false);
- if (this.isVideoWindowOpen()) {
- this.closeVideoPlayer();
- }
-
- // 2. Send API request
- return new Observable((obs) => {
- this.http.post('/api/cleanup', {}).subscribe({
- next: (res) => {
- this.fetchSessions();
- this.fetchStatus();
- obs.next(res);
- obs.complete();
- },
- error: (err) => {
- console.error('Failed to cleanup history:', err);
- this.fetchSessions();
- this.fetchStatus();
- obs.error(err);
- }
- });
- });
- }
-
- /**
- * Connect to the Server-Sent Events stream for a specific session
- * @param sessionId The ID of the session to select
- * @param isUserAction Whether this selection was explicitly triggered by user interaction
- */
- public selectSession(sessionId: string, isUserAction: boolean = false): void {
- if (isUserAction) {
- if (sessionId && sessionId === this.runningSessionId() && this.agentStatus() === 'running') {
- // User explicitly clicked the currently running task -> follow live runner
- this.userPinnedSessionId.set(null);
- } else if (sessionId) {
- // User explicitly clicked a non-running or historical task -> pin to it
- this.userPinnedSessionId.set(sessionId);
- } else {
- this.userPinnedSessionId.set(null);
- }
- }
-
- if (!sessionId) {
- this.sessionLoadGeneration++;
- this.pendingSnapshotRequests.clear();
- this.discardPendingStreamChunks();
- this.isSessionContentLoading.set(false);
- this.currentSessionId.set(null);
- this.sessionLogs.set([]);
- this.currentNotes.set({});
- return;
- }
-
- if (this.currentSessionId() === sessionId) {
- return; // Already selected
- }
-
- const loadGeneration = ++this.sessionLoadGeneration;
- this.pendingSnapshotRequests.clear();
- this.discardPendingStreamChunks();
- this.sessionSnapshotAppliedId = 0;
- this.isSessionContentLoading.set(true);
- this.currentSessionId.set(sessionId);
- this.sessionLogs.set([]); // Reset logs for new session selection
- this.activePauseCardKey = null;
- this.isPaused.set(false);
- this.pausedError.set(null);
- this.fetchNotes(sessionId);
- this.fetchChecks(sessionId);
-
- // If video window is currently open, dynamically sync/refresh video for new session
- if (this.isVideoWindowOpen()) {
- this.openVideoPlayer(sessionId);
- }
-
- console.debug(`Selecting session: ${sessionId}`);
- // Start reading the persisted snapshot immediately
- this.backfillSessionSteps(sessionId, loadGeneration);
- this.ensureLiveStream();
- }
-
- /**
- * Ensure persistent single-channel live stream is established via /api/stream.
- * Eliminates reconnect tearing, race conditions, and disconnect dropouts.
- */
- public ensureLiveStream(): void {
- if (this.eventSource) {
- return;
- }
-
- console.debug('Establishing persistent unified live stream via /api/stream');
- // The stream is registered outside the Angular zone: high-frequency SSE
- // callbacks must not schedule a change-detection pass each. Signal writes
- // still notify the render scheduler, so the UI stays live.
- this.zone.runOutsideAngular(() => {
- this.eventSource = new EventSource('/api/stream');
-
- this.eventSource.addEventListener('info', () => {
- // Reconcile current session if active
- const curId = this.currentSessionId();
- if (curId) {
- this.backfillSessionSteps(curId);
- }
- });
-
- this.eventSource.addEventListener('keep-alive', () => {
- // Keep-alive tick
- });
-
- const eventTypes = [
- 'llm_stream',
- 'llm_stream_reset',
- 'trace_recorded',
- 'step_recorded',
- 'step_updated',
- 'background_tasks_updated',
- 'task_paused',
- 'task_resumed',
- 'llm_retrying',
- 'startup_progress',
- 'session_started',
- 'session_ended',
- 'recording_ready',
- 'recording_failed',
- 'checker_event'
- ];
-
- eventTypes.forEach((eventType) => {
- this.eventSource?.addEventListener(eventType, (event: MessageEvent) => {
- try {
- const parsedData = JSON.parse(event.data);
- const evtSessionId = parsedData?.session_id;
-
- // Preserve ordering: buffered stream text must land before any
- // non-stream event is appended to the session logs.
- if (eventType !== 'llm_stream') {
- this.flushStreamChunks();
- }
-
- if (eventType === 'recording_ready' || eventType === 'recording_failed') {
- if (
- this.isVideoWindowOpen()
- && evtSessionId
- && String(evtSessionId) === String(this.activeVideoSessionId)
- ) {
- if (eventType === 'recording_ready') {
- this.refreshActiveRecording(true);
- } else {
- this.cancelVideoRetry();
- this.isVideoLoading.set(false);
- this.activeVideoUrl.set(null);
- this.activeVideoSegments.set([]);
- this.recordingPlaybackStatus.set('failed');
- this.recordingPlaybackMessage.set(
- parsedData?.error || 'Recording finalization failed.'
- );
- }
- }
- return;
- }
-
- if (eventType === 'background_tasks_updated') {
- this.fetchStatus();
- return;
- }
-
- if (eventType === 'session_started') {
- this.agentStatus.set('running');
- if (parsedData?.session_id) {
- this.runningSessionId.set(parsedData.session_id);
- }
- if (parsedData?.initial_goal) {
- this.runningGoal.set(parsedData.initial_goal);
- }
- this.fetchSessions();
-
- // Auto-follow: if user has not explicitly pinned a historical session, switch view to new active session
- if (!this.userPinnedSessionId() && parsedData?.session_id) {
- this.selectSession(parsedData.session_id, false);
- }
- return;
- }
-
- if (eventType === 'session_ended') {
- const endedId = evtSessionId || this.runningSessionId();
- if (endedId) {
- this.applySessionEndedStatus(endedId, parsedData);
- this.invalidateStatusSignatures();
- this.activeTasks.update(list => list.filter(at => at.session_id !== endedId));
- }
- const remaining = this.activeTasks();
- if (remaining.length > 0) {
- this.agentStatus.set('running');
- this.runningSessionId.set(remaining[0].session_id);
- this.runningGoal.set(remaining[0].goal || null);
- } else if (this.pendingQueue().length > 0) {
- const nextPending = this.pendingQueue()[0];
- this.agentStatus.set('running');
- this.runningSessionId.set(nextPending.session_id);
- this.runningGoal.set(nextPending.initial_goal || null);
- } else {
- this.agentStatus.set('idle');
- this.runningSessionId.set(null);
- this.runningGoal.set(null);
- }
- this.isRetrying.set(false);
- this.isPaused.set(false);
- this.fetchSessions();
- this.fetchStatus();
- if (endedId) {
- this.fetchNotes(endedId);
- if (String(endedId) === String(this.currentSessionId() || '')) {
- // The exit final review / run outcome may have landed while the
- // stream was reconnecting: reconcile from the persisted ledger.
- this.fetchChecks(endedId);
- }
- if (
- this.isVideoWindowOpen()
- && this.activeVideoSessionId === endedId
- && this.recordingPlaybackStatus() === 'live'
- ) {
- this.beginRecordingFinalization(endedId);
- }
- }
- return;
- }
-
- if (eventType === 'startup_progress') {
- const targetSid = evtSessionId || this.currentSessionId();
- if (targetSid) {
- this.appendStartupProgress(parsedData, targetSid);
- const curId = this.currentSessionId();
- if (
- !this.userPinnedSessionId() &&
- (!curId || String(targetSid).trim().toLowerCase() !== String(curId).trim().toLowerCase())
- ) {
- this.agentStatus.set('running');
- this.runningSessionId.set(targetSid);
- this.selectSession(targetSid, false);
- }
- }
- return;
- }
-
- // If the event is for a different session than what user is currently viewing, ignore for logs
- const curId = this.currentSessionId();
- if (evtSessionId && curId && String(evtSessionId).trim().toLowerCase() !== String(curId).trim().toLowerCase()) {
- return;
- }
-
- if (eventType === 'llm_retrying') {
- this.isRetrying.set(true);
- const attempt = Number(parsedData.attempt || 0);
- const max = Number(parsedData.max_retries || 0);
- const delay = Number(parsedData.delay || 0);
- const attemptText = attempt && max ? ` (Attempt ${attempt}/${max})` : '';
- const delayText = delay > 0 ? `; retrying in ${delay.toFixed(2).replace(/\.00$/, '')}s` : '';
- this.retryMessage.set(`AI service is temporarily busy${attemptText}${delayText}...`);
- if (curId) {
- this.appendLiveLLMRetryTrace(parsedData, curId);
- }
- return;
- }
-
- if (eventType === 'task_paused') {
- this.isPaused.set(true);
- this.isRetrying.set(false);
- this.agentStatus.set('paused');
- if (curId) {
- this.runningSessionId.set(curId);
- this.setSessionStatus(curId, 'paused');
- const pauseError = parsedData.error || 'AI call failed';
- this.pausedError.set(pauseError);
- this.appendPausedErrorCard(
- pauseError,
- curId,
- parsedData.timestamp,
- parsedData.step_id,
- parsedData
- );
- }
- return;
- }
-
- if (eventType === 'task_resumed') {
- this.isPaused.set(false);
- this.isRetrying.set(false);
- this.pausedError.set(null);
- if (curId && this.runningSessionId() === curId) {
- this.agentStatus.set('running');
- this.setSessionStatus(curId, 'running');
- }
- this.activePauseCardKey = null;
- return;
- }
-
- if (eventType === 'step_updated' || eventType === 'step_recorded' || eventType === 'llm_stream') {
- this.isRetrying.set(false);
- }
-
- if (eventType === 'llm_stream_reset') {
- const streamExecId = parsedData?.stream_exec_id || parsedData?.stream_execution_id;
- if (streamExecId) {
- for (const key of Array.from(this.pendingStreamChunks.keys())) {
- if (key.startsWith(`${streamExecId}|`) || key === streamExecId) {
- this.pendingStreamChunks.delete(key);
- }
- }
- }
- const resetMessage = parsedData?.message || DEFAULT_STREAM_RESET_MESSAGE;
- this.streamResetEvent.set({
- stream_exec_id: streamExecId,
- stream_execution_id: streamExecId,
- step_id: parsedData?.step_id,
- session_id: parsedData?.session_id,
- action: parsedData?.action || 'discard',
- reason: parsedData?.reason || 'mid_stream_failure',
- category: parsedData?.category,
- error: parsedData?.error,
- message: resetMessage,
- retry_attempt: parsedData?.retry_attempt,
- timestamp: Date.now()
- });
-
- this.sessionLogs.update((logs) => {
- let updated = false;
- const next = logs.map((log) => {
- if (log.type === 'llm_stream' && log.data?.execution_id === streamExecId) {
- updated = true;
- return {
- ...log,
- data: {
- ...log.data,
- isReset: true,
- resetMessage
- }
- };
- }
- return log;
- });
- if (!updated && streamExecId) {
- next.push({
- type: 'llm_stream',
- timestamp: new Date().toISOString(),
- session_id: parsedData?.session_id,
- data: {
- execution_id: streamExecId,
- step_id: parsedData?.step_id,
- stream_type: 'text',
- text: '',
- isCompleted: false,
- isReset: true,
- resetMessage
- }
- });
- }
- return next;
- });
- return;
- }
-
- if (eventType === 'llm_stream') {
- this.queueStreamChunk(parsedData);
- } else {
- this.sessionLogs.update((logs) => {
- // Copying every log object on every event is wasted work when no
- // stream is open; only rewrite entries that actually change.
- const hasOpenStream = logs.some(
- (l) => l.type === 'llm_stream' && !l.data.isCompleted
- );
- const updatedLogs = !hasOpenStream ? [...logs] : logs.map((l) => {
- if (l.type === 'llm_stream' && !l.data.isCompleted) {
- return {
- ...l,
- data: { ...l.data, isCompleted: true }
- };
- }
- return l;
- });
-
- const evtTime = typeof parsedData?.timestamp === 'number'
- ? new Date(parsedData.timestamp * 1000).toISOString()
- : new Date().toISOString();
-
- const nextLog = {
- type: eventType,
- timestamp: evtTime,
- data: parsedData
- };
-
- if (eventType === 'trace_recorded' && parsedData?.trace_id) {
- const existingTraceIndex = updatedLogs.findIndex((log) =>
- log.type === 'trace_recorded'
- && log.data?.trace_id === parsedData.trace_id
- );
- if (existingTraceIndex > -1) {
- const deduplicatedLogs = [...updatedLogs];
- deduplicatedLogs[existingTraceIndex] = nextLog;
- return deduplicatedLogs;
- }
- }
-
- return [...updatedLogs, nextLog];
- });
- }
-
- if (eventType === 'trace_recorded' && parsedData) {
- const trName = parsedData.name || '';
- if (['save_note', 'read_note', 'update_note', 'append_note', 'list_notes', 'outputter'].includes(trName.toLowerCase())) {
- const curSessionId = this.currentSessionId();
- if (curSessionId) {
- this.fetchNotes(curSessionId);
- }
- }
- }
- } catch (e) {
- console.error(`Failed to parse ${eventType} event data:`, e);
- this.sessionLogs.update((logs) => [
- ...logs,
- {
- type: eventType,
- timestamp: new Date().toISOString(),
- data: event.data
- }
- ]);
- }
- });
- });
-
- this.eventSource.onerror = (err) => {
- console.warn('Persistent live stream issue, browser will auto-reconnect:', err);
- };
- });
- }
-
- /**
- * Buffer one llm_stream chunk; the buffer is flushed into sessionLogs at most
- * once per short window (and immediately before any non-stream event so the
- * relative ordering of stream text and step events is preserved).
- */
- private queueStreamChunk(parsedData: any): void {
- const execId = parsedData.execution_id;
- const stepId = parsedData.step_id;
- const sessionId = parsedData.session_id || null;
- // The emitting agent's trace: lets the aggregator route a concurrent
- // agent's stream (e.g. the Checker) to its own block instead of the
- // Operator step that happens to be current.
- const parentTraceId = parsedData.parent_trace_id || null;
- const streamType = parsedData.stream_type || 'text';
- const key = `${execId}|${streamType}`;
- const chunk = parsedData.chunk !== undefined ? parsedData.chunk : (parsedData.text || '');
- const pending = this.pendingStreamChunks.get(key);
- if (pending) {
- pending.chunk += chunk;
- if (stepId) pending.stepId = stepId;
- if (sessionId) pending.sessionId = sessionId;
- if (parentTraceId) pending.parentTraceId = parentTraceId;
- } else {
- this.pendingStreamChunks.set(key, { execId, stepId, sessionId, parentTraceId, streamType, chunk });
- }
- if (!this.streamFlushTimer) {
- const delay = typeof document !== 'undefined' && document.hidden ? 500 : 80;
- this.streamFlushTimer = setTimeout(() => this.flushStreamChunks(), delay);
- }
- }
-
- private discardPendingStreamChunks(): void {
- this.pendingStreamChunks?.clear();
- if (this.streamFlushTimer) {
- clearTimeout(this.streamFlushTimer);
- this.streamFlushTimer = null;
- }
- }
-
- private flushStreamChunks(): void {
- if (this.streamFlushTimer) {
- clearTimeout(this.streamFlushTimer);
- this.streamFlushTimer = null;
- }
- if (!this.pendingStreamChunks?.size) return;
- const batches = Array.from(this.pendingStreamChunks.values());
- this.pendingStreamChunks.clear();
-
- this.sessionLogs.update((logs) => {
- let next = [...logs];
- for (const batch of batches) {
- const existingIndex = next.findIndex(
- (l) => l.type === 'llm_stream' &&
- l.data.execution_id === batch.execId &&
- (l.data.stream_type || 'text') === batch.streamType
- );
-
- if (existingIndex > -1) {
- const existingLog = next[existingIndex];
- next[existingIndex] = {
- ...existingLog,
- session_id: batch.sessionId || existingLog.session_id,
- data: {
- ...existingLog.data,
- session_id: batch.sessionId || existingLog.data?.session_id,
- text: (existingLog.data.text || '') + batch.chunk,
- step_id: batch.stepId || existingLog.data.step_id,
- parent_trace_id: batch.parentTraceId || existingLog.data.parent_trace_id || null
- }
- };
- } else {
- // A new execution closes the previous open stream of the same agent
- // lane only; concurrent agents (Operator vs. Checker) keep theirs.
- next = next.map((l) => {
- if (l.type === 'llm_stream' &&
- (l.data.stream_type || 'text') === batch.streamType &&
- !l.data.isCompleted &&
- (l.data.parent_trace_id || null) === (batch.parentTraceId || null)) {
- return { ...l, data: { ...l.data, isCompleted: true } };
- }
- return l;
- });
- next.push({
- type: 'llm_stream',
- timestamp: new Date().toISOString(),
- session_id: batch.sessionId || undefined,
- data: {
- execution_id: batch.execId,
- session_id: batch.sessionId || undefined,
- step_id: batch.stepId,
- parent_trace_id: batch.parentTraceId || null,
- text: batch.chunk,
- stream_type: batch.streamType,
- isCompleted: false
- }
- });
- }
- }
- return next;
- });
- }
-
- /**
- * Load a fresh persisted snapshot without replacing events already received
- * from SSE. Snapshot logs are replaceable, while live logs remain append-only;
- * the stream aggregator merges matching steps and trace IDs within each step.
- */
- private backfillSessionSteps(sessionId: string, loadGeneration = this.sessionLoadGeneration): void {
- const requestId = ++this.sessionSnapshotRequestId;
- this.pendingSnapshotRequests.add(requestId);
- this.http.get(`/api/sessions/${sessionId}/steps`).subscribe({
- next: (steps) => {
- this.pendingSnapshotRequests.delete(requestId);
- if (
- this.currentSessionId() !== sessionId
- || loadGeneration !== this.sessionLoadGeneration
- || requestId < this.sessionSnapshotAppliedId
- ) return;
- this.sessionSnapshotAppliedId = requestId;
- const historicalLogs = steps.map((step) => ({
- type: 'step_updated',
- session_id: sessionId,
- timestamp: new Date((step.timestamp || Date.now() / 1000) * 1000).toISOString(),
- data: step,
- history_snapshot: true
- }));
-
- this.sessionLogs.update((logs) => [
- ...historicalLogs,
- ...logs.filter((log) => !log.history_snapshot)
- ]);
- this.isSessionContentLoading.set(false);
- },
- error: (err) => {
- this.pendingSnapshotRequests.delete(requestId);
- console.error('Failed to backfill session steps:', err);
- if (
- this.currentSessionId() === sessionId
- && loadGeneration === this.sessionLoadGeneration
- && this.pendingSnapshotRequests.size === 0
- ) {
- this.isSessionContentLoading.set(false);
- }
- }
- });
-
- this.http.get(`/api/sessions/${sessionId}/startup_progress`).subscribe({
- next: (events) => {
- if (Array.isArray(events) && events.length > 0) {
- this.startupProgressBySession.update((current) => {
- const existing = current[sessionId] || [];
- const merged = [...existing];
- for (const ev of events) {
- if (!merged.some((m) => m.stage === ev.stage)) {
- merged.push(ev);
- }
- }
- return {
- ...current,
- [sessionId]: merged
- };
- });
- }
- },
- error: () => {}
- });
- }
-
- private restoreSessionsCache(): void {
- try {
- const cached = localStorage.getItem(SESSION_CACHE_KEY);
- if (!cached) return;
- const sessions = JSON.parse(cached);
- if (Array.isArray(sessions)) {
- this.lastPersistedSessionsJson = cached;
- this.rawSessions.set(sessions);
- }
- } catch {
- this.clearSessionsCache();
- }
- }
-
- private persistSessionsCache(sessions: Session[]): void {
- try {
- const serialized = JSON.stringify(sessions);
- if (serialized === this.lastPersistedSessionsJson) return;
- this.lastPersistedSessionsJson = serialized;
- localStorage.setItem(SESSION_CACHE_KEY, serialized);
- } catch {
- // Storage can be unavailable in private browsing or embedded contexts.
- }
- }
-
- private clearSessionsCache(): void {
- this.lastPersistedSessionsJson = null;
- try {
- localStorage.removeItem(SESSION_CACHE_KEY);
- } catch {
- // No-op when browser storage is unavailable.
- }
- }
-
- /**
- * Convert the dedicated live retry event into the same trace shape used by
- * history APIs. This keeps live streaming and post-refresh rendering on one
- * data contract instead of maintaining a separate retry UI path.
- */
- private appendLiveLLMRetryTrace(data: any, sessionId: string): void {
- const timestampSeconds = typeof data?.timestamp === 'number'
- ? (data.timestamp > 1e11 ? data.timestamp / 1000 : data.timestamp)
- : Date.now() / 1000;
- const traceId = data?.trace_id
- || `llm-retry-live-${data?.request_id || 'unknown'}-${timestampSeconds}`;
- const payload = {
- error: data?.error,
- delay: data?.delay,
- attempt: data?.attempt,
- max_retries: data?.max_retries,
- provider: data?.provider,
- source: data?.source,
- recoverable: data?.recoverable,
- request_id: data?.request_id,
- scheduled_at: data?.scheduled_at ?? timestampSeconds
- };
- const retryTrace = {
- trace_id: traceId,
- session_id: sessionId,
- step_id: data?.step_id || null,
- type: 'llm_call',
- name: 'llm_retry',
- timestamp: timestampSeconds,
- status: 'retrying',
- payload
- };
-
- this.sessionLogs.update((logs) => {
- const existingIndex = logs.findIndex((log) =>
- log.type === 'trace_recorded'
- && (
- log.data?.trace_id === traceId
- || (
- data?.request_id
- && log.data?.payload?.request_id === data.request_id
- && Number(log.data?.payload?.scheduled_at) === Number(payload.scheduled_at)
- )
- )
- );
- const retryLog = {
- type: 'trace_recorded',
- session_id: sessionId,
- timestamp: new Date(timestampSeconds * 1000).toISOString(),
- data: retryTrace
- };
- if (existingIndex < 0) return [...logs, retryLog];
-
- const updatedLogs = [...logs];
- updatedLogs[existingIndex] = retryLog;
- return updatedLogs;
- });
- }
-
- /**
- * Preserve pause failures in the normal failed-LLM card stream. New runners
- * emit a persisted failed trace first; this fallback also supports older
- * runners whose task_paused event only carried an error string.
- */
- private appendPausedErrorCard(
- error: unknown,
- sessionId: string,
- timestamp?: number | string,
- stepId?: string | null,
- details?: any
- ): void {
- const errorText = typeof error === 'string' ? error : JSON.stringify(error);
- const pauseKey = `${sessionId}:${errorText}`;
- if (this.activePauseCardKey === pauseKey) return;
-
- this.sessionLogs.update((logs) => {
- const alreadyRecorded = logs.some((log) => {
- const traces = log?.type === 'trace_recorded'
- ? [log.data]
- : (Array.isArray(log?.data?.generic_tools) ? log.data.generic_tools : []);
- return traces.some((trace: any) =>
- trace?.type === 'llm_call'
- && trace?.status === 'failed'
- && String(trace?.payload?.error ?? trace?.error ?? '') === errorText
- );
- });
- if (alreadyRecorded) return logs;
-
- const latestStepId = stepId || [...logs].reverse().find((log) =>
- (log.type === 'step_updated' || log.type === 'step_recorded') && log.data?.step_id
- )?.data?.step_id || null;
- const timestampMs = typeof timestamp === 'number'
- ? (timestamp < 1e11 ? timestamp * 1000 : timestamp)
- : (timestamp ? new Date(timestamp).getTime() : Date.now());
-
- return [
- ...logs,
- {
- type: 'trace_recorded',
- session_id: sessionId,
- timestamp: new Date(timestampMs).toISOString(),
- data: {
- trace_id: `task-paused-${sessionId}-${timestampMs}`,
- session_id: sessionId,
- step_id: latestStepId,
- type: 'llm_call',
- name: 'llm_pause',
- status: 'failed',
- timestamp: timestampMs / 1000,
- payload: {
- error: errorText,
- pause: true,
- request_id: details?.request_id,
- provider: details?.provider,
- waited_seconds: details?.waited_seconds,
- retries: Array.isArray(details?.retries) ? details.retries : []
- }
- }
- }
- ];
- });
- this.activePauseCardKey = pauseKey;
- }
-
- private pollCounter: number = 0;
-
- private setSessionStatus(sessionId: string | null, status: Session['status']): void {
- if (!sessionId) return;
-
- this.invalidateStatusSignatures();
- this.rawSessions.update((sessions) => sessions.map((session) =>
- session.session_id === sessionId ? { ...session, status } : session
- ));
- this.pendingQueue.update((sessions) => sessions.map((session) =>
- session.session_id === sessionId ? { ...session, status } : session
- ));
- }
-
- private applySessionEndedStatus(sessionId: unknown, data: any): void {
- if (!sessionId) return;
-
- const reportedStatus = String(data?.status || '').toLowerCase();
- const status: Session['status'] | null = data?.was_stopped_manually || reportedStatus === 'cancelled'
- ? 'cancelled'
- : reportedStatus === 'failed'
- ? 'failed'
- : reportedStatus === 'completed' || reportedStatus === 'success'
- ? 'completed'
- : null;
-
- if (status) {
- this.activeSessionTracking.delete(String(sessionId));
- this.setSessionStatus(String(sessionId), status);
- this.invalidateStatusSignatures();
- this.activeTasks.update((list) => list.filter((at) => at.session_id !== String(sessionId)));
- }
- }
-
- /**
- * Start periodic status polling from the backend
- */
- private startStatusPolling(): void {
- this.fetchStatus();
- // The interval runs outside the Angular zone so a poll tick alone never
- // schedules change detection; only actual signal updates do.
- this.zone.runOutsideAngular(() => {
- this.statusInterval = setInterval(() => {
- if (typeof document !== 'undefined' && document.hidden) {
- return; // Paused while the tab is in the background; resumed by visibilitychange.
- }
- this.fetchStatus();
- this.pollCounter++;
- // Periodically refresh sessions every 6 seconds (every 3 polling intervals of 2s) to stay in sync with external DB modifications
- if (this.pollCounter % 3 === 0) {
- this.fetchSessions();
- }
- }, 2000);
- });
- }
-
- /**
- * Optimistic local mutations of queue/active state must force the next poll
- * to re-apply backend data even when its payload has not changed.
- */
- private invalidateStatusSignatures(): void {
- this.lastQueueSignature = null;
- this.lastActiveTasksSignature = null;
- }
-
- /**
- * Fetch current agent runner process status
- */
- public fetchStatus(): void {
- this.http.get('/api/status').subscribe({
- next: (data) => {
- if (data && data.status) {
- const oldStatus = this.agentStatus();
- const oldRunningSessionId = this.runningSessionId();
- const isActive = data.status === 'running' || data.status === 'paused';
- this.agentStatus.set(data.status);
- this.runningSessionId.set(data.session_id || null);
- this.runningGoal.set(data.goal || null);
- if (data.model_info) {
- this.activeModel.set(data.model_info);
- }
- this.isPaused.set(data.status === 'paused');
-
- if (data.status === 'paused') {
- this.isRetrying.set(false);
- const pauseError = data.paused_error
- || this.pausedError()
- || 'AI model request failed. The task is paused.';
- this.pausedError.set(pauseError);
- if (data.session_id && this.currentSessionId() === data.session_id) {
- this.appendPausedErrorCard(pauseError, data.session_id);
- }
- } else {
- this.pausedError.set(null);
- this.activePauseCardKey = null;
- }
-
- // Skip signal updates when backend payloads did not change: setting a
- // fresh array reference every 2s poll would force the whole sessions
- // computed chain to re-derive for identical data.
- const queueSignature = JSON.stringify(data.queue || []);
- if (queueSignature !== this.lastQueueSignature) {
- this.lastQueueSignature = queueSignature;
- // Map backend pending queue to real Session / TaskQueueItem objects
- const pending: Session[] = (data.queue || []).map((item: any, index: number) => {
- if (typeof item === 'object' && item !== null) {
- return {
- session_id: item.session_id || `pending-task-${index}`,
- initial_goal: item.goal || '',
- start_time: item.start_time || item.created_at || (Date.now() / 1000 + index),
- status: item.status || 'pending',
- device_serial: item.device_serial || item.device_id || null
- };
- }
- return {
- session_id: `task-queued-${index}`,
- initial_goal: String(item),
- start_time: (Date.now() / 1000) + index,
- status: 'pending'
- };
- });
- this.pendingQueue.set(pending);
- }
- const activeTasksSignature = JSON.stringify(data.active_tasks || []);
- if (activeTasksSignature !== this.lastActiveTasksSignature) {
- this.lastActiveTasksSignature = activeTasksSignature;
- this.activeTasks.set(data.active_tasks || []);
- }
-
- if (oldStatus !== data.status || oldRunningSessionId !== data.session_id) {
- this.fetchSessions();
- }
-
- if (
- !isActive
- && (oldStatus === 'running' || oldStatus === 'paused')
- && oldRunningSessionId
- && this.isVideoWindowOpen()
- && this.activeVideoSessionId === oldRunningSessionId
- && this.recordingPlaybackStatus() === 'live'
- ) {
- this.beginRecordingFinalization(oldRunningSessionId);
- }
-
- // Auto-select the active session if user has not explicitly pinned a historical session
- if (isActive && data.session_id) {
- const currentId = this.currentSessionId();
- if (!currentId || (!this.userPinnedSessionId() && currentId !== data.session_id)) {
- this.selectSession(data.session_id, false);
- }
- }
- }
- },
- error: (err) => {
- console.error('Failed to fetch status from backend:', err);
- this.agentStatus.set('offline');
- this.runningSessionId.set(null);
- this.runningGoal.set(null);
- }
- });
- }
-
- /**
- * Clean up event source connection and timers on service destruction
- */
- public destroy(): void {
- if (this.statusInterval) {
- clearInterval(this.statusInterval);
- this.statusInterval = null;
- }
- if (this.eventSource) {
- this.eventSource.close();
- this.eventSource = null;
- }
- this.discardPendingStreamChunks();
- if (typeof document !== 'undefined') {
- document.removeEventListener('visibilitychange', this.onVisibilityChange);
- }
- this.cancelVideoRetry();
- }
-
- /**
- * Fetch notes for a specific session
- */
- /**
- * Backfill the Checker's attempts and run outcome from the persisted verdict
- * ledger (`/api/sessions/{id}/checks`) as synthetic `checker_event` logs, so
- * historical sessions show the same timeline blocks as live ones. Live
- * events for attempts already known are merged by attempt id in the
- * aggregator, so re-fetching is idempotent.
- */
- public fetchChecks(sessionId: string): void {
- if (!sessionId) return;
- this.http.get<{ records?: any[]; streams?: PersistedCheckerStream[]; run_outcome?: any }>(`/api/sessions/${sessionId}/checks`).subscribe({
- next: (res) => {
- if (this.currentSessionId() !== sessionId) return;
- const snapshot = this.buildCheckerSnapshotLogs(
- sessionId, res?.records || [], res?.run_outcome || null, res?.streams || []
- );
- this.sessionLogs.update((logs) => [
- ...logs.filter((log) => !log.checks_snapshot),
- ...snapshot
- ]);
- },
- error: (err) => {
- console.error(`Failed to fetch checks for session ${sessionId}:`, err);
- }
- });
- }
-
- /**
- * `streams` are the attempts' persisted transcripts (what the Checker
- * streamed while reaching its verdict): they become the attempt's
- * `stream_segments`, the same shape the live `llm_stream` chunks build, so
- * a reopened session interleaves Thought/Work text with tool rows exactly
- * like the live view did.
- */
- private buildCheckerSnapshotLogs(
- sessionId: string,
- records: any[],
- runOutcome: any,
- streams: PersistedCheckerStream[] = []
- ): any[] {
- const byAttempt = new Map();
- const segmentsByAttempt = new Map();
- for (const stream of streams || []) {
- if (!stream || !stream.attempt_id) continue;
- const segments = persistedStreamToSegments(stream);
- if (segments.length > 0) segmentsByAttempt.set(String(stream.attempt_id), segments);
- }
- for (const rec of records) {
- if (!rec || !rec.attempt_id) continue;
- const attemptId = String(rec.attempt_id);
- const checkpointId = String(rec.checkpoint_id || '');
- let attempt = byAttempt.get(attemptId);
- if (!attempt) {
- attempt = {
- event: 'attempt_finished',
- phase: checkpointId === 'final' ? 'final' : 'checkpoint',
- attempt_id: attemptId,
- checkpoint_id: checkpointId,
- subgoal_text: String(rec.subgoal_text || (checkpointId === 'final'
- ? "Final review against the user's original goal"
- : `Subgoal ${checkpointId.slice(0, 8)}`)),
- anchor_step_id: rec.anchor_step_id ?? null,
- trace_id: rec.trace_id ?? null,
- status: 'done',
- verdicts: [],
- findings: [],
- ts: typeof rec.ts === 'number' ? rec.ts : undefined,
- session_id: sessionId
- };
- byAttempt.set(attemptId, attempt);
- }
- if (typeof rec.ts === 'number') {
- attempt.ts = attempt.ts === undefined ? rec.ts : Math.min(attempt.ts, rec.ts);
- }
- if (!attempt.trace_id && rec.trace_id) {
- attempt.trace_id = rec.trace_id;
- }
- attempt.verdicts.push({
- item_text: String(rec.item_text || ''),
- kind: String(rec.kind || ''),
- status: String(rec.status || ''),
- evidence: String(rec.evidence || ''),
- suggestion: rec.suggestion ? String(rec.suggestion) : '',
- when: rec.when ? String(rec.when) : undefined
- });
- }
-
- const logs: any[] = [];
- for (const attempt of byAttempt.values()) {
- const statuses = new Set(attempt.verdicts.map((v: any) => v.status));
- if (statuses.size === 1) {
- const only = [...statuses][0];
- if (only === 'superseded' || only === 'unchecked') attempt.status = only;
- }
- const ts = attempt.ts ?? Date.now() / 1000;
- attempt.timestamp = ts;
- const segments = segmentsByAttempt.get(attempt.attempt_id);
- if (segments) attempt.stream_segments = segments;
- logs.push({
- type: 'checker_event',
- session_id: sessionId,
- timestamp: new Date(ts * 1000).toISOString(),
- checks_snapshot: true,
- data: attempt
- });
- }
- if (runOutcome && typeof runOutcome === 'object') {
- const lastTs = logs.length > 0
- ? Math.max(...logs.map((l) => new Date(l.timestamp).getTime() / 1000))
- : Date.now() / 1000;
- const ts = lastTs + 0.001;
- logs.push({
- type: 'checker_event',
- session_id: sessionId,
- timestamp: new Date(ts * 1000).toISOString(),
- checks_snapshot: true,
- data: { event: 'run_outcome', phase: 'outcome', ...runOutcome, ts, timestamp: ts, session_id: sessionId }
- });
- }
- return logs;
- }
-
- public fetchNotes(sessionId: string): void {
- if (!sessionId) {
- this.currentNotes.set({});
- return;
- }
- this.http.get(`/api/sessions/${sessionId}/notes`).subscribe({
- next: (res) => {
- if (res && res.notes) {
- this.currentNotes.set(res.notes);
-
- // Default to task_plan.md if available, otherwise first note
- const keys = Object.keys(res.notes);
- if (keys.length > 0) {
- const currentSelected = this.selectedNoteKey();
- if (!keys.includes(currentSelected)) {
- if (keys.includes('task_plan.md')) {
- this.selectedNoteKey.set('task_plan.md');
- } else {
- this.selectedNoteKey.set(keys[0]);
- }
- }
- }
- }
- },
- error: (err) => {
- console.error(`Failed to fetch notes for session ${sessionId}:`, err);
- }
- });
- }
-
- /**
- * Open the video player for a given session or the current session
- */
- public openVideoPlayer(
- sessionId?: string,
- videoUrl?: string,
- title?: string,
- seekSeconds?: number,
- stepIndex?: number
- ): void {
- const targetSessionId = sessionId || this.currentSessionId();
- const session = this.sessions().find(s => s.session_id === targetSessionId);
- const targetUrl = videoUrl || session?.video_url || null;
- const goalTitle = title || session?.initial_goal || (targetSessionId ? `Task: ${targetSessionId.slice(0, 8)}...` : 'Screen Recording');
-
- this.activeVideoTitle.set(goalTitle);
- this.isVideoWindowOpen.set(true);
- this.isVideoMinimized.set(false);
- this.activeVideoSessionId = targetSessionId;
- this.cancelVideoRetry();
- this.videoRequestGeneration++;
- this.shouldAutoplayVideo.set(false);
- this.recordingPlaybackMessage.set('');
- this.activeVideoSegments.set([]);
- if (Number.isFinite(seekSeconds)) this.requestVideoSeek(Number(seekSeconds));
- if (Number.isFinite(stepIndex)) {
- this.playerMode?.set('steps');
- this.requestStepSeek(Number(stepIndex));
- }
- if (!targetSessionId) {
- this.activeVideoUrl.set(null);
- this.isVideoLoading.set(false);
- this.recordingPlaybackStatus.set('unavailable');
- return;
- }
-
- if (session?.status === 'running' || session?.status === 'paused') {
- this.activeVideoUrl.set(null);
- this.isVideoLoading.set(false);
- this.recordingPlaybackStatus.set('live');
- return;
- }
-
- this.activeVideoUrl.set(targetUrl);
- this.shouldAutoplayVideo.set(true);
- this.videoWaitStartedAt = Date.now();
- this.isVideoLoading.set(true);
- this.recordingPlaybackStatus.set('processing');
- this.recordingPlaybackMessage.set('Loading screen recording...');
- if (targetUrl) {
- this.playerMode?.set('video');
- } else if (this.hasCurrentSessionStepFrames?.()) {
- this.playerMode?.set('steps');
- } else {
- this.playerMode?.set('video');
- }
- this.requestSessionVideo(targetSessionId, this.videoRequestGeneration);
- }
-
- private appendStartupProgress(data: any, sessionId: string): void {
- if (!data?.stage || !data?.message) return;
-
- const timestamp = typeof data.timestamp === 'number'
- ? (data.timestamp > 1e11 ? data.timestamp / 1000 : data.timestamp)
- : Date.now() / 1000;
- const event: StartupProgressEvent = {
- session_id: String(data.session_id || sessionId),
- stage: String(data.stage),
- message: String(data.message),
- timestamp
- };
-
- const rawKey = String(data.session_id || sessionId).trim();
- this.startupProgressBySession.update((allEvents) => {
- const current = [...(allEvents[rawKey] || allEvents[sessionId] || [])];
- const existingIndex = current.findIndex((item) => item.stage === event.stage);
- if (existingIndex >= 0) {
- current[existingIndex] = event;
- } else {
- current.push(event);
- }
- current.sort((a, b) => a.timestamp - b.timestamp);
- const updated = { ...allEvents, [rawKey]: current };
- if (sessionId && sessionId !== rawKey) {
- updated[sessionId] = current;
- }
- return updated;
- });
- }
-
- private beginRecordingFinalization(sessionId: string): void {
- if (this.activeVideoSessionId !== sessionId) return;
- this.cancelVideoRetry();
- this.videoRequestGeneration++;
- this.videoWaitStartedAt = Date.now();
- this.activeVideoUrl.set(null);
- this.activeVideoSegments.set([]);
- this.shouldAutoplayVideo.set(true);
- this.isVideoLoading.set(true);
- this.recordingPlaybackStatus.set('processing');
- this.recordingPlaybackMessage.set('Finalizing screen recording...');
- this.requestSessionVideo(sessionId, this.videoRequestGeneration);
- }
-
- private requestSessionVideo(sessionId: string, generation: number): void {
- this.http.get(`/api/sessions/${sessionId}/video`).subscribe({
- next: (res) => {
- if (generation !== this.videoRequestGeneration || sessionId !== this.activeVideoSessionId) {
- return;
- }
- const status = res.status || (res.has_video && res.video_url ? 'ready' : 'unavailable');
- if (status === 'ready' && res.video_url) {
- this.cancelVideoRetry();
- this.isVideoLoading.set(false);
- this.recordingPlaybackStatus.set('ready');
- this.recordingPlaybackMessage.set('');
- this.activeVideoUrl.set(res.video_url);
- this.activeVideoSegments.set(res.video_segments || []);
- this.playerMode?.set('video');
- this.rawSessions.update((list) =>
- list.map((s) => s.session_id === sessionId
- ? { ...s, video_url: res.video_url || undefined, recording_status: 'ready' }
- : s)
- );
- return;
- }
- if (status === 'processing') {
- this.activeVideoUrl.set(null);
- this.activeVideoSegments.set([]);
- this.isVideoLoading.set(true);
- this.recordingPlaybackStatus.set('processing');
- this.recordingPlaybackMessage.set('Finalizing screen recording...');
- this.scheduleVideoRetry(sessionId, generation, res.retry_after_ms);
- return;
- }
-
- this.cancelVideoRetry();
- this.isVideoLoading.set(false);
- this.activeVideoUrl.set(null);
- this.activeVideoSegments.set([]);
- this.recordingPlaybackStatus.set(status === 'failed' ? 'failed' : 'unavailable');
- this.recordingPlaybackMessage.set(
- res.message || (status === 'failed'
- ? 'Recording finalization failed.'
- : 'No screen recording is available for this task.')
- );
- if (this.hasCurrentSessionStepFrames?.()) {
- this.playerMode?.set('steps');
- }
- },
- error: () => {
- if (generation !== this.videoRequestGeneration || sessionId !== this.activeVideoSessionId) {
- return;
- }
- if (this.recordingPlaybackStatus() === 'processing') {
- this.scheduleVideoRetry(sessionId, generation, 1000);
- return;
- }
- this.isVideoLoading.set(false);
- this.recordingPlaybackStatus.set('failed');
- this.recordingPlaybackMessage.set('Unable to load the screen recording.');
- if (this.hasCurrentSessionStepFrames?.()) {
- this.playerMode?.set('steps');
- }
- }
- });
- }
-
- private scheduleVideoRetry(sessionId: string, generation: number, retryAfterMs = 1000): void {
- this.cancelVideoRetry();
- if (Date.now() - this.videoWaitStartedAt > 120_000) {
- this.isVideoLoading.set(false);
- this.recordingPlaybackStatus.set('failed');
- this.recordingPlaybackMessage.set('Recording finalization timed out. You can retry.');
- return;
- }
- const delay = Math.max(500, Math.min(3000, retryAfterMs));
- this.videoRetryTimer = setTimeout(() => {
- this.videoRetryTimer = null;
- this.requestSessionVideo(sessionId, generation);
- }, delay);
- }
-
- private cancelVideoRetry(): void {
- if (this.videoRetryTimer) {
- clearTimeout(this.videoRetryTimer);
- this.videoRetryTimer = null;
- }
- }
-
- private refreshActiveRecording(autoplay: boolean): void {
- const sessionId = this.activeVideoSessionId;
- if (!sessionId) return;
- this.cancelVideoRetry();
- this.videoRequestGeneration++;
- if (autoplay) this.shouldAutoplayVideo.set(true);
- this.requestSessionVideo(sessionId, this.videoRequestGeneration);
- }
-
- public retryVideoRecording(): void {
- const sessionId = this.activeVideoSessionId;
- if (!sessionId) return;
- this.beginRecordingFinalization(sessionId);
- }
-
- public consumeVideoAutoplay(): boolean {
- const shouldAutoplay = this.shouldAutoplayVideo();
- this.shouldAutoplayVideo.set(false);
- return shouldAutoplay;
- }
-
- public requestVideoSeek(seconds: number): void {
- if (!Number.isFinite(seconds)) return;
- this.videoSeekRequest.set({
- seconds: Math.max(0, seconds),
- requestId: ++this.videoSeekRequestId
- });
- }
-
- /**
- * Toggle the video player for the current session
- */
- public toggleVideoPlayer(): void {
- if (this.isVideoWindowOpen()) {
- this.isVideoWindowOpen.set(false);
- } else {
- this.openVideoPlayer();
- }
- }
-
- /**
- * Close the video player
- */
- public closeVideoPlayer(): void {
- this.isVideoWindowOpen.set(false);
- this.cancelVideoRetry();
- this.videoRequestGeneration++;
- this.activeVideoSessionId = null;
- }
-}
diff --git a/apps/showcase_ui/src/app/services/system.service.spec.ts b/apps/showcase_ui/src/app/services/system.service.spec.ts
deleted file mode 100644
index bd2c6d51c..000000000
--- a/apps/showcase_ui/src/app/services/system.service.spec.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-import { provideHttpClient, withXhr } from '@angular/common/http';
-import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
-import { TestBed } from '@angular/core/testing';
-
-import { SystemReadinessReport } from '../core/models/system.model';
-import { SystemService } from './system.service';
-
-describe('SystemService readiness polling', () => {
- let service: SystemService;
- let http: HttpTestingController;
-
- const report = (timestamp: number): SystemReadinessReport => ({
- overall_ready: true,
- blocker_count: 3,
- passed_blocker_count: 3,
- probes: [],
- active_device: null,
- os_type: 'windows',
- timestamp
- });
-
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [
- SystemService,
- provideHttpClient(withXhr()),
- provideHttpClientTesting()
- ]
- });
- service = TestBed.inject(SystemService);
- http = TestBed.inject(HttpTestingController);
- service.stopAutoPolling();
- });
-
- afterEach(() => http.verify());
-
- it('shares one HTTP request across overlapping readiness callers', () => {
- let firstTimestamp = 0;
- let secondTimestamp = 0;
-
- service.fetchReadiness().subscribe(value => firstTimestamp = value.timestamp);
- service.fetchReadiness(true).subscribe(value => secondTimestamp = value.timestamp);
-
- const request = http.expectOne('/api/system/readiness');
- request.flush(report(10));
-
- expect(firstTimestamp).toBe(10);
- expect(secondTimestamp).toBe(10);
- expect(service.readinessReport()?.timestamp).toBe(10);
- expect(service.isLoading()).toBeFalse();
- });
-
- it('does not let an older snapshot replace a newer report', () => {
- service.fetchReadiness().subscribe();
- http.expectOne('/api/system/readiness').flush(report(20));
-
- service.fetchReadiness().subscribe();
- http.expectOne('/api/system/readiness').flush(report(10));
-
- expect(service.readinessReport()?.timestamp).toBe(20);
- });
-
- it('requests a forced backend refresh for a manual re-check', () => {
- service.fetchReadiness(false, true).subscribe();
-
- const request = http.expectOne(req =>
- req.url === '/api/system/readiness' && req.params.get('force') === 'true'
- );
- expect(request.request.params.get('force')).toBe('true');
- request.flush(report(30));
- });
-
- it('loads the active ADB server endpoint', () => {
- service.fetchAdbServerStatus().subscribe();
-
- const request = http.expectOne('/api/system/adb/server');
- request.flush({
- endpoint: {
- host: '127.0.0.1',
- port: 5038,
- socket: 'tcp:127.0.0.1:5038',
- mode: 'remote',
- is_local_default: false
- }
- });
-
- expect(service.isRemoteAdbServer()).toBeTrue();
- expect(service.adbServerStatus()?.endpoint.port).toBe(5038);
- });
-
- it('activates an ADB server endpoint and applies its readiness report', () => {
- service.connectAdbServer('127.0.0.1', 5038, true).subscribe();
-
- const request = http.expectOne('/api/system/adb/server/connect');
- expect(request.request.body).toEqual({
- host: '127.0.0.1',
- port: 5038,
- persist: true
- });
- request.flush({
- connection_result: {
- success: true,
- message: 'Connected.',
- endpoint: {
- host: '127.0.0.1',
- port: 5038,
- socket: 'tcp:127.0.0.1:5038',
- mode: 'remote',
- is_local_default: false
- },
- devices: []
- },
- report: report(40)
- });
-
- expect(service.isRemoteAdbServer()).toBeTrue();
- expect(service.readinessReport()?.timestamp).toBe(40);
- });
-
- it('probes an ADB server without changing the active endpoint', () => {
- service.probeAdbServer('127.0.0.1', 5038).subscribe();
-
- const request = http.expectOne('/api/system/adb/server/probe');
- expect(request.request.body).toEqual({
- host: '127.0.0.1',
- port: 5038,
- persist: false
- });
- request.flush({
- connection_result: {
- success: true,
- message: 'Endpoint is reachable.',
- endpoint: {
- host: '127.0.0.1',
- port: 5038,
- socket: 'tcp:127.0.0.1:5038',
- identity: 'tcp:127.0.0.1:5038',
- mode: 'remote',
- is_local_default: false
- },
- devices: []
- }
- });
-
- expect(service.adbServerStatus()).toBeNull();
- });
-
- it('restores the standard local ADB server explicitly', () => {
- service.useLocalAdbServer(true).subscribe();
-
- const request = http.expectOne(req =>
- req.url === '/api/system/adb/server/local' && req.params.get('persist') === 'true'
- );
- request.flush({
- connection_result: {
- success: true,
- message: 'Using local ADB.',
- endpoint: {
- host: '127.0.0.1',
- port: 5037,
- socket: 'tcp:127.0.0.1:5037',
- mode: 'local',
- is_local_default: true
- },
- devices: []
- },
- report: report(50)
- });
-
- expect(service.isRemoteAdbServer()).toBeFalse();
- expect(service.adbServerStatus()?.endpoint.port).toBe(5037);
- });
-});
diff --git a/apps/showcase_ui/src/app/services/system.service.ts b/apps/showcase_ui/src/app/services/system.service.ts
deleted file mode 100644
index b69697d9f..000000000
--- a/apps/showcase_ui/src/app/services/system.service.ts
+++ /dev/null
@@ -1,610 +0,0 @@
-/**
- * Copyright 2026 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Injectable, signal, computed, inject, DestroyRef, NgZone } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
-import { Observable, finalize, shareReplay, tap } from 'rxjs';
-import {
- AdbServerConnectionResponse,
- AdbServerStatus,
- SystemReadinessReport,
- DeviceInfo,
- ProbeResult,
- EmulatorLaunchState,
- EmulatorLaunchStage
-} from '../core/models/system.model';
-
-@Injectable({
- providedIn: 'root'
-})
-export class SystemService {
- private http = inject(HttpClient);
-
- // Core reactive signals
- public readinessReport = signal(null);
- public hasReadinessReport = computed(() => this.readinessReport() !== null);
- public isLoading = signal