@harness-mix/cli 0.1.7 → 0.1.9

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 (132) hide show
  1. package/NOTICE +12 -0
  2. package/README.md +51 -43
  3. package/docs/harness-management.md +1 -1
  4. package/docs/native-acp.md +7 -2
  5. package/docs/task-handoff-design.md +65 -0
  6. package/licenses/codex-styler-Apache-2.0.txt +193 -0
  7. package/licenses/heige-codex-skin-studio-MIT.txt +22 -0
  8. package/output/native-build/desktop-controller.mjs +2 -1
  9. package/output/native-build/renderer-extension.js +651 -9
  10. package/package.json +188 -188
  11. package/scripts/acp-capabilities-probe.cjs +1 -0
  12. package/scripts/build-native.cjs +1 -1
  13. package/scripts/codex-adapter-test.cjs +44 -0
  14. package/scripts/collaboration-recovery-test.cjs +3 -0
  15. package/scripts/e2e-codex.cjs +3 -1
  16. package/scripts/e2e-native-acp.cjs +1 -1
  17. package/scripts/error-kind-test.cjs +107 -0
  18. package/scripts/handoff-checkpoint-test.cjs +38 -0
  19. package/scripts/integrations-test.cjs +5 -2
  20. package/scripts/kiro-cursor-adapters-test.cjs +18 -0
  21. package/scripts/native-acp-depth-test.cjs +1 -1
  22. package/scripts/native-protocol-test.cjs +60 -4
  23. package/scripts/native-ui-smoke.cjs +92 -2
  24. package/scripts/switch-harness-test.cjs +79 -12
  25. package/scripts/tool-concurrency-regression-test.cjs +8 -6
  26. package/src/assets/icons/cline-color.svg +1 -0
  27. package/src/assets/skins/codex-styler/ASSET_LICENSES.md +36 -0
  28. package/src/assets/skins/codex-styler/NOTICE +11 -0
  29. package/src/assets/skins/codex-styler/themes/gilded-grandeur/hero.webp +0 -0
  30. package/src/assets/skins/codex-styler/themes/merry-big-top/hero.webp +0 -0
  31. package/src/assets/skins/codex-styler/themes/nocturne-studio/hero.webp +0 -0
  32. package/src/assets/skins/codex-styler/themes/quiet-garden/hero.webp +0 -0
  33. package/src/assets/skins/heige/ASSET_PROVENANCE.md +56 -0
  34. package/src/assets/skins/heige/NOTICE.md +13 -0
  35. package/src/assets/skins/heige/themes/dalao-dianyan/hero.webp +0 -0
  36. package/src/assets/skins/heige/themes/dalao-dianyan/theme.json +14 -0
  37. package/src/assets/skins/heige/themes/deepspace-dawn/hero.webp +0 -0
  38. package/src/assets/skins/heige/themes/deepspace-dawn/theme.json +14 -0
  39. package/src/assets/skins/heige/themes/deepspace-star/hero.webp +0 -0
  40. package/src/assets/skins/heige/themes/deepspace-star/theme.json +14 -0
  41. package/src/assets/skins/heige/themes/dragonball-nimbus/hero.webp +0 -0
  42. package/src/assets/skins/heige/themes/dragonball-nimbus/theme.json +16 -0
  43. package/src/assets/skins/heige/themes/dragonball-super-saiyan/hero.webp +0 -0
  44. package/src/assets/skins/heige/themes/dragonball-super-saiyan/theme.json +16 -0
  45. package/src/assets/skins/heige/themes/genshin-dawn/hero.webp +0 -0
  46. package/src/assets/skins/heige/themes/genshin-dawn/theme.json +14 -0
  47. package/src/assets/skins/heige/themes/genshin-night/hero.webp +0 -0
  48. package/src/assets/skins/heige/themes/genshin-night/theme.json +14 -0
  49. package/src/assets/skins/heige/themes/miku-488137/hero.webp +0 -0
  50. package/src/assets/skins/heige/themes/miku-488137/logo.webp +0 -0
  51. package/src/assets/skins/heige/themes/miku-488137/polaroid.webp +0 -0
  52. package/src/assets/skins/heige/themes/miku-488137/theme.json +16 -0
  53. package/src/assets/skins/heige/themes/naruto-hokage/hero.webp +0 -0
  54. package/src/assets/skins/heige/themes/naruto-hokage/theme.json +14 -0
  55. package/src/assets/skins/heige/themes/naruto-sasuke/hero.webp +0 -0
  56. package/src/assets/skins/heige/themes/naruto-sasuke/theme.json +14 -0
  57. package/src/assets/skins/heige/themes/wuthering-echo/hero.webp +0 -0
  58. package/src/assets/skins/heige/themes/wuthering-echo/theme.json +14 -0
  59. package/src/assets/skins/heige/themes/wuthering-tide/hero.webp +0 -0
  60. package/src/assets/skins/heige/themes/wuthering-tide/theme.json +14 -0
  61. package/src/main/adapters/cline.js +22 -0
  62. package/src/main/adapters/codex.js +78 -8
  63. package/src/main/adapters/index.js +2 -1
  64. package/src/main/adapters/native-acp-command.js +1 -0
  65. package/src/main/harness-adapter/error-kind.js +82 -0
  66. package/src/main/harness-adapter/event-normalizer.js +7 -1
  67. package/src/main/host/core-session.js +1 -1
  68. package/src/main/host/handoff-access.js +23 -4
  69. package/src/main/host/handoff-checkpoints.js +9 -2
  70. package/src/main/host/handoff-tools.js +3 -0
  71. package/src/main/host/handoff.js +11 -2
  72. package/src/main/host/runtime.js +272 -163
  73. package/src/main/host/session-history.js +54 -15
  74. package/src/main/native/icons.js +3 -2
  75. package/src/main/native/pets.js +234 -0
  76. package/src/main/native/protocol.js +197 -56
  77. package/src/main/protocol-core/projector.js +44 -41
  78. package/src/native-ui/desktop-control/dist/production-controller.d.ts.map +1 -1
  79. package/src/native-ui/desktop-control/dist/production-controller.js +1 -0
  80. package/src/native-ui/desktop-control/dist/production-controller.js.map +1 -1
  81. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  82. package/src/native-ui/desktop-control/src/production-controller.ts +1 -0
  83. package/src/native-ui/desktop-control/test/production-controller.test.ts +1 -0
  84. package/src/native-ui/renderer-extension/dist/types/agent-selection-state.d.ts +4 -2
  85. package/src/native-ui/renderer-extension/dist/types/agent-selection-state.d.ts.map +1 -1
  86. package/src/native-ui/renderer-extension/dist/types/renderer-agent-icon.d.ts.map +1 -1
  87. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -1
  88. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  89. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
  90. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  91. package/src/native-ui/renderer-extension/dist/types/renderer-sidebar-agent-icons.d.ts.map +1 -1
  92. package/src/native-ui/renderer-extension/dist/types/settings/connections-page.d.ts.map +1 -1
  93. package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts +1 -1
  94. package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts.map +1 -1
  95. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  96. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +1 -1
  97. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  98. package/src/native-ui/renderer-extension/dist/types/settings/shell.d.ts.map +1 -1
  99. package/src/native-ui/renderer-extension/dist/types/settings/skin-market.d.ts +4 -0
  100. package/src/native-ui/renderer-extension/dist/types/settings/skin-market.d.ts.map +1 -0
  101. package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts +31 -0
  102. package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts.map +1 -0
  103. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  104. package/src/native-ui/renderer-extension/dist/types/versioned-renderer-adapter.d.ts.map +1 -1
  105. package/src/native-ui/renderer-extension/package.json +1 -1
  106. package/src/native-ui/renderer-extension/src/agent-selection-state.ts +16 -0
  107. package/src/native-ui/renderer-extension/src/assets.d.ts +5 -0
  108. package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +5 -4
  109. package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +1 -0
  110. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +9 -6
  111. package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +2 -2
  112. package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +2 -0
  113. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
  114. package/src/native-ui/renderer-extension/src/renderer-sidebar-agent-icons.ts +1 -0
  115. package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -0
  116. package/src/native-ui/renderer-extension/src/settings/icons.ts +6 -0
  117. package/src/native-ui/renderer-extension/src/settings/localization.ts +4 -2
  118. package/src/native-ui/renderer-extension/src/settings/pages.ts +5 -2
  119. package/src/native-ui/renderer-extension/src/settings/shell.ts +3 -2
  120. package/src/native-ui/renderer-extension/src/settings/skin-market.css +213 -0
  121. package/src/native-ui/renderer-extension/src/settings/skin-market.ts +174 -0
  122. package/src/native-ui/renderer-extension/src/settings/skin-runtime.ts +399 -0
  123. package/src/native-ui/renderer-extension/src/versioned-renderer-adapter.ts +2 -1
  124. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +11 -1
  125. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
  126. package/src/native-ui/renderer-extension/test/settings/skin-runtime.test.ts +136 -0
  127. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +7 -0
  128. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
  129. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +3 -0
  130. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
  131. package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
  132. package/src/native-ui/shared-contracts/src/thread-harness-switch.ts +3 -0
@@ -39,7 +39,7 @@ export function rendererHarnessHandoffMessages(locale: RendererSettingsLocale) {
39
39
  includeLabel: "将要交接",
40
40
  includeOptions: { conversation: "近期对话", plan: "当前计划", evidence: "测试与工具证据", files: "文件变更", unresolved: "未解决问题" },
41
41
  notePlaceholder: "例如:按上面的方案继续实现,先核对当前文件状态和未完成步骤",
42
- context: "下一条消息会自动携带近期对话、最新方案、计划和变更文件。",
42
+ context: "确认后会先连接目标 Harness,失败将自动恢复。下一条消息会携带已选的近期对话、最新方案、计划和变更文件。",
43
43
  running: "若当前回合仍在运行,请先停止或等待完成。",
44
44
  cancel: "取消",
45
45
  confirm: "确认接力",
@@ -54,7 +54,7 @@ export function rendererHarnessHandoffMessages(locale: RendererSettingsLocale) {
54
54
  includeLabel: "Include",
55
55
  includeOptions: { conversation: "Recent conversation", plan: "Current plan", evidence: "Test and tool evidence", files: "File changes", unresolved: "Open issues" },
56
56
  notePlaceholder: "For example: implement the plan above; verify the working tree and remaining steps first",
57
- context: "Your next message will include recent context, the latest solution, plan, and changed files.",
57
+ context: "The target Harness is connected before the switch commits; failures roll back automatically. Your next message carries the selected recent conversation, latest solution, plan, and changed files.",
58
58
  running: "If a turn is still running, stop it or wait for it to finish first.",
59
59
  cancel: "Cancel",
60
60
  confirm: "Hand off",
@@ -70,6 +70,7 @@ const AGENT_DESCRIPTIONS: Record<string, string> = {
70
70
  trae: 'Trae 原生协同',
71
71
  'kiro-cli': 'Kiro CLI 智能编程',
72
72
  'cursor-cli': 'Cursor CLI 原生协同',
73
+ cline: 'Cline 自主编程智能体',
73
74
  };
74
75
 
75
76
  const AGENT_COLORS: Record<string, string> = {
@@ -93,6 +94,7 @@ const AGENT_COLORS: Record<string, string> = {
93
94
  zcode: '#4f46e5',
94
95
  trae: '#0284c7',
95
96
  'cursor-cli': '#475569',
97
+ cline: '#be185d',
96
98
  };
97
99
 
98
100
  function getAgentColor(agentId: string): string {
@@ -20,6 +20,7 @@ import {
20
20
  installRendererSettingsHeaderTrigger,
21
21
  type RendererSettingsHeaderTriggerControl,
22
22
  } from "./settings/trigger.js";
23
+ import { restoreRendererSkin } from "./settings/skin-runtime.js";
23
24
 
24
25
  const UPDATE_CHECK_TIMEOUT_MS = 5_000;
25
26
  const UPDATE_RETRY_DELAYS_MS = [1_000, 3_000, 10_000, 30_000] as const;
@@ -45,6 +46,7 @@ export function installRendererSettingsLifecycle(
45
46
  ownerWindow: Window = window,
46
47
  options: RendererSettingsLifecycleOptions = {},
47
48
  ): RendererSettingsLifecycleControl {
49
+ restoreRendererSkin(ownerWindow);
48
50
  const lifecycleController = new AbortController();
49
51
  let locale = resolveRendererSettingsLocale(ownerWindow.navigator.languages);
50
52
  let shell: RendererSettingsShell | null = null;
@@ -147,6 +147,7 @@ export function rendererAgentForThreadOwnership(
147
147
  if (ownership.harnessId === "zcode") return "zcode";
148
148
  if (ownership.harnessId === "trae") return "trae";
149
149
  if (ownership.harnessId === "cursor-cli") return "cursor-cli";
150
+ if (ownership.harnessId === "cline") return "cline";
150
151
  if (ownership.harnessId === 'codex-harness') return 'codex-harness';
151
152
  return null;
152
153
  }
@@ -31,6 +31,7 @@ export const HARNESS_INSTALL_COMMANDS: Readonly<Partial<Record<ExternalRendererA
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" },
34
+ cline: { command: "npm install -g cline" },
34
35
  "codex-harness": { command: "npm install -g @openai/codex" },
35
36
  });
36
37
 
@@ -50,6 +51,7 @@ const HARNESS_INSTALL_URLS: Readonly<Record<ExternalRendererAgent, string>> = Ob
50
51
  zcode: "https://zcode.z.ai/",
51
52
  trae: "https://www.trae.ai/",
52
53
  'cursor-cli': "https://cursor.com/docs/cli/installation",
54
+ cline: "https://docs.cline.bot/usage/cli-overview",
53
55
  'codex-harness': 'https://developers.openai.com/codex/',
54
56
  });
55
57
 
@@ -33,6 +33,8 @@ import X from "lucide/dist/esm/icons/x.mjs";
33
33
  import Users from "lucide/dist/esm/icons/users.mjs";
34
34
  import Plus from "lucide/dist/esm/icons/plus.mjs";
35
35
  import Database from 'lucide/dist/esm/icons/database.mjs';
36
+ import Palette from "lucide/dist/esm/icons/palette.mjs";
37
+ import ShieldCheck from "lucide/dist/esm/icons/shield-check.mjs";
36
38
  import harnessMixLogoUrl from "../../../../assets/brand-harness-mix.png";
37
39
 
38
40
  export const RENDERER_SETTINGS_ICON_NAMES = [
@@ -69,6 +71,8 @@ export const RENDERER_SETTINGS_ICON_NAMES = [
69
71
  "terminal",
70
72
  "search",
71
73
  "help",
74
+ "palette",
75
+ "shield",
72
76
  "storage",
73
77
  ] as const;
74
78
 
@@ -108,6 +112,8 @@ const iconNodes = {
108
112
  terminal: Terminal,
109
113
  search: Search,
110
114
  help: CircleHelp,
115
+ palette: Palette,
116
+ shield: ShieldCheck,
111
117
  storage: Database,
112
118
  } satisfies Record<RendererSettingsIconName, IconNode>;
113
119
 
@@ -515,7 +515,8 @@ const ENGLISH_MESSAGES: RendererSettingsMessages = Object.freeze({
515
515
  skills: 'Skills',
516
516
  "session-import": "Session Import",
517
517
  storage: "Storage & verification",
518
- updates: "Updates",
518
+ skins: "Skins",
519
+ updates: "Updates",
519
520
  about: "About",
520
521
  }),
521
522
  });
@@ -773,7 +774,8 @@ const CHINESE_MESSAGES: RendererSettingsMessages = Object.freeze({
773
774
  skills: 'Skills',
774
775
  "session-import": "会话导入",
775
776
  storage: "存储与验证",
776
- updates: "更新",
777
+ skins: "皮肤",
778
+ updates: "更新",
777
779
  about: "关于",
778
780
  }),
779
781
  });
@@ -31,6 +31,7 @@ import { createMcpSettingsPage, createSkillsSettingsPage } from './integrations-
31
31
  import type { RendererIntegrationsClient } from '../renderer-integrations-client.js';
32
32
  import { createAccountsSettingsPage, type RendererCodexAccountClient } from "./accounts-page.js";
33
33
  import { createStorageSettingsPage, type RendererStorageClient } from './storage-page.js';
34
+ import { createSkinSettingsPage } from "./skin-market.js";
34
35
 
35
36
  export type {
36
37
  RendererConnectionAgentSnapshot,
@@ -79,7 +80,8 @@ export const DEFAULT_RENDERER_SETTINGS_PAGE_IDS = [
79
80
  "skills",
80
81
  "session-import",
81
82
  "storage",
82
- "updates",
83
+ "skins",
84
+ "updates",
83
85
  "about",
84
86
  ] as const;
85
87
 
@@ -637,7 +639,8 @@ export function createDefaultRendererSettingsPages(
637
639
  createSkillsSettingsPage(messages, getIntegrationsClient),
638
640
  createSessionImportSettingsPage(messages, getSessionImportClient, openImportedThread),
639
641
  createStorageSettingsPage(messages, getStorageClient),
640
- updatesPage(messages, getUpdateClient),
642
+ createSkinSettingsPage(messages),
643
+ updatesPage(messages, getUpdateClient),
641
644
  aboutPage(messages, getUpdateClient),
642
645
  ]);
643
646
  }
@@ -1,5 +1,6 @@
1
1
  import settingsCss from "./shell.css";
2
- import accountsCss from "./accounts.css";
2
+ import accountsCss from "./accounts.css";
3
+ import skinMarketCss from "./skin-market.css";
3
4
  import {
4
5
  RendererSettingsNavigationState,
5
6
  RendererSettingsPageScope,
@@ -68,7 +69,7 @@ export function mountRendererSettingsShell(
68
69
  root.style.colorScheme = RENDERER_SETTINGS_COLOR_SCHEME;
69
70
  const shadow = root.attachShadow({ mode: "open" });
70
71
  const style = ownerDocument.createElement("style");
71
- style.textContent = `${settingsCss}\n${accountsCss}`;
72
+ style.textContent = `${settingsCss}\n${accountsCss}\n${skinMarketCss}`;
72
73
 
73
74
  const dialog = ownerDocument.createElement("dialog");
74
75
  dialog.className = "codexhost-settings-dialog";
@@ -0,0 +1,213 @@
1
+ .skin-market__intro {
2
+ max-width: 680px;
3
+ margin: -8px 0 18px;
4
+ color: var(--settings-muted);
5
+ font-size: 13px;
6
+ }
7
+
8
+ .skin-safety-note {
9
+ display: grid;
10
+ grid-template-columns: 18px minmax(0, 1fr);
11
+ align-items: start;
12
+ gap: 10px;
13
+ margin: 0 0 22px;
14
+ padding: 12px 14px;
15
+ color: var(--settings-muted);
16
+ background: var(--settings-inset);
17
+ border: 1px solid var(--settings-border);
18
+ border-radius: 10px;
19
+ }
20
+
21
+ .skin-safety-note > span,
22
+ .skin-safety-note strong {
23
+ display: block;
24
+ }
25
+
26
+ .skin-safety-note strong {
27
+ margin-bottom: 2px;
28
+ color: var(--settings-text);
29
+ font-weight: 600;
30
+ }
31
+
32
+ .skin-market-grid {
33
+ display: grid;
34
+ grid-template-columns: repeat(3, minmax(0, 1fr));
35
+ gap: 13px;
36
+ }
37
+
38
+ .skin-card {
39
+ overflow: hidden;
40
+ position: relative;
41
+ min-width: 0;
42
+ background: var(--settings-panel);
43
+ border: 1px solid var(--settings-border);
44
+ border-radius: 12px;
45
+ }
46
+
47
+ .skin-card[data-selected] {
48
+ border-color: color-mix(in srgb, var(--settings-focus) 62%, var(--settings-border));
49
+ box-shadow: inset 3px 0 0 var(--settings-focus);
50
+ }
51
+
52
+ .skin-preview {
53
+ position: relative;
54
+ height: 128px;
55
+ overflow: hidden;
56
+ border-bottom: 1px solid rgb(255 255 255 / 10%);
57
+ }
58
+
59
+ .skin-preview::after {
60
+ content: "";
61
+ position: absolute;
62
+ inset: 0;
63
+ background: linear-gradient(180deg, rgb(255 255 255 / 8%), transparent 36%);
64
+ pointer-events: none;
65
+ }
66
+
67
+ .skin-preview__dots {
68
+ position: absolute;
69
+ top: 11px;
70
+ left: 12px;
71
+ width: 5px;
72
+ height: 5px;
73
+ border-radius: 50%;
74
+ background: rgb(255 255 255 / 70%);
75
+ box-shadow: 9px 0 rgb(255 255 255 / 42%), 18px 0 rgb(255 255 255 / 24%);
76
+ }
77
+
78
+ .skin-preview__sidebar,
79
+ .skin-preview__conversation,
80
+ .skin-preview__composer {
81
+ position: absolute;
82
+ background: rgb(5 10 16 / 48%);
83
+ border: 1px solid rgb(255 255 255 / 14%);
84
+ backdrop-filter: blur(8px);
85
+ }
86
+
87
+ .skin-preview__sidebar {
88
+ inset: 27px auto 10px 10px;
89
+ width: 26%;
90
+ border-radius: 6px;
91
+ }
92
+
93
+ .skin-preview__conversation {
94
+ inset: 27px 10px 42px 31%;
95
+ border-radius: 7px;
96
+ }
97
+
98
+ .skin-preview__conversation::before,
99
+ .skin-preview__conversation::after {
100
+ content: "";
101
+ position: absolute;
102
+ left: 12px;
103
+ height: 5px;
104
+ border-radius: 3px;
105
+ background: rgb(255 255 255 / 48%);
106
+ }
107
+
108
+ .skin-preview__conversation::before {
109
+ top: 14px;
110
+ width: 56%;
111
+ }
112
+
113
+ .skin-preview__conversation::after {
114
+ top: 27px;
115
+ width: 38%;
116
+ opacity: 0.55;
117
+ }
118
+
119
+ .skin-preview__composer {
120
+ right: 10px;
121
+ bottom: 10px;
122
+ left: 31%;
123
+ height: 24px;
124
+ border-radius: 8px;
125
+ }
126
+
127
+ .skin-card__body {
128
+ padding: 14px;
129
+ }
130
+
131
+ .skin-card__title-row,
132
+ .skin-card__footer {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 10px;
136
+ }
137
+
138
+ .skin-card__title {
139
+ font-size: 14px;
140
+ font-weight: 600;
141
+ }
142
+
143
+ .skin-card__current {
144
+ margin-left: auto;
145
+ color: var(--settings-success);
146
+ font-size: 11px;
147
+ font-weight: 600;
148
+ }
149
+
150
+ .skin-card__description {
151
+ min-height: 38px;
152
+ margin: 7px 0 13px;
153
+ color: var(--settings-muted);
154
+ font-size: 12px;
155
+ line-height: 1.55;
156
+ }
157
+
158
+ .skin-card__description a {
159
+ color: inherit;
160
+ text-decoration: underline;
161
+ text-decoration-color: var(--settings-border);
162
+ text-underline-offset: 3px;
163
+ }
164
+
165
+ .skin-card__description a:hover {
166
+ color: var(--settings-text);
167
+ }
168
+
169
+ .skin-card__palette {
170
+ display: flex;
171
+ align-items: center;
172
+ }
173
+
174
+ .skin-card__palette i {
175
+ width: 18px;
176
+ height: 18px;
177
+ margin-left: -4px;
178
+ border: 2px solid var(--settings-panel);
179
+ border-radius: 50%;
180
+ }
181
+
182
+ .skin-card__palette i:first-child {
183
+ margin-left: 0;
184
+ }
185
+
186
+ .skin-card__apply {
187
+ margin-left: auto;
188
+ }
189
+
190
+ .skin-card__apply:disabled {
191
+ color: var(--settings-muted);
192
+ background: var(--settings-surface);
193
+ cursor: default;
194
+ }
195
+
196
+ @media (max-width: 900px) {
197
+ .skin-market-grid {
198
+ grid-template-columns: repeat(2, minmax(0, 1fr));
199
+ }
200
+ }
201
+
202
+ @media (max-width: 620px) {
203
+ .skin-market-grid {
204
+ grid-template-columns: 1fr;
205
+ }
206
+ }
207
+
208
+ @media (prefers-reduced-motion: reduce) {
209
+ .skin-card,
210
+ .skin-card * {
211
+ transition: none !important;
212
+ }
213
+ }
@@ -0,0 +1,174 @@
1
+ import type { RendererSettingsMessages } from "./localization.js";
2
+ import type {
3
+ RendererSettingsPageDefinition,
4
+ RendererSettingsPageMountContext,
5
+ } from "./core.js";
6
+ import { createRendererSettingsIcon } from "./icons.js";
7
+ import {
8
+ RENDERER_SKINS,
9
+ applyRendererSkin,
10
+ readActiveRendererSkin,
11
+ type RendererSkinDefinition,
12
+ type RendererSkinId,
13
+ } from "./skin-runtime.js";
14
+
15
+ const COPY = {
16
+ en: {
17
+ title: "Skins",
18
+ intro: "Change the atmosphere, not the app. Every skin keeps Codex controls and native Harness behavior intact.",
19
+ safe: "Visual layer only",
20
+ safeDetail: "No replacement controls, scripts, credentials, model routes, tools, or permission changes.",
21
+ selected: "Current skin",
22
+ apply: "Apply skin",
23
+ applied: "Applied",
24
+ native: "The untouched Codex appearance. Select it anytime to remove every Harness Mix skin override.",
25
+ included: "Bundled theme from",
26
+ light: "Light",
27
+ dark: "Dark",
28
+ attribution: "Open source reference",
29
+ },
30
+ "zh-CN": {
31
+ title: "皮肤",
32
+ intro: "只换氛围,不换内核。所有皮肤都保留 Codex 原生控件与各 Harness 的真实行为。",
33
+ safe: "仅视觉层",
34
+ safeDetail: "不替换控件,不运行主题脚本,不改变凭据、模型路由、工具或权限。",
35
+ selected: "当前皮肤",
36
+ apply: "应用皮肤",
37
+ applied: "已应用",
38
+ native: "未经修改的 Codex 外观。随时选择它,即可移除 Harness Mix 的全部皮肤覆盖。",
39
+ included: "内置主题来自",
40
+ light: "浅色",
41
+ dark: "深色",
42
+ attribution: "开源参考项目",
43
+ },
44
+ } as const;
45
+
46
+ function previewChrome(document: Document, skin: RendererSkinDefinition): HTMLElement {
47
+ const preview = document.createElement("div");
48
+ preview.className = "skin-preview";
49
+ preview.style.backgroundImage = skin.heroUrl
50
+ ? `${skin.preview}, url("${skin.heroUrl}")`
51
+ : skin.preview;
52
+ preview.style.backgroundPosition = `center, ${skin.focus}`;
53
+ preview.style.backgroundSize = "cover";
54
+ preview.setAttribute("aria-hidden", "true");
55
+ const dots = document.createElement("span");
56
+ dots.className = "skin-preview__dots";
57
+ const sidebar = document.createElement("span");
58
+ sidebar.className = "skin-preview__sidebar";
59
+ const conversation = document.createElement("span");
60
+ conversation.className = "skin-preview__conversation";
61
+ const composer = document.createElement("span");
62
+ composer.className = "skin-preview__composer";
63
+ preview.append(dots, sidebar, conversation, composer);
64
+ return preview;
65
+ }
66
+
67
+ export function createSkinSettingsPage(
68
+ messages: RendererSettingsMessages,
69
+ ): RendererSettingsPageDefinition {
70
+ const copy = COPY[messages.locale];
71
+ return Object.freeze({
72
+ id: "skins",
73
+ label: messages.pageLabels.skins,
74
+ icon: "palette",
75
+ mount(context: RendererSettingsPageMountContext) {
76
+ const document = context.content.ownerDocument;
77
+ const heading = document.createElement("div");
78
+ heading.className = "settings-section-label";
79
+ heading.textContent = copy.title;
80
+ const introduction = document.createElement("p");
81
+ introduction.className = "skin-market__intro";
82
+ introduction.textContent = copy.intro;
83
+ const safety = document.createElement("aside");
84
+ safety.className = "skin-safety-note";
85
+ safety.append(createRendererSettingsIcon("shield", 17));
86
+ const safetyCopy = document.createElement("span");
87
+ const safetyTitle = document.createElement("strong");
88
+ safetyTitle.textContent = copy.safe;
89
+ const safetyDetail = document.createElement("span");
90
+ safetyDetail.textContent = copy.safeDetail;
91
+ safetyCopy.append(safetyTitle, safetyDetail);
92
+ safety.append(safetyCopy);
93
+
94
+ const grid = document.createElement("div");
95
+ grid.className = "skin-market-grid";
96
+ let selected = readActiveRendererSkin(document);
97
+ const cards = new Map<RendererSkinId, HTMLElement>();
98
+
99
+ const renderSelection = (): void => {
100
+ for (const [id, card] of cards) {
101
+ const active = id === selected;
102
+ card.toggleAttribute("data-selected", active);
103
+ const button = card.querySelector<HTMLButtonElement>("button[data-skin-action]");
104
+ if (button) {
105
+ button.disabled = active;
106
+ button.textContent = active ? copy.applied : copy.apply;
107
+ }
108
+ const badge = card.querySelector<HTMLElement>("[data-skin-current]");
109
+ if (badge) badge.hidden = !active;
110
+ }
111
+ };
112
+
113
+ for (const skin of RENDERER_SKINS) {
114
+ const card = document.createElement("article");
115
+ card.className = "skin-card";
116
+ card.dataset.skinId = skin.id;
117
+ card.append(previewChrome(document, skin));
118
+ const body = document.createElement("div");
119
+ body.className = "skin-card__body";
120
+ const titleRow = document.createElement("div");
121
+ titleRow.className = "skin-card__title-row";
122
+ const title = document.createElement("strong");
123
+ title.className = "skin-card__title";
124
+ title.textContent = skin.name;
125
+ const current = document.createElement("span");
126
+ current.className = "skin-card__current";
127
+ current.dataset.skinCurrent = "true";
128
+ current.textContent = copy.selected;
129
+ titleRow.append(title, current);
130
+ const description = document.createElement("p");
131
+ description.className = "skin-card__description";
132
+ if (skin.sourceUrl) {
133
+ description.append(`${copy.included} `);
134
+ const source = document.createElement("a");
135
+ source.href = skin.sourceUrl;
136
+ source.target = "_blank";
137
+ source.rel = "noopener noreferrer";
138
+ source.textContent = skin.sourceName;
139
+ source.title = copy.attribution;
140
+ description.append(source);
141
+ description.append(` · ${skin.dark ? copy.dark : copy.light}`);
142
+ } else {
143
+ description.textContent = copy.native;
144
+ }
145
+ const footer = document.createElement("div");
146
+ footer.className = "skin-card__footer";
147
+ const palette = document.createElement("span");
148
+ palette.className = "skin-card__palette";
149
+ palette.setAttribute("aria-hidden", "true");
150
+ for (const color of skin.palette) {
151
+ const swatch = document.createElement("i");
152
+ swatch.style.backgroundColor = color;
153
+ palette.append(swatch);
154
+ }
155
+ const apply = document.createElement("button");
156
+ apply.type = "button";
157
+ apply.className = "settings-command-button skin-card__apply";
158
+ apply.dataset.skinAction = skin.id;
159
+ apply.addEventListener("click", () => {
160
+ selected = applyRendererSkin(skin.id, document);
161
+ renderSelection();
162
+ });
163
+ footer.append(palette, apply);
164
+ body.append(titleRow, description, footer);
165
+ card.append(body);
166
+ cards.set(skin.id, card);
167
+ grid.append(card);
168
+ }
169
+ renderSelection();
170
+ context.content.append(heading, introduction, safety, grid);
171
+ return undefined;
172
+ },
173
+ });
174
+ }