diff --git a/.changelog/v2.59.0.md b/.changelog/v2.59.0.md new file mode 100644 index 0000000000..1ad9992181 --- /dev/null +++ b/.changelog/v2.59.0.md @@ -0,0 +1,40 @@ +# Release v2.59.0 + +Released: 2026-09-06 + +## Highlights + +**Coding-agent providers & review harness** +- Added the Pi coding agent as a full provider — model picker, review runner, and harness catalog entries. +- The Codex harness can now run on a local LM Studio or Ollama model, completing account-agnostic local hosting for that harness. +- The CoS Codex picker now offers the signed-in ChatGPT account's real model catalog, and every model picker now routes through one account-aware resolver instead of divergent per-surface logic. +- OpenCode gained an attachable Stage 3 pr-reviewer recipe, exposing a live shell session in its agent card. +- Fixed a stray unrecognized-model telemetry line leaking into Stage 2 reviewer stderr, and shrank the pr-reviewer Stage 3 prompt from ~400KB to ~50KB. + +**Video generation** +- Added Episode Composer for continuous multi-scene video, backed by a new script-to-beats compiler and a deterministic video-prompt linter that guards against malformed clip input. +- Added Reactor.inc fast-h3 and fal.ai queue REST API backends to MediaGen and FableLoom. +- Reactor renders now provision their runtime automatically on first use and tolerate slow downloads instead of failing outright; EXIF orientation is honored when fitting a starting frame. +- Fixed a cluster of video-hold and Remix-handoff bugs: held cohorts staying aligned with runtime model selection, stranded handoffs waiting correctly for Retry, and video holds surviving sharing imports and damaged snapshots. + +**Eidoverse** +- Added federated guest teleportation and Mind chat, so a Mind can visit another host's Eidoverse world. +- Objects can now be explained and given explicit display aliases, and a hosted world can name and describe its own PortOS host. +- Landscapes and departure navigation are now preserved correctly, and PortOS Commons ships with a grassy pedestrian park. + +**Model comparison & AI Providers** +- Added reasoning-effort curves, chart axis scaling, and stretch controls to the model comparison view, plus AA-catalog cost sync so hand-entered costs no longer masquerade as AA-published. +- Added detection and a setup prompt for federated LLM hosts. + +**Themes** +- Added the Kestrel Neon theme family and brought the other eight themes up to the same visual fidelity, backed by a shared, token-driven effects layer. + +**Reliability and infra** +- `gh`-backed background checks (update checks, backoff-gated calls) now back off on consecutive failures instead of retrying every tick, while a manual "check now" is exempted from that backoff. +- Fixed a data-loss bug where the data.reference seed could wipe CoS settings on an install crossing the #6182 update. +- LoRA downloads now share one download slot instead of racing independent ones, and raw filesystem writers under `data/` were routed through guarded primitives. +- Pinokio duplicate model weights can now be reclaimed via verified hardlinks, bounded to safe, valid paths. + +## Full Changelog + +**Full Diff**: https://github.com/atomantic/PortOS/compare/v2.58.0...v2.59.0 diff --git a/.claude/skills/portos-file-issue/SKILL.md b/.claude/skills/portos-file-issue/SKILL.md index fcfd9336c1..42d88d1c4e 100644 --- a/.claude/skills/portos-file-issue/SKILL.md +++ b/.claude/skills/portos-file-issue/SKILL.md @@ -16,5 +16,5 @@ description: PortOS conventions for filing GitHub issues and capturing deferred - `help wanted` — extra hands welcome and the body is scoped enough to pick up cold. - **Planner attribution (`planner:`) records who WROTE the plan.** It is a third independent axis — the author, not a recommendation about how to run the work. Take the label verbatim from the "Planner Attribution" section of your own run's prompt (PortOS resolves it from the provider/model it dispatched you with); **never guess it from what you believe you are**, and omit the axis entirely when your run was given none. Create it like any other label: `gh label create planner: --color C2185B --description 'Plan authored by the model' 2>/dev/null || true`. Omit an axis rather than guessing. Do not stamp `medium` on both dispatch axes. Do not put `[model:…]` / `[effort:…]` / `[category]` / `[SEVERITY]` in the title. Create each missing label immediately before applying it (`gh label create --color 2>/dev/null || true`; colors: model:light `D4C5F9`, model:medium `A371F7`, model:heavy `6F42C1`, effort:low `BFE5E5`, effort:medium `76C7C7`, effort:high `1D7874`, effort:xhigh `0E4F4C`, effort:max `05403D`, good first issue `7057FF`, help wanted `008672`). Use repeated `--label` flags. Never relabel a deduplicated existing issue. - - **`future` is almost never correct.** Do NOT use it to park a choice you could make yourself. The narrow legitimate park is work a **human must personally drive** — a real fidelity/validation run only they can judge, or a step that needs **specific hardware/credentials** the agent doesn't have (e.g. a CUDA box, an Apple-Silicon-only validation). Mark those `blocked` (skipped by autonomous claim runs), not `future`, and say in the body exactly what human action unblocks it. `blocked` also covers a genuine dependency on another unshipped issue/PR. + - **`future` is almost never correct.** Do NOT use it to park a choice you could make yourself. The narrow legitimate park is work a **human must personally drive** — a real fidelity/validation run only they can judge, or a step that needs **specific hardware/credentials** the agent doesn't have (e.g. a CUDA box, an Apple-Silicon-only validation). Mark those `blocked` (skipped by autonomous claim runs), not `future`, and say in the body exactly what human action unblocks it. `blocked` also covers a genuine dependency on another unshipped issue/PR — for that case, name the dependency on its own line as `Blocked by #N` (or `Blocked by #N, #M` for more than one): the `issue-reconcile` scheduled task (`blockedIssueReconcile.js`) parses exactly that line, and removes the label automatically once every named issue is closed. An issue with no `Blocked by` line is never auto-unblocked, so the human/hardware case is unaffected. - **`needs-input` is a last resort, not a reflex.** When claiming an ambiguous issue, prefer proceeding on your best interpretation and shipping it. Fall back to `needs-input` only when proceeding would be **destructive/irreversible** or genuinely requires the human (hardware, credentials, a personal judgment call) — never merely because more than one reasonable reading exists. diff --git a/.env.example b/.env.example index 7883c2ded6..8dcff149b8 100644 --- a/.env.example +++ b/.env.example @@ -110,6 +110,19 @@ PGPASSWORD=portos # GROK_TIMEOUT_MS — keep it above the 20-minute cloud-lane idle watchdog. # GROK_VIDEO_TIMEOUT_MS=1800000 +# Maximum fal.ai queue video-generation runtime in milliseconds (videoGen/fal.js; +# default: 1200000 / 20 minutes) — a queue render can sit behind other tenants' +# jobs before it starts, so this bounds the whole submit→poll→download run. +# FAL_VIDEO_TIMEOUT_MS=1200000 + +# Maximum reactor.inc fast-h3 video-generation runtime in milliseconds +# (videoGen/reactor.js; default: 1200000 / 20 minutes) — same reasoning as +# FAL_VIDEO_TIMEOUT_MS above. +# REACTOR_VIDEO_TIMEOUT_MS=1200000 +# Optional Python executable in an environment with reactor-sdk installed. +# Defaults to data/venvs/reactor created by npm run setup:reactor. +# REACTOR_PYTHON_PATH= + # Maximum Antigravity image-generation runtime in milliseconds (default: 1200000 / 20 minutes) # AGY_IMAGEGEN_TIMEOUT_MS=1200000 @@ -150,6 +163,17 @@ PGPASSWORD=portos # CivitAI API key for LoRA / model downloads from civitai.com # CIVITAI_API_KEY=your_civitai_api_key_here +# fal.ai API key for the fal.ai queue video backend (Video Gen page, FableLoom). +# Settings > Video Gen's stored key wins when both are set. +# FAL_KEY=your_fal_api_key_here + +# reactor.inc API key for the reactor.inc fast-h3 video backend (Video Gen +# page, FableLoom). Settings > Video Gen's stored key wins when both are set. +# REACTOR_API_KEY=your_reactor_api_key_here + +# Artificial Analysis API key for synchronizing model quality, speed, and pricing comparison benchmarks +# ARTIFICIAL_ANALYSIS_API_KEY=your_artificial_analysis_api_key_here + # Absolute path to the bash binary used to run PortOS's bundled *.sh scripts # (e.g. scripts/db.sh). Auto-detected if unset; on Windows it prefers Git Bash, # because a bare `bash` often resolves to WSL, which can't see drive paths. @@ -181,6 +205,11 @@ PGPASSWORD=portos # Path to the media-models registry JSON file (defaults to data/media-models.json) # PORTOS_MEDIA_MODELS_FILE=/path/to/custom/media-models.json +# Pinokio installation used by the explicit Models Status duplicate scan. +# PINOKIO_HOME takes precedence over PINOKIO_PATH; default: ~/pinokio. +# PINOKIO_HOME=/path/to/pinokio +# PINOKIO_PATH=/path/to/pinokio + # LM Studio models directory override (default: ~/LM Studio/models) # LM_STUDIO_MODELS_DIR=/path/to/lm-studio/models @@ -292,3 +321,7 @@ PGPASSWORD=portos # Auto-generated and appended to .env on the first vault write if unset. Back # it up: losing it makes every encrypted vault value unrecoverable. # PRIVACY_VAULT_KEY=<64 hex chars> + +# Set by AI Providers > Model host setup after successful dedicated setup. +# Restores the authenticated queue; the container restart policy restores Qwen. +# PORTOS_FLEET_LLM_ENABLED=0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bab7471dfb..06f7bea497 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: lint_files: ${{ steps.plan.outputs.lint_files }} build: ${{ steps.plan.outputs.build }} smoke: ${{ steps.plan.outputs.smoke }} + slashdo: ${{ steps.plan.outputs.slashdo }} windows: ${{ steps.plan.outputs.windows }} windows_mode: ${{ steps.plan.outputs.windows_mode }} windows_files: ${{ steps.plan.outputs.windows_files }} @@ -96,6 +97,7 @@ jobs: LINT_MODE: ${{ steps.plan.outputs.lint_mode }} BUILD_MODE: ${{ steps.plan.outputs.build }} SMOKE_MODE: ${{ steps.plan.outputs.smoke }} + SLASHDO_MODE: ${{ steps.plan.outputs.slashdo }} WINDOWS_MODE: ${{ steps.plan.outputs.windows }} WINDOWS_TEST_MODE: ${{ steps.plan.outputs.windows_mode }} SERVER_SHARDS: ${{ steps.plan.outputs.server_shards }} @@ -113,6 +115,7 @@ jobs: echo "- Client lint: \`${LINT_MODE}\`" echo "- Client build: \`${BUILD_MODE}\`" echo "- Server smoke: \`${SMOKE_MODE}\`" + echo "- Slashdo submodule: \`${SLASHDO_MODE}\`" echo "- Windows server tests: \`${WINDOWS_MODE}\` (\`${WINDOWS_TEST_MODE}\`, shards \`${WINDOWS_SHARDS}\`)" echo "- Suite selection reasons: \`${SUITE_REASONS}\`" } >> "$GITHUB_STEP_SUMMARY" @@ -274,6 +277,16 @@ jobs: - name: Check server entry-point syntax run: node --check server/index.js + # The submodule ships no source into this checkout by default (see + # AGENTS.md "Slashdo Commands"), so the planner's `slashdo` output is + # the only signal that the selected tests need it — an unrelated scoped + # PR must not pay for the fetch. `CI_EXPECT_SLASHDO_SUBMODULE` below + # tells the two contract suites to fail instead of skip if this step + # somehow leaves the checkout without it. + - name: Initialize slashdo submodule + if: needs.impact.outputs.slashdo == 'true' + run: git submodule update --init lib/slashdo + - name: Run server tests if: needs.impact.outputs.server_mode != 'skip' env: @@ -281,6 +294,7 @@ jobs: CI_TEST_FILES: ${{ needs.impact.outputs.server_files }} CI_TEST_SOURCES: ${{ needs.impact.outputs.server_sources }} CI_SHARD: ${{ matrix.shard }}/${{ strategy.job-total }} + CI_EXPECT_SLASHDO_SUBMODULE: ${{ needs.impact.outputs.slashdo }} # Keep successful-test output signal-dense. Developers can reproduce # locally without this flag when assertion context needs app logs. PORTOS_TEST_QUIET: 1 diff --git a/AGENTS.md b/AGENTS.md index 13b63f1909..a0fdd0a66c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,12 +60,13 @@ regression-detection value are the goals. - **Auth** — an optional instance password (`server/services/auth.js`, enforced by `server/services/authGate.js`) gates all of `/api/*` and `/data/*` when set, with a small always-public set (`/api/auth/status`, `/api/system/health`). Peers reach a password-gated instance via a per-peer Basic credential on the peer record, attached to every outbound hop by `peerFetch` (`server/lib/peerHttpClient.js`). - **HTTPS** — provisioned by `npm run setup:cert`; `:5555` flips to TLS with a loopback-only HTTP mirror on `:5553`. Peer hops set `rejectUnauthorized: false` ("Tailnet is the trust boundary") — between two tailnet nodes WireGuard already supplies mutual auth, but a non-tailnet peer (plain LAN IP / non-`.ts.net` host, see `peerRequiresTailscale()`) gets no server authentication. -Because both are off by default, **never treat "the password is set" as an available guarantee** when designing a feature — gate on it explicitly, or design for the default posture. **PII must not ride the federation layer at all** — see ADR [privacy records machine-local](docs/decisions/2026-08-08-privacy-records-machine-local.md). That rule governs *records* and *status/capability payloads*; its one scoped carve-out is a submitted job body carrying the conditioning an allowlisted peer is being asked to render — the prompt (ADR [federated visual prompts](docs/decisions/2026-08-20-federated-visual-prompts.md)), and, under the same gate, lyrics and conditioning images (ADR [conditioning crosses to an allowlisted peer](docs/decisions/2026-08-22-federated-media-input-assets.md), which also records what stays refused: model weights and multi-step chain state). +Because both are off by default, **never treat "the password is set" as an available guarantee** when designing a feature — gate on it explicitly, or design for the default posture. **PII must not ride the federation layer at all** — see ADR [privacy records machine-local](docs/decisions/2026-08-08-privacy-records-machine-local.md). That rule governs *records* and *status/capability payloads*; one scoped carve-out is a submitted job body carrying the conditioning an allowlisted peer is being asked to render — the prompt (ADR [federated visual prompts](docs/decisions/2026-08-20-federated-visual-prompts.md)), and, under the same gate, lyrics and conditioning images (ADR [conditioning crosses to an allowlisted peer](docs/decisions/2026-08-22-federated-media-input-assets.md), which also records what stays refused: model weights and multi-step chain state). Explicit Eidoverse guest conversations have a separate narrow carve-out: submitted world-chat messages and live replies may cross an enabled registered peer under guest admission and the independent default-off Mind travel grant (ADR [Eidoverse guest chat](docs/decisions/2026-09-05-eidoverse-guest-chat.md)). No private records, secrets, automatic conversation-history exports, or record-bearing status payloads are authorized by that exception. **Distribution model (across installs).** PortOS is distributed software: many independent people each run their own install, on their own machine(s), each upgrading on their own schedule — and a single user commonly runs **several machines federated as sync peers**. Backward/forward compatibility across installs and versions is first-class, NOT optional: - **Never delete or skip migration / compatibility code on the grounds that "there's only one install."** There isn't — other people and other machines run this code and update it independently. - On-disk format changes need a migration in `scripts/migrations/` (applied-list tracked per install in `data/migrations.applied.json`). Seed files must ship in `data.reference/`. +- **A migration that DERIVES `data/` from an install's existing records ships NO `data.reference/` seed, and gates on the presence of its INPUT — never the absence of its output.** `setup-data.js` runs before `run-migrations.js` and copies every `data.reference/` file the install lacks, so a seed lands first; a migration that skips because its output already exists then silently leaves shipped defaults where the user's data was, and the next write destroys the original. Declare such a path in `scripts/lib/migrationOwnedPaths.js` (its test fails if a seed reappears). The applied-list already provides re-run safety, so gating on the input costs nothing. This is not hypothetical: it replaced CoS settings with shipped defaults on any install whose update crossed #6182 — see `scripts/migrations/340-cos-config-seed-repair.js`. - Prompt-default changes need a `PROMPT_VERSIONS` bump AND the prior default preserved in `PREVIOUS_DEFAULT_PROMPTS`, so other installs recognize their stored prompt and auto-upgrade it (`server/services/taskSchedule.js`). - Cross-machine sync payloads stay version-gated (`server/lib/schemaVersions.js`) so a newer peer can't corrupt an older one. - The self-update path stays fork-aware — other users run forks, not just upstream. **Never add `--force` to the server-side `gh repo sync`** (it would discard a user's fork commits; the 409 `FORK_DIVERGED` message points them at running it themselves). Don't re-hardcode the upstream slug outside `server/lib/gitRemote.js`, and new UI claiming "you are running PortOS" must read `remoteInfo.isUpstream`, not just `currentVersion`, or it lies to fork users. Mechanism: [docs/SELF_UPDATE.md](docs/SELF_UPDATE.md). @@ -122,7 +123,9 @@ Client- and server-specific conventions live in nested memory files that load wh ### Slashdo Commands (`lib/slashdo`) -PortOS bundles [slashdo](https://github.com/atomantic/slashdo) as a git submodule at `lib/slashdo`, providing slash commands (`/do:next`, `/do:review`, `/do:pr`, `/do:push`, `/do:release`, …) without a separate global install. The `.claude/commands/do/` symlinks expose them as project-level slash commands. `/do:next` claims the next PLAN.md item (or GitHub issue with `--issues`) in an isolated worktree and ships a PR. CoS agents can inline a command's content into a prompt via `loadSlashdoCommand(name)` from `server/services/subAgentSpawner.js` (it resolves `!cat` lib includes automatically). +PortOS bundles [slashdo](https://github.com/atomantic/slashdo) as a git submodule at `lib/slashdo`, providing slash commands (`/do:next`, `/do:review`, `/do:pr`, `/do:push`, `/do:release`, …) without a separate global install. The `.claude/commands/do/` symlinks expose them as project-level slash commands. `/do:next` claims the next PLAN.md item (or GitHub issue with `--issues`) in an isolated worktree and ships a PR. CoS agents use the shared slashdo renderer: file-tool hosts receive a short entrypoint with supporting files read by phase; `loadSlashdoCommand(name)` from `server/services/subAgentSpawner.js` remains the eager, self-contained form for other consumers. + +Project command symlinks expose slashdo source directly. In that source, `!read lib/.md` means read `lib/slashdo/lib/.md` when the named phase applies; resolve nested library references (including `~/.claude/lib/.md` tokens) against that same bundled package root. These are required reads, not shell commands. Do not preload every phase or depend on a global slashdo installation. Staged CoS bundles instead resolve each reference relative to the file containing it. ## Module Organization diff --git a/README.md b/README.md index 9e1654a640..97df766bf9 100644 --- a/README.md +++ b/README.md @@ -314,7 +314,7 @@ For HTTPS (recommended — required for the in-browser microphone away from loop npm run setup:cert # Tailscale + Let's Encrypt (trusted) npm run setup:guide # show the next setup action + correct URL npm run setup:cert -- --self-signed # fallback if you don't have Tailscale -pm2 restart ecosystem.config.cjs +npm run pm2:restart ``` After that, `https://..ts.net:5555` is the user-facing URL on every device. The same `:5555` is used for both HTTP and HTTPS — only the scheme changes. When HTTPS is enabled a loopback-only HTTP mirror also spawns at `http://localhost:5553` so local curl/scripts skip cert warnings (override with `PORTOS_HTTP_PORT`). @@ -323,7 +323,7 @@ After that, `https://..ts.net:5555` is the user-facing URL on The same walkthrough is always available in **Settings → Setup**. It verifies one runnable AI provider as well as secure remote access, and links directly to every action PortOS cannot perform at the account level. The Dashboard and Instances page surface the same network state; Unix and Windows update scripts retry certificate provisioning and print the guide after every update. See the [complete setup guide](./docs/SETUP.md). -PM2 keeps PortOS running in the background and auto-restarts on reboot (with `pm2 save` + `pm2 startup`). +PM2 keeps PortOS running in the background. To configure startup after a reboot, run `npm run pm2:startup` from the repository root and follow the platform-specific instructions it prints, then run `npm run pm2:save` to save the process list. `npm start` already saves the process list, but does not install the startup service. ### Development Mode @@ -373,13 +373,15 @@ PortOS/ ## PM2 Commands +Run these from the repository root after setup. They use the PM2 bundled with PortOS, so a global `pm2` command is not required. + ```bash -pm2 start ecosystem.config.cjs # Start PortOS -pm2 status # View status -pm2 logs portos-server --lines 100 # View server logs -pm2 restart portos-server portos-ui # Restart processes -pm2 stop portos-server portos-ui # Stop processes -pm2 save # Save process list (survives reboot) +npm start # Build the client and start PortOS +npm run pm2:status # View status +npm run pm2:logs -- portos-server --lines 100 # View server logs +npm run pm2:restart # Restart the PortOS ecosystem +npm run pm2:stop # Stop the PortOS ecosystem +npm run pm2:save # Save the process list for startup ``` ## Configuration diff --git a/client/src/App.jsx b/client/src/App.jsx index add1caffe6..3184e3136d 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,4 +1,5 @@ import { Suspense, useEffect } from 'react'; +import { isPublicGuestRoute } from './lib/publicGuestRoutes'; import { Routes, Route, Navigate, useLocation } from 'react-router'; import Layout from './components/Layout'; import { getSettings, updateSettings, getSelfInstance, PORTOS_APP_ID } from './services/api'; @@ -130,6 +131,7 @@ const PipelineContinuityBible = lazyWithReload(() => import('./pages/PipelineCon const PipelineManuscriptEditor = lazyWithReload(() => import('./pages/PipelineManuscriptEditor')); const PipelineExport = lazyWithReload(() => import('./pages/PipelineExport')); const PipelineIssue = lazyWithReload(() => import('./pages/PipelineIssue')); +const EidoverseGuest = lazyWithReload(() => import('./pages/EidoverseGuest')); const Login = lazyWithReload(() => import('./pages/Login')); const NotFound = lazyWithReload(() => import('./pages/NotFound')); @@ -244,13 +246,14 @@ function useDocumentTitle(enabled = true) { export default function App() { const { pathname } = useLocation(); - const isHostedAudienceRoute = pathname.replace(/\/+$/, '') === '/fableloom/join'; + const isHostedAudienceRoute = isPublicGuestRoute(pathname); useTimezoneBootstrap(!isHostedAudienceRoute); useDocumentTitle(!isHostedAudienceRoute); const routeContent = ( }> + } /> } /> } /> {/* Hosted audience devices need the full dynamic viewport, without the diff --git a/client/src/components/Layout.jsx b/client/src/components/Layout.jsx index 0d3c4cb501..5d2842a134 100644 --- a/client/src/components/Layout.jsx +++ b/client/src/components/Layout.jsx @@ -289,6 +289,7 @@ export const NAV_PRESENTATION = { '/models/llms': { icon: Cpu }, '/models/loras': { icon: Sparkles }, '/models/media': { icon: HardDrive }, + '/models/comparison': { icon: Gauge }, '/models/performance': { icon: Gauge }, '/local-llm/playground': { icon: FlaskConical }, '/models/status': { icon: Activity }, @@ -305,6 +306,7 @@ export const NAV_PRESENTATION = { '/settings/general': { icon: Settings }, '/settings/mortalloom': { icon: Activity }, '/openclaw': { icon: MessagesSquare }, + '/settings/orchestration': { icon: Cpu }, '/prompts': { icon: FileText }, '/ai': { icon: Bot }, '/settings/security': { icon: Lock }, @@ -552,7 +554,8 @@ const EXACT_FULL_WIDTH_PATHS = [ // OpenClaw lives under the Settings nav group; it's a full-bleed // chat surface (sidebar + message pane) that owns its own internal // scroll, so it needs the bare full-width main like the other - // Settings pages (/ai, /prompts, /settings/*). + // Full-width Settings pages (/prompts, /settings/*) and the Models Providers + // page at /ai. '/openclaw', '/prompts', '/review', diff --git a/client/src/components/apps/tabs/IssuesTab.jsx b/client/src/components/apps/tabs/IssuesTab.jsx index f01032c1c2..2e75e1d122 100644 --- a/client/src/components/apps/tabs/IssuesTab.jsx +++ b/client/src/components/apps/tabs/IssuesTab.jsx @@ -600,7 +600,7 @@ export default function IssuesTab({ appId, appName }) { )}