@cats-inc/cats-platform 0.2.8 → 0.3.5

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.
Files changed (205) hide show
  1. package/README.md +25 -1
  2. package/build/desktop/bootstrapPage.d.ts.map +1 -1
  3. package/build/desktop/bootstrapPage.js +26 -129
  4. package/build/desktop/bootstrapPage.js.map +1 -1
  5. package/build/desktop/cliInventoryProbe.d.ts +6 -0
  6. package/build/desktop/cliInventoryProbe.d.ts.map +1 -1
  7. package/build/desktop/cliInventoryProbe.js +6 -3
  8. package/build/desktop/cliInventoryProbe.js.map +1 -1
  9. package/build/desktop/contracts.d.ts +2 -0
  10. package/build/desktop/contracts.d.ts.map +1 -1
  11. package/build/desktop/contracts.js.map +1 -1
  12. package/build/desktop/main.js +95 -41
  13. package/build/desktop/main.js.map +1 -1
  14. package/build/desktop/preload.cjs +8 -6
  15. package/build/desktop/preload.cjs.map +1 -1
  16. package/build/desktop/providerManager.d.ts +46 -0
  17. package/build/desktop/providerManager.d.ts.map +1 -0
  18. package/build/desktop/providerManager.js +220 -0
  19. package/build/desktop/providerManager.js.map +1 -0
  20. package/build/desktop/providerSelection.d.ts +1 -0
  21. package/build/desktop/providerSelection.d.ts.map +1 -1
  22. package/build/desktop/providerSelection.js +7 -1
  23. package/build/desktop/providerSelection.js.map +1 -1
  24. package/build/desktop/setupAssets.d.ts.map +1 -1
  25. package/build/desktop/setupAssets.js +12 -0
  26. package/build/desktop/setupAssets.js.map +1 -1
  27. package/build/desktop/setupAudit.d.ts +3 -1
  28. package/build/desktop/setupAudit.d.ts.map +1 -1
  29. package/build/desktop/setupAudit.js +12 -8
  30. package/build/desktop/setupAudit.js.map +1 -1
  31. package/build/desktop/setupBridge.js +1 -1
  32. package/build/desktop/setupBridge.js.map +1 -1
  33. package/build/desktop/setupPrerequisites.d.ts +11 -0
  34. package/build/desktop/setupPrerequisites.d.ts.map +1 -0
  35. package/build/desktop/setupPrerequisites.js +47 -0
  36. package/build/desktop/setupPrerequisites.js.map +1 -0
  37. package/build/renderer/assets/ActiveSessionPermissionChip-BugyYoVY.js +1 -0
  38. package/build/renderer/assets/{App-BZA23O2J.js → App-1MFRMMld.js} +1 -1
  39. package/build/renderer/assets/{App-YqprEL2n.js → App-BWgnfutX.js} +1 -1
  40. package/build/renderer/assets/{App-rGw9J8PZ.js → App-Cm_tok6f.js} +1 -1
  41. package/build/renderer/assets/{WorkspaceAppRoutes-BCHCikmJ.js → WorkspaceAppRoutes-B8U5FWao.js} +1 -1
  42. package/build/renderer/assets/app-CqnsY5ed.css +1 -0
  43. package/build/renderer/assets/app-DTDTNtih.js +85 -0
  44. package/build/renderer/assets/{client-BdGHAUoK.js → client-DREGSZqQ.js} +1 -1
  45. package/build/renderer/assets/conversationSidebarRecentEntries-y_bEg3Cp.js +6 -0
  46. package/build/renderer/assets/{screenshotOverlay-DfBcolX-.js → screenshotOverlay-EF07cXvw.js} +1 -1
  47. package/build/renderer/assets/{useDraftSessionChips-D9BX6X92.js → useDraftSessionChips-BD6ZGG_y.js} +1 -1
  48. package/build/renderer/desktop/overlay/index.html +2 -2
  49. package/build/renderer/index.html +3 -3
  50. package/build/server/app/renderer/auth/api.d.ts.map +1 -1
  51. package/build/server/app/renderer/auth/api.js +4 -1
  52. package/build/server/app/renderer/auth/api.js.map +1 -1
  53. package/build/server/app/renderer/providerCatalogClient.d.ts.map +1 -1
  54. package/build/server/app/renderer/providerCatalogClient.js +19 -6
  55. package/build/server/app/renderer/providerCatalogClient.js.map +1 -1
  56. package/build/server/app/renderer/providerClientInvalidation.d.ts +6 -0
  57. package/build/server/app/renderer/providerClientInvalidation.d.ts.map +1 -0
  58. package/build/server/app/renderer/providerClientInvalidation.js +12 -0
  59. package/build/server/app/renderer/providerClientInvalidation.js.map +1 -0
  60. package/build/server/app/renderer/providerReadLoop.d.ts +5 -0
  61. package/build/server/app/renderer/providerReadLoop.d.ts.map +1 -0
  62. package/build/server/app/renderer/providerReadLoop.js +52 -0
  63. package/build/server/app/renderer/providerReadLoop.js.map +1 -0
  64. package/build/server/app/renderer/providerRegistryClient.d.ts +2 -1
  65. package/build/server/app/renderer/providerRegistryClient.d.ts.map +1 -1
  66. package/build/server/app/renderer/providerRegistryClient.js +33 -17
  67. package/build/server/app/renderer/providerRegistryClient.js.map +1 -1
  68. package/build/server/app/server/authGatePolicy.js +7 -0
  69. package/build/server/app/server/authGatePolicy.js.map +1 -1
  70. package/build/server/app/server/shutdown.d.ts.map +1 -1
  71. package/build/server/app/server/shutdown.js +3 -1
  72. package/build/server/app/server/shutdown.js.map +1 -1
  73. package/build/server/app/server/startup.d.ts +4 -3
  74. package/build/server/app/server/startup.d.ts.map +1 -1
  75. package/build/server/app/server/startup.js +8 -0
  76. package/build/server/app/server/startup.js.map +1 -1
  77. package/build/server/app/server/subscribeRoutes.d.ts.map +1 -1
  78. package/build/server/app/server/subscribeRoutes.js +5 -3
  79. package/build/server/app/server/subscribeRoutes.js.map +1 -1
  80. package/build/server/design/components/providerModelFieldsSupport.d.ts +2 -21
  81. package/build/server/design/components/providerModelFieldsSupport.d.ts.map +1 -1
  82. package/build/server/design/components/providerModelFieldsSupport.js +32 -36
  83. package/build/server/design/components/providerModelFieldsSupport.js.map +1 -1
  84. package/build/server/design/components/useProviderCatalogState.d.ts +1 -2
  85. package/build/server/design/components/useProviderCatalogState.d.ts.map +1 -1
  86. package/build/server/design/components/useProviderCatalogState.js +71 -152
  87. package/build/server/design/components/useProviderCatalogState.js.map +1 -1
  88. package/build/server/design/components/useProviderModelFieldActions.js +2 -2
  89. package/build/server/design/components/useProviderModelFieldActions.js.map +1 -1
  90. package/build/server/design/components/useProviderRegistryState.d.ts +1 -7
  91. package/build/server/design/components/useProviderRegistryState.d.ts.map +1 -1
  92. package/build/server/design/components/useProviderRegistryState.js +41 -91
  93. package/build/server/design/components/useProviderRegistryState.js.map +1 -1
  94. package/build/server/design/components/useProviderTargetReconciliation.d.ts +1 -0
  95. package/build/server/design/components/useProviderTargetReconciliation.d.ts.map +1 -1
  96. package/build/server/design/components/useProviderTargetReconciliation.js +1 -1
  97. package/build/server/design/components/useProviderTargetReconciliation.js.map +1 -1
  98. package/build/server/index.js +30 -0
  99. package/build/server/index.js.map +1 -1
  100. package/build/server/platform/orchestration/entitySubscriptions/channel.d.ts +2 -1
  101. package/build/server/platform/orchestration/entitySubscriptions/channel.d.ts.map +1 -1
  102. package/build/server/platform/orchestration/entitySubscriptions/channel.js +3 -18
  103. package/build/server/platform/orchestration/entitySubscriptions/channel.js.map +1 -1
  104. package/build/server/platform/orchestration/entitySubscriptions/index.d.ts +1 -0
  105. package/build/server/platform/orchestration/entitySubscriptions/index.d.ts.map +1 -1
  106. package/build/server/platform/orchestration/entitySubscriptions/index.js.map +1 -1
  107. package/build/server/products/chat/api/routeSupport.d.ts +9 -1
  108. package/build/server/products/chat/api/routeSupport.d.ts.map +1 -1
  109. package/build/server/products/chat/api/routeSupport.js +22 -0
  110. package/build/server/products/chat/api/routeSupport.js.map +1 -1
  111. package/build/server/products/chat/state/model/readModels.d.ts +1 -0
  112. package/build/server/products/chat/state/model/readModels.d.ts.map +1 -1
  113. package/build/server/products/chat/state/model/readModels.js +2 -1
  114. package/build/server/products/chat/state/model/readModels.js.map +1 -1
  115. package/build/server/products/shared/renderer/api/appShell.d.ts +1 -0
  116. package/build/server/products/shared/renderer/api/appShell.d.ts.map +1 -1
  117. package/build/server/products/shared/renderer/api/appShell.js +9 -0
  118. package/build/server/products/shared/renderer/api/appShell.js.map +1 -1
  119. package/build/server/runtime/client.d.ts.map +1 -1
  120. package/build/server/runtime/client.js +5 -1
  121. package/build/server/runtime/client.js.map +1 -1
  122. package/build/server/runtime/setup.d.ts +4 -0
  123. package/build/server/runtime/setup.d.ts.map +1 -1
  124. package/build/server/runtime/setup.js +3 -1
  125. package/build/server/runtime/setup.js.map +1 -1
  126. package/build/server/server/routes/providers.d.ts.map +1 -1
  127. package/build/server/server/routes/providers.js +21 -50
  128. package/build/server/server/routes/providers.js.map +1 -1
  129. package/build/server/shared/cliInteraction.d.ts +17 -0
  130. package/build/server/shared/cliInteraction.d.ts.map +1 -0
  131. package/build/server/shared/cliInteraction.js +102 -0
  132. package/build/server/shared/cliInteraction.js.map +1 -0
  133. package/build/server/shared/desktopRecoveryBridge.d.ts +2 -1
  134. package/build/server/shared/desktopRecoveryBridge.d.ts.map +1 -1
  135. package/build/server/shared/desktopRecoveryBridge.js.map +1 -1
  136. package/build/server/shared/providerCatalogData.d.ts +168 -96
  137. package/build/server/shared/providerCatalogData.d.ts.map +1 -1
  138. package/build/server/shared/providerCatalogData.js +68 -42
  139. package/build/server/shared/providerCatalogData.js.map +1 -1
  140. package/build/server/shared/providerRegistryWarnings.d.ts +4 -0
  141. package/build/server/shared/providerRegistryWarnings.d.ts.map +1 -1
  142. package/build/server/shared/providerRegistryWarnings.js +7 -0
  143. package/build/server/shared/providerRegistryWarnings.js.map +1 -1
  144. package/package.json +13 -6
  145. package/packages/provider-setup/manager.d.ts +79 -0
  146. package/packages/provider-setup/manager.js +441 -0
  147. package/scripts/README.md +23 -0
  148. package/scripts/build-desktop-installer.mjs +124 -16
  149. package/scripts/dev-server.mjs +4 -1
  150. package/scripts/linux/install-github-cli.sh +5 -0
  151. package/scripts/linux/install-node.sh +9 -3
  152. package/scripts/linux/node-cli-common.sh +87 -48
  153. package/scripts/linux/provider-cli-common.sh +80 -105
  154. package/scripts/linux/provider-version-common.sh +247 -0
  155. package/scripts/linux/test-linux-package-smoke.sh +2 -0
  156. package/scripts/linux/verified-npm-update.sh +20 -0
  157. package/scripts/macos/install-github-cli.sh +5 -0
  158. package/scripts/macos/install-node.sh +9 -3
  159. package/scripts/macos/node-cli-common.sh +87 -48
  160. package/scripts/macos/provider-cli-common.sh +80 -105
  161. package/scripts/macos/provider-version-common.sh +247 -0
  162. package/scripts/macos/test-macos-package-smoke.sh +2 -0
  163. package/scripts/macos/verified-npm-update.sh +20 -0
  164. package/scripts/testing/conversation-navigation-smoke-electron.cjs +134 -0
  165. package/scripts/testing/conversation-navigation-smoke-renderer.tsx +102 -0
  166. package/scripts/testing/conversation-navigation-smoke.mjs +67 -0
  167. package/scripts/validate-release-assets.mjs +4 -1
  168. package/scripts/windows/Install-Antigravity.ps1 +26 -3
  169. package/scripts/windows/Install-ClaudeCode.ps1 +25 -7
  170. package/scripts/windows/Install-CursorAgent.ps1 +45 -30
  171. package/scripts/windows/Install-Devin.ps1 +16 -47
  172. package/scripts/windows/Install-GitHubCli.ps1 +4 -0
  173. package/scripts/windows/Install-Goose.ps1 +18 -1
  174. package/scripts/windows/Install-Grok.ps1 +14 -1
  175. package/scripts/windows/Install-Junie.ps1 +32 -11
  176. package/scripts/windows/Install-KiroCli.ps1 +37 -13
  177. package/scripts/windows/Install-MetaCli.ps1 +8 -1
  178. package/scripts/windows/Install-Node.ps1 +6 -1
  179. package/scripts/windows/Setup-NodeGlobalPrefix.ps1 +10 -0
  180. package/scripts/windows/Test-WindowsInstallerSmoke.ps1 +2 -0
  181. package/scripts/windows/_HiddenProcess.ps1 +14 -3
  182. package/scripts/windows/_NativeInstallerSupport.ps1 +308 -0
  183. package/scripts/windows/_NpmCliInstaller.ps1 +35 -14
  184. package/scripts/windows/_PackagedUninstall.ps1 +9 -1
  185. package/scripts/windows/_VerifiedNpmUpdate.ps1 +25 -0
  186. package/build/renderer/assets/ActiveSessionPermissionChip-CAgN-tXv.js +0 -1
  187. package/build/renderer/assets/app-BV3UAQkR.css +0 -1
  188. package/build/renderer/assets/app-CVPqqfZo.js +0 -45
  189. package/build/renderer/assets/conversationSidebarRecentEntries-iqaLqA1n.js +0 -6
  190. package/build/server/app/renderer/settings/runtimeLifecycleHelpers.d.ts +0 -48
  191. package/build/server/app/renderer/settings/runtimeLifecycleHelpers.d.ts.map +0 -1
  192. package/build/server/app/renderer/settings/runtimeLifecycleHelpers.js +0 -535
  193. package/build/server/app/renderer/settings/runtimeLifecycleHelpers.js.map +0 -1
  194. package/build/server/design/components/useProviderRegistryAutoRecheck.d.ts +0 -13
  195. package/build/server/design/components/useProviderRegistryAutoRecheck.d.ts.map +0 -1
  196. package/build/server/design/components/useProviderRegistryAutoRecheck.js +0 -70
  197. package/build/server/design/components/useProviderRegistryAutoRecheck.js.map +0 -1
  198. package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.d.ts +0 -19
  199. package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.d.ts.map +0 -1
  200. package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.js +0 -53
  201. package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.js.map +0 -1
  202. package/build/server/products/shared/renderer/api/providers.d.ts +0 -17
  203. package/build/server/products/shared/renderer/api/providers.d.ts.map +0 -1
  204. package/build/server/products/shared/renderer/api/providers.js +0 -50
  205. package/build/server/products/shared/renderer/api/providers.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cats-inc/cats-platform",
3
- "version": "0.2.8",
3
+ "version": "0.3.5",
4
4
  "description": "Technical host package for the Cats platform, backed by cats-runtime.",
5
5
  "author": "Cats Inc. <sammykenny2@gmail.com>",
6
6
  "homepage": "https://github.com/cats-inc/cats-platform",
@@ -18,6 +18,7 @@
18
18
  "build/desktop",
19
19
  "scripts",
20
20
  "packages/app-sdk",
21
+ "packages/provider-setup",
21
22
  "config/provider-capability-bootstrap.yaml.example",
22
23
  ".env.example",
23
24
  "README.md",
@@ -49,7 +50,7 @@
49
50
  "desktop:start": "npm run build && npm run desktop:host",
50
51
  "dev": "npm run dev:web",
51
52
  "dev:server": "node scripts/dev-server.mjs",
52
- "dev:server:tsx": "tsx watch src/index.ts",
53
+ "dev:server:tsx": "tsx watch src/index.ts --startup-mode app-managed --managed-by tsx-watch --ready-output plain",
53
54
  "dev:toggle-flag": "tsx scripts/dev-toggle-feature-flag.mts",
54
55
  "dev:web": "vite",
55
56
  "ingress:smoke": "node scripts/testing/check-platform-ingress.mjs",
@@ -93,7 +94,7 @@
93
94
  "@types/react-dom": "^18.3.0",
94
95
  "@vitejs/plugin-react": "^4.3.1",
95
96
  "electron": "^41.2.0",
96
- "electron-builder": "^26.8.1",
97
+ "electron-builder": "^26.16.1",
97
98
  "esbuild": "^0.27.0",
98
99
  "jsdom": "^24.1.3",
99
100
  "png-to-ico": "^3.0.1",
@@ -131,6 +132,7 @@
131
132
  "files": [
132
133
  "build/desktop/**/*",
133
134
  "packages/app-sdk/**/*",
135
+ "packages/provider-setup/**/*",
134
136
  "assets/build/icon.ico",
135
137
  "assets/build/icon.png",
136
138
  "assets/tray-icon*.png",
@@ -140,12 +142,16 @@
140
142
  {
141
143
  "from": "build/desktop-packaging/shared/official-apps",
142
144
  "to": "app-sidecar/official-apps",
143
- "filter": ["**/*"]
145
+ "filter": [
146
+ "**/*"
147
+ ]
144
148
  },
145
149
  {
146
150
  "from": "build/desktop-packaging/shared/app-sidecar/packages/app-sdk",
147
151
  "to": "app-sidecar/packages/app-sdk",
148
- "filter": ["**/*"]
152
+ "filter": [
153
+ "**/*"
154
+ ]
149
155
  },
150
156
  {
151
157
  "from": "build/desktop-packaging/shared/build/server",
@@ -253,8 +259,9 @@
253
259
  "icon": "icon.icns",
254
260
  "artifactName": "Cats-${version}-${arch}.${ext}",
255
261
  "category": "public.app-category.productivity",
256
- "hardenedRuntime": false,
262
+ "hardenedRuntime": true,
257
263
  "gatekeeperAssess": false,
264
+ "notarize": false,
258
265
  "extendInfo": {
259
266
  "NSSpeechRecognitionUsageDescription": "Cats uses speech recognition for voice input in the chat composer.",
260
267
  "NSMicrophoneUsageDescription": "Cats uses the microphone for voice input in the chat composer."
@@ -0,0 +1,79 @@
1
+ export interface ProviderTarget {
2
+ provider: string;
3
+ backend: 'cli' | 'api' | 'local' | 'agent';
4
+ instance: string;
5
+ endpoint?: string;
6
+ }
7
+ export interface ProviderSelection {
8
+ state: 'missing' | 'invalid' | 'empty' | 'selected';
9
+ revision: string;
10
+ targets: ProviderTarget[];
11
+ nativeSetupTargets: ProviderTarget[];
12
+ diskChanged: boolean;
13
+ error: string | null;
14
+ }
15
+ export interface ProviderObservation extends ProviderTarget {
16
+ commandStatus: string;
17
+ available: boolean;
18
+ version?: string | null;
19
+ authStatus?: string;
20
+ observedAt: string;
21
+ configurationStatus: 'unchanged' | 'changed' | 'not_selected';
22
+ connectionStatus?: 'connected' | 'failed' | 'not_checked';
23
+ detail?: string;
24
+ }
25
+ export interface ProviderSetupHelper {
26
+ id: string;
27
+ available: boolean;
28
+ supported: boolean;
29
+ supportsApply: boolean;
30
+ supportsUpgrade: boolean;
31
+ supportsForce: boolean;
32
+ supportsUninstall: boolean;
33
+ }
34
+ export interface ProviderSetupOutcome {
35
+ source?: 'installer' | 'detection';
36
+ status: string | null;
37
+ summary: string;
38
+ manualSteps: string[];
39
+ warnings: string[];
40
+ plannedActions: string[];
41
+ runState: string;
42
+ }
43
+ export interface DesktopPrerequisite {
44
+ helperId: string;
45
+ checking: boolean;
46
+ result: ProviderSetupOutcome | null;
47
+ }
48
+ export interface ProviderManagerSnapshot {
49
+ runtime: {
50
+ bootstrapRequired?: boolean;
51
+ selection: ProviderSelection;
52
+ universe: Array<ProviderTarget & { familyLabel: string; binaryName: string;
53
+ install?: { auth?: { hint?: string; docsUrl?: string }; install?: { docsUrl?: string } } | null }>;
54
+ observations?: ProviderObservation[];
55
+ connections?: Array<ProviderTarget & { endpoint: string; editable: boolean; source?: string }>;
56
+ state?: { status?: string; error?: string | null } | null;
57
+ };
58
+ helpers: ProviderSetupHelper[];
59
+ platform: 'windows' | 'macos' | 'linux';
60
+ operations: Array<{ targets: ProviderTarget[]; stage: string }>;
61
+ outcomes: Record<string, ProviderSetupOutcome>;
62
+ prerequisites?: DesktopPrerequisite[];
63
+ preview?: { target: ProviderTarget; result: ProviderSetupOutcome; revision: string };
64
+ }
65
+ export interface ProviderManagerBridge {
66
+ runSetupHelper?(helperId: string, mode: 'check' | 'apply'):
67
+ Promise<{ state?: { lastAction: ProviderSetupOutcome | null } }>;
68
+ getProviderSetup(context?: 'onboarding' | 'settings'): Promise<ProviderManagerSnapshot>;
69
+ applyProviderSetup(input: { targets: ProviderTarget[]; expectedRevision: string;
70
+ detectAfter: boolean; reload?: boolean }): Promise<ProviderManagerSnapshot>;
71
+ runProviderSetup(input: { action: 'detect' | 'install' | 'upgrade' | 'repair' | 'uninstall' | 'preview_uninstall';
72
+ targets: ProviderTarget[]; expectedRevision: string }): Promise<ProviderManagerSnapshot>;
73
+ }
74
+ export function mountProviderManager(root: HTMLElement, bridge: ProviderManagerBridge, options?: {
75
+ context?: 'onboarding' | 'settings';
76
+ locale?: string;
77
+ onContinue?: () => void | Promise<void>;
78
+ onFeedback?: (message: string) => void;
79
+ }): { refresh(): Promise<void>; destroy(): void };
@@ -0,0 +1,441 @@
1
+ /** Browser-only, self-contained so the host can embed it before Platform starts. */
2
+ export function mountProviderManager(root, bridge, options = {}) {
3
+ const doc = root.ownerDocument;
4
+ const win = doc.defaultView;
5
+ const zh = String(options.locale || 'en').startsWith('zh');
6
+ const text = (en, tw) => zh ? tw : en;
7
+ const context = options.context || 'settings';
8
+ const key = (target) => JSON.stringify([target.provider, target.backend, target.instance]);
9
+ const identity = (target) => ({ provider: target.provider, backend: target.backend, instance: target.instance });
10
+ let snapshot = null;
11
+ let draft = null;
12
+ let revision = null;
13
+ let detectAfter = null;
14
+ let busy = false;
15
+ let stage = '';
16
+ let error = '';
17
+ let notice = '';
18
+ let destroyed = false;
19
+ let refreshing = false;
20
+ let epoch = 0;
21
+ let filter = '';
22
+ let signature = '';
23
+ let confirmation = null;
24
+ let expanded = false;
25
+ const endpointEdits = new Map();
26
+ const elements = (...items) => items.flat().filter((item) => item !== null && item !== undefined);
27
+ function el(tag, attrs = {}, ...children) {
28
+ const node = doc.createElement(tag);
29
+ for (const [name, value] of Object.entries(attrs)) {
30
+ if (name.startsWith('on')) node.addEventListener(name.slice(2), value);
31
+ else if (name === 'checked' || name === 'disabled') node[name] = Boolean(value);
32
+ else if (name === 'value') node.value = value;
33
+ else if (value !== null && value !== false) node.setAttribute(name, String(value));
34
+ }
35
+ node.append(...elements(...children));
36
+ return node;
37
+ }
38
+ const button = (label, action, disabled = false, attrs = {}) => el('button', {
39
+ type: 'button', disabled, onclick: action, ...attrs,
40
+ }, label);
41
+ const spinner = () => el('span', { class: 'pm-spinner', 'aria-hidden': 'true' });
42
+ function changed() {
43
+ if (!snapshot || !draft) return false;
44
+ const saved = snapshot.runtime.selection.targets.map(key);
45
+ return endpointEdits.size > 0 || saved.length !== draft.size || saved.some((value) => !draft.has(value));
46
+ }
47
+ function feedback(message) {
48
+ if (destroyed) return;
49
+ if (options.onFeedback) options.onFeedback(message);
50
+ else notice = message;
51
+ }
52
+ function accept(next, reset = false) {
53
+ if (confirmation && (confirmation.revision !== next.runtime.selection.revision || next.runtime.selection.diskChanged)) confirmation = null;
54
+ snapshot = next;
55
+ signature = JSON.stringify(next);
56
+ if (draft === null || reset || (!changed() && revision === next.runtime.selection.revision)) {
57
+ draft = new Set(next.runtime.selection.targets.map(key));
58
+ revision = next.runtime.selection.revision;
59
+ if (reset) endpointEdits.clear();
60
+ }
61
+ if (detectAfter === null) {
62
+ detectAfter = context === 'onboarding' && ['missing', 'invalid'].includes(next.runtime.selection.state);
63
+ }
64
+ }
65
+ async function refresh() {
66
+ if (destroyed || refreshing) return;
67
+ refreshing = true;
68
+ const readEpoch = epoch;
69
+ try {
70
+ const next = await bridge.getProviderSetup(context);
71
+ if (destroyed || readEpoch !== epoch) return;
72
+ // Preserve dirty drafts, including their original revision, across external edits.
73
+ const dirty = draft !== null && changed();
74
+ if (!dirty && revision !== next.runtime.selection.revision) {
75
+ draft = null;
76
+ endpointEdits.clear();
77
+ }
78
+ const previousSignature = signature;
79
+ accept(next);
80
+ const hadError = Boolean(error);
81
+ error = '';
82
+ const nextSignature = JSON.stringify(next);
83
+ if (hadError || nextSignature !== previousSignature) render();
84
+ } catch (reason) {
85
+ if (!destroyed && readEpoch === epoch) {
86
+ const message = String(reason?.message || reason);
87
+ if (error !== message) { error = message; render(); }
88
+ }
89
+ } finally { refreshing = false; }
90
+ }
91
+ async function perform(label, action) {
92
+ if (busy) return;
93
+ epoch++;
94
+ busy = true; stage = label; error = ''; notice = ''; render();
95
+ try {
96
+ const next = await action();
97
+ epoch++;
98
+ if (!destroyed && next) accept(next);
99
+ } catch (reason) {
100
+ if (!destroyed) {
101
+ const message = String(reason?.message || reason);
102
+ if (options.onFeedback) options.onFeedback(message);
103
+ else error = message;
104
+ }
105
+ } finally {
106
+ epoch++;
107
+ busy = false; stage = '';
108
+ if (!destroyed) { render(); void refresh(); }
109
+ }
110
+ }
111
+ function apply(reload = false) {
112
+ const targets = [...draft].map((value) => {
113
+ const [provider, backend, instance] = JSON.parse(value);
114
+ return { provider, backend, instance,
115
+ ...(endpointEdits.has(value) ? { endpoint: endpointEdits.get(value) } : {}) };
116
+ });
117
+ void perform(detectAfter && !reload ? text('Applying and detecting…', '正在套用並偵測…') : text('Applying…', '正在套用…'), async () => {
118
+ const next = await bridge.applyProviderSetup({ targets, expectedRevision: revision,
119
+ detectAfter: !reload && detectAfter, reload });
120
+ if (destroyed) return next;
121
+ accept(next, true);
122
+ feedback(detectAfter && !reload
123
+ ? text('Saved. Check the results on your selected tools.', '已套用,請查看所選工具的偵測結果。')
124
+ : text('Saved.', '已套用。'));
125
+ return next;
126
+ });
127
+ }
128
+ function run(action, targets, expectedRevision = revision) {
129
+ const labels = {
130
+ detect: text('Detecting…', '偵測中…'), install: text('Installing and verifying…', '安裝並確認中…'),
131
+ upgrade: text('Updating and verifying…', '更新並確認中…'), repair: text('Repairing and verifying…', '修復並確認中…'),
132
+ uninstall: text('Uninstalling and verifying…', '解除安裝並確認中…'), preview_uninstall: text('Preparing removal preview…', '準備移除預覽…'),
133
+ };
134
+ void perform(labels[action], async () => {
135
+ const next = await bridge.runProviderSetup({ action, targets: targets.map(identity), expectedRevision });
136
+ if (destroyed) return next;
137
+ if (action === 'preview_uninstall') confirmation = next.preview || null;
138
+ else {
139
+ const outcomes = targets.map((target) => next.outcomes[key(target)]).filter(Boolean);
140
+ const failed = outcomes.find((outcome) => outcome.runState === 'failed' || outcome.status === 'failed');
141
+ feedback(failed ? failed.summary : action === 'detect'
142
+ ? text('Check finished.', '偵測完成。')
143
+ : text('Finished. Check the tool’s status.', '已完成,請查看工具的狀態。'));
144
+ }
145
+ return next;
146
+ });
147
+ }
148
+ function row(target, selection, operationBusy) {
149
+ const id = key(target);
150
+ const selected = selection.targets.some((entry) => key(entry) === id);
151
+ const wanted = draft.has(id);
152
+ const observation = (snapshot.runtime.observations || []).find((entry) => key(entry) === id && entry.configurationStatus !== 'not_selected');
153
+ const stale = observation?.configurationStatus === 'changed';
154
+ const connection = (snapshot.runtime.connections || []).find((entry) => key(entry) === id);
155
+ const isService = Boolean(connection) || target.provider === 'ollama' || target.provider === 'openclaw';
156
+ const eligible = selection.nativeSetupTargets.some((entry) => key(entry) === id);
157
+ const suffix = target.provider === 'ollama' ? 'local-model' : 'native';
158
+ const helper = eligible ? snapshot.helpers.find((entry) => entry.id === `${snapshot.platform}-${target.provider}-${suffix}-installer` && entry.available && entry.supported) : null;
159
+ const outcome = snapshot.outcomes[id];
160
+ const operation = snapshot.operations.find((entry) => entry.targets.some((value) => key(value) === id));
161
+ const installed = !stale && observation?.commandStatus === 'ready';
162
+ let status = text('Not detected', '尚未偵測');
163
+ let tone = '';
164
+ if (!selected) status = wanted ? text('Apply to get started', '套用後開始使用') : text('Not selected', '未選用');
165
+ else if (stale) status = text('Settings changed · detect again', '設定已變更,請重新偵測');
166
+ else if (observation?.connectionStatus === 'connected') { status = text('Connection verified', '連線已確認'); tone = 'pm-ok'; }
167
+ else if (observation?.connectionStatus === 'failed') { status = text('Connection failed', '連線失敗'); tone = 'pm-warn'; }
168
+ else if (installed) { status = text('Installed', '已安裝'); tone = 'pm-ok'; }
169
+ else if (observation?.commandStatus === 'probe_failed') { status = text('Detection failed · try again', '偵測失敗,請重試'); tone = 'pm-warn'; }
170
+ else if (observation && ['missing_install', 'missing_path', 'missing'].includes(observation.commandStatus)) { status = text('Not installed', '尚未安裝'); tone = 'pm-warn'; }
171
+ else if (isService) status = text('Connection not checked', '尚未檢查連線');
172
+ if (operation) { status = stage || text('Working…', '處理中…'); tone = ''; }
173
+ const failed = outcome && (outcome.runState === 'failed' || outcome.status === 'failed');
174
+ if (failed && !operation) { status = text('Needs attention', '需要處理'); tone = 'pm-warn'; }
175
+ const toggle = el('input', { type: 'checkbox', checked: wanted, disabled: busy || Boolean(operation) || Boolean(confirmation),
176
+ 'data-focus': `choice:${id}`, 'aria-label': text('Use ', '使用 ') + target.familyLabel + (target.custom ? ` (${target.backend}/${target.instance})` : ''),
177
+ onchange: (event) => { if (event.target.checked) draft.add(id); else { draft.delete(id); endpointEdits.delete(id); } notice = ''; render(); },
178
+ });
179
+ const details = el('div', { class: 'pm-detail' },
180
+ el('label', { class: 'pm-name' }, toggle, el('strong', {}, target.familyLabel || target.provider)),
181
+ el('div', { class: `pm-status ${tone}`, 'data-status': id }, operation ? spinner() : null, status),
182
+ );
183
+ const more = el('details', { class: 'pm-more', 'data-details': `more:${id}` },
184
+ el('summary', {}, text('Details', '詳細資訊')));
185
+ if (target.custom) more.append(el('small', {}, text('Connection: ', '連線:') + `${target.backend} / ${target.instance}`));
186
+ if (selected && observation) {
187
+ if (installed) more.append(el('small', {}, text('Installation: Installed', '安裝狀態:已安裝')));
188
+ more.append(el('small', {}, text('Last checked: ', '上次偵測:') + new Date(observation.observedAt).toLocaleString(zh ? 'zh-TW' : 'en')));
189
+ if (observation.version) more.append(el('small', {}, text('Version: ', '版本:') + observation.version));
190
+ if (installed) more.append(el('small', {}, observation.authStatus === 'not_required'
191
+ ? text('No sign-in required', '不需要登入') : observation.authStatus === 'missing'
192
+ ? text('Sign-in required', '需要登入') : text('Sign-in has not been verified', '尚未驗證登入狀態')));
193
+ if (observation.detail) more.append(el('small', {}, observation.detail));
194
+ }
195
+ if (installed && observation.authStatus === 'missing') details.append(el('small', {}, text('Sign in to get started', '登入後即可開始使用')));
196
+ if (isService && wanted) {
197
+ const endpoint = endpointEdits.get(id) ?? connection?.endpoint ?? '';
198
+ more.append(el('label', { class: 'pm-endpoint' }, text('Connection address', '連線位址'), el('input', {
199
+ type: 'text', inputmode: 'url', value: endpoint, disabled: busy || Boolean(confirmation) || connection?.editable === false,
200
+ placeholder: target.provider === 'ollama' ? 'http://127.0.0.1:11434' : 'ws://127.0.0.1:18789',
201
+ 'data-focus': `endpoint:${id}`, 'aria-label': `${target.familyLabel} ${text('endpoint', '服務位址')}`,
202
+ oninput: (event) => { endpointEdits.set(id, event.target.value.trim()); render(); },
203
+ })));
204
+ }
205
+ const canAct = wanted && selected && !endpointEdits.has(id) && !busy && !operationBusy && !error && !selection.diskChanged && !confirmation;
206
+ const controls = el('div', { class: 'pm-actions' });
207
+ if (wanted) controls.append(button(isService ? text('Test Connection', '測試連線') : observation ? text('Detect Again', '重新偵測') : text('Detect', '偵測'), () => run('detect', [target]), !canAct, { 'data-action': 'detect', 'data-target': id }));
208
+ if (helper && wanted && !installed) controls.append(button(text('Install', '安裝'), () => run('install', [target]), !canAct || !helper.supportsApply, { 'data-action': 'install', 'data-target': id }));
209
+ if (helper && wanted && installed) {
210
+ if (helper.supportsUpgrade) more.append(button(text('Upgrade', '更新'), () => run('upgrade', [target]), !canAct));
211
+ if (helper.supportsForce) more.append(button(text('Repair', '修復'), () => run('repair', [target]), !canAct));
212
+ if (helper.supportsUninstall) more.append(button(text('Uninstall…', '解除安裝…'), () => run('preview_uninstall', [target]), !canAct));
213
+ }
214
+ const docsUrl = target.install?.auth?.docsUrl || target.install?.install?.docsUrl;
215
+ if (wanted && docsUrl && /^https?:\/\//.test(docsUrl)) controls.append(el('a', { href: docsUrl, target: '_blank', rel: 'noopener noreferrer' }, text('Install / sign-in guide', '安裝/登入說明')));
216
+ if (wanted && !helper && !docsUrl) more.append(el('small', {}, isService
217
+ ? text('Configure the service, then test its connection.', '請先設定服務,再測試連線。')
218
+ : text('Install this provider in its configured environment, then detect again.', '請在設定的執行環境中安裝,再重新偵測。')));
219
+ if (selected && target.install?.auth?.hint && installed) more.append(el('small', {}, target.install.auth.hint));
220
+ if (selected && outcome) {
221
+ const remaining = [...outcome.warnings, ...outcome.manualSteps];
222
+ more.append(el('p', { class: failed ? 'pm-warn' : 'pm-intro' }, outcome.summary));
223
+ if (remaining.length) more.append(el('ul', {}, remaining.map((value) => el('li', {}, value))));
224
+ }
225
+ const extra = (wanted || target.custom) && more.childNodes.length > 1 ? more : null;
226
+ if (extra && context === 'settings') details.append(extra);
227
+ return el('div', { class: context === 'onboarding' ? 'pm-card cli-card' : 'pm-row', 'data-provider': target.provider, 'data-target': id },
228
+ details, controls, context === 'onboarding' ? extra : null);
229
+ }
230
+
231
+ function prerequisite(suffix) {
232
+ return snapshot.prerequisites?.find((entry) => entry.helperId === `${snapshot.platform}-${suffix}`);
233
+ }
234
+ function prerequisiteHelper(suffix) {
235
+ return snapshot.helpers.find((entry) => entry.id === `${snapshot.platform}-${suffix}` && entry.available && entry.supported);
236
+ }
237
+ function runPrerequisite(suffix, mode) {
238
+ void perform(mode === 'check' ? text('Checking…', '偵測中…') : text('Setting up…', '準備中…'), async () => {
239
+ async function execute(name, helperMode) {
240
+ const result = await bridge.runSetupHelper(`${snapshot.platform}-${name}`, helperMode);
241
+ const action = result.state?.lastAction;
242
+ if (!action || action.runState === 'failed' || action.status === 'failed') {
243
+ throw new Error(action?.summary || text('Could not finish setup. Try again.', '無法完成設定,請重試。'));
244
+ }
245
+ return action;
246
+ }
247
+ async function prepare(name) {
248
+ const applied = await execute(name, 'apply');
249
+ const verified = await execute(name, 'check');
250
+ if (verified.status !== 'ready') throw new Error([verified.summary, ...(applied.manualSteps || []), ...(verified.manualSteps || [])].join(' '));
251
+ return verified;
252
+ }
253
+ const result = mode === 'apply' ? await prepare(suffix) : await execute(suffix, mode);
254
+ if (suffix === 'node-host-installer' && result.status === 'ready') {
255
+ // Node/npm and the location where their tools are installed are one
256
+ // consumer-facing preparation action. Both helpers remain allowlisted.
257
+ const prefix = await execute('npm-prefix-helper', 'check');
258
+ if (mode === 'apply' && prefix.status !== 'ready') await prepare('npm-prefix-helper');
259
+ }
260
+ feedback(mode === 'check' ? text('Check finished.', '偵測完成。') : text('Setup finished.', '準備完成。'));
261
+ return bridge.getProviderSetup(context);
262
+ });
263
+ }
264
+ function prerequisiteCard() {
265
+ const node = prerequisite('node-host-installer');
266
+ const npm = prerequisite('npm-prefix-helper');
267
+ const github = prerequisite('github-cli-installer');
268
+ const nodeReady = node?.result?.status === 'ready' && node.result.runState !== 'failed';
269
+ const prefixReady = npm?.result?.status === 'ready' && npm.result.runState !== 'failed';
270
+ const checking = node?.checking || npm?.checking;
271
+ const failed = [node, npm].some((entry) => entry?.result?.runState === 'failed' || entry?.result?.status === 'failed');
272
+ const status = checking ? text('Checking…', '偵測中…') : failed ? text('Check needs attention', '偵測需要處理')
273
+ : !node?.result ? text('Not checked yet', '尚未偵測') : !nodeReady ? text('Not installed', '尚未安裝')
274
+ : !npm?.result ? text('Checking setup…', '正在確認設定…')
275
+ : prefixReady ? text('Installed', '已安裝') : text('Finish setup', '尚待設定');
276
+ const controls = el('div', { class: 'pm-actions' });
277
+ const helper = prerequisiteHelper(!nodeReady ? 'node-host-installer' : 'npm-prefix-helper');
278
+ const unavailable = busy || checking || Boolean(confirmation) || !bridge.runSetupHelper;
279
+ if (node?.result && (!nodeReady || (npm?.result && !prefixReady))) {
280
+ controls.append(button(!nodeReady ? text('Install', '安裝') : text('Finish setup', '完成設定'),
281
+ () => runPrerequisite(!nodeReady ? 'node-host-installer' : 'npm-prefix-helper', 'apply'),
282
+ unavailable || !helper?.supportsApply, { 'data-action': 'prepare-node' }));
283
+ }
284
+ controls.append(button(text('Detect', '偵測'), () => runPrerequisite('node-host-installer', 'check'),
285
+ unavailable || !prerequisiteHelper('node-host-installer'), { 'data-action': 'check-node' }));
286
+ const more = el('details', { class: 'pm-more', 'data-details': 'prerequisites' },
287
+ el('summary', {}, text('Details', '詳細資訊')),
288
+ el('small', {}, text('Needed by the tools in this group. Cats checks your computer; nothing is installed automatically.', '這一類工具需要 Node.js/npm。Cats 會先檢查,不會自動安裝。')));
289
+ for (const [entry, label] of [[node, 'Node.js / npm'], [npm, text('Tool installation location', '工具安裝位置')], [github, 'GitHub CLI']]) {
290
+ if (!entry) continue;
291
+ more.append(el('small', {}, `${label}: ${entry.checking ? text('Checking…', '偵測中…') : entry.result?.status === 'ready'
292
+ ? text('Ready', '已就緒') : entry.result?.runState === 'failed' ? text('Check failed', '偵測失敗') : entry.result ? text('Needs setup', '尚待設定') : text('Not checked', '尚未偵測')}`));
293
+ if (entry.result && entry.result.status !== 'ready') more.append(el('small', {}, entry.result.summary));
294
+ }
295
+ if (github && !github.checking && github.result?.status !== 'ready') {
296
+ more.append(button(text('Set up GitHub CLI', '安裝 GitHub CLI'), () => runPrerequisite('github-cli-installer', 'apply'),
297
+ unavailable || !prerequisiteHelper('github-cli-installer')?.supportsApply));
298
+ }
299
+ more.append(button(text('Check GitHub CLI', '偵測 GitHub CLI'), () => runPrerequisite('github-cli-installer', 'check'),
300
+ unavailable || github?.checking || !prerequisiteHelper('github-cli-installer')));
301
+ return el('div', { class: context === 'onboarding' ? 'pm-card cli-card pm-prerequisite' : 'pm-row pm-prerequisite', 'data-prerequisite': 'node' },
302
+ el('div', { class: 'pm-detail' }, el('strong', { class: 'pm-name' }, 'Node.js / npm'),
303
+ el('div', { class: `pm-status ${nodeReady && prefixReady ? 'pm-ok' : ''}` }, checking ? spinner() : null, status)), controls, more);
304
+ }
305
+ function render() {
306
+ if (destroyed) return;
307
+ const scrollTop = root.querySelector('.pm-list')?.scrollTop || 0;
308
+ const opened = new Set([...root.querySelectorAll('details[open][data-details]')].map((node) => node.getAttribute('data-details')));
309
+ const focus = doc.activeElement?.getAttribute('data-focus');
310
+ const cursor = doc.activeElement?.selectionStart;
311
+ root.classList.add('catsProviderManager');
312
+ root.classList.toggle('pm-onboarding', context === 'onboarding');
313
+ const style = el('style', {}, `
314
+ .catsProviderManager{--pm-border:#ddd8d0;--pm-muted:#6b6560;--pm-accent:#a84724;font:inherit;color:inherit;max-width:960px;margin:auto}
315
+ .catsProviderManager *{box-sizing:border-box}.catsProviderManager h2{font-size:1.35rem;margin:0 0 8px}.catsProviderManager p{margin:8px 0}
316
+ .catsProviderManager .pm-intro,.catsProviderManager small{color:var(--pm-muted);font-size:.85rem;display:block;line-height:1.6}
317
+ .catsProviderManager .pm-row{display:flex;justify-content:space-between;align-items:start;gap:16px;padding:18px 0;border-bottom:1px solid var(--pm-border)}
318
+ .catsProviderManager .pm-detail{flex:1;min-width:0}.catsProviderManager .pm-name{display:flex;align-items:center;gap:10px;cursor:pointer}
319
+ .catsProviderManager .pm-list{max-height:min(60vh,640px);overflow-y:auto;overscroll-behavior:contain;padding-right:4px}
320
+ .catsProviderManager input[type=checkbox]{width:19px;height:19px;accent-color:var(--pm-accent);flex-shrink:0;opacity:1}
321
+ .catsProviderManager .pm-status{display:flex;align-items:center;gap:8px;margin:6px 0;font-size:.88rem}.catsProviderManager .pm-ok{color:#207a53}.catsProviderManager .pm-warn{color:#986515}
322
+ .catsProviderManager .pm-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;max-width:330px}
323
+ .catsProviderManager button{font:inherit;font-size:.84rem;cursor:pointer;border:1px solid var(--pm-border);border-radius:7px;background:transparent;color:inherit;padding:8px 12px}
324
+ .catsProviderManager button:hover:not(:disabled){border-color:var(--pm-accent)}.catsProviderManager button:disabled{cursor:default;opacity:.5}
325
+ .catsProviderManager button.pm-primary{background:var(--pm-accent);border-color:var(--pm-accent);color:white}
326
+ .catsProviderManager input[inputmode=url],.catsProviderManager input[type=search]{font:inherit;font-size:.87rem;color:inherit;background:transparent;border:1px solid var(--pm-border);border-radius:6px;padding:8px;width:100%}
327
+ .catsProviderManager .pm-endpoint{display:block;font-size:.8rem;margin-top:9px}.catsProviderManager .pm-endpoint input{margin-top:4px}
328
+ .catsProviderManager .pm-footer{position:sticky;bottom:0;z-index:2;background:var(--panel,#fff);border-top:1px solid var(--pm-border);padding:16px 0;margin-top:16px;display:flex;gap:12px;align-items:center;flex-wrap:wrap}
329
+ .catsProviderManager .pm-footer label{display:flex;align-items:center;gap:8px;font-size:.86rem}.catsProviderManager .pm-continue{margin-left:auto}
330
+ .catsProviderManager .pm-progress{display:flex;gap:10px;align-items:center;padding:12px;background:rgba(196,101,58,.08);border-radius:8px;margin:12px 0}
331
+ .catsProviderManager .pm-spinner{display:inline-block;width:19px;height:19px;border:2px solid var(--pm-border);border-top-color:var(--pm-accent);border-radius:50%;animation:pm-spin .8s linear infinite;flex-shrink:0}
332
+ @keyframes pm-spin{to{transform:rotate(360deg)}}.catsProviderManager a{font-size:.8rem;color:var(--pm-accent)}.catsProviderManager summary{font-size:.83rem;cursor:pointer;padding:7px 0}
333
+ .catsProviderManager .pm-more button{display:block;margin:4px 0}.catsProviderManager .pm-dialog{border:2px solid var(--pm-accent);border-radius:10px;padding:20px;margin:16px 0;background:var(--panel,#fff);max-height:45vh;overflow:auto}
334
+ .catsProviderManager .pm-more{max-width:100%;overflow-wrap:anywhere}.catsProviderManager .pm-row.pm-prerequisite{flex-wrap:wrap}.catsProviderManager .pm-row.pm-prerequisite>.pm-more{flex-basis:100%}
335
+ .catsProviderManager.pm-onboarding{max-width:720px;text-align:center;width:100%}
336
+ .catsProviderManager.pm-onboarding h2{font-size:1.1rem;font-weight:500}.catsProviderManager.pm-onboarding .pm-intro{font-size:.87rem}
337
+ .catsProviderManager.pm-onboarding .pm-list{max-height:none;overflow:visible;padding:0}
338
+ .catsProviderManager .pm-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;text-align:center}
339
+ .catsProviderManager .pm-card{background:var(--panel,#fff);border:1px solid var(--pm-border);border-radius:12px;padding:14px 10px 12px;display:flex;flex-direction:column;align-items:stretch;gap:8px;min-width:0;box-shadow:0 1px 3px rgba(0,0,0,.04)}
340
+ .catsProviderManager .pm-card .pm-name{justify-content:center;gap:7px;font-size:.92rem;line-height:1.4;min-height:24px}
341
+ .catsProviderManager .pm-card input[type=checkbox]{width:16px;height:16px}.catsProviderManager .pm-card .pm-status{justify-content:center;font-size:.74rem;margin:8px 0 0;min-height:20px}
342
+ .catsProviderManager .pm-card .pm-actions{flex-direction:column;align-items:stretch;max-width:none;gap:6px}
343
+ .catsProviderManager .pm-card button{font-size:.78rem;border-radius:8px;padding:6px 8px;width:100%}
344
+ .catsProviderManager .pm-card .pm-more{margin-top:auto}.catsProviderManager .pm-card .pm-more[open]{text-align:left}
345
+ .catsProviderManager .pm-card .pm-more small{font-size:.76rem}.catsProviderManager .pm-card .pm-more summary{text-align:center;font-size:.74rem;padding:0}
346
+ .catsProviderManager .pm-group{grid-column:1/-1;text-align:left;color:var(--pm-muted);font-size:.78rem;font-weight:500;padding:10px 0 0;border-top:1px solid var(--pm-border)}
347
+ .catsProviderManager.pm-onboarding .pm-footer{position:static;justify-content:center;border:0;background:transparent;margin:14px 0 22px;padding:0;gap:10px}
348
+ .catsProviderManager.pm-onboarding .pm-continue{margin-left:0}.catsProviderManager.pm-onboarding button.pm-primary{background:var(--panel,#fff);border-color:var(--pm-border);color:inherit}
349
+ .catsProviderManager.pm-onboarding .pm-progress{justify-content:center}.catsProviderManager.pm-onboarding .pm-discard{font-size:.78rem}
350
+ .catsProviderManager [role=alert]{color:#b2342c;overflow-wrap:anywhere}.catsProviderManager li{margin-left:20px;font-size:.83rem;overflow-wrap:anywhere}
351
+ @media(max-width:580px){.catsProviderManager .pm-row{flex-direction:column}.catsProviderManager .pm-actions{max-width:none;justify-content:flex-start}.catsProviderManager .pm-footer{gap:8px}.catsProviderManager .pm-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
352
+ @media(prefers-reduced-motion:reduce){.catsProviderManager .pm-spinner{animation-duration:2s}}
353
+ `);
354
+ const heading = el('h2', {}, context === 'onboarding' ? text('Choose your AI tools', '選擇你的 AI 工具') : text('AI tools', 'AI 工具'));
355
+ const intro = el('p', { class: 'pm-intro' }, text('Choose what you want to use. Cats can help you install it. You can change this later in Settings.', '勾選想使用的工具,Cats 可以協助安裝。之後也能在設定中調整。'));
356
+ const content = [style, heading, intro];
357
+ if (error) content.push(el('div', { role: 'alert' }, error, ' ', button(text('Refresh', '重新讀取'), () => { signature = ''; void refresh(); }, busy)));
358
+ if (!snapshot) {
359
+ content.push(el('div', { class: 'pm-progress', role: 'status' }, spinner(), text('Loading tools…', '正在載入工具…')));
360
+ root.replaceChildren(...content); return;
361
+ }
362
+ const selection = snapshot.runtime.selection;
363
+ const invalid = ['missing', 'invalid'].includes(selection.state);
364
+ const operationBusy = snapshot.operations.length > 0 || snapshot.runtime.state?.status === 'scanning';
365
+ const dirty = changed();
366
+ if (selection.diskChanged || revision !== selection.revision) content.push(el('div', { role: 'alert' }, text('Configuration changed elsewhere. Refresh or reload before applying your changes.', '設定已在其他地方變更。請重新讀取或載入後再套用。'), ' ', button(text('Discard draft and reload', '捨棄草稿並重新載入'), () => {
367
+ revision = selection.revision; apply(true);
368
+ }, busy || operationBusy)));
369
+ else if (selection.error) content.push(el('p', { role: 'alert' }, selection.error));
370
+ if (snapshot.runtime.state?.error) content.push(el('p', { role: 'alert' }, snapshot.runtime.state.error));
371
+ if (busy || operationBusy) content.push(el('div', { class: 'pm-progress', role: 'status', 'aria-live': 'polite' }, spinner(), stage || text('Checking your tools…', '正在檢查工具…')));
372
+ if (notice && !busy && !operationBusy) content.push(el('p', { role: 'status' }, notice));
373
+ if (context === 'settings') content.push(el('input', { type: 'search', value: filter, placeholder: text('Find a tool', '尋找工具'), 'data-focus': 'search',
374
+ 'aria-label': text('Find a tool', '尋找工具'), oninput: (event) => { filter = event.target.value; render(); } }));
375
+ const catalog = snapshot.runtime.universe.slice();
376
+ for (const target of selection.targets) if (!catalog.some((entry) => key(entry) === key(target))) catalog.push({ ...target, familyLabel: `${target.provider} · ${target.instance}`, custom: true });
377
+ const list = el('div', { class: 'pm-list' });
378
+ if (context === 'onboarding') {
379
+ const nativeOrder = ['claude', 'antigravity', 'cursor', 'kiro', 'junie', 'goose', 'grok', 'devin', 'muse', 'ollama'];
380
+ const npmOrder = ['codex', 'copilot', 'opencode', 'kilo', 'auggie', 'pi', 'cline'];
381
+ const primary = ['claude', 'antigravity', 'codex'];
382
+ const show = (target) => expanded || primary.includes(target.provider) || draft.has(key(target));
383
+ const grid = el('div', { class: 'pm-grid cli-grid' });
384
+ for (const id of nativeOrder) for (const target of catalog.filter((entry) => !entry.custom && entry.provider === id && show(entry))) grid.append(row(target, selection, operationBusy));
385
+ if (expanded) grid.append(el('div', { class: 'pm-group', 'data-group': 'npm' }, text('Tools that use Node.js / npm', '使用 Node.js/npm 的工具')));
386
+ grid.append(prerequisiteCard());
387
+ for (const id of npmOrder) for (const target of catalog.filter((entry) => !entry.custom && entry.provider === id && show(entry))) grid.append(row(target, selection, operationBusy));
388
+ const others = catalog.filter((entry) => (entry.custom || (!nativeOrder.includes(entry.provider) && !npmOrder.includes(entry.provider))) && show(entry));
389
+ if (others.length) grid.append(el('div', { class: 'pm-group', 'data-group': 'connections' }, text('Other connections', '其他連線')),
390
+ ...others.map((target) => row(target, selection, operationBusy)));
391
+ list.append(grid);
392
+ } else {
393
+ if (!filter) list.append(prerequisiteCard());
394
+ list.append(...catalog.filter((target) => `${target.familyLabel} ${target.provider}`.toLowerCase().includes(filter.toLowerCase())).map((target) => row(target, selection, operationBusy)));
395
+ }
396
+ content.push(list);
397
+ if (confirmation) {
398
+ const current = confirmation;
399
+ content.push(el('div', { role: 'dialog', 'aria-modal': 'true', 'aria-label': text('Confirm uninstall', '確認解除安裝'), class: 'pm-dialog' },
400
+ el('h3', {}, text('Uninstall ', '解除安裝 ') + current.target.provider + '?'),
401
+ el('p', {}, current.result.summary),
402
+ el('ul', {}, [...current.result.plannedActions, ...current.result.warnings, ...current.result.manualSteps].map((value) => el('li', {}, value))),
403
+ button(text('Cancel', '取消'), () => { confirmation = null; render(); }, busy), ' ',
404
+ button(text('Uninstall', '解除安裝'), () => { confirmation = null; run('uninstall', [current.target], current.revision); }, busy || current.revision !== revision || changed() || selection.diskChanged || current.result.runState === 'failed' || !current.result.plannedActions.length),
405
+ ));
406
+ }
407
+ if (dirty) content.push(button(text('Discard changes', '捨棄變更'), () => { confirmation = null; accept(snapshot, true); render(); }, busy || operationBusy || Boolean(confirmation), { class: 'pm-discard' }));
408
+ const footer = el('div', { class: 'pm-footer' });
409
+ if (dirty || invalid) {
410
+ footer.append(button(text('Apply', 'Apply'), () => apply(), busy || operationBusy || Boolean(confirmation) || Boolean(error) || selection.diskChanged || revision !== selection.revision, { class: 'pm-primary', 'data-action': 'apply' }),
411
+ el('label', {}, el('input', { type: 'checkbox', checked: detectAfter, disabled: busy,
412
+ 'data-focus': 'detect-after', 'data-action': 'detect-after', onchange: (event) => { detectAfter = event.target.checked; } }), text('Detect after applying', '套用後偵測')));
413
+ } else {
414
+ footer.append(button(text('Detect selected', '重新偵測已選項目'), () => run('detect', selection.targets), busy || operationBusy || Boolean(confirmation) || Boolean(error) || selection.diskChanged || !selection.targets.length, { 'data-action': 'detect-selected' }));
415
+ }
416
+ if (options.onContinue) footer.append(button(text('Continue', '繼續'), () => void perform(text('Continuing…', '繼續設定…'), async () => { await options.onContinue(); }), invalid || dirty || busy || operationBusy || Boolean(confirmation) || Boolean(error) || selection.diskChanged, { class: 'pm-primary pm-continue', 'data-action': 'continue' }));
417
+ if (context === 'onboarding') {
418
+ footer.append(button(expanded ? text('Show fewer', '顯示較少') : text('Show more', '顯示更多'), () => { expanded = !expanded; render(); }, false, { 'data-action': 'show-more' }));
419
+ content.splice(content.indexOf(list), 0, footer);
420
+ } else content.push(footer);
421
+ if (!invalid && !dirty) {
422
+ content.push(el('p', { class: 'pm-intro' }, selection.targets.length === 0
423
+ ? text('You can choose your tools later in Settings.', '你可以稍後再到設定中選擇工具。')
424
+ : text(`${selection.targets.length} tools selected. You can continue and finish installing later.`, `已選擇 ${selection.targets.length} 項工具。可以先繼續,稍後再完成安裝。`)));
425
+ }
426
+ root.replaceChildren(...content);
427
+ for (const node of root.querySelectorAll('details[data-details]')) node.open = opened.has(node.getAttribute('data-details'));
428
+ if (list) list.scrollTop = scrollTop;
429
+ if (focus) {
430
+ const next = [...root.querySelectorAll('[data-focus]')].find((node) => node.getAttribute('data-focus') === focus);
431
+ next?.focus({ preventScroll: true });
432
+ if (typeof cursor === 'number') { try { next?.setSelectionRange(cursor, cursor); } catch { /* URL inputs have no selection API. */ } }
433
+ }
434
+ }
435
+ render();
436
+ void refresh();
437
+ const timer = win.setInterval(() => { if (doc.visibilityState !== 'hidden') void refresh(); }, 2000);
438
+ const onFocus = () => void refresh();
439
+ win.addEventListener('focus', onFocus);
440
+ return { refresh, destroy() { destroyed = true; win.clearInterval(timer); win.removeEventListener('focus', onFocus); root.replaceChildren(); } };
441
+ }
package/scripts/README.md CHANGED
@@ -17,6 +17,29 @@ scripts/
17
17
 
18
18
  Follow `docs/SCRIPT-STANDARDS.md` for naming and documentation rules.
19
19
 
20
+ ## Desktop provider helpers (2026-09-16)
21
+
22
+ Onboarding and Settings > Runtime use the same selection and per-provider
23
+ operations. Apply only saves intent; optional detection and explicit Install,
24
+ Upgrade, Repair, and previewed Uninstall are separate actions. Installing an
25
+ npm provider prepares Node/npm only when needed. An explicit npm-provider
26
+ Upgrade also checks and updates npm in its current prefix, with engine checks
27
+ and post-install verification; Apply and Detect do not upgrade npm.
28
+
29
+ The native helper port is reconciled through environment-bootstrap `752dc13`
30
+ from the prior Muse port `6f59feb`. See PLAN-107 for the commit applicability
31
+ matrix. `_NativeInstallerSupport.ps1` / `provider-version-common.sh` contain
32
+ bounded published-version queries and guarded old-build cleanup. The verified
33
+ npm support files are packaged on all three native operating systems.
34
+
35
+ Check and DryRun never invoke installers. Native vendor failure remains a
36
+ failure even if an older command survives. Results include `previousVersion`,
37
+ `observedChange`, and a summary; an unchanged version does not establish that
38
+ files were refreshed. Authentication remains a separate manual step. Custom
39
+ npm prefixes are preserved; helpers refresh their own command paths before
40
+ post-install checks. WSL/Docker variants are outside this native onboarding
41
+ rollout.
42
+
20
43
  ## Ingress Smoke Helper
21
44
 
22
45
  `cats-platform` now also ships a small cross-platform ingress probe: