@harness-mix/cli 0.1.12 → 0.2.1

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 (84) hide show
  1. package/CHANGELOG.md +15 -2
  2. package/NOTICE +14 -0
  3. package/README.md +4 -24
  4. package/docs/multi-agent-collaboration.md +1 -1
  5. package/licenses/anthropic-codex-theme-MIT.txt +21 -0
  6. package/licenses/codex-dream-skin-MIT.txt +21 -0
  7. package/output/native-build/renderer-extension.js +703 -58
  8. package/package.json +192 -189
  9. package/scripts/collaboration-test.cjs +262 -200
  10. package/scripts/collaboration-ui-smoke.cjs +85 -77
  11. package/scripts/core-concurrency-test.cjs +23 -1
  12. package/scripts/e2e-pi-cancel.cjs +99 -0
  13. package/scripts/native-file-change-test.cjs +42 -2
  14. package/scripts/native-sidebar-test.cjs +8 -0
  15. package/scripts/native-vendor-adapters-test.cjs +67 -1
  16. package/scripts/pets-test.cjs +63 -0
  17. package/scripts/pi-cancel-race-test.cjs +102 -0
  18. package/scripts/stuck-turn-test.cjs +73 -0
  19. package/scripts/workspace-test.cjs +9 -0
  20. package/src/assets/skins/dream-skin/NOTICE.md +13 -0
  21. package/src/assets/skins/dream-skin/themes/gothic-void-crusade/hero.webp +0 -0
  22. package/src/assets/skins/dream-skin/themes/gothic-void-crusade/theme.json +34 -0
  23. package/src/main/adapters/claude.js +9 -6
  24. package/src/main/adapters/pi-family.js +41 -8
  25. package/src/main/harness-adapter/event-normalizer.js +270 -258
  26. package/src/main/host/collaboration-mcp.cjs +8 -1
  27. package/src/main/host/collaboration-tools.js +1 -1
  28. package/src/main/host/collaboration.js +81 -11
  29. package/src/main/host/runtime.js +1420 -1341
  30. package/src/main/native/pets.js +79 -2
  31. package/src/main/native/protocol.js +1160 -1145
  32. package/src/main/native/thread-list.js +6 -1
  33. package/src/main/workspace/core-review.js +75 -0
  34. package/src/main/workspace/diff.js +5 -1
  35. package/src/main/workspace/file-changes.js +9 -4
  36. package/src/main/workspace/review-controller.js +115 -102
  37. package/src/main/workspace/review.js +90 -88
  38. package/src/native-ui/renderer-extension/dist/contract-audit.js +16637 -0
  39. package/src/native-ui/renderer-extension/dist/index.js +34853 -0
  40. package/src/native-ui/renderer-extension/dist/production.js +34263 -0
  41. package/src/native-ui/renderer-extension/dist/renderer-binding-probe.js +34254 -0
  42. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  43. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +11 -0
  44. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  45. package/src/native-ui/renderer-extension/dist/types/renderer-pet-switcher.d.ts +73 -0
  46. package/src/native-ui/renderer-extension/dist/types/renderer-pet-switcher.d.ts.map +1 -0
  47. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
  48. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  49. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
  50. package/src/native-ui/renderer-extension/dist/types/settings/collaboration-page.d.ts +12 -0
  51. package/src/native-ui/renderer-extension/dist/types/settings/collaboration-page.d.ts.map +1 -0
  52. package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts +1 -1
  53. package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts.map +1 -1
  54. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  55. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +5 -3
  56. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  57. package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts +1 -1
  58. package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts.map +1 -1
  59. package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts +9 -0
  60. package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts.map +1 -1
  61. package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts +1 -1
  62. package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts.map +1 -1
  63. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  64. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3181 -3171
  65. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +14 -0
  66. package/src/native-ui/renderer-extension/src/renderer-pet-switcher.ts +308 -0
  67. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
  68. package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +76 -21
  69. package/src/native-ui/renderer-extension/src/settings/collaboration-page.ts +139 -0
  70. package/src/native-ui/renderer-extension/src/settings/icons.ts +155 -153
  71. package/src/native-ui/renderer-extension/src/settings/localization.ts +785 -783
  72. package/src/native-ui/renderer-extension/src/settings/pages.ts +694 -681
  73. package/src/native-ui/renderer-extension/src/settings/pet-market.css +110 -1
  74. package/src/native-ui/renderer-extension/src/settings/pet-market.ts +276 -22
  75. package/src/native-ui/renderer-extension/src/settings/pets-client.ts +32 -0
  76. package/src/native-ui/renderer-extension/src/settings/shell.css +2294 -2255
  77. package/src/native-ui/renderer-extension/src/settings/skin-runtime.ts +51 -1
  78. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +2 -0
  79. package/src/native-ui/renderer-extension/test/renderer-pet-switcher.test.ts +400 -0
  80. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +80 -79
  81. package/src/native-ui/renderer-extension/test/settings/pet-market.test.ts +378 -0
  82. package/src/native-ui/renderer-extension/test/settings/pets-client.test.ts +83 -0
  83. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
  84. package/src/native-ui/renderer-extension/test/settings/skin-runtime.test.ts +15 -1
@@ -1,681 +1,694 @@
1
- import type {
2
- UpdateCheckResult,
3
- UpdateInstallation,
4
- UpdateStartResult,
5
- UpdateStatus,
6
- UpdateStatusResult,
7
- } from "@harnessmix/shared-contracts";
8
-
9
- import {
10
- createRendererSettingsPageRegistry,
11
- type RendererSettingsPageDefinition,
12
- type RendererSettingsPageMountContext,
13
- type RendererSettingsPageRegistry,
14
- } from "./core.js";
15
- import { createRendererSettingsIcon, type RendererSettingsIconName } from "./icons.js";
16
- import {
17
- DEFAULT_RENDERER_SETTINGS_MESSAGES,
18
- type RendererSettingsMessages,
19
- } from "./localization.js";
20
- import {
21
- createConnectionsSettingsPage,
22
- type RendererConnectionDiagnostics,
23
- } from "./connections-page.js";
24
- import {
25
- createSessionImportSettingsPage,
26
- type RendererSessionImportClient,
27
- type RendererImportedThreadOpener,
28
- } from "./session-import-page.js";
29
- import { createReleaseNotesElement } from "./release-notes.js";
30
- import { createMcpSettingsPage, createSkillsSettingsPage } from './integrations-page.js';
31
- import type { RendererIntegrationsClient } from '../renderer-integrations-client.js';
32
- import { createAccountsSettingsPage, type RendererCodexAccountClient } from "./accounts-page.js";
33
- import { createStorageSettingsPage, type RendererStorageClient } from './storage-page.js';
34
- import { createSkinSettingsPage } from "./skin-market.js";
35
- import { createPetSettingsPage } from "./pet-market.js";
36
- import type { RendererPetsClient } from "./pets-client.js";
37
-
38
- export type {
39
- RendererConnectionAgentSnapshot,
40
- RendererConnectionDiagnostics,
41
- RendererConnectionHostSnapshot,
42
- RendererConnectionSnapshot,
43
- } from "./connections-page.js";
44
- import {
45
- RendererUpdateRequestTimeoutError,
46
- runBoundedRendererUpdateRequest,
47
- } from "./update-request.js";
48
-
49
- export const HARNESSMIX_GITHUB_REPOSITORY_URL = "https://github.com/emo-xiaoyu/harness-mix";
50
- export const HARNESSMIX_RELEASES_LATEST_URL = `${HARNESSMIX_GITHUB_REPOSITORY_URL}/releases/latest`;
51
- export const HARNESSMIX_NPM_MANUAL_UPDATE_COMMAND = "npm install -g harness-mix@latest";
52
-
53
- interface RendererUserAgentData {
54
- readonly platform?: string;
55
- readonly architecture?: string;
56
- readonly bitness?: string;
57
- }
58
-
59
- function rendererUserAgentData(navigator: Navigator): RendererUserAgentData | undefined {
60
- return (navigator as Navigator & { userAgentData?: RendererUserAgentData }).userAgentData;
61
- }
62
-
63
- function isWindowsRenderer(window: Window | null | undefined): boolean {
64
- const navigator = window?.navigator;
65
- if (!navigator) return false;
66
- const identity = `${rendererUserAgentData(navigator)?.platform ?? ""} ${navigator.platform ?? ""} ${navigator.userAgent}`;
67
- return /windows|win32|win64/iu.test(identity);
68
- }
69
-
70
- function windowsInstallerDownloadUrl(window: Window | null | undefined, version: string): string {
71
- const navigator = window?.navigator;
72
- const hints = navigator ? rendererUserAgentData(navigator) : undefined;
73
- const identity = `${hints?.architecture ?? ""} ${hints?.platform ?? ""} ${navigator?.platform ?? ""} ${navigator?.userAgent ?? ""}`;
74
- const architecture = /arm64|aarch64|\barm\b/iu.test(identity) ? "arm64" : "x64";
75
- return `https://github.com/emo-xiaoyu/harness-mix/releases/download/v${version}/harness-mix-${version}-windows-${architecture}.exe`;
76
- }
77
-
78
- export const DEFAULT_RENDERER_SETTINGS_PAGE_IDS = [
79
- "connections",
80
- "accounts",
81
- "mcp",
82
- "skills",
83
- "session-import",
84
- "storage",
85
- "skins",
86
- "pets",
87
- "updates",
88
- "about",
89
- ] as const;
90
-
91
- export type DefaultRendererSettingsPageId = (typeof DEFAULT_RENDERER_SETTINGS_PAGE_IDS)[number];
92
-
93
- export interface RendererUpdateClient {
94
- checkUpdate(): Promise<UpdateCheckResult>;
95
- startUpdate(): Promise<UpdateStartResult>;
96
- readUpdateStatus(): Promise<UpdateStatusResult>;
97
- readCurrentVersion?(): Promise<{ version: string }>;
98
- }
99
-
100
- function panelIconName(view: string): RendererSettingsIconName {
101
- if (view === "failed" || view === "error") return "alert";
102
- if (view === "unavailable") return "unavailable";
103
- if (view === "current") return "check";
104
- return "updates";
105
- }
106
-
107
- function createPanelHead(document: Document, view: string, title: string): HTMLElement {
108
- const head = document.createElement("div");
109
- head.className = "settings-update-panel__head";
110
- const label = document.createElement("strong");
111
- label.className = "settings-update-panel__title";
112
- label.textContent = title;
113
- head.append(createRendererSettingsIcon(panelIconName(view), 16), label);
114
- return head;
115
- }
116
-
117
- function createPanelActions(document: Document, ...buttons: readonly HTMLElement[]): HTMLElement {
118
- const actions = document.createElement("div");
119
- actions.className = "settings-update-actions";
120
- actions.append(...buttons);
121
- return actions;
122
- }
123
-
124
- function installationLabel(
125
- installation: UpdateInstallation | null,
126
- messages: RendererSettingsMessages,
127
- ): string {
128
- if (installation === "npm") return messages.updateInstallationNpm;
129
- if (installation === "windows-installer") {
130
- return messages.updateInstallationWindowsInstaller;
131
- }
132
- if (installation === "macos-dmg") return messages.updateInstallationMacOsDmg;
133
- return messages.updateInstallationUnknown;
134
- }
135
-
136
- function isPendingStatus(status: UpdateStatus | null): boolean {
137
- return status !== null && status.phase !== "succeeded" && status.phase !== "failed";
138
- }
139
-
140
- function statusMessage(
141
- status: UpdateStatus | null,
142
- messages: RendererSettingsMessages,
143
- ): string | null {
144
- if (!status) return null;
145
- if (status.phase === "succeeded") return messages.updateSucceeded;
146
- if (status.phase === "failed") return status.error ?? messages.updateFailed;
147
- if (status.phase === "waiting-for-exit") return messages.updateWaitingForExit;
148
- if (status.phase === "installing") {
149
- return status.installation === "npm" ? messages.updateInstallingNpm : messages.updateInstalling;
150
- }
151
- if (status.phase === "restarting") return messages.updateRestarting;
152
- if (status.phase === "downloading") return messages.updateDownloading;
153
- return messages.updatePreparing;
154
- }
155
-
156
- function formatUpdateBytes(value: number): string {
157
- if (value < 1024) return `${value} B`;
158
- const units = ["KB", "MB", "GB"];
159
- let scaled = value;
160
- let unit = "B";
161
- for (const nextUnit of units) {
162
- scaled /= 1024;
163
- unit = nextUnit;
164
- if (scaled < 1024 || nextUnit === units.at(-1)) break;
165
- }
166
- return `${scaled.toFixed(scaled >= 10 ? 0 : 1)} ${unit}`;
167
- }
168
-
169
- export function aboutPage(
170
- messages: RendererSettingsMessages,
171
- getClient: () => RendererUpdateClient | null = () => null,
172
- ): RendererSettingsPageDefinition {
173
- return Object.freeze({
174
- id: "about",
175
- label: messages.pageLabels.about,
176
- icon: "about",
177
- mount(context: RendererSettingsPageMountContext) {
178
- const document = context.content.ownerDocument;
179
- const heading = document.createElement("div");
180
- heading.className = "settings-section-label";
181
- heading.textContent = messages.pageLabels.about;
182
-
183
- const panel = document.createElement("section");
184
- panel.className = "settings-about-panel";
185
- const product = document.createElement("strong");
186
- product.className = "settings-about-product";
187
- product.textContent = "Harness Mix";
188
- const tagline = document.createElement("strong");
189
- tagline.className = "settings-about-tagline";
190
- tagline.textContent = messages.aboutTagline;
191
- const versionRow = document.createElement("div");
192
- versionRow.className = "settings-about-version-row";
193
- const versionBadge = document.createElement("span");
194
- versionBadge.className = "settings-about-version-badge";
195
- versionBadge.textContent = "v…";
196
- const checkUpdateBtn = document.createElement("button");
197
- checkUpdateBtn.type = "button";
198
- checkUpdateBtn.className = "settings-command-button settings-command-button--secondary";
199
- checkUpdateBtn.append(createRendererSettingsIcon("updates", 14), messages.checkUpdateNow);
200
- checkUpdateBtn.addEventListener("click", () => {
201
- const shell = document.querySelector("[data-harnessmix-settings-shell]");
202
- const updatesNavBtn = (shell?.shadowRoot ?? document).querySelector<HTMLButtonElement>(
203
- 'button[data-page-id="updates"]',
204
- );
205
- updatesNavBtn?.click();
206
- });
207
- versionRow.append(versionBadge, checkUpdateBtn);
208
- const introduction = document.createElement("div");
209
- introduction.className = "settings-about-copy";
210
- for (const paragraphText of messages.aboutParagraphs) {
211
- const paragraph = document.createElement("p");
212
- paragraph.textContent = paragraphText;
213
- introduction.append(paragraph);
214
- }
215
- const starCallout = document.createElement("p");
216
- starCallout.className = "settings-about-star-callout";
217
- starCallout.textContent = messages.aboutStarCallout;
218
- const repositorySection = document.createElement("div");
219
- repositorySection.className = "settings-about-repository";
220
- const openSource = document.createElement("p");
221
- openSource.textContent = messages.aboutOpenSource;
222
- const repository = document.createElement("a");
223
- repository.className = "settings-about-repository-link";
224
- repository.href = HARNESSMIX_GITHUB_REPOSITORY_URL;
225
- repository.target = "_blank";
226
- repository.rel = "noopener noreferrer";
227
- const repositoryUrl = document.createElement("code");
228
- repositoryUrl.textContent = HARNESSMIX_GITHUB_REPOSITORY_URL;
229
- repository.append(
230
- createRendererSettingsIcon("external-link", 14),
231
- messages.aboutRepository,
232
- repositoryUrl,
233
- );
234
- repositorySection.append(openSource, repository);
235
- panel.append(product, tagline, versionRow, introduction, starCallout, repositorySection);
236
- context.content.append(heading, panel);
237
- const client = getClient();
238
- const readVersion = client?.readCurrentVersion
239
- ? () => client.readCurrentVersion!()
240
- : client
241
- ? () => client.checkUpdate().then(({ currentVersion }) => ({ version: currentVersion }))
242
- : null;
243
- if (readVersion) {
244
- void context.runLatest(readVersion, {
245
- success(result) {
246
- versionBadge.textContent = `v${result.version}`;
247
- },
248
- failure() {
249
- versionBadge.textContent = "v—";
250
- },
251
- });
252
- } else {
253
- versionBadge.textContent = "v—";
254
- }
255
- return undefined;
256
- },
257
- });
258
- }
259
-
260
- export function updatesPage(
261
- messages: RendererSettingsMessages,
262
- getClient: () => RendererUpdateClient | null,
263
- ): RendererSettingsPageDefinition {
264
- return Object.freeze({
265
- id: "updates",
266
- label: messages.pageLabels.updates,
267
- icon: "updates",
268
- mount(context: RendererSettingsPageMountContext) {
269
- const document = context.content.ownerDocument;
270
- const windows = isWindowsRenderer(document.defaultView);
271
- const heading = document.createElement("div");
272
- heading.className = "settings-section-label";
273
- heading.textContent = messages.pageLabels.updates;
274
-
275
- // Version summary: current, latest, and installation sit side by side so the
276
- // comparison is readable without scrolling.
277
- const metadata = document.createElement("div");
278
- metadata.className = "settings-update-metadata";
279
- const createMetadataItem = (label: string): HTMLElement => {
280
- const item = document.createElement("div");
281
- item.className = "settings-update-metadata__item";
282
- const name = document.createElement("span");
283
- name.textContent = label;
284
- const value = document.createElement("strong");
285
- value.textContent = "-";
286
- item.append(name, value);
287
- metadata.append(item);
288
- return value;
289
- };
290
- const currentVersionValue = createMetadataItem(messages.updateCurrentVersion);
291
- const latestVersionValue = createMetadataItem(messages.updateLatestVersion);
292
- const installationValue = createMetadataItem(messages.updateInstallation);
293
-
294
- const panel = document.createElement("section");
295
- panel.className = "settings-update-panel";
296
- panel.setAttribute("aria-live", "polite");
297
-
298
- // Manual update stays visible directly under the status panel: automatic
299
- // updates can fail for reasons local to the machine, and the fallback path
300
- // should never be more than a glance away.
301
- const controls = document.createElement("div");
302
- controls.className = "settings-update-controls";
303
- const manualTitle = document.createElement("div");
304
- manualTitle.className = "settings-update-manual-title";
305
- manualTitle.textContent = messages.updateManualTitle;
306
- const manualNpm = document.createElement("div");
307
- manualNpm.className = "settings-update-manual";
308
- manualNpm.hidden = true;
309
- const manualNpmDescription = document.createElement("p");
310
- manualNpmDescription.className = "settings-update-manual-description";
311
- manualNpmDescription.textContent = messages.updateManualNpmDescription;
312
- const manualNpmCommandRow = document.createElement("div");
313
- manualNpmCommandRow.className = "settings-update-command";
314
- const manualNpmCommand = document.createElement("code");
315
- manualNpmCommand.textContent = HARNESSMIX_NPM_MANUAL_UPDATE_COMMAND;
316
- const copyCommand = document.createElement("button");
317
- copyCommand.type = "button";
318
- copyCommand.className = "settings-update-command__copy";
319
- const setCopyLabel = (label: string): void => {
320
- copyCommand.replaceChildren(createRendererSettingsIcon("copy", 14), label);
321
- };
322
- setCopyLabel(messages.updateCopyCommand);
323
- copyCommand.addEventListener("click", () => {
324
- const clipboard = document.defaultView?.navigator.clipboard;
325
- const restore = (label: string): void => {
326
- setCopyLabel(label);
327
- document.defaultView?.setTimeout(() => setCopyLabel(messages.updateCopyCommand), 2_000);
328
- };
329
- if (!clipboard) {
330
- restore(messages.updateCopyFailed);
331
- return;
332
- }
333
- void clipboard.writeText(HARNESSMIX_NPM_MANUAL_UPDATE_COMMAND).then(
334
- () => restore(messages.updateCommandCopied),
335
- () => restore(messages.updateCopyFailed),
336
- );
337
- });
338
- manualNpmCommandRow.append(manualNpmCommand, copyCommand);
339
- manualNpm.append(manualNpmDescription, manualNpmCommandRow);
340
- const manualWindowsInstaller = document.createElement("div");
341
- manualWindowsInstaller.className = "settings-update-manual";
342
- manualWindowsInstaller.hidden = true;
343
- const manualWindowsInstallerDescription = document.createElement("p");
344
- manualWindowsInstallerDescription.className = "settings-update-manual-description";
345
- manualWindowsInstallerDescription.textContent = messages.updateWindowsInstallerDescription;
346
- const manualWindowsInstallerActions = document.createElement("div");
347
- manualWindowsInstallerActions.className = "settings-update-actions";
348
- const manualWindowsInstallerLink = document.createElement("a");
349
- manualWindowsInstallerLink.className = "settings-update-link";
350
- manualWindowsInstallerLink.href = HARNESSMIX_RELEASES_LATEST_URL;
351
- manualWindowsInstallerLink.target = "_blank";
352
- manualWindowsInstallerLink.rel = "noopener noreferrer";
353
- manualWindowsInstallerLink.append(
354
- messages.updateDownloadWindowsInstaller,
355
- createRendererSettingsIcon("external-link", 14),
356
- );
357
- manualWindowsInstallerActions.append(manualWindowsInstallerLink);
358
- manualWindowsInstaller.append(
359
- manualWindowsInstallerDescription,
360
- manualWindowsInstallerActions,
361
- );
362
- const actions = document.createElement("div");
363
- actions.className = "settings-update-actions";
364
- const releaseLink = document.createElement("a");
365
- releaseLink.className = "settings-update-link";
366
- releaseLink.href = HARNESSMIX_RELEASES_LATEST_URL;
367
- releaseLink.target = "_blank";
368
- releaseLink.rel = "noopener noreferrer";
369
- releaseLink.append(
370
- messages.updateDownloadFromReleases,
371
- createRendererSettingsIcon("external-link", 14),
372
- );
373
- actions.append(releaseLink);
374
- controls.append(manualTitle, manualNpm, manualWindowsInstaller, actions);
375
-
376
- // Release notes render below the fold, in the page scroller rather than a
377
- // nested one.
378
- const notes = document.createElement("div");
379
- notes.className = "settings-update-notes-section";
380
-
381
- context.content.append(heading, metadata, panel, controls, notes);
382
-
383
- // Presentation-only: emphasise the manual path once the automatic one has
384
- // visibly failed.
385
- const setManualFallback = (fallback: boolean): void => {
386
- manualNpmDescription.textContent = windows
387
- ? messages.updateWindowsNpmDescription
388
- : fallback
389
- ? messages.updateManualFallbackDescription
390
- : messages.updateManualNpmDescription;
391
- manualNpmDescription.className = fallback
392
- ? "settings-update-manual-description is-fallback"
393
- : "settings-update-manual-description";
394
- };
395
- let pollTimer: number | undefined;
396
- let pollAttempts = 0;
397
- let pending = false;
398
-
399
- const clearPoll = (): void => {
400
- if (pollTimer !== undefined) {
401
- document.defaultView?.clearTimeout(pollTimer);
402
- pollTimer = undefined;
403
- }
404
- };
405
-
406
- const renderUnavailable = (detail: string): void => {
407
- panel.dataset.updateState = "unavailable";
408
- panel.replaceChildren();
409
- const copy = document.createElement("p");
410
- copy.className = "settings-update-summary";
411
- copy.textContent = detail;
412
- panel.append(createPanelHead(document, "unavailable", messages.notAvailable), copy);
413
- notes.replaceChildren();
414
- };
415
-
416
- const renderRequestFailure = (error: unknown): void => {
417
- renderPendingStatus(
418
- null,
419
- error instanceof RendererUpdateRequestTimeoutError
420
- ? messages.updateRequestTimeout
421
- : error instanceof Error
422
- ? error.message
423
- : messages.updateFailed,
424
- "failed",
425
- );
426
- };
427
-
428
- const scheduleStatusPoll = (client: RendererUpdateClient, resetAttempts = false): void => {
429
- clearPoll();
430
- if (resetAttempts) pollAttempts = 0;
431
- if (pollAttempts >= 320) {
432
- renderPendingStatus(null, messages.updateRequestTimeout, "failed");
433
- return;
434
- }
435
- pollAttempts += 1;
436
- pollTimer = document.defaultView?.setTimeout(() => {
437
- void context.runLatest(
438
- (signal) => runBoundedRendererUpdateRequest(() => client.readUpdateStatus(), signal),
439
- {
440
- success(result) {
441
- const message = statusMessage(result.status, messages);
442
- if (isPendingStatus(result.status)) scheduleStatusPoll(client);
443
- if (message) renderPendingStatus(result.status, message);
444
- },
445
- failure(error) {
446
- renderRequestFailure(error);
447
- },
448
- },
449
- );
450
- }, 750);
451
- };
452
-
453
- const renderPendingStatus = (
454
- status: UpdateStatus | null,
455
- message: string,
456
- viewPhase: UpdateStatus["phase"] | "pending" = status?.phase ?? "pending",
457
- ): void => {
458
- panel.dataset.updateState = viewPhase;
459
- panel.replaceChildren();
460
- panel.append(createPanelHead(document, viewPhase, message));
461
- setManualFallback(viewPhase === "failed");
462
- if (
463
- status?.phase === "downloading" &&
464
- status.totalBytes !== undefined &&
465
- status.downloadedBytes !== undefined
466
- ) {
467
- const progress = document.createElement("progress");
468
- progress.className = "settings-update-progress";
469
- progress.max = status.totalBytes;
470
- progress.value = Math.min(status.downloadedBytes, status.totalBytes);
471
- progress.setAttribute("aria-label", messages.updateDownloading);
472
- const detail = document.createElement("span");
473
- detail.className = "settings-update-progress-detail";
474
- const percent = Math.min(
475
- 100,
476
- Math.round((status.downloadedBytes / status.totalBytes) * 1000) / 10,
477
- );
478
- detail.textContent = `${percent}% · ${formatUpdateBytes(status.downloadedBytes)} / ${formatUpdateBytes(status.totalBytes)}`;
479
- panel.append(progress, detail);
480
- }
481
- if (viewPhase === "failed") {
482
- const retry = document.createElement("button");
483
- retry.type = "button";
484
- retry.className = "settings-command-button";
485
- retry.append(createRendererSettingsIcon("refresh", 16), messages.updateRetry);
486
- retry.addEventListener("click", () => void load());
487
- panel.append(createPanelActions(document, retry));
488
- }
489
- };
490
-
491
- const start = (client: RendererUpdateClient): void => {
492
- if (pending) return;
493
- pending = true;
494
- renderPendingStatus(null, messages.updatePreparing);
495
- void context.runLatest(
496
- (signal) => runBoundedRendererUpdateRequest(() => client.startUpdate(), signal),
497
- {
498
- success(result) {
499
- pending = false;
500
- renderPendingStatus(
501
- result.status,
502
- statusMessage(result.status, messages) ?? messages.updatePreparing,
503
- );
504
- if (isPendingStatus(result.status)) scheduleStatusPoll(client, true);
505
- },
506
- failure(error) {
507
- pending = false;
508
- renderRequestFailure(error);
509
- },
510
- },
511
- );
512
- };
513
-
514
- const renderCheck = (result: UpdateCheckResult, client: RendererUpdateClient): void => {
515
- currentVersionValue.textContent = `v${result.currentVersion}`;
516
- latestVersionValue.textContent = result.latestVersion ? `v${result.latestVersion}` : "-";
517
- latestVersionValue.className = result.updateAvailable
518
- ? "settings-update-metadata__value--newer"
519
- : "";
520
- installationValue.textContent = installationLabel(result.installation, messages);
521
- manualNpm.hidden = result.installation !== "npm";
522
- manualWindowsInstaller.hidden = !windows || result.installation !== "windows-installer";
523
- releaseLink.hidden = windows;
524
- manualTitle.hidden =
525
- windows && !["npm", "windows-installer"].includes(result.installation ?? "");
526
- if (windows && result.installation === "windows-installer" && result.latestVersion) {
527
- manualWindowsInstallerLink.href = windowsInstallerDownloadUrl(
528
- document.defaultView,
529
- result.latestVersion,
530
- );
531
- }
532
- if (result.releaseNotesUrl) releaseLink.href = result.releaseNotesUrl;
533
- const operationMessage = statusMessage(result.status, messages);
534
- if (isPendingStatus(result.status)) {
535
- renderPendingStatus(result.status, operationMessage ?? messages.updatePreparing);
536
- scheduleStatusPoll(client, true);
537
- return;
538
- }
539
- const actionableStatus =
540
- result.status?.phase === "failed" && result.status.version === result.latestVersion
541
- ? result.status
542
- : null;
543
- const view = result.error ? "error" : result.updateAvailable ? "available" : "current";
544
- panel.dataset.updateState = view;
545
- panel.replaceChildren();
546
- setManualFallback(Boolean(result.error) || actionableStatus !== null);
547
- if (result.error || !result.updateAvailable || windows || actionableStatus) {
548
- panel.append(
549
- createPanelHead(
550
- document,
551
- view,
552
- actionableStatus
553
- ? (statusMessage(actionableStatus, messages) ?? messages.updateFailed)
554
- : result.error
555
- ? messages.updateFailed
556
- : result.updateAvailable
557
- ? messages.updateWindowsManualRequired
558
- : messages.updateUpToDate,
559
- ),
560
- );
561
- }
562
- if (actionableStatus?.error) {
563
- const error = document.createElement("p");
564
- error.className = "settings-update-error";
565
- error.textContent = actionableStatus.error;
566
- panel.append(error);
567
- }
568
- if (result.error) {
569
- const error = document.createElement("p");
570
- error.className = "settings-update-error";
571
- error.textContent = result.error;
572
- panel.append(error);
573
- }
574
- const buttons: HTMLElement[] = [];
575
- if (!windows && result.updateAvailable && result.installationAvailable) {
576
- const update = document.createElement("button");
577
- update.type = "button";
578
- update.className = "settings-command-button";
579
- update.append(createRendererSettingsIcon("updates", 16), messages.updateAndRestart);
580
- update.addEventListener("click", () => start(client));
581
- buttons.push(update);
582
- }
583
- if (result.error) {
584
- const retry = document.createElement("button");
585
- retry.type = "button";
586
- retry.className = "settings-command-button settings-command-button--secondary";
587
- retry.append(createRendererSettingsIcon("refresh", 16), messages.updateRetry);
588
- retry.addEventListener("click", () => void load());
589
- buttons.push(retry);
590
- }
591
- if (buttons.length > 0) panel.append(createPanelActions(document, ...buttons));
592
- notes.replaceChildren();
593
- if (result.releaseNotes) {
594
- notes.append(createReleaseNotesElement(document, result.releaseNotes));
595
- }
596
- };
597
-
598
- const load = (): Promise<void> => {
599
- const client = getClient();
600
- if (!client) {
601
- renderUnavailable(messages.runtimeCapabilityNotInstalled);
602
- return Promise.resolve();
603
- }
604
- pending = true;
605
- renderPendingStatus(null, messages.updateChecking);
606
- return context.runLatest(
607
- (signal) => runBoundedRendererUpdateRequest(() => client.checkUpdate(), signal),
608
- {
609
- success(result) {
610
- pending = false;
611
- renderCheck(result, client);
612
- },
613
- failure(error) {
614
- pending = false;
615
- renderRequestFailure(error);
616
- },
617
- },
618
- );
619
- };
620
-
621
- void load();
622
- return clearPoll;
623
- },
624
- });
625
- }
626
-
627
- export function createDefaultRendererSettingsPages(
628
- messages: RendererSettingsMessages = DEFAULT_RENDERER_SETTINGS_MESSAGES,
629
- getUpdateClient: () => RendererUpdateClient | null = () => null,
630
- getDiagnostics: () => RendererConnectionDiagnostics | null = () => null,
631
- getAccountClient: () => RendererCodexAccountClient | null = () => null,
632
- getSessionImportClient: () => RendererSessionImportClient | null = () => null,
633
- openImportedThread: RendererImportedThreadOpener = () =>
634
- Promise.reject(new Error("Imported Thread navigation is unavailable")),
635
- getIntegrationsClient: () => RendererIntegrationsClient | null = () => null,
636
- getStorageClient: () => RendererStorageClient | null = () => null,
637
- getPetClient: () => RendererPetsClient | null = () => null,
638
- ): readonly RendererSettingsPageDefinition[] {
639
- return Object.freeze([
640
- createConnectionsSettingsPage(messages, getDiagnostics),
641
- createAccountsSettingsPage(messages, getAccountClient),
642
- createMcpSettingsPage(messages, getIntegrationsClient),
643
- createSkillsSettingsPage(messages, getIntegrationsClient),
644
- createSessionImportSettingsPage(messages, getSessionImportClient, openImportedThread),
645
- createStorageSettingsPage(messages, getStorageClient),
646
- createSkinSettingsPage(messages),
647
- createPetSettingsPage(messages, getPetClient),
648
- updatesPage(messages, getUpdateClient),
649
- aboutPage(messages, getUpdateClient),
650
- ]);
651
- }
652
-
653
- export function createDefaultRendererSettingsRegistry(
654
- messages: RendererSettingsMessages = DEFAULT_RENDERER_SETTINGS_MESSAGES,
655
- getUpdateClient: () => RendererUpdateClient | null = () => null,
656
- getDiagnostics: () => RendererConnectionDiagnostics | null = () => null,
657
- getAccountClient: () => RendererCodexAccountClient | null = () => null,
658
- getSessionImportClient: () => RendererSessionImportClient | null = () => null,
659
- openImportedThread?: RendererImportedThreadOpener,
660
- getIntegrationsClient?: () => RendererIntegrationsClient | null,
661
- getStorageClient?: () => RendererStorageClient | null,
662
- getPetClient?: () => RendererPetsClient | null,
663
- ): RendererSettingsPageRegistry {
664
- return createRendererSettingsPageRegistry(
665
- createDefaultRendererSettingsPages(
666
- messages,
667
- getUpdateClient,
668
- getDiagnostics,
669
- getAccountClient,
670
- getSessionImportClient,
671
- openImportedThread,
672
- getIntegrationsClient,
673
- getStorageClient,
674
- getPetClient,
675
- ),
676
- );
677
- }
678
-
679
- export type { RendererCodexAccountClient } from "./accounts-page.js";
680
- export type { RendererStorageClient, RendererStorageInspection } from './storage-page.js';
681
- export type { RendererPetsClient } from "./pets-client.js";
1
+ import type {
2
+ UpdateCheckResult,
3
+ UpdateInstallation,
4
+ UpdateStartResult,
5
+ UpdateStatus,
6
+ UpdateStatusResult,
7
+ } from "@harnessmix/shared-contracts";
8
+
9
+ import {
10
+ createRendererSettingsPageRegistry,
11
+ type RendererSettingsPageDefinition,
12
+ type RendererSettingsPageMountContext,
13
+ type RendererSettingsPageRegistry,
14
+ } from "./core.js";
15
+ import { createRendererSettingsIcon, type RendererSettingsIconName } from "./icons.js";
16
+ import {
17
+ DEFAULT_RENDERER_SETTINGS_MESSAGES,
18
+ type RendererSettingsMessages,
19
+ } from "./localization.js";
20
+ import {
21
+ createConnectionsSettingsPage,
22
+ type RendererConnectionDiagnostics,
23
+ } from "./connections-page.js";
24
+ import {
25
+ createSessionImportSettingsPage,
26
+ type RendererSessionImportClient,
27
+ type RendererImportedThreadOpener,
28
+ } from "./session-import-page.js";
29
+ import { createReleaseNotesElement } from "./release-notes.js";
30
+ import { createMcpSettingsPage, createSkillsSettingsPage } from './integrations-page.js';
31
+ import type { RendererIntegrationsClient } from '../renderer-integrations-client.js';
32
+ import { createAccountsSettingsPage, type RendererCodexAccountClient } from "./accounts-page.js";
33
+ import { createStorageSettingsPage, type RendererStorageClient } from './storage-page.js';
34
+ import { createSkinSettingsPage } from "./skin-market.js";
35
+ import { createPetSettingsPage } from "./pet-market.js";
36
+ import type { RendererPetsClient } from "./pets-client.js";
37
+ import {
38
+ createCollaborationSettingsPage,
39
+ type RendererCollaborationClient,
40
+ } from "./collaboration-page.js";
41
+
42
+ export type {
43
+ RendererConnectionAgentSnapshot,
44
+ RendererConnectionDiagnostics,
45
+ RendererConnectionHostSnapshot,
46
+ RendererConnectionSnapshot,
47
+ } from "./connections-page.js";
48
+ import {
49
+ RendererUpdateRequestTimeoutError,
50
+ runBoundedRendererUpdateRequest,
51
+ } from "./update-request.js";
52
+
53
+ export const HARNESSMIX_GITHUB_REPOSITORY_URL = "https://github.com/emo-xiaoyu/harness-mix";
54
+ export const HARNESSMIX_RELEASES_LATEST_URL = `${HARNESSMIX_GITHUB_REPOSITORY_URL}/releases/latest`;
55
+ export const HARNESSMIX_NPM_MANUAL_UPDATE_COMMAND = "npm install -g harness-mix@latest";
56
+
57
+ interface RendererUserAgentData {
58
+ readonly platform?: string;
59
+ readonly architecture?: string;
60
+ readonly bitness?: string;
61
+ }
62
+
63
+ function rendererUserAgentData(navigator: Navigator): RendererUserAgentData | undefined {
64
+ return (navigator as Navigator & { userAgentData?: RendererUserAgentData }).userAgentData;
65
+ }
66
+
67
+ function isWindowsRenderer(window: Window | null | undefined): boolean {
68
+ const navigator = window?.navigator;
69
+ if (!navigator) return false;
70
+ const identity = `${rendererUserAgentData(navigator)?.platform ?? ""} ${navigator.platform ?? ""} ${navigator.userAgent}`;
71
+ return /windows|win32|win64/iu.test(identity);
72
+ }
73
+
74
+ function windowsInstallerDownloadUrl(window: Window | null | undefined, version: string): string {
75
+ const navigator = window?.navigator;
76
+ const hints = navigator ? rendererUserAgentData(navigator) : undefined;
77
+ const identity = `${hints?.architecture ?? ""} ${hints?.platform ?? ""} ${navigator?.platform ?? ""} ${navigator?.userAgent ?? ""}`;
78
+ const architecture = /arm64|aarch64|\barm\b/iu.test(identity) ? "arm64" : "x64";
79
+ return `https://github.com/emo-xiaoyu/harness-mix/releases/download/v${version}/harness-mix-${version}-windows-${architecture}.exe`;
80
+ }
81
+
82
+ export const DEFAULT_RENDERER_SETTINGS_PAGE_IDS = [
83
+ "connections",
84
+ "collaboration",
85
+ "accounts",
86
+ "mcp",
87
+ "skills",
88
+ "session-import",
89
+ "storage",
90
+ "skins",
91
+ "pets",
92
+ "updates",
93
+ "about",
94
+ ] as const;
95
+
96
+ export type DefaultRendererSettingsPageId = (typeof DEFAULT_RENDERER_SETTINGS_PAGE_IDS)[number];
97
+
98
+ export interface RendererUpdateClient {
99
+ checkUpdate(): Promise<UpdateCheckResult>;
100
+ startUpdate(): Promise<UpdateStartResult>;
101
+ readUpdateStatus(): Promise<UpdateStatusResult>;
102
+ readCurrentVersion?(): Promise<{ version: string }>;
103
+ }
104
+
105
+ function panelIconName(view: string): RendererSettingsIconName {
106
+ if (view === "failed" || view === "error") return "alert";
107
+ if (view === "unavailable") return "unavailable";
108
+ if (view === "current") return "check";
109
+ return "updates";
110
+ }
111
+
112
+ function createPanelHead(document: Document, view: string, title: string): HTMLElement {
113
+ const head = document.createElement("div");
114
+ head.className = "settings-update-panel__head";
115
+ const label = document.createElement("strong");
116
+ label.className = "settings-update-panel__title";
117
+ label.textContent = title;
118
+ head.append(createRendererSettingsIcon(panelIconName(view), 16), label);
119
+ return head;
120
+ }
121
+
122
+ function createPanelActions(document: Document, ...buttons: readonly HTMLElement[]): HTMLElement {
123
+ const actions = document.createElement("div");
124
+ actions.className = "settings-update-actions";
125
+ actions.append(...buttons);
126
+ return actions;
127
+ }
128
+
129
+ function installationLabel(
130
+ installation: UpdateInstallation | null,
131
+ messages: RendererSettingsMessages,
132
+ ): string {
133
+ if (installation === "npm") return messages.updateInstallationNpm;
134
+ if (installation === "windows-installer") {
135
+ return messages.updateInstallationWindowsInstaller;
136
+ }
137
+ if (installation === "macos-dmg") return messages.updateInstallationMacOsDmg;
138
+ return messages.updateInstallationUnknown;
139
+ }
140
+
141
+ function isPendingStatus(status: UpdateStatus | null): boolean {
142
+ return status !== null && status.phase !== "succeeded" && status.phase !== "failed";
143
+ }
144
+
145
+ function statusMessage(
146
+ status: UpdateStatus | null,
147
+ messages: RendererSettingsMessages,
148
+ ): string | null {
149
+ if (!status) return null;
150
+ if (status.phase === "succeeded") return messages.updateSucceeded;
151
+ if (status.phase === "failed") return status.error ?? messages.updateFailed;
152
+ if (status.phase === "waiting-for-exit") return messages.updateWaitingForExit;
153
+ if (status.phase === "installing") {
154
+ return status.installation === "npm" ? messages.updateInstallingNpm : messages.updateInstalling;
155
+ }
156
+ if (status.phase === "restarting") return messages.updateRestarting;
157
+ if (status.phase === "downloading") return messages.updateDownloading;
158
+ return messages.updatePreparing;
159
+ }
160
+
161
+ function formatUpdateBytes(value: number): string {
162
+ if (value < 1024) return `${value} B`;
163
+ const units = ["KB", "MB", "GB"];
164
+ let scaled = value;
165
+ let unit = "B";
166
+ for (const nextUnit of units) {
167
+ scaled /= 1024;
168
+ unit = nextUnit;
169
+ if (scaled < 1024 || nextUnit === units.at(-1)) break;
170
+ }
171
+ return `${scaled.toFixed(scaled >= 10 ? 0 : 1)} ${unit}`;
172
+ }
173
+
174
+ export function aboutPage(
175
+ messages: RendererSettingsMessages,
176
+ getClient: () => RendererUpdateClient | null = () => null,
177
+ ): RendererSettingsPageDefinition {
178
+ return Object.freeze({
179
+ id: "about",
180
+ label: messages.pageLabels.about,
181
+ icon: "about",
182
+ mount(context: RendererSettingsPageMountContext) {
183
+ const document = context.content.ownerDocument;
184
+ const heading = document.createElement("div");
185
+ heading.className = "settings-section-label";
186
+ heading.textContent = messages.pageLabels.about;
187
+
188
+ const panel = document.createElement("section");
189
+ panel.className = "settings-about-panel";
190
+ const product = document.createElement("strong");
191
+ product.className = "settings-about-product";
192
+ product.textContent = "Harness Mix";
193
+ const tagline = document.createElement("strong");
194
+ tagline.className = "settings-about-tagline";
195
+ tagline.textContent = messages.aboutTagline;
196
+ const versionRow = document.createElement("div");
197
+ versionRow.className = "settings-about-version-row";
198
+ const versionBadge = document.createElement("span");
199
+ versionBadge.className = "settings-about-version-badge";
200
+ versionBadge.textContent = "v…";
201
+ const checkUpdateBtn = document.createElement("button");
202
+ checkUpdateBtn.type = "button";
203
+ checkUpdateBtn.className = "settings-command-button settings-command-button--secondary";
204
+ checkUpdateBtn.append(createRendererSettingsIcon("updates", 14), messages.checkUpdateNow);
205
+ checkUpdateBtn.addEventListener("click", () => {
206
+ const shell = document.querySelector("[data-harnessmix-settings-shell]");
207
+ const updatesNavBtn = (shell?.shadowRoot ?? document).querySelector<HTMLButtonElement>(
208
+ 'button[data-page-id="updates"]',
209
+ );
210
+ updatesNavBtn?.click();
211
+ });
212
+ versionRow.append(versionBadge, checkUpdateBtn);
213
+ const introduction = document.createElement("div");
214
+ introduction.className = "settings-about-copy";
215
+ for (const paragraphText of messages.aboutParagraphs) {
216
+ const paragraph = document.createElement("p");
217
+ paragraph.textContent = paragraphText;
218
+ introduction.append(paragraph);
219
+ }
220
+ const starCallout = document.createElement("p");
221
+ starCallout.className = "settings-about-star-callout";
222
+ starCallout.textContent = messages.aboutStarCallout;
223
+ const repositorySection = document.createElement("div");
224
+ repositorySection.className = "settings-about-repository";
225
+ const openSource = document.createElement("p");
226
+ openSource.textContent = messages.aboutOpenSource;
227
+ const repository = document.createElement("a");
228
+ repository.className = "settings-about-repository-link";
229
+ repository.href = HARNESSMIX_GITHUB_REPOSITORY_URL;
230
+ repository.target = "_blank";
231
+ repository.rel = "noopener noreferrer";
232
+ const repositoryUrl = document.createElement("code");
233
+ repositoryUrl.textContent = HARNESSMIX_GITHUB_REPOSITORY_URL;
234
+ repository.append(
235
+ createRendererSettingsIcon("external-link", 14),
236
+ messages.aboutRepository,
237
+ repositoryUrl,
238
+ );
239
+ repositorySection.append(openSource, repository);
240
+ panel.append(product, tagline, versionRow, introduction, starCallout, repositorySection);
241
+ context.content.append(heading, panel);
242
+ const client = getClient();
243
+ const readVersion = client?.readCurrentVersion
244
+ ? () => client.readCurrentVersion!()
245
+ : client
246
+ ? () => client.checkUpdate().then(({ currentVersion }) => ({ version: currentVersion }))
247
+ : null;
248
+ if (readVersion) {
249
+ void context.runLatest(readVersion, {
250
+ success(result) {
251
+ versionBadge.textContent = `v${result.version}`;
252
+ },
253
+ failure() {
254
+ versionBadge.textContent = "v—";
255
+ },
256
+ });
257
+ } else {
258
+ versionBadge.textContent = "v—";
259
+ }
260
+ return undefined;
261
+ },
262
+ });
263
+ }
264
+
265
+ export function updatesPage(
266
+ messages: RendererSettingsMessages,
267
+ getClient: () => RendererUpdateClient | null,
268
+ ): RendererSettingsPageDefinition {
269
+ return Object.freeze({
270
+ id: "updates",
271
+ label: messages.pageLabels.updates,
272
+ icon: "updates",
273
+ mount(context: RendererSettingsPageMountContext) {
274
+ const document = context.content.ownerDocument;
275
+ const windows = isWindowsRenderer(document.defaultView);
276
+ const heading = document.createElement("div");
277
+ heading.className = "settings-section-label";
278
+ heading.textContent = messages.pageLabels.updates;
279
+
280
+ // Version summary: current, latest, and installation sit side by side so the
281
+ // comparison is readable without scrolling.
282
+ const metadata = document.createElement("div");
283
+ metadata.className = "settings-update-metadata";
284
+ const createMetadataItem = (label: string): HTMLElement => {
285
+ const item = document.createElement("div");
286
+ item.className = "settings-update-metadata__item";
287
+ const name = document.createElement("span");
288
+ name.textContent = label;
289
+ const value = document.createElement("strong");
290
+ value.textContent = "-";
291
+ item.append(name, value);
292
+ metadata.append(item);
293
+ return value;
294
+ };
295
+ const currentVersionValue = createMetadataItem(messages.updateCurrentVersion);
296
+ const latestVersionValue = createMetadataItem(messages.updateLatestVersion);
297
+ const installationValue = createMetadataItem(messages.updateInstallation);
298
+
299
+ const panel = document.createElement("section");
300
+ panel.className = "settings-update-panel";
301
+ panel.setAttribute("aria-live", "polite");
302
+
303
+ // Manual update stays visible directly under the status panel: automatic
304
+ // updates can fail for reasons local to the machine, and the fallback path
305
+ // should never be more than a glance away.
306
+ const controls = document.createElement("div");
307
+ controls.className = "settings-update-controls";
308
+ const manualTitle = document.createElement("div");
309
+ manualTitle.className = "settings-update-manual-title";
310
+ manualTitle.textContent = messages.updateManualTitle;
311
+ const manualNpm = document.createElement("div");
312
+ manualNpm.className = "settings-update-manual";
313
+ manualNpm.hidden = true;
314
+ const manualNpmDescription = document.createElement("p");
315
+ manualNpmDescription.className = "settings-update-manual-description";
316
+ manualNpmDescription.textContent = messages.updateManualNpmDescription;
317
+ const manualNpmCommandRow = document.createElement("div");
318
+ manualNpmCommandRow.className = "settings-update-command";
319
+ const manualNpmCommand = document.createElement("code");
320
+ manualNpmCommand.textContent = HARNESSMIX_NPM_MANUAL_UPDATE_COMMAND;
321
+ const copyCommand = document.createElement("button");
322
+ copyCommand.type = "button";
323
+ copyCommand.className = "settings-update-command__copy";
324
+ const setCopyLabel = (label: string): void => {
325
+ copyCommand.replaceChildren(createRendererSettingsIcon("copy", 14), label);
326
+ };
327
+ setCopyLabel(messages.updateCopyCommand);
328
+ copyCommand.addEventListener("click", () => {
329
+ const clipboard = document.defaultView?.navigator.clipboard;
330
+ const restore = (label: string): void => {
331
+ setCopyLabel(label);
332
+ document.defaultView?.setTimeout(() => setCopyLabel(messages.updateCopyCommand), 2_000);
333
+ };
334
+ if (!clipboard) {
335
+ restore(messages.updateCopyFailed);
336
+ return;
337
+ }
338
+ void clipboard.writeText(HARNESSMIX_NPM_MANUAL_UPDATE_COMMAND).then(
339
+ () => restore(messages.updateCommandCopied),
340
+ () => restore(messages.updateCopyFailed),
341
+ );
342
+ });
343
+ manualNpmCommandRow.append(manualNpmCommand, copyCommand);
344
+ manualNpm.append(manualNpmDescription, manualNpmCommandRow);
345
+ const manualWindowsInstaller = document.createElement("div");
346
+ manualWindowsInstaller.className = "settings-update-manual";
347
+ manualWindowsInstaller.hidden = true;
348
+ const manualWindowsInstallerDescription = document.createElement("p");
349
+ manualWindowsInstallerDescription.className = "settings-update-manual-description";
350
+ manualWindowsInstallerDescription.textContent = messages.updateWindowsInstallerDescription;
351
+ const manualWindowsInstallerActions = document.createElement("div");
352
+ manualWindowsInstallerActions.className = "settings-update-actions";
353
+ const manualWindowsInstallerLink = document.createElement("a");
354
+ manualWindowsInstallerLink.className = "settings-update-link";
355
+ manualWindowsInstallerLink.href = HARNESSMIX_RELEASES_LATEST_URL;
356
+ manualWindowsInstallerLink.target = "_blank";
357
+ manualWindowsInstallerLink.rel = "noopener noreferrer";
358
+ manualWindowsInstallerLink.append(
359
+ messages.updateDownloadWindowsInstaller,
360
+ createRendererSettingsIcon("external-link", 14),
361
+ );
362
+ manualWindowsInstallerActions.append(manualWindowsInstallerLink);
363
+ manualWindowsInstaller.append(
364
+ manualWindowsInstallerDescription,
365
+ manualWindowsInstallerActions,
366
+ );
367
+ const actions = document.createElement("div");
368
+ actions.className = "settings-update-actions";
369
+ const releaseLink = document.createElement("a");
370
+ releaseLink.className = "settings-update-link";
371
+ releaseLink.href = HARNESSMIX_RELEASES_LATEST_URL;
372
+ releaseLink.target = "_blank";
373
+ releaseLink.rel = "noopener noreferrer";
374
+ releaseLink.append(
375
+ messages.updateDownloadFromReleases,
376
+ createRendererSettingsIcon("external-link", 14),
377
+ );
378
+ actions.append(releaseLink);
379
+ controls.append(manualTitle, manualNpm, manualWindowsInstaller, actions);
380
+
381
+ // Release notes render below the fold, in the page scroller rather than a
382
+ // nested one.
383
+ const notes = document.createElement("div");
384
+ notes.className = "settings-update-notes-section";
385
+
386
+ context.content.append(heading, metadata, panel, controls, notes);
387
+
388
+ // Presentation-only: emphasise the manual path once the automatic one has
389
+ // visibly failed.
390
+ const setManualFallback = (fallback: boolean): void => {
391
+ manualNpmDescription.textContent = windows
392
+ ? messages.updateWindowsNpmDescription
393
+ : fallback
394
+ ? messages.updateManualFallbackDescription
395
+ : messages.updateManualNpmDescription;
396
+ manualNpmDescription.className = fallback
397
+ ? "settings-update-manual-description is-fallback"
398
+ : "settings-update-manual-description";
399
+ };
400
+ let pollTimer: number | undefined;
401
+ let pollAttempts = 0;
402
+ let pending = false;
403
+
404
+ const clearPoll = (): void => {
405
+ if (pollTimer !== undefined) {
406
+ document.defaultView?.clearTimeout(pollTimer);
407
+ pollTimer = undefined;
408
+ }
409
+ };
410
+
411
+ const renderUnavailable = (detail: string): void => {
412
+ panel.dataset.updateState = "unavailable";
413
+ panel.replaceChildren();
414
+ const copy = document.createElement("p");
415
+ copy.className = "settings-update-summary";
416
+ copy.textContent = detail;
417
+ panel.append(createPanelHead(document, "unavailable", messages.notAvailable), copy);
418
+ notes.replaceChildren();
419
+ };
420
+
421
+ const renderRequestFailure = (error: unknown): void => {
422
+ renderPendingStatus(
423
+ null,
424
+ error instanceof RendererUpdateRequestTimeoutError
425
+ ? messages.updateRequestTimeout
426
+ : error instanceof Error
427
+ ? error.message
428
+ : messages.updateFailed,
429
+ "failed",
430
+ );
431
+ };
432
+
433
+ const scheduleStatusPoll = (client: RendererUpdateClient, resetAttempts = false): void => {
434
+ clearPoll();
435
+ if (resetAttempts) pollAttempts = 0;
436
+ if (pollAttempts >= 320) {
437
+ renderPendingStatus(null, messages.updateRequestTimeout, "failed");
438
+ return;
439
+ }
440
+ pollAttempts += 1;
441
+ pollTimer = document.defaultView?.setTimeout(() => {
442
+ void context.runLatest(
443
+ (signal) => runBoundedRendererUpdateRequest(() => client.readUpdateStatus(), signal),
444
+ {
445
+ success(result) {
446
+ const message = statusMessage(result.status, messages);
447
+ if (isPendingStatus(result.status)) scheduleStatusPoll(client);
448
+ if (message) renderPendingStatus(result.status, message);
449
+ },
450
+ failure(error) {
451
+ renderRequestFailure(error);
452
+ },
453
+ },
454
+ );
455
+ }, 750);
456
+ };
457
+
458
+ const renderPendingStatus = (
459
+ status: UpdateStatus | null,
460
+ message: string,
461
+ viewPhase: UpdateStatus["phase"] | "pending" = status?.phase ?? "pending",
462
+ ): void => {
463
+ panel.dataset.updateState = viewPhase;
464
+ panel.replaceChildren();
465
+ panel.append(createPanelHead(document, viewPhase, message));
466
+ setManualFallback(viewPhase === "failed");
467
+ if (
468
+ status?.phase === "downloading" &&
469
+ status.totalBytes !== undefined &&
470
+ status.downloadedBytes !== undefined
471
+ ) {
472
+ const progress = document.createElement("progress");
473
+ progress.className = "settings-update-progress";
474
+ progress.max = status.totalBytes;
475
+ progress.value = Math.min(status.downloadedBytes, status.totalBytes);
476
+ progress.setAttribute("aria-label", messages.updateDownloading);
477
+ const detail = document.createElement("span");
478
+ detail.className = "settings-update-progress-detail";
479
+ const percent = Math.min(
480
+ 100,
481
+ Math.round((status.downloadedBytes / status.totalBytes) * 1000) / 10,
482
+ );
483
+ detail.textContent = `${percent}% · ${formatUpdateBytes(status.downloadedBytes)} / ${formatUpdateBytes(status.totalBytes)}`;
484
+ panel.append(progress, detail);
485
+ }
486
+ if (viewPhase === "failed") {
487
+ const retry = document.createElement("button");
488
+ retry.type = "button";
489
+ retry.className = "settings-command-button";
490
+ retry.append(createRendererSettingsIcon("refresh", 16), messages.updateRetry);
491
+ retry.addEventListener("click", () => void load());
492
+ panel.append(createPanelActions(document, retry));
493
+ }
494
+ };
495
+
496
+ const start = (client: RendererUpdateClient): void => {
497
+ if (pending) return;
498
+ pending = true;
499
+ renderPendingStatus(null, messages.updatePreparing);
500
+ void context.runLatest(
501
+ (signal) => runBoundedRendererUpdateRequest(() => client.startUpdate(), signal),
502
+ {
503
+ success(result) {
504
+ pending = false;
505
+ renderPendingStatus(
506
+ result.status,
507
+ statusMessage(result.status, messages) ?? messages.updatePreparing,
508
+ );
509
+ if (isPendingStatus(result.status)) scheduleStatusPoll(client, true);
510
+ },
511
+ failure(error) {
512
+ pending = false;
513
+ renderRequestFailure(error);
514
+ },
515
+ },
516
+ );
517
+ };
518
+
519
+ const renderCheck = (result: UpdateCheckResult, client: RendererUpdateClient): void => {
520
+ currentVersionValue.textContent = `v${result.currentVersion}`;
521
+ latestVersionValue.textContent = result.latestVersion ? `v${result.latestVersion}` : "-";
522
+ latestVersionValue.className = result.updateAvailable
523
+ ? "settings-update-metadata__value--newer"
524
+ : "";
525
+ installationValue.textContent = installationLabel(result.installation, messages);
526
+ manualNpm.hidden = result.installation !== "npm";
527
+ manualWindowsInstaller.hidden = !windows || result.installation !== "windows-installer";
528
+ releaseLink.hidden = windows;
529
+ manualTitle.hidden =
530
+ windows && !["npm", "windows-installer"].includes(result.installation ?? "");
531
+ if (windows && result.installation === "windows-installer" && result.latestVersion) {
532
+ manualWindowsInstallerLink.href = windowsInstallerDownloadUrl(
533
+ document.defaultView,
534
+ result.latestVersion,
535
+ );
536
+ }
537
+ if (result.releaseNotesUrl) releaseLink.href = result.releaseNotesUrl;
538
+ const operationMessage = statusMessage(result.status, messages);
539
+ if (isPendingStatus(result.status)) {
540
+ renderPendingStatus(result.status, operationMessage ?? messages.updatePreparing);
541
+ scheduleStatusPoll(client, true);
542
+ return;
543
+ }
544
+ const actionableStatus =
545
+ result.status?.phase === "failed" && result.status.version === result.latestVersion
546
+ ? result.status
547
+ : null;
548
+ const view = result.error ? "error" : result.updateAvailable ? "available" : "current";
549
+ panel.dataset.updateState = view;
550
+ panel.replaceChildren();
551
+ setManualFallback(Boolean(result.error) || actionableStatus !== null);
552
+ if (result.error || !result.updateAvailable || windows || actionableStatus) {
553
+ panel.append(
554
+ createPanelHead(
555
+ document,
556
+ view,
557
+ actionableStatus
558
+ ? (statusMessage(actionableStatus, messages) ?? messages.updateFailed)
559
+ : result.error
560
+ ? messages.updateFailed
561
+ : result.updateAvailable
562
+ ? messages.updateWindowsManualRequired
563
+ : messages.updateUpToDate,
564
+ ),
565
+ );
566
+ }
567
+ if (actionableStatus?.error) {
568
+ const error = document.createElement("p");
569
+ error.className = "settings-update-error";
570
+ error.textContent = actionableStatus.error;
571
+ panel.append(error);
572
+ }
573
+ if (result.error) {
574
+ const error = document.createElement("p");
575
+ error.className = "settings-update-error";
576
+ error.textContent = result.error;
577
+ panel.append(error);
578
+ }
579
+ const buttons: HTMLElement[] = [];
580
+ if (!windows && result.updateAvailable && result.installationAvailable) {
581
+ const update = document.createElement("button");
582
+ update.type = "button";
583
+ update.className = "settings-command-button";
584
+ update.append(createRendererSettingsIcon("updates", 16), messages.updateAndRestart);
585
+ update.addEventListener("click", () => start(client));
586
+ buttons.push(update);
587
+ }
588
+ if (result.error) {
589
+ const retry = document.createElement("button");
590
+ retry.type = "button";
591
+ retry.className = "settings-command-button settings-command-button--secondary";
592
+ retry.append(createRendererSettingsIcon("refresh", 16), messages.updateRetry);
593
+ retry.addEventListener("click", () => void load());
594
+ buttons.push(retry);
595
+ }
596
+ if (buttons.length > 0) panel.append(createPanelActions(document, ...buttons));
597
+ notes.replaceChildren();
598
+ if (result.releaseNotes) {
599
+ notes.append(createReleaseNotesElement(document, result.releaseNotes));
600
+ }
601
+ };
602
+
603
+ const load = (): Promise<void> => {
604
+ const client = getClient();
605
+ if (!client) {
606
+ renderUnavailable(messages.runtimeCapabilityNotInstalled);
607
+ return Promise.resolve();
608
+ }
609
+ pending = true;
610
+ renderPendingStatus(null, messages.updateChecking);
611
+ return context.runLatest(
612
+ (signal) => runBoundedRendererUpdateRequest(() => client.checkUpdate(), signal),
613
+ {
614
+ success(result) {
615
+ pending = false;
616
+ renderCheck(result, client);
617
+ },
618
+ failure(error) {
619
+ pending = false;
620
+ renderRequestFailure(error);
621
+ },
622
+ },
623
+ );
624
+ };
625
+
626
+ void load();
627
+ return clearPoll;
628
+ },
629
+ });
630
+ }
631
+
632
+ export function createDefaultRendererSettingsPages(
633
+ messages: RendererSettingsMessages = DEFAULT_RENDERER_SETTINGS_MESSAGES,
634
+ getUpdateClient: () => RendererUpdateClient | null = () => null,
635
+ getDiagnostics: () => RendererConnectionDiagnostics | null = () => null,
636
+ getAccountClient: () => RendererCodexAccountClient | null = () => null,
637
+ getSessionImportClient: () => RendererSessionImportClient | null = () => null,
638
+ openImportedThread: RendererImportedThreadOpener = () =>
639
+ Promise.reject(new Error("Imported Thread navigation is unavailable")),
640
+ getIntegrationsClient: () => RendererIntegrationsClient | null = () => null,
641
+ getStorageClient: () => RendererStorageClient | null = () => null,
642
+ getPetClient: () => RendererPetsClient | null = () => null,
643
+ getCollaborationClient: () => RendererCollaborationClient | null = () => null,
644
+ ): readonly RendererSettingsPageDefinition[] {
645
+ return Object.freeze([
646
+ createConnectionsSettingsPage(messages, getDiagnostics),
647
+ createCollaborationSettingsPage(messages, getCollaborationClient),
648
+ createAccountsSettingsPage(messages, getAccountClient),
649
+ createMcpSettingsPage(messages, getIntegrationsClient),
650
+ createSkillsSettingsPage(messages, getIntegrationsClient),
651
+ createSessionImportSettingsPage(messages, getSessionImportClient, openImportedThread),
652
+ createStorageSettingsPage(messages, getStorageClient),
653
+ createSkinSettingsPage(messages),
654
+ createPetSettingsPage(messages, getPetClient),
655
+ updatesPage(messages, getUpdateClient),
656
+ aboutPage(messages, getUpdateClient),
657
+ ]);
658
+ }
659
+
660
+ export function createDefaultRendererSettingsRegistry(
661
+ messages: RendererSettingsMessages = DEFAULT_RENDERER_SETTINGS_MESSAGES,
662
+ getUpdateClient: () => RendererUpdateClient | null = () => null,
663
+ getDiagnostics: () => RendererConnectionDiagnostics | null = () => null,
664
+ getAccountClient: () => RendererCodexAccountClient | null = () => null,
665
+ getSessionImportClient: () => RendererSessionImportClient | null = () => null,
666
+ openImportedThread?: RendererImportedThreadOpener,
667
+ getIntegrationsClient?: () => RendererIntegrationsClient | null,
668
+ getStorageClient?: () => RendererStorageClient | null,
669
+ getPetClient?: () => RendererPetsClient | null,
670
+ getCollaborationClient?: () => RendererCollaborationClient | null,
671
+ ): RendererSettingsPageRegistry {
672
+ return createRendererSettingsPageRegistry(
673
+ createDefaultRendererSettingsPages(
674
+ messages,
675
+ getUpdateClient,
676
+ getDiagnostics,
677
+ getAccountClient,
678
+ getSessionImportClient,
679
+ openImportedThread,
680
+ getIntegrationsClient,
681
+ getStorageClient,
682
+ getPetClient,
683
+ getCollaborationClient,
684
+ ),
685
+ );
686
+ }
687
+
688
+ export type { RendererCodexAccountClient } from "./accounts-page.js";
689
+ export type { RendererStorageClient, RendererStorageInspection } from './storage-page.js';
690
+ export type { RendererPetsClient } from "./pets-client.js";
691
+ export type {
692
+ RendererCollaborationClient,
693
+ RendererCollaborationPreferences,
694
+ } from "./collaboration-page.js";