@thevinci/web 1.0.0
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/README.md +197 -0
- package/bin/cli-entry.js +55 -0
- package/bin/cli-output.js +145 -0
- package/bin/cli.js +4887 -0
- package/bin/cli.test.js +64 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +528 -0
- package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
- package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
- package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
- package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
- package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
- package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
- package/dist/assets/es-4o9ciP61.js +15 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-DLTDToSP.css +1 -0
- package/dist/assets/index-DgiFEKGN.js +1 -0
- package/dist/assets/ko-B20imCHE.js +15 -0
- package/dist/assets/main-BV3KOtdA.css +1 -0
- package/dist/assets/main-CDKJj0sH.js +226 -0
- package/dist/assets/main-LC-PSNVM.js +2 -0
- package/dist/assets/miniChat-CQUiG_cr.js +2 -0
- package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
- package/dist/assets/pl-DQJ7LSzj.js +15 -0
- package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
- package/dist/assets/uk-BNFxOlO4.js +15 -0
- package/dist/assets/vendor--DBfsbEis.css +1 -0
- package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
- package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
- package/dist/assets/worker-CbT6TVo7.js +155 -0
- package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +528 -0
- package/dist/index.html +607 -0
- package/dist/logo-dark-192x192.png +0 -0
- package/dist/logo-dark-512x512.png +0 -0
- package/dist/logo-dark-512x512.svg +528 -0
- package/dist/logo-light-192x192.png +0 -0
- package/dist/logo-light-512x512.png +0 -0
- package/dist/logo-light-512x512.svg +528 -0
- package/dist/mini-chat.html +16 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/pwa-maskable-192.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/site.webmanifest +21 -0
- package/dist/sw.js +1 -0
- package/package.json +118 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +528 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +528 -0
- package/public/logo-dark-192x192.png +0 -0
- package/public/logo-dark-512x512.png +0 -0
- package/public/logo-dark-512x512.svg +528 -0
- package/public/logo-light-192x192.png +0 -0
- package/public/logo-light-512x512.png +0 -0
- package/public/logo-light-512x512.svg +528 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/pwa-maskable-192.png +0 -0
- package/public/pwa-maskable-512.png +0 -0
- package/public/site.webmanifest +21 -0
- package/server/TERMINAL_WS_PROTOCOL.md +48 -0
- package/server/index.d.ts +39 -0
- package/server/index.js +1311 -0
- package/server/lib/cloudflare-tunnel.js +650 -0
- package/server/lib/event-stream/DOCUMENTATION.md +61 -0
- package/server/lib/event-stream/directory-ws-bridge.js +185 -0
- package/server/lib/event-stream/global-hub.js +158 -0
- package/server/lib/event-stream/global-hub.test.js +140 -0
- package/server/lib/event-stream/global-ws-bridge.js +206 -0
- package/server/lib/event-stream/index.js +25 -0
- package/server/lib/event-stream/protocol.js +131 -0
- package/server/lib/event-stream/protocol.test.js +182 -0
- package/server/lib/event-stream/runtime.js +180 -0
- package/server/lib/event-stream/runtime.test.js +512 -0
- package/server/lib/event-stream/upstream-reader.js +226 -0
- package/server/lib/event-stream/upstream-reader.test.js +276 -0
- package/server/lib/fs/DOCUMENTATION.md +36 -0
- package/server/lib/fs/routes.js +1040 -0
- package/server/lib/fs/search.js +238 -0
- package/server/lib/git/DOCUMENTATION.md +152 -0
- package/server/lib/git/credentials.js +74 -0
- package/server/lib/git/identity-storage.js +112 -0
- package/server/lib/git/index.js +6 -0
- package/server/lib/git/routes.js +972 -0
- package/server/lib/git/service.js +3432 -0
- package/server/lib/git/service.test.js +39 -0
- package/server/lib/github/DOCUMENTATION.md +171 -0
- package/server/lib/github/auth.js +307 -0
- package/server/lib/github/device-flow.js +50 -0
- package/server/lib/github/index.js +24 -0
- package/server/lib/github/octokit.js +10 -0
- package/server/lib/github/pr-status.js +519 -0
- package/server/lib/github/repo/fork-detection.js +102 -0
- package/server/lib/github/repo/index.js +55 -0
- package/server/lib/github/routes.js +1560 -0
- package/server/lib/magic-prompts/routes.js +63 -0
- package/server/lib/magic-prompts/runtime.js +119 -0
- package/server/lib/notifications/DOCUMENTATION.md +122 -0
- package/server/lib/notifications/emitter-runtime.js +102 -0
- package/server/lib/notifications/index.js +4 -0
- package/server/lib/notifications/message.js +52 -0
- package/server/lib/notifications/message.test.js +34 -0
- package/server/lib/notifications/push-runtime.js +304 -0
- package/server/lib/notifications/routes.js +315 -0
- package/server/lib/notifications/runtime.js +566 -0
- package/server/lib/notifications/template-runtime.js +349 -0
- package/server/lib/notifications/template-runtime.test.js +26 -0
- package/server/lib/opencode/DOCUMENTATION.md +362 -0
- package/server/lib/opencode/agents.js +634 -0
- package/server/lib/opencode/auth-state-runtime.js +88 -0
- package/server/lib/opencode/auth.js +83 -0
- package/server/lib/opencode/bootstrap-runtime.js +131 -0
- package/server/lib/opencode/cli-entry-runtime.js +43 -0
- package/server/lib/opencode/cli-options.js +128 -0
- package/server/lib/opencode/commands.js +339 -0
- package/server/lib/opencode/config-entity-routes.js +370 -0
- package/server/lib/opencode/core-routes.js +500 -0
- package/server/lib/opencode/core-routes.test.js +26 -0
- package/server/lib/opencode/env-config.js +74 -0
- package/server/lib/opencode/env-keys.js +68 -0
- package/server/lib/opencode/env-runtime.js +1162 -0
- package/server/lib/opencode/env-runtime.test.js +116 -0
- package/server/lib/opencode/feature-routes-runtime.js +244 -0
- package/server/lib/opencode/hmr-state-runtime.js +85 -0
- package/server/lib/opencode/index.js +66 -0
- package/server/lib/opencode/lifecycle.js +1019 -0
- package/server/lib/opencode/lifecycle.test.js +240 -0
- package/server/lib/opencode/mcp.js +278 -0
- package/server/lib/opencode/network-runtime.js +104 -0
- package/server/lib/opencode/network-runtime.test.js +37 -0
- package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
- package/server/lib/opencode/path-utils.js +100 -0
- package/server/lib/opencode/path-utils.test.js +71 -0
- package/server/lib/opencode/project-directory-runtime.js +124 -0
- package/server/lib/opencode/project-icon-routes.js +399 -0
- package/server/lib/opencode/project-icon-routes.test.js +107 -0
- package/server/lib/opencode/providers.js +96 -0
- package/server/lib/opencode/proxy.js +445 -0
- package/server/lib/opencode/pwa-manifest-routes.js +257 -0
- package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
- package/server/lib/opencode/routes.js +541 -0
- package/server/lib/opencode/server-startup-runtime.js +156 -0
- package/server/lib/opencode/server-utils-runtime.js +168 -0
- package/server/lib/opencode/server-utils-runtime.test.js +135 -0
- package/server/lib/opencode/session-runtime.js +356 -0
- package/server/lib/opencode/session-runtime.test.js +151 -0
- package/server/lib/opencode/settings-helpers.js +770 -0
- package/server/lib/opencode/settings-helpers.test.js +109 -0
- package/server/lib/opencode/settings-normalization-runtime.js +428 -0
- package/server/lib/opencode/settings-runtime.js +826 -0
- package/server/lib/opencode/settings-runtime.test.js +85 -0
- package/server/lib/opencode/shared.js +615 -0
- package/server/lib/opencode/shutdown-runtime.js +139 -0
- package/server/lib/opencode/shutdown-runtime.test.js +58 -0
- package/server/lib/opencode/skill-routes.js +701 -0
- package/server/lib/opencode/skills.js +548 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
- package/server/lib/opencode/static-routes-runtime.js +65 -0
- package/server/lib/opencode/theme-runtime.js +167 -0
- package/server/lib/opencode/tunnel-auth.js +591 -0
- package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
- package/server/lib/opencode/vinci-routes.js +76 -0
- package/server/lib/opencode/watcher.js +115 -0
- package/server/lib/opencode/watcher.test.js +239 -0
- package/server/lib/preview/proxy-runtime.js +1333 -0
- package/server/lib/preview/proxy-runtime.test.js +144 -0
- package/server/lib/projects/project-config.js +567 -0
- package/server/lib/projects/project-config.test.js +175 -0
- package/server/lib/projects/project-id.js +13 -0
- package/server/lib/quota/DOCUMENTATION.md +58 -0
- package/server/lib/quota/index.js +25 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +113 -0
- package/server/lib/quota/providers/copilot.js +165 -0
- package/server/lib/quota/providers/google/api.js +92 -0
- package/server/lib/quota/providers/google/auth.js +108 -0
- package/server/lib/quota/providers/google/index.js +124 -0
- package/server/lib/quota/providers/google/transforms.js +109 -0
- package/server/lib/quota/providers/index.js +168 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
- package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/ollama-cloud.js +112 -0
- package/server/lib/quota/providers/openai.js +91 -0
- package/server/lib/quota/providers/openrouter.js +92 -0
- package/server/lib/quota/providers/zai.js +91 -0
- package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
- package/server/lib/quota/providers/zhipuai.js +114 -0
- package/server/lib/quota/routes.js +27 -0
- package/server/lib/quota/utils/auth.js +50 -0
- package/server/lib/quota/utils/formatters.js +85 -0
- package/server/lib/quota/utils/formatters.test.js +54 -0
- package/server/lib/quota/utils/index.js +10 -0
- package/server/lib/quota/utils/transformers.js +55 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
- package/server/lib/scheduled-tasks/routes.js +235 -0
- package/server/lib/scheduled-tasks/runtime.js +773 -0
- package/server/lib/scheduled-tasks/runtime.test.js +100 -0
- package/server/lib/security/request-security.js +115 -0
- package/server/lib/session-folders/routes.js +63 -0
- package/server/lib/session-folders/routes.test.js +102 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
- package/server/lib/skills-catalog/cache.js +29 -0
- package/server/lib/skills-catalog/clawdhub/api.js +158 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +238 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
- package/server/lib/skills-catalog/curated-sources.js +21 -0
- package/server/lib/skills-catalog/git.js +77 -0
- package/server/lib/skills-catalog/index.js +42 -0
- package/server/lib/skills-catalog/install.js +294 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +87 -0
- package/server/lib/terminal/DOCUMENTATION.md +76 -0
- package/server/lib/terminal/index.js +31 -0
- package/server/lib/terminal/output-replay-buffer.js +78 -0
- package/server/lib/terminal/output-replay-buffer.test.js +75 -0
- package/server/lib/terminal/runtime.js +850 -0
- package/server/lib/terminal/runtime.test.js +96 -0
- package/server/lib/terminal/terminal-ws-protocol.js +68 -0
- package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
- package/server/lib/text/DOCUMENTATION.md +35 -0
- package/server/lib/text/summarization.js +138 -0
- package/server/lib/text/summarization.test.js +34 -0
- package/server/lib/tts/DOCUMENTATION.md +146 -0
- package/server/lib/tts/base-url.js +62 -0
- package/server/lib/tts/capability-runtime.js +31 -0
- package/server/lib/tts/index.js +19 -0
- package/server/lib/tts/routes.js +261 -0
- package/server/lib/tts/routes.test.js +53 -0
- package/server/lib/tts/service.js +178 -0
- package/server/lib/tts/stt.js +75 -0
- package/server/lib/tunnels/DOCUMENTATION.md +18 -0
- package/server/lib/tunnels/index.js +166 -0
- package/server/lib/tunnels/managed-config.js +201 -0
- package/server/lib/tunnels/providers/cloudflare.js +260 -0
- package/server/lib/tunnels/registry.js +51 -0
- package/server/lib/tunnels/routes.js +605 -0
- package/server/lib/tunnels/types.js +219 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
- package/server/lib/ui-auth/ui-auth.js +673 -0
- package/server/lib/ui-auth/ui-passkeys.js +545 -0
- package/server/opencode-proxy.test.js +151 -0
- package/server/proxy-headers.js +61 -0
- package/server/proxy-headers.test.js +58 -0
- package/server/sse-routes.test.js +152 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
applyForwardProxyResponseHeaders,
|
|
5
|
+
collectForwardProxyHeaders,
|
|
6
|
+
shouldForwardProxyResponseHeader,
|
|
7
|
+
} from '../../proxy-headers.js';
|
|
8
|
+
|
|
9
|
+
export const waitForSseDrain = (res, signal) => new Promise((resolve) => {
|
|
10
|
+
if (signal?.aborted || res.writableEnded || res.destroyed) {
|
|
11
|
+
resolve();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const cleanup = () => {
|
|
16
|
+
res.off?.('drain', onDone);
|
|
17
|
+
res.off?.('close', onDone);
|
|
18
|
+
res.off?.('error', onDone);
|
|
19
|
+
signal?.removeEventListener?.('abort', onDone);
|
|
20
|
+
};
|
|
21
|
+
const onDone = () => {
|
|
22
|
+
cleanup();
|
|
23
|
+
resolve();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
res.once?.('drain', onDone);
|
|
27
|
+
res.once?.('close', onDone);
|
|
28
|
+
res.once?.('error', onDone);
|
|
29
|
+
signal?.addEventListener?.('abort', onDone, { once: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const writeSseChunkWithBackpressure = async (res, value, signal) => {
|
|
33
|
+
if (!value || value.length === 0 || signal?.aborted || res.writableEnded || res.destroyed) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const flushed = res.write(value);
|
|
38
|
+
if (flushed !== false) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await waitForSseDrain(res, signal);
|
|
43
|
+
return !signal?.aborted && !res.writableEnded && !res.destroyed;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const createSseBoundaryTracker = () => {
|
|
47
|
+
const decoder = new TextDecoder();
|
|
48
|
+
let tail = '';
|
|
49
|
+
|
|
50
|
+
const normalize = (value) => value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
observe(value) {
|
|
54
|
+
const text = typeof value === 'string'
|
|
55
|
+
? value
|
|
56
|
+
: decoder.decode(value, { stream: true });
|
|
57
|
+
if (text.length > 0) {
|
|
58
|
+
tail = `${tail}${normalize(text)}`;
|
|
59
|
+
if (tail.length > 4096) {
|
|
60
|
+
tail = tail.slice(-4096);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return this.isAtBoundary();
|
|
64
|
+
},
|
|
65
|
+
isAtBoundary() {
|
|
66
|
+
return tail.length === 0 || tail.endsWith('\n\n');
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const registerOpenCodeProxy = (app, deps) => {
|
|
72
|
+
const {
|
|
73
|
+
fs,
|
|
74
|
+
os,
|
|
75
|
+
path,
|
|
76
|
+
OPEN_CODE_READY_GRACE_MS,
|
|
77
|
+
getRuntime,
|
|
78
|
+
getOpenCodeAuthHeaders,
|
|
79
|
+
buildOpenCodeUrl,
|
|
80
|
+
ensureOpenCodeApiPrefix,
|
|
81
|
+
} = deps;
|
|
82
|
+
|
|
83
|
+
if (app.get('opencodeProxyConfigured')) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const runtime = getRuntime();
|
|
88
|
+
if (runtime.openCodePort) {
|
|
89
|
+
console.log(`[proxy] Setting up proxy to OpenCode on port ${runtime.openCodePort}`);
|
|
90
|
+
} else {
|
|
91
|
+
console.log('[proxy] Setting up OpenCode API gate (OpenCode not started yet)');
|
|
92
|
+
}
|
|
93
|
+
console.log('[proxy] Runtime state:', JSON.stringify({
|
|
94
|
+
openCodePort: runtime.openCodePort,
|
|
95
|
+
openCodeBaseUrl: runtime.openCodeBaseUrl,
|
|
96
|
+
isOpenCodeReady: runtime.isOpenCodeReady,
|
|
97
|
+
}));
|
|
98
|
+
app.set('opencodeProxyConfigured', true);
|
|
99
|
+
|
|
100
|
+
const isAbortError = (error) => error?.name === 'AbortError';
|
|
101
|
+
const FALLBACK_PROXY_TARGET = 'http://127.0.0.1:3902';
|
|
102
|
+
|
|
103
|
+
const normalizeProxyTarget = (candidate) => {
|
|
104
|
+
if (typeof candidate !== 'string') {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const trimmed = candidate.trim();
|
|
109
|
+
if (!trimmed) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return trimmed.replace(/\/+$/, '');
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Keep generic proxy requests on the same upstream base URL that health checks
|
|
117
|
+
// and direct fetch helpers use. This avoids split-brain state where /health
|
|
118
|
+
// succeeds against an external host but /api/* still proxies to 127.0.0.1.
|
|
119
|
+
const resolveProxyTarget = () => {
|
|
120
|
+
try {
|
|
121
|
+
const resolved = normalizeProxyTarget(buildOpenCodeUrl('/', ''));
|
|
122
|
+
if (resolved) {
|
|
123
|
+
return resolved;
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const runtimeState = getRuntime();
|
|
129
|
+
const externalBase = normalizeProxyTarget(runtimeState.openCodeBaseUrl);
|
|
130
|
+
if (externalBase) {
|
|
131
|
+
return externalBase;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (runtimeState.openCodePort) {
|
|
135
|
+
return `http://localhost:${runtimeState.openCodePort}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return FALLBACK_PROXY_TARGET;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Log proxy target resolution for debugging
|
|
142
|
+
const resolveProxyTargetLogged = () => {
|
|
143
|
+
const target = resolveProxyTarget();
|
|
144
|
+
console.log('[proxy] resolveProxyTarget ->', target);
|
|
145
|
+
return target;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const forwardSseRequest = async (req, res) => {
|
|
149
|
+
const abortController = new AbortController();
|
|
150
|
+
const closeUpstream = () => abortController.abort();
|
|
151
|
+
let upstream = null;
|
|
152
|
+
let reader = null;
|
|
153
|
+
let heartbeatTimer = null;
|
|
154
|
+
let writeQueue = Promise.resolve(true);
|
|
155
|
+
const sseBoundary = createSseBoundaryTracker();
|
|
156
|
+
|
|
157
|
+
req.on('close', closeUpstream);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const requestUrl = typeof req.originalUrl === 'string' && req.originalUrl.length > 0
|
|
161
|
+
? req.originalUrl
|
|
162
|
+
: (typeof req.url === 'string' ? req.url : '');
|
|
163
|
+
const upstreamPath = requestUrl.startsWith('/api') ? requestUrl.slice(4) || '/' : requestUrl;
|
|
164
|
+
const headers = collectForwardProxyHeaders(req.headers, getOpenCodeAuthHeaders());
|
|
165
|
+
headers.accept ??= 'text/event-stream';
|
|
166
|
+
headers['cache-control'] ??= 'no-cache';
|
|
167
|
+
|
|
168
|
+
const targetUrl = buildOpenCodeUrl(upstreamPath, '');
|
|
169
|
+
console.log(`[proxy] SSE ${req.method} ${requestUrl} -> ${targetUrl}`);
|
|
170
|
+
|
|
171
|
+
upstream = await fetch(targetUrl, {
|
|
172
|
+
method: 'GET',
|
|
173
|
+
headers,
|
|
174
|
+
signal: abortController.signal,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
console.log(`[proxy] SSE upstream status: ${upstream.status}, content-type: ${upstream.headers.get('content-type')}`);
|
|
178
|
+
|
|
179
|
+
res.status(upstream.status);
|
|
180
|
+
applyForwardProxyResponseHeaders(upstream.headers, res);
|
|
181
|
+
|
|
182
|
+
const contentType = upstream.headers.get('content-type') || 'text/event-stream';
|
|
183
|
+
const isEventStream = contentType.toLowerCase().includes('text/event-stream');
|
|
184
|
+
|
|
185
|
+
if (!upstream.body) {
|
|
186
|
+
res.end(await upstream.text().catch(() => ''));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!isEventStream) {
|
|
191
|
+
res.end(await upstream.text());
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
res.setHeader('Content-Type', contentType);
|
|
196
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
197
|
+
res.setHeader('Connection', 'keep-alive');
|
|
198
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
199
|
+
if (typeof res.flushHeaders === 'function') {
|
|
200
|
+
res.flushHeaders();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Disable TCP Nagle's algorithm so small SSE chunks are sent immediately
|
|
204
|
+
// instead of being buffered up to ~200ms by the TCP stack.
|
|
205
|
+
if (res.socket && typeof res.socket.setNoDelay === 'function') {
|
|
206
|
+
res.socket.setNoDelay(true);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const SSE_HEARTBEAT_INTERVAL_MS = 20_000;
|
|
210
|
+
|
|
211
|
+
const scheduleHeartbeat = () => {
|
|
212
|
+
heartbeatTimer = setTimeout(async () => {
|
|
213
|
+
if (abortController.signal.aborted || res.writableEnded || res.destroyed) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (!sseBoundary.isAtBoundary()) {
|
|
217
|
+
scheduleHeartbeat();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const canContinue = await enqueueSseWrite(':heartbeat\n\n');
|
|
221
|
+
if (canContinue) {
|
|
222
|
+
scheduleHeartbeat();
|
|
223
|
+
}
|
|
224
|
+
}, SSE_HEARTBEAT_INTERVAL_MS);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const enqueueSseWrite = (value) => {
|
|
228
|
+
writeQueue = writeQueue
|
|
229
|
+
.catch(() => false)
|
|
230
|
+
.then((canContinue) => {
|
|
231
|
+
if (!canContinue) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return writeSseChunkWithBackpressure(res, value, abortController.signal);
|
|
235
|
+
});
|
|
236
|
+
return writeQueue;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
scheduleHeartbeat();
|
|
240
|
+
|
|
241
|
+
reader = upstream.body.getReader();
|
|
242
|
+
while (!abortController.signal.aborted) {
|
|
243
|
+
const { done, value } = await reader.read();
|
|
244
|
+
if (done) {
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
if (value && value.length > 0) {
|
|
248
|
+
sseBoundary.observe(value);
|
|
249
|
+
const canContinue = await enqueueSseWrite(value);
|
|
250
|
+
if (!canContinue) {
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
res.end();
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (isAbortError(error)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
console.error('[proxy] OpenCode SSE proxy error:', error?.message ?? error);
|
|
262
|
+
if (!res.headersSent) {
|
|
263
|
+
res.status(503).json({ error: 'OpenCode service unavailable' });
|
|
264
|
+
} else {
|
|
265
|
+
res.end();
|
|
266
|
+
}
|
|
267
|
+
} finally {
|
|
268
|
+
if (heartbeatTimer) {
|
|
269
|
+
clearTimeout(heartbeatTimer);
|
|
270
|
+
heartbeatTimer = null;
|
|
271
|
+
}
|
|
272
|
+
req.off('close', closeUpstream);
|
|
273
|
+
try {
|
|
274
|
+
if (reader) {
|
|
275
|
+
await reader.cancel();
|
|
276
|
+
reader.releaseLock();
|
|
277
|
+
} else if (upstream?.body && !upstream.body.locked) {
|
|
278
|
+
await upstream.body.cancel();
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Ensure API prefix is detected before proxying
|
|
286
|
+
app.use('/api', (_req, _res, next) => {
|
|
287
|
+
ensureOpenCodeApiPrefix();
|
|
288
|
+
next();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Readiness gate — return 503 while OpenCode is starting/restarting
|
|
292
|
+
app.use('/api', (req, res, next) => {
|
|
293
|
+
if (
|
|
294
|
+
req.path.startsWith('/themes/custom') ||
|
|
295
|
+
req.path.startsWith('/push') ||
|
|
296
|
+
req.path.startsWith('/config/agents') ||
|
|
297
|
+
req.path.startsWith('/config/opencode-resolution') ||
|
|
298
|
+
req.path.startsWith('/config/settings') ||
|
|
299
|
+
req.path.startsWith('/config/skills') ||
|
|
300
|
+
req.path === '/config/reload' ||
|
|
301
|
+
req.path === '/health'
|
|
302
|
+
) {
|
|
303
|
+
return next();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const runtimeState = getRuntime();
|
|
307
|
+
const waitElapsed = runtimeState.openCodeNotReadySince === 0 ? 0 : Date.now() - runtimeState.openCodeNotReadySince;
|
|
308
|
+
const stillWaiting =
|
|
309
|
+
(!runtimeState.isOpenCodeReady && (runtimeState.openCodeNotReadySince === 0 || waitElapsed < OPEN_CODE_READY_GRACE_MS)) ||
|
|
310
|
+
runtimeState.isRestartingOpenCode ||
|
|
311
|
+
!runtimeState.openCodePort;
|
|
312
|
+
|
|
313
|
+
if (stillWaiting) {
|
|
314
|
+
console.log(`[proxy] Readiness gate BLOCKED ${req.method} ${req.originalUrl} — OpenCode not ready (port=${runtimeState.openCodePort}, ready=${runtimeState.isOpenCodeReady}, restarting=${runtimeState.isRestartingOpenCode})`);
|
|
315
|
+
return res.status(503).json({
|
|
316
|
+
error: 'OpenCode is restarting',
|
|
317
|
+
restarting: true,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
next();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Windows: session merge for cross-directory session listing
|
|
325
|
+
if (process.platform === 'win32') {
|
|
326
|
+
app.get('/api/session', async (req, res, next) => {
|
|
327
|
+
const rawUrl = req.originalUrl || req.url || '';
|
|
328
|
+
if (rawUrl.includes('directory=')) return next();
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const authHeaders = getOpenCodeAuthHeaders();
|
|
332
|
+
const fetchOpts = {
|
|
333
|
+
method: 'GET',
|
|
334
|
+
headers: { Accept: 'application/json', ...authHeaders },
|
|
335
|
+
signal: AbortSignal.timeout(10000),
|
|
336
|
+
};
|
|
337
|
+
const globalRes = await fetch(buildOpenCodeUrl('/session', ''), fetchOpts);
|
|
338
|
+
const globalPayload = globalRes.ok ? await globalRes.json().catch(() => []) : [];
|
|
339
|
+
const globalSessions = Array.isArray(globalPayload) ? globalPayload : [];
|
|
340
|
+
|
|
341
|
+
const settingsDir = process.env.VINCI_DATA_DIR
|
|
342
|
+
? path.resolve(process.env.VINCI_DATA_DIR)
|
|
343
|
+
: path.join(os.homedir(), '.vinci');
|
|
344
|
+
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
345
|
+
let projectDirs = [];
|
|
346
|
+
try {
|
|
347
|
+
const settingsRaw = fs.readFileSync(settingsPath, 'utf8');
|
|
348
|
+
const settings = JSON.parse(settingsRaw);
|
|
349
|
+
projectDirs = (settings.projects || [])
|
|
350
|
+
.map((project) => (typeof project?.path === 'string' ? project.path.trim() : ''))
|
|
351
|
+
.filter(Boolean);
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const seen = new Set(
|
|
356
|
+
globalSessions
|
|
357
|
+
.map((session) => (session && typeof session.id === 'string' ? session.id : null))
|
|
358
|
+
.filter((id) => typeof id === 'string')
|
|
359
|
+
);
|
|
360
|
+
const extraSessions = [];
|
|
361
|
+
for (const dir of projectDirs) {
|
|
362
|
+
const candidates = Array.from(new Set([
|
|
363
|
+
dir,
|
|
364
|
+
dir.replace(/\\/g, '/'),
|
|
365
|
+
dir.replace(/\//g, '\\'),
|
|
366
|
+
]));
|
|
367
|
+
for (const candidateDir of candidates) {
|
|
368
|
+
const encoded = encodeURIComponent(candidateDir);
|
|
369
|
+
try {
|
|
370
|
+
const dirRes = await fetch(buildOpenCodeUrl(`/session?directory=${encoded}`, ''), fetchOpts);
|
|
371
|
+
if (dirRes.ok) {
|
|
372
|
+
const dirPayload = await dirRes.json().catch(() => []);
|
|
373
|
+
const dirSessions = Array.isArray(dirPayload) ? dirPayload : [];
|
|
374
|
+
for (const session of dirSessions) {
|
|
375
|
+
const id = session && typeof session.id === 'string' ? session.id : null;
|
|
376
|
+
if (id && !seen.has(id)) {
|
|
377
|
+
seen.add(id);
|
|
378
|
+
extraSessions.push(session);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const merged = [...globalSessions, ...extraSessions];
|
|
388
|
+
merged.sort((a, b) => {
|
|
389
|
+
const aTime = a && typeof a.time_updated === 'number' ? a.time_updated : 0;
|
|
390
|
+
const bTime = b && typeof b.time_updated === 'number' ? b.time_updated : 0;
|
|
391
|
+
return bTime - aTime;
|
|
392
|
+
});
|
|
393
|
+
console.log(`[SessionMerge] ${globalSessions.length} global + ${extraSessions.length} extra = ${merged.length} total`);
|
|
394
|
+
return res.json(merged);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
console.log(`[SessionMerge] Error: ${error.message}, falling through`);
|
|
397
|
+
next();
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
app.get('/api/global/event', forwardSseRequest);
|
|
403
|
+
app.get('/api/event', forwardSseRequest);
|
|
404
|
+
|
|
405
|
+
// Generic proxy for non-SSE OpenCode API routes.
|
|
406
|
+
const apiProxy = createProxyMiddleware({
|
|
407
|
+
target: resolveProxyTarget(),
|
|
408
|
+
changeOrigin: true,
|
|
409
|
+
pathRewrite: { '^/api': '' },
|
|
410
|
+
// Dynamic target — port can change after restart
|
|
411
|
+
router: () => resolveProxyTargetLogged(),
|
|
412
|
+
on: {
|
|
413
|
+
proxyReq: (proxyReq, req) => {
|
|
414
|
+
// Inject OpenCode auth headers
|
|
415
|
+
const authHeaders = getOpenCodeAuthHeaders();
|
|
416
|
+
if (authHeaders.Authorization) {
|
|
417
|
+
proxyReq.setHeader('Authorization', authHeaders.Authorization);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Defensive: request identity encoding from upstream OpenCode.
|
|
421
|
+
// This avoids compressed-body/header mismatches in multi-proxy setups.
|
|
422
|
+
proxyReq.setHeader('accept-encoding', 'identity');
|
|
423
|
+
|
|
424
|
+
console.log(`[proxy] ${req.method} ${req.originalUrl} -> ${proxyReq.getHeader('host')}${proxyReq.path}`);
|
|
425
|
+
},
|
|
426
|
+
proxyRes: (proxyRes) => {
|
|
427
|
+
for (const key of Object.keys(proxyRes.headers || {})) {
|
|
428
|
+
if (!shouldForwardProxyResponseHeader(key)) {
|
|
429
|
+
delete proxyRes.headers[key];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
error: (err, req, res) => {
|
|
434
|
+
console.error(`[proxy] OpenCode proxy error for ${req.method} ${req.originalUrl}:`, err.message);
|
|
435
|
+
if (err.code) console.error(`[proxy] code: ${err.code}`);
|
|
436
|
+
if (err.stack) console.error(`[proxy] stack: ${err.stack.split('\n').slice(0, 3).join('\n')}`);
|
|
437
|
+
if (res && !res.headersSent && typeof res.status === 'function') {
|
|
438
|
+
res.status(503).json({ error: 'OpenCode service unavailable' });
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
app.use('/api', apiProxy);
|
|
445
|
+
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
const DEFAULT_PWA_APP_NAME = 'Vinci - AI Coding Assistant';
|
|
2
|
+
const mapPwaOrientationToManifest = (value) => {
|
|
3
|
+
if (value === 'portrait') {
|
|
4
|
+
return 'portrait-primary';
|
|
5
|
+
}
|
|
6
|
+
if (value === 'landscape') {
|
|
7
|
+
return 'landscape-primary';
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const registerPwaManifestRoute = (app, dependencies) => {
|
|
13
|
+
const {
|
|
14
|
+
process,
|
|
15
|
+
resolveProjectDirectory,
|
|
16
|
+
buildOpenCodeUrl,
|
|
17
|
+
getOpenCodeAuthHeaders,
|
|
18
|
+
readSettingsFromDiskMigrated,
|
|
19
|
+
normalizePwaAppName,
|
|
20
|
+
normalizePwaOrientation,
|
|
21
|
+
} = dependencies;
|
|
22
|
+
|
|
23
|
+
const recentPwaSessionsCache = new Map();
|
|
24
|
+
|
|
25
|
+
const getRecentPwaSessionShortcuts = async (req) => {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
|
|
28
|
+
const resolvedDirectoryResult = await resolveProjectDirectory(req).catch(() => ({ directory: null }));
|
|
29
|
+
const preferredDirectory = typeof resolvedDirectoryResult?.directory === 'string'
|
|
30
|
+
? resolvedDirectoryResult.directory
|
|
31
|
+
: null;
|
|
32
|
+
|
|
33
|
+
const cacheKey = preferredDirectory ? `dir:${preferredDirectory}` : 'global';
|
|
34
|
+
const cached = recentPwaSessionsCache.get(cacheKey);
|
|
35
|
+
if (cached && now - cached.at < 5000) {
|
|
36
|
+
return cached.data;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const normalizeShortcutTitle = (value, fallback) => {
|
|
40
|
+
const normalized = normalizePwaAppName(value, fallback);
|
|
41
|
+
return normalized.length > 48 ? normalized.slice(0, 48) : normalized;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const toFiniteNumber = (value) => {
|
|
45
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
49
|
+
const parsed = Number(value);
|
|
50
|
+
if (Number.isFinite(parsed)) {
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const normalizeDirectory = (value) => {
|
|
58
|
+
if (typeof value !== 'string') {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
const trimmed = value.trim();
|
|
62
|
+
if (!trimmed) {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
const normalized = trimmed.replace(/\\/g, '/');
|
|
66
|
+
if (normalized === '/') {
|
|
67
|
+
return '/';
|
|
68
|
+
}
|
|
69
|
+
return normalized.length > 1 ? normalized.replace(/\/+$/, '') : normalized;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const sessionUpdatedAt = (session) => {
|
|
73
|
+
const time = session && typeof session.time === 'object' ? session.time : null;
|
|
74
|
+
return toFiniteNumber(time?.updated) ?? toFiniteNumber(time?.created) ?? 0;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const filterSessionsByDirectory = (sessions, directory) => {
|
|
78
|
+
const normalizedDirectory = normalizeDirectory(directory);
|
|
79
|
+
if (!normalizedDirectory) {
|
|
80
|
+
return sessions;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const prefix = normalizedDirectory === '/' ? '/' : `${normalizedDirectory}/`;
|
|
84
|
+
return sessions.filter((session) => {
|
|
85
|
+
const sessionDirectory = normalizeDirectory(session?.directory);
|
|
86
|
+
if (!sessionDirectory) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return sessionDirectory === normalizedDirectory || sessionDirectory.startsWith(prefix);
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const listSessions = async (directory) => {
|
|
94
|
+
const query = (() => {
|
|
95
|
+
if (typeof directory !== 'string' || directory.length === 0) {
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
const preparedDirectory = process.platform === 'win32'
|
|
99
|
+
? directory.replace(/\//g, '\\\\')
|
|
100
|
+
: directory;
|
|
101
|
+
return `?directory=${encodeURIComponent(preparedDirectory)}`;
|
|
102
|
+
})();
|
|
103
|
+
|
|
104
|
+
const response = await fetch(buildOpenCodeUrl(`/session${query}`, ''), {
|
|
105
|
+
method: 'GET',
|
|
106
|
+
headers: {
|
|
107
|
+
Accept: 'application/json',
|
|
108
|
+
...getOpenCodeAuthHeaders(),
|
|
109
|
+
},
|
|
110
|
+
signal: AbortSignal.timeout(2500),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const payload = await response.json().catch(() => null);
|
|
118
|
+
return Array.isArray(payload) ? payload : [];
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
let payload = [];
|
|
123
|
+
|
|
124
|
+
if (preferredDirectory) {
|
|
125
|
+
const scopedPayload = await listSessions(preferredDirectory);
|
|
126
|
+
const filteredScopedPayload = filterSessionsByDirectory(scopedPayload, preferredDirectory);
|
|
127
|
+
|
|
128
|
+
if (filteredScopedPayload.length > 0) {
|
|
129
|
+
payload = filteredScopedPayload;
|
|
130
|
+
} else {
|
|
131
|
+
const globalPayload = await listSessions(null);
|
|
132
|
+
const filteredGlobalPayload = filterSessionsByDirectory(globalPayload, preferredDirectory);
|
|
133
|
+
payload = filteredGlobalPayload;
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
payload = await listSessions(null);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
const rows = [];
|
|
141
|
+
|
|
142
|
+
for (const item of payload) {
|
|
143
|
+
if (!item || typeof item !== 'object') {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const id = typeof item.id === 'string' ? item.id.trim().slice(0, 160) : '';
|
|
148
|
+
if (!id || seen.has(id)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
seen.add(id);
|
|
153
|
+
const title = normalizeShortcutTitle(item.title, `Session ${rows.length + 1}`);
|
|
154
|
+
const updatedAt = sessionUpdatedAt(item);
|
|
155
|
+
|
|
156
|
+
rows.push({ id, title, updatedAt });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
rows.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
160
|
+
|
|
161
|
+
const shortcuts = rows.slice(0, 3).map((session) => ({
|
|
162
|
+
name: session.title,
|
|
163
|
+
short_name: session.title.length > 32 ? session.title.slice(0, 32) : session.title,
|
|
164
|
+
description: 'Open recent session',
|
|
165
|
+
url: `/?session=${encodeURIComponent(session.id)}`,
|
|
166
|
+
icons: [{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png' }],
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
recentPwaSessionsCache.set(cacheKey, { at: now, data: shortcuts });
|
|
170
|
+
return shortcuts;
|
|
171
|
+
} catch {
|
|
172
|
+
recentPwaSessionsCache.set(cacheKey, { at: now, data: [] });
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
app.get('/manifest.webmanifest', async (req, res) => {
|
|
178
|
+
const hasQueryOverride =
|
|
179
|
+
typeof req.query?.pwa_name === 'string'
|
|
180
|
+
|| typeof req.query?.app_name === 'string'
|
|
181
|
+
|| typeof req.query?.appName === 'string';
|
|
182
|
+
|
|
183
|
+
let queryValueRaw = '';
|
|
184
|
+
if (typeof req.query?.pwa_name === 'string') {
|
|
185
|
+
queryValueRaw = req.query.pwa_name;
|
|
186
|
+
} else if (typeof req.query?.app_name === 'string') {
|
|
187
|
+
queryValueRaw = req.query.app_name;
|
|
188
|
+
} else if (typeof req.query?.appName === 'string') {
|
|
189
|
+
queryValueRaw = req.query.appName;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const queryOverrideName = normalizePwaAppName(queryValueRaw, '');
|
|
193
|
+
const hasOrientationOverride = typeof req.query?.orientation === 'string';
|
|
194
|
+
const queryOverrideOrientation = normalizePwaOrientation(req.query?.orientation, 'system');
|
|
195
|
+
|
|
196
|
+
let storedName = '';
|
|
197
|
+
let storedOrientation = 'system';
|
|
198
|
+
try {
|
|
199
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
200
|
+
storedName = normalizePwaAppName(settings?.pwaAppName, '');
|
|
201
|
+
storedOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system');
|
|
202
|
+
} catch {
|
|
203
|
+
storedName = '';
|
|
204
|
+
storedOrientation = 'system';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const appName = hasQueryOverride
|
|
208
|
+
? (queryOverrideName || DEFAULT_PWA_APP_NAME)
|
|
209
|
+
: (storedName || DEFAULT_PWA_APP_NAME);
|
|
210
|
+
const manifestOrientation = mapPwaOrientationToManifest(
|
|
211
|
+
hasOrientationOverride ? queryOverrideOrientation : storedOrientation
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const shortName = appName.length > 30 ? appName.slice(0, 30) : appName;
|
|
215
|
+
const recentSessionShortcuts = await getRecentPwaSessionShortcuts(req);
|
|
216
|
+
|
|
217
|
+
const manifest = {
|
|
218
|
+
name: appName,
|
|
219
|
+
short_name: shortName,
|
|
220
|
+
description: 'Web interface companion for OpenCode AI coding agent',
|
|
221
|
+
id: '/',
|
|
222
|
+
start_url: '/',
|
|
223
|
+
scope: '/',
|
|
224
|
+
display: 'standalone',
|
|
225
|
+
display_override: ['window-controls-overlay'],
|
|
226
|
+
background_color: '#151313',
|
|
227
|
+
theme_color: '#edb449',
|
|
228
|
+
...(manifestOrientation ? { orientation: manifestOrientation } : {}),
|
|
229
|
+
icons: [
|
|
230
|
+
{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
|
|
231
|
+
{ src: '/pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
|
232
|
+
{ src: '/pwa-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'any maskable' },
|
|
233
|
+
{ src: '/pwa-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' },
|
|
234
|
+
{ src: '/apple-touch-icon-180x180.png', sizes: '180x180', type: 'image/png', purpose: 'any' },
|
|
235
|
+
{ src: '/apple-touch-icon-152x152.png', sizes: '152x152', type: 'image/png', purpose: 'any' },
|
|
236
|
+
{ src: '/favicon-32.png', sizes: '32x32', type: 'image/png' },
|
|
237
|
+
{ src: '/favicon-16.png', sizes: '16x16', type: 'image/png' },
|
|
238
|
+
],
|
|
239
|
+
shortcuts: [
|
|
240
|
+
{
|
|
241
|
+
name: 'Appearance Settings',
|
|
242
|
+
short_name: 'Settings',
|
|
243
|
+
description: 'Open appearance settings',
|
|
244
|
+
url: '/?settings=appearance',
|
|
245
|
+
icons: [{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png' }],
|
|
246
|
+
},
|
|
247
|
+
...recentSessionShortcuts,
|
|
248
|
+
],
|
|
249
|
+
categories: ['developer', 'tools', 'productivity'],
|
|
250
|
+
lang: 'en',
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
res.setHeader('Cache-Control', 'no-store, must-revalidate');
|
|
254
|
+
res.type('application/manifest+json');
|
|
255
|
+
res.send(JSON.stringify(manifest));
|
|
256
|
+
});
|
|
257
|
+
};
|