@surething/cockpit 1.0.234 → 1.0.236
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next-prod/BUILD_ID +1 -1
- package/.next-prod/app-path-routes-manifest.json +1 -1
- package/.next-prod/build-manifest.json +3 -3
- package/.next-prod/prerender-manifest.json +3 -3
- package/.next-prod/react-loadable-manifest.json +7 -0
- package/.next-prod/server/app/_global-error/page.js.nft.json +1 -1
- package/.next-prod/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next-prod/server/app/_global-error.html +1 -1
- package/.next-prod/server/app/_global-error.rsc +1 -1
- package/.next-prod/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next-prod/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next-prod/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next-prod/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next-prod/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next-prod/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next-prod/server/app/_not-found/page.js +1 -1
- package/.next-prod/server/app/_not-found/page.js.nft.json +1 -1
- package/.next-prod/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next-prod/server/app/_not-found.html +10 -2
- package/.next-prod/server/app/_not-found.rsc +29 -20
- package/.next-prod/server/app/_not-found.segments/_full.segment.rsc +29 -20
- package/.next-prod/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next-prod/server/app/_not-found.segments/_index.segment.rsc +12 -4
- package/.next-prod/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next-prod/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next-prod/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next-prod/server/app/api/agent/test/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/bash/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/codex/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/deepseek/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/kimi/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/ollama/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/pty-input/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/stop/route.js +1 -1
- package/.next-prod/server/app/api/chat/stop/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/claude-stats/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/commands/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/comments/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/db/columns/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/db/connect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/db/disconnect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/db/export/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/db/query/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/db/schemas/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/dev/spans/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/extension/version/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/file/route.js +1 -1
- package/.next-prod/server/app/api/file/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/blame/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/clipboard/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/copy/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/delete/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/expanded/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/index/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/init/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/paste/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/read/route.js +1 -1
- package/.next-prod/server/app/api/files/read/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/readdir/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/recent/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/save/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/search/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/stat/route.js +1 -1
- package/.next-prod/server/app/api/files/stat/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/text/route.js +1 -1
- package/.next-prod/server/app/api/files/text/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/branch-diff/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/branches/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/commit-diff/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/commits/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/current-branch/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/diff/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/discard/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/stage/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/status/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/unstage/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/worktree/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/global-state/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/health/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/jupyter/load/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/jupyter/save/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/jupyter/shutdown/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/lsp/definition/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/lsp/hover/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/lsp/references/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/lsp/status/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/lsp/warmup/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/mysql/columns/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/mysql/connect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/mysql/disconnect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/mysql/export/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/mysql/query/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/mysql/schemas/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/neo4j/connect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/neo4j/disconnect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/neo4j/query/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/neo4j/schema/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/note/route.js +1 -1
- package/.next-prod/server/app/api/note/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/ollama/models/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/ollama/start/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/open-cursor/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/open-vscode/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/pick-folder/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/pinned-sessions/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/project-settings/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/project-state/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/affected/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/callees/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/callers/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/coedit/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/context/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/file/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/file-functions/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/impact/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/related/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/risk/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/search/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projects/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/push/public-key/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/push/subscribe/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/push/unsubscribe/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/command/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/connect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/delete/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/disconnect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/get/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/keys/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/set/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/[id]/comments/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/[id]/replies/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/[id]/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/identify/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/order/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/share-info/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/users/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/scheduled-tasks/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/services/config/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/services/scripts/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/session/[sessionId]/fork/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/session/[sessionId]/history/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/session-by-path/route.js +1 -1
- package/.next-prod/server/app/api/session-by-path/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/sessions/projects/[encodedPath]/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/sessions/projects/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/settings/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/skills/[id]/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/skills/content/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/skills/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/terminal/aliases/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/terminal/autocomplete/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/terminal/bubble-order/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/terminal/env/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/terminal/history/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/version/route.js.nft.json +1 -1
- package/.next-prod/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next-prod/server/app/m/page.js +2 -2
- package/.next-prod/server/app/m/page.js.nft.json +1 -1
- package/.next-prod/server/app/m/page_client-reference-manifest.js +1 -1
- package/.next-prod/server/app/manifest.webmanifest/route.js.nft.json +1 -1
- package/.next-prod/server/app/page.js +1 -1
- package/.next-prod/server/app/page.js.nft.json +1 -1
- package/.next-prod/server/app/page_client-reference-manifest.js +1 -1
- package/.next-prod/server/app/project/page.js +1 -1
- package/.next-prod/server/app/project/page.js.nft.json +1 -1
- package/.next-prod/server/app/project/page_client-reference-manifest.js +1 -1
- package/.next-prod/server/app/review/[id]/page.js +1 -1
- package/.next-prod/server/app/review/[id]/page.js.nft.json +1 -1
- package/.next-prod/server/app/review/[id]/page_client-reference-manifest.js +1 -1
- package/.next-prod/server/app-paths-manifest.json +1 -1
- package/.next-prod/server/chunks/2939.js +1 -1
- package/.next-prod/server/chunks/5897.js +1 -1
- package/.next-prod/server/chunks/7270.js +45 -0
- package/.next-prod/server/chunks/7338.js +1 -1
- package/.next-prod/server/chunks/{4257.js → 8523.js} +3 -3
- package/.next-prod/server/chunks/8916.js +1 -1
- package/.next-prod/server/chunks/9658.js +6 -6
- package/.next-prod/server/middleware-build-manifest.js +1 -1
- package/.next-prod/server/middleware-react-loadable-manifest.js +1 -1
- package/.next-prod/server/pages/404.html +10 -2
- package/.next-prod/server/pages/500.html +1 -1
- package/.next-prod/server/server-reference-manifest.json +1 -1
- package/.next-prod/static/chunks/{217-e929c46c0bd599a7.js → 217-1bf54355c7c0c898.js} +4 -4
- package/.next-prod/static/chunks/3283-80d4d836a00d7bb4.js +25 -0
- package/.next-prod/static/chunks/6784-11888818d8e3114e.js +0 -0
- package/.next-prod/static/chunks/{9428-ffc16bc24c6e0da1.js → 6787-866a3046a4a0fa50.js} +3 -3
- package/.next-prod/static/chunks/9b0008ae.0167ac0019d88811.js +45 -0
- package/.next-prod/static/chunks/app/{layout-d202f8e0bcc90661.js → layout-3781fa5b8e04f272.js} +1 -1
- package/.next-prod/static/chunks/app/m/page-bf48756850ebe349.js +1 -0
- package/.next-prod/static/chunks/app/{page-814b50b5b440249d.js → page-e63d872a9af204ae.js} +1 -1
- package/.next-prod/static/chunks/app/project/{page-814b50b5b440249d.js → page-e63d872a9af204ae.js} +1 -1
- package/.next-prod/static/chunks/webpack-fb9d282647d95d0a.js +1 -0
- package/.next-prod/static/css/7ed891f30e531791.css +1 -0
- package/.next-prod/trace +13 -13
- package/.next-prod/trace-build +1 -1
- package/README.md +27 -1
- package/README.zh.md +27 -1
- package/bin/postinstall.mjs +8 -0
- package/dist/{chunk-3M3JSZ5R.mjs → chunk-IQXDXTI7.mjs} +7 -3
- package/dist/scheduledTasks.mjs +8 -14
- package/dist/wsServer.mjs +4 -1
- package/next.config.mjs +4 -2
- package/package.json +10 -6
- package/public/boot.js +8 -0
- package/public/pdfjs/pdf.worker.min.mjs +29 -0
- package/scripts/copy-pdfjs-worker.mjs +74 -0
- package/.next-prod/static/chunks/2576-11e41242c7dd98f1.js +0 -25
- package/.next-prod/static/chunks/6784-8a99d051f03bb100.js +0 -0
- package/.next-prod/static/chunks/app/m/page-71570a066f3eacf8.js +0 -1
- package/.next-prod/static/chunks/webpack-2c71a79c277961bc.js +0 -1
- package/.next-prod/static/css/fa7218e60ff464b5.css +0 -1
- /package/.next-prod/static/{JfwcuC5YjyhQWeQD-4Gvn → VRVzMV9rhbwaTwXonUCzN}/_buildManifest.js +0 -0
- /package/.next-prod/static/{JfwcuC5YjyhQWeQD-4Gvn → VRVzMV9rhbwaTwXonUCzN}/_ssgManifest.js +0 -0
package/.next-prod/trace-build
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
[{"name":"run-webpack","duration":
|
|
1
|
+
[{"name":"run-webpack","duration":13094140,"timestamp":92586508,"id":14,"parentId":1,"tags":{},"startTime":1783345301168,"traceId":"32a6e89d3c090b4d"},{"name":"run-typescript","duration":10343512,"timestamp":105683410,"id":1606,"parentId":1,"tags":{},"startTime":1783345314265,"traceId":"32a6e89d3c090b4d"},{"name":"static-check","duration":664290,"timestamp":116060179,"id":1609,"parentId":1,"tags":{},"startTime":1783345324641,"traceId":"32a6e89d3c090b4d"},{"name":"static-generation","duration":3406583,"timestamp":116960668,"id":1881,"parentId":1,"tags":{},"startTime":1783345325542,"traceId":"32a6e89d3c090b4d"},{"name":"collect-build-traces","duration":10918258,"timestamp":116724991,"id":1878,"parentId":1,"tags":{},"startTime":1783345325306,"traceId":"32a6e89d3c090b4d"},{"name":"telemetry-flush","duration":33,"timestamp":127645744,"id":1890,"parentId":1,"tags":{},"startTime":1783345336227,"traceId":"32a6e89d3c090b4d"},{"name":"next-build","duration":35156409,"timestamp":92489376,"id":1,"tags":{"buildMode":"default","version":"16.2.6","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1783345301071,"traceId":"32a6e89d3c090b4d"}]
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
> **OpenCockpit is the open-source Claude Code GUI** — and a single canvas for whatever agent you bring next. Run multi-project Claude sessions out of the box; pop open a tab for **Codex, DeepSeek, Kimi, or local Ollama** whenever you need. Built-in terminal, Chrome control, PostgreSQL / MySQL / Redis bubbles, code review, and slash modes — all local.
|
|
29
|
+
> **OpenCockpit is the open-source Claude Code GUI** — an IDE-like workbench for the whole dev loop, and a single canvas for whatever agent you bring next. Run multi-project Claude sessions out of the box; pop open a tab for **Codex, DeepSeek, Kimi, or local Ollama** whenever you need. Built-in terminal, Chrome control, PostgreSQL / MySQL / Redis bubbles, code review, and slash modes — all local. And it's web client–server: **self-host it on a shared dev box and every teammate gets a seat.**
|
|
30
30
|
|
|
31
31
|
https://github.com/user-attachments/assets/18f1a5dc-64f3-4ff6-b9fc-9cd08181fbb8
|
|
32
32
|
|
|
@@ -53,6 +53,31 @@ Cockpit is the instrument panel. It does **not** replace Claude Code; it stands
|
|
|
53
53
|
| No automation hooks | One-time / interval / cron-based **scheduled tasks** |
|
|
54
54
|
| "Cloud relay" trust concerns | **Fully local**. No telemetry. Keys (Codex / DeepSeek / Kimi) stay in `~/.cockpit/settings.json` on your laptop. |
|
|
55
55
|
|
|
56
|
+
### How it compares
|
|
57
|
+
|
|
58
|
+
An honest snapshot as of July 2026 — each tool wins somewhere. Spotted an error? PRs welcome.
|
|
59
|
+
|
|
60
|
+
| | **OpenCockpit** | Claude Code Desktop (official) | Opcode |
|
|
61
|
+
|---|---|---|---|
|
|
62
|
+
| Positioning | **IDE-like workbench for the whole dev loop** | agent session companion | session manager for Claude Code |
|
|
63
|
+
| Architecture | ✅ **web client–server** — deploy on a shared dev box; the whole team codes with AI in parallel, each on their own project / worktree | single-user desktop app | single-user desktop app |
|
|
64
|
+
| Open source | ✅ MIT | ❌ closed source | ✅ AGPL-3.0 |
|
|
65
|
+
| Engines | ✅ **Claude + Codex / DeepSeek / Kimi / Ollama** (BYOK) | Claude only | Claude only |
|
|
66
|
+
| Parallel multi-project sessions | ✅ | ✅ | ✅ |
|
|
67
|
+
| Agent-drivable browser & DB (Chrome / Postgres / MySQL / Redis) | ✅ Smart Bubbles | ❌ preview pane only | ❌ |
|
|
68
|
+
| LAN-shared code review pages | ✅ | ❌ | ❌ |
|
|
69
|
+
| Fully offline / air-gapped | ✅ via Ollama | ❌ | ❌ |
|
|
70
|
+
| Phone / tablet access | ✅ any LAN browser — code runs on **your** machine | ✅ via Claude Code on the web (cloud sandbox) | ❌ desktop only |
|
|
71
|
+
| Native desktop app | ❌ local web app (needs Node ≥ 20) | ✅ | ✅ Tauri |
|
|
72
|
+
| Newest Claude Code features on day one | ⏳ tracks Agent SDK releases with a lag | ✅ first party | ❌ |
|
|
73
|
+
| Automation triggers | one-time / interval / cron | ✅ Routines: cron + API + GitHub events | background agents |
|
|
74
|
+
| Session checkpoints / rewind | pinning & forking only | ✅ | ✅ checkpoint timeline |
|
|
75
|
+
| Usage / cost analytics | basic token counts | n/a (plan-based) | ✅ full dashboard |
|
|
76
|
+
| Cost | GUI free (MIT); AI billed by whichever engine you bring — can be $0 with local Ollama | app free; needs a paid Claude plan or API billing | GUI free (AGPL); needs a paid Claude plan or API billing |
|
|
77
|
+
| Actively maintained | ✅ | ✅ | ⚠️ last release Aug 2025 |
|
|
78
|
+
|
|
79
|
+
**Pick Claude Code Desktop** if you live inside the Anthropic ecosystem and want first-party polish. **Pick Opcode** if you want a native desktop feel with checkpoints and cost analytics. **Pick OpenCockpit** if you want more than a chat window — an open-source, IDE-like cockpit that covers the whole dev loop (read code, drive your browser and databases, review, automate) with any engine you bring, even from your phone.
|
|
80
|
+
|
|
56
81
|
## Features
|
|
57
82
|
|
|
58
83
|
### Engines — Claude by default, bring any agent you want
|
|
@@ -126,6 +151,7 @@ Cockpit is the instrument panel. It does **not** replace Claude Code; it stands
|
|
|
126
151
|
- **Solo dev, multi-repo:** "I have a refactor running in API, tests writing in Web, and a bug investigation in Pipeline — all at once, all visible."
|
|
127
152
|
- **Day one in an unfamiliar repo:** Open it in Code Map, click through caller/callee pins, walk the auth flow in five clicks instead of a 90-minute file-tree scavenger hunt.
|
|
128
153
|
- **Two-person team:** Senior reviews via LAN-shared review page, no GitHub PR round-trip needed for in-progress work.
|
|
154
|
+
- **Team on a shared dev box:** self-host one Cockpit where the code lives — every teammate opens a browser, takes a seat, and runs their own AI sessions in their own project / worktree.
|
|
129
155
|
- **Reviewing AI-generated PRs:** Switch the changed files into Code Map — changed functions are highlighted with their callers / callees still drawn around them, so blast-radius is one click away.
|
|
130
156
|
- **Full-stack chore mode:** `/fx` in one tab on a backend bug, `/ex` in another to plan the frontend refactor, `/go` to land it — three slash modes, three different agent postures.
|
|
131
157
|
- **Cheap second opinion:** Same prompt in two tabs — Claude in one, **DeepSeek v4-pro** in the other — compare answers before you trust either.
|
package/README.zh.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
> **OpenCockpit 是开源的 Claude Code GUI** ——
|
|
29
|
+
> **OpenCockpit 是开源的 Claude Code GUI** —— 贴合研发全流程的 IDE 式工作台,也是你想接入的任何 Agent 的统一画布。多项目 Claude 会话开箱即用;想用 **Codex、DeepSeek、Kimi 或本地 Ollama**?直接新开一个 tab。内置终端、Chrome 自动化、PostgreSQL / MySQL / Redis 气泡、代码评审与斜杠模式 —— 全部本地。Web client-server 架构:**自托管到共享开发机,全队一起飞。**
|
|
30
30
|
|
|
31
31
|
https://github.com/user-attachments/assets/18f1a5dc-64f3-4ff6-b9fc-9cd08181fbb8
|
|
32
32
|
|
|
@@ -53,6 +53,31 @@ Cockpit 就是那个仪表盘。它**不替代** Claude Code,而是站在官
|
|
|
53
53
|
| 没有自动化触发器 | 一次性 / 间隔 / **Cron** **定时任务** |
|
|
54
54
|
| 担心"云端中转" | **完全本地**。无遥测,Codex / DeepSeek / Kimi 的 Key 仅存在本机 `~/.cockpit/settings.json` |
|
|
55
55
|
|
|
56
|
+
### 横向对比
|
|
57
|
+
|
|
58
|
+
截至 2026 年 7 月的实事求是快照 —— 每个工具都有赢的地方。发现错误?欢迎 PR 指正。
|
|
59
|
+
|
|
60
|
+
| | **OpenCockpit** | Claude Code Desktop(官方) | Opcode |
|
|
61
|
+
|---|---|---|---|
|
|
62
|
+
| 定位 | **贴合研发全流程的 IDE 式工作台** | Agent 会话伴侣 | Claude Code 会话管理器 |
|
|
63
|
+
| 架构 | ✅ **Web client-server** —— 部署到共享开发机,全队一起飞,各占各的项目 / worktree | 单机桌面应用 | 单机桌面应用 |
|
|
64
|
+
| 开源 | ✅ MIT | ❌ 闭源 | ✅ AGPL-3.0 |
|
|
65
|
+
| 引擎 | ✅ **Claude + Codex / DeepSeek / Kimi / Ollama**(BYOK) | 仅 Claude | 仅 Claude |
|
|
66
|
+
| 多项目并行会话 | ✅ | ✅ | ✅ |
|
|
67
|
+
| Agent 可驱动浏览器 & 数据库(Chrome / Postgres / MySQL / Redis) | ✅ 智能气泡 | ❌ 仅只读预览面板 | ❌ |
|
|
68
|
+
| 局域网共享评审页 | ✅ | ❌ | ❌ |
|
|
69
|
+
| 全离线 / 内网隔离 | ✅ 走 Ollama | ❌ | ❌ |
|
|
70
|
+
| 手机 / 平板可用 | ✅ 局域网任意浏览器 —— 代码跑在**你自己的**机器上 | ✅ 走 Claude Code on the web(云端沙箱) | ❌ 仅桌面 |
|
|
71
|
+
| 原生桌面应用 | ❌ 本地 Web 应用(需 Node ≥ 20) | ✅ | ✅ Tauri |
|
|
72
|
+
| 第一时间跟进 Claude Code 新特性 | ⏳ 跟随 Agent SDK 发版,有滞后 | ✅ 官方第一方 | ❌ |
|
|
73
|
+
| 自动化触发 | 一次性 / 间隔 / cron | ✅ Routines:cron + API + GitHub 事件 | 后台 Agent |
|
|
74
|
+
| 会话检查点 / 回滚 | 仅固定 & 分叉 | ✅ | ✅ 检查点时间线 |
|
|
75
|
+
| 用量 / 成本分析 | 仅基础 token 计数 | 不适用(订阅制) | ✅ 完整仪表盘 |
|
|
76
|
+
| 费用 | GUI 免费(MIT);AI 按所带引擎计费 —— 本地 Ollama 可 ¥0 | 应用免费;需付费 Claude 订阅或 API 计费 | GUI 免费(AGPL);需付费 Claude 订阅或 API 计费 |
|
|
77
|
+
| 持续维护 | ✅ | ✅ | ⚠️ 最后发版 2025-08 |
|
|
78
|
+
|
|
79
|
+
**选官方 Desktop**:深度绑定 Anthropic 生态、要第一方打磨体验。**选 Opcode**:要原生桌面手感 + 检查点 + 成本分析。**选 OpenCockpit**:你要的不只是一个聊天窗 —— 而是开源、IDE 式、贴合研发全流程的驾驶舱(读码、驱动浏览器和数据库、评审、自动化),引擎随你带,手机也能开。
|
|
80
|
+
|
|
56
81
|
## 功能特性
|
|
57
82
|
|
|
58
83
|
### 引擎 —— 默认 Claude,也接得住你想要的任何 Agent
|
|
@@ -126,6 +151,7 @@ Cockpit 就是那个仪表盘。它**不替代** Claude Code,而是站在官
|
|
|
126
151
|
- **独立开发者多仓并行:** "API 在重构、Web 在写测试、Pipeline 在排 bug —— 同时跑、同时可见。"
|
|
127
152
|
- **新仓库的第一天:** 用 Code Map 打开它,沿着 caller / callee pin 一路点过去 —— 鉴权流程 5 次点击就走完,不再是 90 分钟的"文件树找地鼠"。
|
|
128
153
|
- **二人小团队:** Senior 用局域网共享评审页 review,半成品代码不用绕 GitHub PR。
|
|
154
|
+
- **共享开发机团队:** 在代码所在的机器上自托管一个 Cockpit —— 队友打开浏览器就能一起飞,各自在自己的项目 / worktree 上跑 AI 会话。
|
|
129
155
|
- **评审 AI 写的 PR:** 把改动文件切到 Code Map,**改动过的函数被高亮**,周围还画着它们的 caller / callee —— 一眼看清这次改动的爆炸半径。
|
|
130
156
|
- **全栈杂活模式:** 后端 bug 一个 tab 跑 `/fx`,另一个 tab 跑 `/ex` 规划前端重构,最后 `/go` 落地 —— 三种姿态、三种 Agent 模式。
|
|
131
157
|
- **便宜的二次意见:** 同一个 prompt 在两个 tab 跑 —— 一个 Claude、一个 **DeepSeek v4-pro**,对比答案后再决定相信谁。
|
package/bin/postinstall.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { copyTreeSitterWasms } from '../scripts/copy-tree-sitter-wasms.mjs';
|
|
9
|
+
import { copyPdfjsWorker } from '../scripts/copy-pdfjs-worker.mjs';
|
|
9
10
|
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const projectRoot = join(__dirname, '..');
|
|
@@ -20,6 +21,13 @@ try {
|
|
|
20
21
|
console.warn('[postinstall] tree-sitter WASM copy failed:', err?.message ?? err);
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
// pdf.js worker → public/pdfjs/. Same graceful contract as above.
|
|
25
|
+
try {
|
|
26
|
+
copyPdfjsWorker(projectRoot);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.warn('[postinstall] pdf.js worker copy failed:', err?.message ?? err);
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
if (process.platform !== 'win32') {
|
|
24
32
|
// node-pty: spawn-helper needs the executable bit
|
|
25
33
|
try {
|
|
@@ -539,7 +539,7 @@ function fanout(state, ev) {
|
|
|
539
539
|
}
|
|
540
540
|
}
|
|
541
541
|
}
|
|
542
|
-
function startRun(key, cwd, promptText) {
|
|
542
|
+
function startRun(key, cwd, promptText, runId) {
|
|
543
543
|
const prev = registry.get(key);
|
|
544
544
|
if (prev?.status === "running") return false;
|
|
545
545
|
const prevSeq = prev?.seq ?? seqByKey.get(key) ?? 0;
|
|
@@ -547,18 +547,22 @@ function startRun(key, cwd, promptText) {
|
|
|
547
547
|
if (prev.evictTimer) clearTimeout(prev.evictTimer);
|
|
548
548
|
for (const k of prev.keys) registry.delete(k);
|
|
549
549
|
}
|
|
550
|
+
const startedAt = Date.now();
|
|
550
551
|
registry.set(key, {
|
|
551
552
|
keys: /* @__PURE__ */ new Set([key]),
|
|
552
553
|
cwd,
|
|
553
554
|
status: "running",
|
|
554
555
|
seq: prevSeq,
|
|
555
556
|
events: [],
|
|
556
|
-
|
|
557
|
+
startedAt,
|
|
558
|
+
updatedAt: startedAt
|
|
557
559
|
});
|
|
558
560
|
if (promptText) {
|
|
559
561
|
appendRun(key, {
|
|
560
562
|
type: "user",
|
|
561
563
|
_human: true,
|
|
564
|
+
...runId ? { _turnId: runId } : {},
|
|
565
|
+
_ts: startedAt,
|
|
562
566
|
message: { role: "user", content: promptText }
|
|
563
567
|
});
|
|
564
568
|
}
|
|
@@ -604,7 +608,7 @@ function markRunIdle(key, status = "idle") {
|
|
|
604
608
|
function getRunSnapshot(key) {
|
|
605
609
|
const r = registry.get(key);
|
|
606
610
|
if (!r) return null;
|
|
607
|
-
return { status: r.status, seq: r.seq, events: r.events.slice() };
|
|
611
|
+
return { status: r.status, seq: r.seq, events: r.events.slice(), startedAt: r.startedAt };
|
|
608
612
|
}
|
|
609
613
|
function isRunActive(key) {
|
|
610
614
|
return registry.get(key)?.status === "running";
|
package/dist/scheduledTasks.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
setRunAbort,
|
|
11
11
|
startRun,
|
|
12
12
|
updateGlobalState
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-IQXDXTI7.mjs";
|
|
14
14
|
import {
|
|
15
15
|
AgentError,
|
|
16
16
|
AppRuntime,
|
|
@@ -1697,7 +1697,7 @@ async function dispatchChat(spec, body, request) {
|
|
|
1697
1697
|
let currentKey = sessionId || runId;
|
|
1698
1698
|
let actualSessionId = sessionId;
|
|
1699
1699
|
let isClosed = false;
|
|
1700
|
-
if (!startRun(currentKey, cwd || "", promptText)) {
|
|
1700
|
+
if (!startRun(currentKey, cwd || "", promptText, runId)) {
|
|
1701
1701
|
return { ok: false, status: 409, error: "run already active" };
|
|
1702
1702
|
}
|
|
1703
1703
|
setRunAbort(currentKey, () => {
|
|
@@ -2593,7 +2593,7 @@ import { streamText, stepCountIs } from "ai";
|
|
|
2593
2593
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2594
2594
|
|
|
2595
2595
|
// packages/feature/agent/src/server/engines/ollama/session.ts
|
|
2596
|
-
import { readFileSync as readFileSync3,
|
|
2596
|
+
import { readFileSync as readFileSync3, existsSync, mkdirSync as mkdirSync2, appendFileSync } from "fs";
|
|
2597
2597
|
import { join as join3 } from "path";
|
|
2598
2598
|
var SESSIONS_ROOT = join3(COCKPIT_DIR, "ollama-sessions");
|
|
2599
2599
|
function getSessionDir(cwd) {
|
|
@@ -2606,22 +2606,16 @@ function readSessionMessages(cwd, sessionId) {
|
|
|
2606
2606
|
const path2 = getSessionPath(cwd, sessionId);
|
|
2607
2607
|
if (!existsSync(path2)) return [];
|
|
2608
2608
|
const lines = readFileSync3(path2, "utf-8").split("\n").filter(Boolean);
|
|
2609
|
-
const legacyMessages = [];
|
|
2610
2609
|
const transcriptEntries = [];
|
|
2611
2610
|
for (const line of lines) {
|
|
2612
2611
|
try {
|
|
2613
2612
|
const obj = JSON.parse(line);
|
|
2614
|
-
if (typeof obj.role === "string" && "content" in obj) {
|
|
2615
|
-
legacyMessages.push(obj);
|
|
2616
|
-
continue;
|
|
2617
|
-
}
|
|
2618
2613
|
if (typeof obj.type === "string") {
|
|
2619
2614
|
transcriptEntries.push(obj);
|
|
2620
2615
|
}
|
|
2621
2616
|
} catch {
|
|
2622
2617
|
}
|
|
2623
2618
|
}
|
|
2624
|
-
if (transcriptEntries.length === 0) return legacyMessages;
|
|
2625
2619
|
const toolNameById = /* @__PURE__ */ new Map();
|
|
2626
2620
|
const toolCallIds = /* @__PURE__ */ new Set();
|
|
2627
2621
|
const toolResultIds = /* @__PURE__ */ new Set();
|
|
@@ -16516,7 +16510,7 @@ function date4(params) {
|
|
|
16516
16510
|
config(en_default());
|
|
16517
16511
|
|
|
16518
16512
|
// packages/feature/agent/src/server/engines/ollama/tools.ts
|
|
16519
|
-
import { readFileSync as readFileSync4, writeFileSync as
|
|
16513
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
16520
16514
|
import { writeFile, mkdir, readdir, stat, unlink } from "fs/promises";
|
|
16521
16515
|
import { isAbsolute, join as join4 } from "path";
|
|
16522
16516
|
import { tmpdir as tmpdir2 } from "os";
|
|
@@ -16638,7 +16632,7 @@ function createTools(context) {
|
|
|
16638
16632
|
execute: async ({ file_path, content }) => {
|
|
16639
16633
|
try {
|
|
16640
16634
|
if (!isAbsolute(file_path)) return "Error: file_path must be an absolute path.";
|
|
16641
|
-
|
|
16635
|
+
writeFileSync3(file_path, content, "utf-8");
|
|
16642
16636
|
return `File written: ${file_path}`;
|
|
16643
16637
|
} catch (err) {
|
|
16644
16638
|
return `Error writing file: ${err.message}`;
|
|
@@ -16669,7 +16663,7 @@ function createTools(context) {
|
|
|
16669
16663
|
return `Error: old_string not found in ${file_path}. The file may have changed.`;
|
|
16670
16664
|
}
|
|
16671
16665
|
const updated = replace_all ? content.split(old_string).join(new_string) : content.replace(old_string, new_string);
|
|
16672
|
-
|
|
16666
|
+
writeFileSync3(file_path, updated, "utf-8");
|
|
16673
16667
|
return `File edited: ${file_path}`;
|
|
16674
16668
|
} catch (err) {
|
|
16675
16669
|
return `Error editing file: ${err.message}`;
|
|
@@ -17252,7 +17246,7 @@ ${block.think}
|
|
|
17252
17246
|
import { spawn as spawn3 } from "child_process";
|
|
17253
17247
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
17254
17248
|
import { createInterface as createInterface2 } from "readline";
|
|
17255
|
-
import { writeFileSync as
|
|
17249
|
+
import { writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3, existsSync as existsSync2 } from "fs";
|
|
17256
17250
|
import { join as join6 } from "path";
|
|
17257
17251
|
import { tmpdir as tmpdir3 } from "os";
|
|
17258
17252
|
var MEDIA_EXT = {
|
|
@@ -17267,7 +17261,7 @@ function writeImagesToTemp(images) {
|
|
|
17267
17261
|
return images.map((img, i) => {
|
|
17268
17262
|
const ext = MEDIA_EXT[img.media_type] || ".png";
|
|
17269
17263
|
const filePath = join6(dir, `img-${Date.now()}-${i}${ext}`);
|
|
17270
|
-
|
|
17264
|
+
writeFileSync4(filePath, Buffer.from(img.data, "base64"));
|
|
17271
17265
|
return filePath;
|
|
17272
17266
|
});
|
|
17273
17267
|
}
|
package/dist/wsServer.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
addRunListener,
|
|
3
3
|
getGlobalSessionsSnapshot,
|
|
4
4
|
getRunSnapshot
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-IQXDXTI7.mjs";
|
|
6
6
|
import {
|
|
7
7
|
addExitListener,
|
|
8
8
|
addOutputListener,
|
|
@@ -1186,6 +1186,9 @@ var handleSessionStream = (conn, sessionId) => Effect_exports.gen(function* () {
|
|
|
1186
1186
|
type: "run-snapshot",
|
|
1187
1187
|
status: snap.status,
|
|
1188
1188
|
seq: snap.seq,
|
|
1189
|
+
// Turn time-boundary: lets clients cut the in-flight turn's disk image by
|
|
1190
|
+
// message timestamp instead of by prompt text (see useLiveStream).
|
|
1191
|
+
startedAt: snap.startedAt,
|
|
1189
1192
|
events: snap.events
|
|
1190
1193
|
});
|
|
1191
1194
|
} else {
|
package/next.config.mjs
CHANGED
|
@@ -12,8 +12,9 @@ const nextConfig = {
|
|
|
12
12
|
// dev 和 prod 使用不同输出目录,避免 Turbopack 热更新影响 prod
|
|
13
13
|
distDir: dev ? '.next' : '.next-prod',
|
|
14
14
|
// Allow loading dev resources (HMR, /_next/*) through tunnels — needed to test
|
|
15
|
-
// the mobile /m route on a real phone via ngrok
|
|
16
|
-
// rotating
|
|
15
|
+
// the mobile /m route on a real phone via ngrok / Cloudflare quick tunnels.
|
|
16
|
+
// Dev-only; wildcards cover the rotating tunnel hostnames so we don't have to
|
|
17
|
+
// edit this each session.
|
|
17
18
|
...(dev
|
|
18
19
|
? {
|
|
19
20
|
allowedDevOrigins: [
|
|
@@ -21,6 +22,7 @@ const nextConfig = {
|
|
|
21
22
|
'*.ngrok-free.app',
|
|
22
23
|
'*.ngrok.app',
|
|
23
24
|
'*.ngrok.io',
|
|
25
|
+
'*.trycloudflare.com',
|
|
24
26
|
],
|
|
25
27
|
}
|
|
26
28
|
: {}),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surething/cockpit",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Claude Code GUI for parallel AI coding
|
|
3
|
+
"version": "1.0.236",
|
|
4
|
+
"description": "The open-source Claude Code GUI — an IDE-like workbench for parallel AI coding. Drive Claude (default), Codex, DeepSeek, Kimi, or local Ollama in tabs. Terminal, browser & DB bubbles, code review. Self-host on a shared dev box — a seat for every teammate. Local-first, MIT.",
|
|
5
5
|
"author": "Robert",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -38,9 +38,10 @@
|
|
|
38
38
|
"ai-code-review",
|
|
39
39
|
"agent-sdk",
|
|
40
40
|
"coding-agent",
|
|
41
|
-
"
|
|
41
|
+
"self-hosted",
|
|
42
|
+
"web-ide",
|
|
43
|
+
"remote-development",
|
|
42
44
|
"multi-project-ai",
|
|
43
|
-
"cockpit",
|
|
44
45
|
"opencockpit",
|
|
45
46
|
"cursor-alternative",
|
|
46
47
|
"aider-alternative",
|
|
@@ -73,14 +74,15 @@
|
|
|
73
74
|
"kernels",
|
|
74
75
|
"next.config.mjs",
|
|
75
76
|
"scripts/copy-tree-sitter-wasms.mjs",
|
|
77
|
+
"scripts/copy-pdfjs-worker.mjs",
|
|
76
78
|
"LICENSE",
|
|
77
79
|
"README.md"
|
|
78
80
|
],
|
|
79
81
|
"scripts": {
|
|
80
|
-
"predev": "node scripts/copy-tree-sitter-wasms.mjs",
|
|
82
|
+
"predev": "node scripts/copy-tree-sitter-wasms.mjs && node scripts/copy-pdfjs-worker.mjs",
|
|
81
83
|
"dev": "COCKPIT_ENV=dev node --import tsx server.mjs",
|
|
82
84
|
"prepublishOnly": "npm run build && npm run build:server && rm -rf .next-prod/cache",
|
|
83
|
-
"prebuild": "node scripts/copy-tree-sitter-wasms.mjs",
|
|
85
|
+
"prebuild": "node scripts/copy-tree-sitter-wasms.mjs && node scripts/copy-pdfjs-worker.mjs",
|
|
84
86
|
"build": "next build --webpack",
|
|
85
87
|
"build:server": "tsup --config tsup.server.ts",
|
|
86
88
|
"setup": "npm run build && npm run build:server && npm link",
|
|
@@ -90,6 +92,7 @@
|
|
|
90
92
|
"test:watch": "vitest",
|
|
91
93
|
"generate-icons": "node scripts/generate-icons.js",
|
|
92
94
|
"copy-wasms": "node scripts/copy-tree-sitter-wasms.mjs",
|
|
95
|
+
"copy-pdfjs-worker": "node scripts/copy-pdfjs-worker.mjs",
|
|
93
96
|
"postinstall": "node bin/postinstall.mjs"
|
|
94
97
|
},
|
|
95
98
|
"dependencies": {
|
|
@@ -128,6 +131,7 @@
|
|
|
128
131
|
"neo4j-driver": "^6.0.1",
|
|
129
132
|
"next": "^16.2.6",
|
|
130
133
|
"node-pty": "^1.1.0",
|
|
134
|
+
"pdfjs-dist": "^6.1.200",
|
|
131
135
|
"pg": "^8.20.0",
|
|
132
136
|
"react": "19.2.3",
|
|
133
137
|
"react-dom": "19.2.3",
|
package/public/boot.js
CHANGED
|
@@ -14,7 +14,15 @@
|
|
|
14
14
|
var onMobileRoute = path === '/m' || path.indexOf('/m/') === 0;
|
|
15
15
|
var forceDesktop = false;
|
|
16
16
|
try { forceDesktop = !!localStorage.getItem('cockpit-force-desktop'); } catch (_e) {}
|
|
17
|
+
// Never redirect inside an iframe: the desktop shell embeds /project in a
|
|
18
|
+
// frame that is 42px (sidebar) narrower than the window, so a tablet-width
|
|
19
|
+
// top page (e.g. 768px iPad) would otherwise nest the mobile UI inside the
|
|
20
|
+
// desktop shell — and drop the sessionId in the process. The media query is
|
|
21
|
+
// only meaningful for the top-level viewport.
|
|
22
|
+
var isTopWindow = true;
|
|
23
|
+
try { isTopWindow = window.self === window.top; } catch (_e) {}
|
|
17
24
|
if (
|
|
25
|
+
isTopWindow &&
|
|
18
26
|
!onMobileRoute &&
|
|
19
27
|
!forceDesktop &&
|
|
20
28
|
window.matchMedia &&
|