harness-mix 0.1.2 → 0.1.3

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 (96) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +60 -15
  3. package/docs/images/codex-desktop-home.png +0 -0
  4. package/docs/images/codex-desktop-session.png +0 -0
  5. package/docs/multi-agent-collaboration.md +68 -68
  6. package/output/native-build/renderer-extension.js +2225 -1761
  7. package/package.json +1 -2
  8. package/scripts/collaboration-recovery-test.cjs +22 -2
  9. package/scripts/collaboration-ui-smoke.cjs +10 -5
  10. package/scripts/native-protocol-test.cjs +20 -0
  11. package/scripts/workspace-test.cjs +2 -14
  12. package/src/main/adapters/antigravity.js +49 -18
  13. package/src/main/adapters/claude.js +3 -2
  14. package/src/main/adapters/codex.js +10 -8
  15. package/src/main/adapters/dsh.js +3 -2
  16. package/src/main/adapters/opencode.js +21 -13
  17. package/src/main/adapters/pi-family.js +3 -3
  18. package/src/main/host/collaboration-worktree.js +63 -4
  19. package/src/main/host/collaboration.js +28 -2
  20. package/src/main/host/runtime.js +22 -1
  21. package/src/main/native/protocol.js +250 -24
  22. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  23. package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
  24. package/src/native-ui/renderer-extension/dist/types/index.d.ts +2 -0
  25. package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
  26. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  27. package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts +40 -0
  28. package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts.map +1 -0
  29. package/src/native-ui/renderer-extension/dist/types/renderer-credits-control.d.ts.map +1 -1
  30. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
  31. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +8 -0
  32. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  33. package/src/native-ui/renderer-extension/dist/types/renderer-usage-control.d.ts.map +1 -1
  34. package/src/native-ui/renderer-extension/dist/types/settings/connections-page.d.ts +3 -1
  35. package/src/native-ui/renderer-extension/dist/types/settings/connections-page.d.ts.map +1 -1
  36. package/src/native-ui/renderer-extension/dist/types/settings/harness-accounts.d.ts +8 -1
  37. package/src/native-ui/renderer-extension/dist/types/settings/harness-accounts.d.ts.map +1 -1
  38. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +16 -0
  39. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  40. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +1 -0
  41. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  42. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  43. package/src/native-ui/renderer-extension/src/assets/README.md +8 -38
  44. package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +9 -1
  45. package/src/native-ui/renderer-extension/src/index.ts +9 -0
  46. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +67 -6
  47. package/src/native-ui/renderer-extension/src/renderer-collab-cards.ts +198 -0
  48. package/src/native-ui/renderer-extension/src/renderer-credits-control.ts +32 -12
  49. package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +139 -46
  50. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +7 -2
  51. package/src/native-ui/renderer-extension/src/renderer-usage-control.ts +34 -3
  52. package/src/native-ui/renderer-extension/src/settings/accounts.css +80 -4
  53. package/src/native-ui/renderer-extension/src/settings/connections-page.ts +68 -29
  54. package/src/native-ui/renderer-extension/src/settings/harness-accounts.ts +89 -28
  55. package/src/native-ui/renderer-extension/src/settings/localization.ts +48 -0
  56. package/src/native-ui/renderer-extension/src/settings/pages.ts +19 -2
  57. package/src/native-ui/renderer-extension/src/settings/shell.css +62 -0
  58. package/src/native-ui/renderer-extension/src/settings/shell.ts +1 -1
  59. package/src/native-ui/renderer-extension/test/renderer-collab-cards.test.ts +45 -0
  60. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +1 -0
  61. package/src/native-ui/shared-contracts/dist/harness-accounts.d.ts +20 -6
  62. package/src/native-ui/shared-contracts/dist/harness-accounts.d.ts.map +1 -1
  63. package/src/native-ui/shared-contracts/dist/harness-accounts.js +4 -1
  64. package/src/native-ui/shared-contracts/dist/harness-accounts.js.map +1 -1
  65. package/src/native-ui/shared-contracts/dist/harness-models.d.ts +22 -0
  66. package/src/native-ui/shared-contracts/dist/harness-models.d.ts.map +1 -1
  67. package/src/native-ui/shared-contracts/dist/harness-models.js +2 -1
  68. package/src/native-ui/shared-contracts/dist/harness-models.js.map +1 -1
  69. package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
  70. package/src/native-ui/shared-contracts/src/harness-accounts.ts +4 -1
  71. package/src/native-ui/shared-contracts/src/harness-models.ts +6 -1
  72. package/docs/images/harness-mix-home.png +0 -0
  73. package/docs/images/harness-mix-session.png +0 -0
  74. package/scripts/git-test.cjs +0 -33
  75. package/src/assets/icons/activity-compact.svg +0 -1
  76. package/src/assets/icons/activity-edit.svg +0 -1
  77. package/src/assets/icons/activity-terminal.svg +0 -1
  78. package/src/assets/icons/commands.svg +0 -1
  79. package/src/assets/icons/home-chat.svg +0 -1
  80. package/src/assets/icons/home-folder.svg +0 -1
  81. package/src/assets/icons/thread-fork.svg +0 -1
  82. package/src/main/workspace/git.js +0 -73
  83. package/src/main/workspace/ipc.js +0 -50
  84. package/src/main/workspace/terminal.js +0 -34
  85. package/src/native-ui/renderer-extension/src/assets/claude-agent.svg +0 -3
  86. package/src/native-ui/renderer-extension/src/assets/codex-logo-bright.png +0 -0
  87. package/src/native-ui/renderer-extension/src/assets/codex-logo-transparent.png +0 -0
  88. package/src/native-ui/renderer-extension/src/assets/codex-logo.png +0 -0
  89. package/src/native-ui/renderer-extension/src/assets/codexhost-icon.png +0 -0
  90. package/src/native-ui/renderer-extension/src/assets/codexhost-logo-transparent.png +0 -0
  91. package/src/native-ui/renderer-extension/src/assets/codexhost-logo.png +0 -0
  92. package/src/native-ui/renderer-extension/src/assets/codexhost-readme.svg +0 -8
  93. package/src/native-ui/renderer-extension/src/assets/pi-agent.svg +0 -4
  94. package/src/native-ui/renderer-extension/src/settings/accounts-quota-interactive.prototype.html +0 -1418
  95. package/src/native-ui/renderer-extension/src/settings/update-notes.preview.html +0 -491
  96. package/src/native-ui/renderer-extension/src/settings/windows-update.preview.html +0 -2135
@@ -30,11 +30,87 @@
30
30
  }
31
31
 
32
32
  .settings-harness-account {
33
- display: grid;
34
- grid-template-columns: minmax(180px, 1fr) minmax(0, 2fr);
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ flex-wrap: wrap;
37
+ gap: 20px;
38
+ padding: 20px 24px;
39
+ }
40
+
41
+ .settings-harness-account > .settings-account-row__person {
42
+ flex: 1 1 280px;
43
+ min-width: 0;
44
+ }
45
+
46
+ .settings-harness-account > .settings-account-usage {
47
+ flex: 1 1 220px;
48
+ min-width: 0;
49
+ }
50
+
51
+ .settings-harness-account > .settings-harness-account__actions {
52
+ flex: 0 0 auto;
53
+ }
54
+
55
+ .settings-harness-account__badge {
56
+ display: inline-flex;
35
57
  align-items: center;
36
- gap: 24px;
37
- padding: 24px;
58
+ padding: 1px 7px;
59
+ border-radius: 999px;
60
+ font-size: 11px;
61
+ font-weight: 500;
62
+ line-height: 16px;
63
+ }
64
+
65
+ .settings-harness-account__badge--ready {
66
+ background: rgba(16, 185, 129, 0.12);
67
+ color: #10b981;
68
+ border: 1px solid rgba(16, 185, 129, 0.28);
69
+ }
70
+
71
+ .settings-harness-account__badge--unconfigured {
72
+ background: rgba(245, 158, 11, 0.12);
73
+ color: #f59e0b;
74
+ border: 1px solid rgba(245, 158, 11, 0.28);
75
+ }
76
+
77
+ .settings-harness-account__badge--not_installed {
78
+ background: rgba(156, 163, 175, 0.12);
79
+ color: var(--settings-muted);
80
+ border: 1px solid rgba(156, 163, 175, 0.25);
81
+ }
82
+
83
+ .settings-harness-account__config-hint {
84
+ margin-top: 6px;
85
+ font-size: 11px;
86
+ color: var(--settings-muted);
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 4px;
90
+ overflow: hidden;
91
+ text-overflow: ellipsis;
92
+ white-space: nowrap;
93
+ }
94
+
95
+ .settings-harness-account__config-hint code {
96
+ font-family: ui-monospace, Consolas, monospace;
97
+ font-size: 11px;
98
+ background: var(--settings-surface);
99
+ border: 1px solid var(--settings-border);
100
+ padding: 1px 5px;
101
+ border-radius: 4px;
102
+ }
103
+
104
+ .settings-harness-account__actions {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 8px;
108
+ }
109
+
110
+ .settings-harness-account__login-btn {
111
+ font-size: 12px;
112
+ padding: 6px 12px;
113
+ white-space: nowrap;
38
114
  }
39
115
 
40
116
  .settings-harness-account + .settings-harness-account {
@@ -27,7 +27,7 @@ export const HARNESS_INSTALL_COMMANDS: Readonly<Partial<Record<ExternalRendererA
27
27
  "deepseek-harness": { command: "pip install deepseek-harness" },
28
28
  opencode: { command: "npm install -g opencode-ai" },
29
29
  grok: { command: "npm install -g @xai/grok-cli" },
30
- omp: { command: "npm install -g @oh-my-prompt/omp" },
30
+ omp: { command: "npm install -g @oh-my-pi/pi-coding-agent" },
31
31
  antigravity: { command: "npm install -g @google/antigravity-cli" },
32
32
  openclaw: { command: "npm install -g openclaw" },
33
33
  hermes: { command: "pip install hermes-agent" },
@@ -76,7 +76,7 @@ export interface RendererConnectionDiagnostics {
76
76
  refresh(): Promise<void>;
77
77
  openWebUi?(hostId: string, agent: ExternalRendererAgent): Promise<void>;
78
78
  inspectHarness?(hostId: string, agent: ExternalRendererAgent): Promise<HarnessInspection>;
79
- installHarness?(hostId: string, agent: ExternalRendererAgent): Promise<{ success: boolean; command?: string; stdout?: string; stderr?: string; error?: string }>;
79
+ installHarness?(hostId: string, agent: ExternalRendererAgent, options?: { terminal?: boolean }): Promise<{ success: boolean; command?: string; stdout?: string; stderr?: string; error?: string }>;
80
80
  subscribe(listener: () => void): () => void;
81
81
  }
82
82
 
@@ -446,52 +446,87 @@ function renderOneClickInstallSection(
446
446
  head.append(icon, copy);
447
447
 
448
448
  const commandInfo = HARNESS_INSTALL_COMMANDS[agent];
449
+
450
+ const actions = document.createElement("div");
451
+ actions.className = "settings-install-actions";
452
+
453
+ const feedback = document.createElement("div");
454
+ feedback.className = "settings-install-feedback";
455
+
456
+ const downloadLink = document.createElement("a");
457
+ downloadLink.href = HARNESS_INSTALL_URLS[agent];
458
+ downloadLink.target = "_blank";
459
+ downloadLink.rel = "noopener noreferrer";
460
+ downloadLink.append(createRendererSettingsIcon("external-link", 15), messages.officialDownload);
461
+
462
+ const refreshBtn = document.createElement("button");
463
+ refreshBtn.type = "button";
464
+ refreshBtn.className = "settings-command-button settings-command-button--secondary";
465
+ refreshBtn.append(createRendererSettingsIcon("diagnose", 15), messages.refreshDetection);
466
+ refreshBtn.addEventListener("click", () => {
467
+ void diagnostics?.refresh();
468
+ });
469
+
470
+ // If this harness does not have a CLI npm/pip install command (e.g. standalone IDEs/tools like Kiro, Cursor, CodeBuddy, Trae, ZCode)
471
+ if (!commandInfo) {
472
+ const manualCard = document.createElement("div");
473
+ manualCard.className = "settings-install-manual-card";
474
+ const manualBadge = document.createElement("div");
475
+ manualBadge.className = "settings-install-manual-badge";
476
+ manualBadge.textContent = messages.manualInstallTitle;
477
+ const manualNotice = document.createElement("p");
478
+ manualNotice.className = "settings-install-manual-notice";
479
+ manualNotice.textContent = messages.manualInstallNotice;
480
+
481
+ const stepsList = document.createElement("ol");
482
+ stepsList.className = "settings-install-steps";
483
+ for (const stepText of [messages.manualInstallStep1, messages.manualInstallStep2, messages.manualInstallStep3]) {
484
+ const stepItem = document.createElement("li");
485
+ stepItem.textContent = stepText;
486
+ stepsList.append(stepItem);
487
+ }
488
+ manualCard.append(manualBadge, manualNotice, stepsList);
489
+
490
+ downloadLink.className = "settings-command-button";
491
+ actions.append(downloadLink, refreshBtn);
492
+ section.append(head, manualCard, actions, feedback);
493
+ container.append(section);
494
+ return;
495
+ }
496
+
497
+ // Harness has a CLI command:
449
498
  const commandBox = document.createElement("div");
450
499
  commandBox.className = "settings-install-command-box";
451
- commandBox.hidden = !commandInfo;
452
500
  const commandLabel = document.createElement("span");
453
501
  commandLabel.className = "settings-install-command-label";
454
502
  commandLabel.textContent = messages.installCommandLabel;
455
503
  const commandRow = document.createElement("div");
456
504
  commandRow.className = "settings-install-command-row";
457
505
  const code = document.createElement("code");
458
- code.textContent = commandInfo?.command ?? `npm install -g ${agent}`;
506
+ code.textContent = commandInfo.command;
459
507
  commandRow.append(code);
460
508
  commandBox.append(commandLabel, commandRow);
461
509
 
462
- const actions = document.createElement("div");
463
- actions.className = "settings-install-actions";
510
+ downloadLink.className = "settings-command-button settings-command-button--secondary";
464
511
 
465
512
  const installBtn = document.createElement("button");
466
513
  installBtn.type = "button";
467
- installBtn.hidden = !commandInfo;
468
- installBtn.disabled = !commandInfo;
469
514
  installBtn.className = "settings-command-button";
470
515
  installBtn.append(createRendererSettingsIcon("download", 15), messages.oneClickInstall);
471
516
 
517
+ const terminalBtn = document.createElement("button");
518
+ terminalBtn.type = "button";
519
+ terminalBtn.className = "settings-command-button settings-command-button--secondary";
520
+ terminalBtn.append(createRendererSettingsIcon("external-link", 15), messages.installInTerminal);
521
+ terminalBtn.title = messages.installInTerminal;
522
+
472
523
  const copyBtn = document.createElement("button");
473
524
  copyBtn.type = "button";
474
- copyBtn.hidden = !commandInfo;
475
525
  copyBtn.className = "settings-command-button settings-command-button--secondary";
476
526
  copyBtn.append(createRendererSettingsIcon("copy", 15), messages.copyInstallCommand);
477
527
 
478
- const downloadLink = document.createElement("a");
479
- downloadLink.className = "settings-command-button settings-command-button--secondary";
480
- downloadLink.href = HARNESS_INSTALL_URLS[agent];
481
- downloadLink.target = "_blank";
482
- downloadLink.rel = "noopener noreferrer";
483
- downloadLink.append(createRendererSettingsIcon("external-link", 15), messages.officialDownload);
484
-
485
- const refreshBtn = document.createElement("button");
486
- refreshBtn.type = "button";
487
- refreshBtn.className = "settings-command-button settings-command-button--secondary";
488
- refreshBtn.append(createRendererSettingsIcon("diagnose", 15), messages.refreshDetection);
489
-
490
- const feedback = document.createElement("div");
491
- feedback.className = "settings-install-feedback";
492
-
493
528
  copyBtn.addEventListener("click", () => {
494
- const text = commandInfo?.command ?? `npm install -g ${agent}`;
529
+ const text = commandInfo.command;
495
530
  const clipboard = document.defaultView?.navigator?.clipboard;
496
531
  if (clipboard) {
497
532
  void clipboard.writeText(text).then(() => {
@@ -503,8 +538,12 @@ function renderOneClickInstallSection(
503
538
  }
504
539
  });
505
540
 
506
- refreshBtn.addEventListener("click", () => {
507
- void diagnostics?.refresh();
541
+ terminalBtn.addEventListener("click", () => {
542
+ feedback.className = "settings-install-feedback settings-feedback-success";
543
+ feedback.textContent = messages.terminalInstalling;
544
+ if (diagnostics?.installHarness) {
545
+ void diagnostics.installHarness(hostId, agent, { terminal: true });
546
+ }
508
547
  });
509
548
 
510
549
  installBtn.addEventListener("click", () => {
@@ -516,7 +555,7 @@ function renderOneClickInstallSection(
516
555
 
517
556
  const run = async () => {
518
557
  if (!diagnostics?.installHarness) {
519
- const text = commandInfo?.command ?? `npm install -g ${agent}`;
558
+ const text = commandInfo.command;
520
559
  await document.defaultView?.navigator?.clipboard?.writeText(text);
521
560
  feedback.className = "settings-install-feedback settings-feedback-warning";
522
561
  feedback.textContent = messages.oneClickInstallFailed;
@@ -549,7 +588,7 @@ function renderOneClickInstallSection(
549
588
  void run();
550
589
  });
551
590
 
552
- actions.append(installBtn, copyBtn, downloadLink, refreshBtn);
591
+ actions.append(installBtn, terminalBtn, copyBtn, downloadLink, refreshBtn);
553
592
  section.append(head, commandBox, actions, feedback);
554
593
  container.append(section);
555
594
  }
@@ -1,12 +1,14 @@
1
1
  import type { HarnessAccountListResult } from "@codexhost/shared-contracts";
2
2
  import { KNOWN_RENDERER_AGENTS } from "../agent-selection-state.js";
3
3
  import { createRendererAgentIcon } from "../renderer-agent-icon.js";
4
+ import { createRendererSettingsIcon } from "./icons.js";
4
5
  import { renderAccountUsage, type AccountUsageDisplay } from "./accounts-usage.js";
5
6
  import type { RendererSettingsPageMountContext } from "./core.js";
6
7
  import type { RendererSettingsMessages } from "./localization.js";
7
8
 
8
9
  export interface RendererHarnessAccountClient {
9
10
  listHarnessAccounts?(): Promise<HarnessAccountListResult>;
11
+ loginHarnessAccount?(input: { harnessId: string }): Promise<{ success: boolean; command?: string | undefined; error?: string | undefined }>;
10
12
  }
11
13
 
12
14
  /** Read-only telemetry, deliberately separate from Codex Account IDs and mutations. */
@@ -22,7 +24,7 @@ export function mountHarnessAccounts(
22
24
  section.hidden = true;
23
25
  const heading = document.createElement("h2");
24
26
  heading.className = "settings-harness-accounts__title";
25
- heading.textContent = messages.harnessAccountsTitle;
27
+ heading.textContent = messages.harnessAccountsPageTitle || messages.harnessAccountsTitle;
26
28
  const list = document.createElement("div");
27
29
  list.className = "settings-account-list";
28
30
  section.append(heading, list);
@@ -31,6 +33,7 @@ export function mountHarnessAccounts(
31
33
  let refreshing = false;
32
34
  let query = "";
33
35
  let display: AccountUsageDisplay = "remaining";
36
+ let refresh: () => Promise<void>;
34
37
 
35
38
  const render = (): void => {
36
39
  section.hidden = accounts.length === 0;
@@ -71,14 +74,44 @@ export function mountHarnessAccounts(
71
74
  plan.textContent = account.plan;
72
75
  metadata.append(plan);
73
76
  }
77
+ if (account.status) {
78
+ if (accountName || account.plan) {
79
+ const separator = document.createElement("span");
80
+ separator.textContent = "·";
81
+ separator.setAttribute("aria-hidden", "true");
82
+ metadata.append(separator);
83
+ }
84
+ const statusBadge = document.createElement("span");
85
+ statusBadge.className = `settings-harness-account__badge settings-harness-account__badge--${account.status}`;
86
+ statusBadge.textContent =
87
+ account.status === "ready"
88
+ ? messages.harnessAccountStatusReady
89
+ : account.status === "not_installed"
90
+ ? messages.harnessAccountStatusNotInstalled
91
+ : messages.harnessAccountStatusUnconfigured;
92
+ metadata.append(statusBadge);
93
+ }
74
94
  if (metadata.childElementCount) identity.append(metadata);
75
- const usage = renderAccountUsage(
76
- document,
77
- { status: "ready", credits: account.credits },
78
- messages,
79
- display,
80
- () => undefined,
81
- );
95
+ if (account.configHint) {
96
+ const configHint = document.createElement("div");
97
+ configHint.className = "settings-harness-account__config-hint";
98
+ configHint.title = account.configHint;
99
+ const label = document.createElement("span");
100
+ label.textContent = `${messages.harnessAccountConfigGuide}: `;
101
+ const code = document.createElement("code");
102
+ code.textContent = account.configHint;
103
+ configHint.append(label, code);
104
+ identity.append(configHint);
105
+ }
106
+ const usage = account.credits
107
+ ? renderAccountUsage(
108
+ document,
109
+ { status: "ready", credits: account.credits },
110
+ messages,
111
+ display,
112
+ () => undefined,
113
+ )
114
+ : null;
82
115
  const person = document.createElement("div");
83
116
  person.className = "settings-account-row__person";
84
117
  const agent = KNOWN_RENDERER_AGENTS.find((agent) => agent === account.harnessId);
@@ -92,6 +125,32 @@ export function mountHarnessAccounts(
92
125
  person.append(identity);
93
126
  row.append(person);
94
127
  if (usage) row.append(usage);
128
+ const actions = document.createElement("div");
129
+ actions.className = "settings-harness-account__actions";
130
+ if (account.loginCommand) {
131
+ const loginBtn = document.createElement("button");
132
+ loginBtn.type = "button";
133
+ loginBtn.className = "settings-command-button settings-command-button--secondary settings-harness-account__login-btn";
134
+ loginBtn.append(createRendererSettingsIcon("external-link", 14), messages.harnessAccountLogin);
135
+ loginBtn.title = account.loginCommand;
136
+ loginBtn.addEventListener("click", () => {
137
+ const client = getClient();
138
+ if (!client?.loginHarnessAccount) return;
139
+ loginBtn.disabled = true;
140
+ loginBtn.textContent = messages.harnessAccountLoginStarted;
141
+ void client.loginHarnessAccount({ harnessId: account.harnessId }).finally(() => {
142
+ document.defaultView?.setTimeout(() => {
143
+ loginBtn.disabled = false;
144
+ loginBtn.replaceChildren(createRendererSettingsIcon("external-link", 14), messages.harnessAccountLogin);
145
+ void refresh();
146
+ }, 3000);
147
+ });
148
+ });
149
+ actions.append(loginBtn);
150
+ }
151
+ if (actions.childElementCount > 0) {
152
+ row.append(actions);
153
+ }
95
154
  list.append(row);
96
155
  }
97
156
  if (accounts.length && !visible.length) {
@@ -101,6 +160,27 @@ export function mountHarnessAccounts(
101
160
  list.append(empty);
102
161
  }
103
162
  };
163
+ refresh = async (): Promise<void> => {
164
+ if (refreshing || context.signal.aborted) return;
165
+ const client = getClient();
166
+ if (!client?.listHarnessAccounts) return;
167
+ refreshing = true;
168
+ onChange();
169
+ try {
170
+ const result = await client.listHarnessAccounts();
171
+ if (!context.signal.aborted) accounts = result.accounts;
172
+ } catch {
173
+ // Older Hosts and unavailable authentication have no read-only rows.
174
+ if (!context.signal.aborted) accounts = [];
175
+ } finally {
176
+ refreshing = false;
177
+ if (!context.signal.aborted) {
178
+ render();
179
+ onChange();
180
+ }
181
+ }
182
+ };
183
+
104
184
  return {
105
185
  get refreshing() {
106
186
  return refreshing;
@@ -110,25 +190,6 @@ export function mountHarnessAccounts(
110
190
  display = nextDisplay;
111
191
  render();
112
192
  },
113
- async refresh(): Promise<void> {
114
- if (refreshing || context.signal.aborted) return;
115
- const client = getClient();
116
- if (!client?.listHarnessAccounts) return;
117
- refreshing = true;
118
- onChange();
119
- try {
120
- const result = await client.listHarnessAccounts();
121
- if (!context.signal.aborted) accounts = result.accounts;
122
- } catch {
123
- // Older Hosts and unavailable authentication have no read-only rows.
124
- if (!context.signal.aborted) accounts = [];
125
- } finally {
126
- refreshing = false;
127
- if (!context.signal.aborted) {
128
- render();
129
- onChange();
130
- }
131
- }
132
- },
193
+ refresh,
133
194
  };
134
195
  }
@@ -172,6 +172,22 @@ export interface RendererSettingsMessages {
172
172
  readonly oneClickInstalling: string;
173
173
  readonly oneClickInstallSuccess: string;
174
174
  readonly oneClickInstallFailed: string;
175
+ readonly installInTerminal: string;
176
+ readonly terminalInstalling: string;
177
+ readonly manualInstallTitle: string;
178
+ readonly manualInstallNotice: string;
179
+ readonly manualInstallStep1: string;
180
+ readonly manualInstallStep2: string;
181
+ readonly manualInstallStep3: string;
182
+ readonly harnessAccountsPageTitle: string;
183
+ readonly harnessAccountsPageDescription: string;
184
+ readonly harnessAccountLogin: string;
185
+ readonly harnessAccountLoginStarted: string;
186
+ readonly harnessAccountConfigGuide: string;
187
+ readonly harnessAccountStatusReady: string;
188
+ readonly harnessAccountStatusUnconfigured: string;
189
+ readonly harnessAccountStatusNotInstalled: string;
190
+ readonly checkUpdateNow: string;
175
191
  readonly installCommandLabel: string;
176
192
  readonly copyInstallCommand: string;
177
193
  readonly installCommandCopied: string;
@@ -401,6 +417,22 @@ const ENGLISH_MESSAGES: RendererSettingsMessages = Object.freeze({
401
417
  oneClickInstalling: "Installing Harness in background...",
402
418
  oneClickInstallSuccess: "Installed successfully! Status refreshed.",
403
419
  oneClickInstallFailed: "Installation failed. Please try running the command manually.",
420
+ installInTerminal: "Install in Terminal",
421
+ terminalInstalling: "Installation launched in a visible terminal window. Click Refresh when done.",
422
+ manualInstallTitle: "Standalone Application / Manual Install",
423
+ manualInstallNotice: "This Harness is a standalone desktop application or binary and must be installed via its official installer.",
424
+ manualInstallStep1: "1. Click the button below to visit the official website and download the installer.",
425
+ manualInstallStep2: "2. Run the application and complete its authentication login.",
426
+ manualInstallStep3: "3. Ensure its CLI executable is in your PATH environment variable, then click Refresh.",
427
+ harnessAccountsPageTitle: "Harness Account Management",
428
+ harnessAccountsPageDescription: "View and manage login status, credentials, and usage quotas for each native harness. Credentials remain stored locally by each harness.",
429
+ harnessAccountLogin: "Log In / Authorize",
430
+ harnessAccountLoginStarted: "Login launched in terminal window. Click Refresh when done.",
431
+ harnessAccountConfigGuide: "Credential Guide",
432
+ harnessAccountStatusReady: "Ready",
433
+ harnessAccountStatusUnconfigured: "Needs Config / Not Logged In",
434
+ harnessAccountStatusNotInstalled: "Not Installed",
435
+ checkUpdateNow: "Check for Updates Now",
404
436
  installCommandLabel: "Install Command",
405
437
  copyInstallCommand: "Copy Command",
406
438
  installCommandCopied: "Command Copied",
@@ -635,6 +667,22 @@ const CHINESE_MESSAGES: RendererSettingsMessages = Object.freeze({
635
667
  oneClickInstalling: "正在后台安装 Harness……",
636
668
  oneClickInstallSuccess: "安装成功!状态已自动刷新。",
637
669
  oneClickInstallFailed: "安装失败,请尝试复制下方命令在终端手动运行。",
670
+ installInTerminal: "在终端中打开安装",
671
+ terminalInstalling: "已在独立终端窗口启动安装,请在终端完成后点击刷新检测。",
672
+ manualInstallTitle: "独立安装程序 / 手动安装指引",
673
+ manualInstallNotice: "此 Harness 为独立客户端程序或二进制工具,不支持通过 npm 一键安装。",
674
+ manualInstallStep1: "1. 点击下方按钮前往官网下载安装包并完成安装;",
675
+ manualInstallStep2: "2. 运行应用并在终端中完成该 Harness 的认证登录;",
676
+ manualInstallStep3: "3. 确保该 CLI 可执行文件已加入系统 PATH 环境变量,然后点击【刷新检测】。",
677
+ harnessAccountsPageTitle: "Harness 账号与授权管理",
678
+ harnessAccountsPageDescription: "查看与管理各原生 Harness 的登录状态、授权凭据与模型用量配额。所有认证均保存在本地原生 Harness 中,Harness Mix 绝不代存您的账号密码。",
679
+ harnessAccountLogin: "登录 / 授权",
680
+ harnessAccountLoginStarted: "已在终端窗口启动登录认证,请在终端完成后点击刷新状态。",
681
+ harnessAccountConfigGuide: "凭据配置指引",
682
+ harnessAccountStatusReady: "已就绪",
683
+ harnessAccountStatusUnconfigured: "待配置 / 未登录",
684
+ harnessAccountStatusNotInstalled: "未安装",
685
+ checkUpdateNow: "立即检查更新",
638
686
  installCommandLabel: "推荐安装命令",
639
687
  copyInstallCommand: "复制安装命令",
640
688
  installCommandCopied: "已复制命令",
@@ -173,6 +173,23 @@ export function aboutPage(messages: RendererSettingsMessages): RendererSettingsP
173
173
  const tagline = document.createElement("strong");
174
174
  tagline.className = "settings-about-tagline";
175
175
  tagline.textContent = messages.aboutTagline;
176
+ const versionRow = document.createElement("div");
177
+ versionRow.className = "settings-about-version-row";
178
+ const versionBadge = document.createElement("span");
179
+ versionBadge.className = "settings-about-version-badge";
180
+ versionBadge.textContent = "v0.1.2";
181
+ const checkUpdateBtn = document.createElement("button");
182
+ checkUpdateBtn.type = "button";
183
+ checkUpdateBtn.className = "settings-command-button settings-command-button--secondary";
184
+ checkUpdateBtn.append(createRendererSettingsIcon("updates", 14), messages.checkUpdateNow);
185
+ checkUpdateBtn.addEventListener("click", () => {
186
+ const shell = document.querySelector("[data-codexhost-settings-shell]");
187
+ const updatesNavBtn = (shell?.shadowRoot ?? document).querySelector<HTMLButtonElement>(
188
+ 'button[data-page-id="updates"]',
189
+ );
190
+ updatesNavBtn?.click();
191
+ });
192
+ versionRow.append(versionBadge, checkUpdateBtn);
176
193
  const introduction = document.createElement("div");
177
194
  introduction.className = "settings-about-copy";
178
195
  for (const paragraphText of messages.aboutParagraphs) {
@@ -200,14 +217,14 @@ export function aboutPage(messages: RendererSettingsMessages): RendererSettingsP
200
217
  repositoryUrl,
201
218
  );
202
219
  repositorySection.append(openSource, repository);
203
- panel.append(product, tagline, introduction, starCallout, repositorySection);
220
+ panel.append(product, tagline, versionRow, introduction, starCallout, repositorySection);
204
221
  context.content.append(heading, panel);
205
222
  return undefined;
206
223
  },
207
224
  });
208
225
  }
209
226
 
210
- function updatesPage(
227
+ export function updatesPage(
211
228
  messages: RendererSettingsMessages,
212
229
  getClient: () => RendererUpdateClient | null,
213
230
  ): RendererSettingsPageDefinition {
@@ -1390,6 +1390,25 @@ button {
1390
1390
  line-height: 24px;
1391
1391
  }
1392
1392
 
1393
+ .settings-about-version-row {
1394
+ display: flex;
1395
+ align-items: center;
1396
+ gap: 12px;
1397
+ margin: 4px 0 8px;
1398
+ }
1399
+
1400
+ .settings-about-version-badge {
1401
+ display: inline-flex;
1402
+ align-items: center;
1403
+ font-size: 13px;
1404
+ font-weight: 600;
1405
+ color: var(--settings-text);
1406
+ background: var(--settings-surface);
1407
+ border: 1px solid var(--settings-border);
1408
+ padding: 4px 10px;
1409
+ border-radius: 6px;
1410
+ }
1411
+
1393
1412
  .settings-about-copy {
1394
1413
  display: grid;
1395
1414
  max-width: 660px;
@@ -2174,3 +2193,46 @@ button {
2174
2193
  margin-top: 2px;
2175
2194
  overflow-wrap: anywhere;
2176
2195
  }
2196
+
2197
+ .settings-install-manual-card {
2198
+ display: flex;
2199
+ flex-direction: column;
2200
+ gap: 8px;
2201
+ background: var(--settings-inset);
2202
+ padding: 12px 14px;
2203
+ border-radius: 8px;
2204
+ border: 1px solid var(--settings-border);
2205
+ margin-top: 6px;
2206
+ margin-bottom: 4px;
2207
+ }
2208
+
2209
+ .settings-install-manual-badge {
2210
+ display: inline-flex;
2211
+ align-items: center;
2212
+ font-size: 11px;
2213
+ font-weight: 600;
2214
+ color: var(--settings-accent, #10a37f);
2215
+ background: var(--settings-accent-subtle, rgba(16, 163, 127, 0.1));
2216
+ padding: 2px 8px;
2217
+ border-radius: 4px;
2218
+ width: fit-content;
2219
+ }
2220
+
2221
+ .settings-install-manual-notice {
2222
+ font-size: 12px;
2223
+ line-height: 18px;
2224
+ color: var(--settings-text);
2225
+ margin: 0;
2226
+ }
2227
+
2228
+ .settings-install-steps {
2229
+ margin: 0;
2230
+ padding-left: 18px;
2231
+ font-size: 12px;
2232
+ line-height: 20px;
2233
+ color: var(--settings-muted);
2234
+ }
2235
+
2236
+ .settings-install-steps li {
2237
+ margin-bottom: 2px;
2238
+ }
@@ -186,7 +186,7 @@ export function mountRendererSettingsShell(
186
186
  let otherSectionAdded = false;
187
187
  appendNavigationSection(messages.generalSection);
188
188
  for (const definition of resolvedRegistry.pages) {
189
- if (definition.id === "about" && !otherSectionAdded) {
189
+ if ((definition.id === "updates" || definition.id === "about") && !otherSectionAdded) {
190
190
  appendNavigationSection(messages.otherSection);
191
191
  otherSectionAdded = true;
192
192
  }
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ parseCollabPayload,
4
+ formatDiffHtml,
5
+ installCollabCards,
6
+ } from "../src/renderer-collab-cards.js";
7
+
8
+ describe("renderer-collab-cards", () => {
9
+ it("parses collaboration payloads correctly", () => {
10
+ expect(parseCollabPayload("")).toBeNull();
11
+ expect(parseCollabPayload("hello world")).toBeNull();
12
+
13
+ const payload = {
14
+ task_id: "task-123",
15
+ child_thread_id: "thread-child-456",
16
+ agent_type: "pi",
17
+ status: "completed",
18
+ };
19
+ const text = `Agent 协作 · Pi\n${JSON.stringify(payload)}`;
20
+ const parsed = parseCollabPayload(text);
21
+ expect(parsed).not.toBeNull();
22
+ expect(parsed?.task_id).toBe("task-123");
23
+ expect(parsed?.child_thread_id).toBe("thread-child-456");
24
+ expect(parsed?.agent_type).toBe("pi");
25
+ });
26
+
27
+ it("formats diff HTML with color coding", () => {
28
+ expect(formatDiffHtml("")).toContain("暂无文件改动");
29
+
30
+ const diff = `--- a/file.txt\n+++ b/file.txt\n@@ -1,3 +1,3 @@\n-old line\n+new line\n context`;
31
+ const html = formatDiffHtml(diff);
32
+ expect(html).toContain("+new line");
33
+ expect(html).toContain("-old line");
34
+ expect(html).toContain("#22c55e1a");
35
+ expect(html).toContain("#ef44441a");
36
+ });
37
+
38
+ it("handles environment safely when document is not in global scope", () => {
39
+ const control = installCollabCards();
40
+ expect(typeof control.scan).toBe("function");
41
+ expect(typeof control.dispose).toBe("function");
42
+ control.scan();
43
+ control.dispose();
44
+ });
45
+ });
@@ -306,6 +306,7 @@ describe("Renderer fixed Model request client", () => {
306
306
  "listHarnessSessions",
307
307
  "listSessionImportSources",
308
308
  "listThreadOwnership",
309
+ "loginHarnessAccount",
309
310
  "messageThread",
310
311
  "openHarnessWebUi",
311
312
  "readUpdateStatus",