pi-web-ui 0.69.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1110,9 +1110,15 @@ export class ClientSession {
1110
1110
  // 复用现有重命名路径(内存标题 + 磁盘 session_info)
1111
1111
  void this.renameConversation(convId, title);
1112
1112
  },
1113
+ // 标记 widget 合并进扩展 widget 里,跟随当前活动会话渲染(切换会话即刷新)。
1114
+ refreshMarkers: () => this.webUi.refresh(),
1113
1115
  // issue #91:标记引导/错误按客户端 UI 语言出中英(英文默认)。
1114
1116
  lang: () => this.getLang(),
1115
1117
  });
1118
+ // 标记 widget 动态渲染「当前活动会话」的 todo/overlay:切换会话时只要刷新
1119
+ // webUi(见 switchConversation/setCwd/newChat)就会显示对应会话的标记,
1120
+ // 且与扩展 widget 合并下发、不会互相覆盖。
1121
+ this.webUi.setDynamicWidget("markers", () => this.markerSvc.overlayLines(this.activeId));
1116
1122
  this.settingsSvc = new SettingsService({
1117
1123
  clientId,
1118
1124
  stateStore,
@@ -2839,7 +2845,10 @@ export class ClientSession {
2839
2845
  }
2840
2846
  /** Persist + apply a partial settings update (prompt text/mode, toggles). */
2841
2847
  async setSettings(partial) {
2842
- const { markersEnabled, disabledMarkers, ...rest } = partial;
2848
+ const { markersEnabled, disabledMarkers, quickPhrasesSeeded, ...rest } = partial;
2849
+ // 快捷短语「已 seed」是全局标记(非 per-clientId):置位一次后永久生效。
2850
+ if (quickPhrasesSeeded)
2851
+ this.stateStore.markQuickPhrasesSeeded();
2843
2852
  let markerChanged = false;
2844
2853
  if (markersEnabled !== undefined || disabledMarkers !== undefined) {
2845
2854
  this.markerSvc.setAll({
@@ -3475,7 +3484,7 @@ export class ClientSession {
3475
3484
  reviewing: conv.goal.reviewing,
3476
3485
  wizardRunning: conv.wizardRunning,
3477
3486
  streaming: conv.session.isStreaming,
3478
- openTerminals: conv.terminals.list().length,
3487
+ openTerminals: conv.terminals.countLive(),
3479
3488
  listed: conv.listed,
3480
3489
  promptedSinceActive: conv.promptedSinceActive,
3481
3490
  hasActiveSubagentRun: () => hasActiveSubagentRun({ sessionId: conv.session.sessionFile }),
@@ -3872,7 +3881,7 @@ export class ClientSession {
3872
3881
  reviewing: conv.goal.reviewing,
3873
3882
  wizardRunning: conv.wizardRunning,
3874
3883
  streaming: conv.session.isStreaming,
3875
- openTerminals: conv.terminals.list().length,
3884
+ openTerminals: conv.terminals.countLive(),
3876
3885
  listed: conv.listed,
3877
3886
  promptedSinceActive: conv.promptedSinceActive,
3878
3887
  hasActiveSubagentRun: () => hasActiveSubagentRun({ sessionId: conv.session.sessionFile }),
@@ -3887,7 +3896,7 @@ export class ClientSession {
3887
3896
  textEn: `Conversation "${conv.title}" is still running — wait for it to finish or press Stop before removing`,
3888
3897
  });
3889
3898
  }
3890
- else if (conv.terminals.list().length > 0) {
3899
+ else if (conv.terminals.countLive() > 0) {
3891
3900
  this.emit({
3892
3901
  type: "notice",
3893
3902
  level: "warning",
@@ -76,6 +76,14 @@ export class ClientStateStore {
76
76
  constructor(filePath) {
77
77
  this.filePath = filePath;
78
78
  }
79
+ /** 长期设置(设置面板 config + 预设 + 标记开关)的固定存储键。
80
+ *
81
+ * 为什么用固定全局键而非 per-clientId:clientId 存 sessionStorage(每标签页独立、
82
+ * 关浏览器即失),按 clientId 存设置会在每次新会话/重启后生成新 id → 设置全部重置、
83
+ * 且各标签页/浏览器各有一套互不同步。改为全局共享后:所有客户端(标签页/浏览器)
84
+ * 使用同一套配置,且持久化在服务端,重启不丢(「同一套配置」)。会话级状态
85
+ * (最近项目 / lastCwd / 项目模型与密钥等)仍按 clientId 各自保留。 */
86
+ static GLOBAL_SETTINGS_KEY = "__settings__";
79
87
  /** <dataDir>(client-state.json 的上一级)——共享配置(子代理模板库等)落在这里。 */
80
88
  get dataDir() {
81
89
  return dirname(this.filePath);
@@ -197,9 +205,9 @@ export class ClientStateStore {
197
205
  }
198
206
  return list;
199
207
  }
200
- /** Last-used settings-panel state for a client, or defaults. */
201
- getSettings(clientId) {
202
- const s = this.load()[clientId];
208
+ /** 设置面板状态(系统提示词模式/文字 + 禁用技能/扩展)——全局共享同一套配置。 */
209
+ getSettings(_clientId) {
210
+ const s = this.load()[ClientStateStore.GLOBAL_SETTINGS_KEY];
203
211
  const stored = s?.settings;
204
212
  // 旧存档(promptMode/customSystemPrompt)迁移到 compose:追加文字成为独立
205
213
  // {{append}} 覆盖、替换文字成为 {{soul}} 覆盖;无自定义则用默认模板。
@@ -242,10 +250,10 @@ export class ClientStateStore {
242
250
  disabledPlugins: stored?.disabledPlugins ?? [],
243
251
  };
244
252
  }
245
- /** Persist the client's settings-panel state (partial merge). */
246
- saveSettings(clientId, settings) {
253
+ /** Persist the settings-panel state (partial merge) — global shared config. */
254
+ saveSettings(_clientId, settings) {
247
255
  const all = this.load();
248
- const state = (all[clientId] ??= { projects: [] });
256
+ const state = (all[ClientStateStore.GLOBAL_SETTINGS_KEY] ??= { projects: [] });
249
257
  const cur = state.settings ?? {};
250
258
  state.settings = {
251
259
  promptMode: settings.promptMode ?? cur.promptMode ?? "append",
@@ -276,9 +284,9 @@ export class ClientStateStore {
276
284
  };
277
285
  this.save();
278
286
  }
279
- /** Named settings presets for a client (empty if never saved). */
280
- getPresets(clientId) {
281
- return (this.load()[clientId]?.presets ?? []).map((p) => ({
287
+ /** Named settings presets for a client (empty if never saved) — global shared. */
288
+ getPresets(_clientId) {
289
+ return (this.load()[ClientStateStore.GLOBAL_SETTINGS_KEY]?.presets ?? []).map((p) => ({
282
290
  ...p,
283
291
  // Older client-state files predate review settings.
284
292
  reviewPrompt: p.reviewPrompt ?? "",
@@ -287,10 +295,10 @@ export class ClientStateStore {
287
295
  retryMaxAttempts: normalizeRetryMaxAttempts(p.retryMaxAttempts),
288
296
  }));
289
297
  }
290
- /** Persist the client's named settings presets. */
291
- savePresets(clientId, presets) {
298
+ /** Persist the named settings presets — global shared config. */
299
+ savePresets(_clientId, presets) {
292
300
  const all = this.load();
293
- const state = (all[clientId] ??= { projects: [] });
301
+ const state = (all[ClientStateStore.GLOBAL_SETTINGS_KEY] ??= { projects: [] });
294
302
  state.presets = presets;
295
303
  this.save();
296
304
  }
@@ -346,17 +354,36 @@ export class ClientStateStore {
346
354
  delete all[clientId].projectModels;
347
355
  this.save();
348
356
  }
349
- /** 内置标记工具开关(全局 + 按 marker 禁用)。 */
350
- getMarkerSettings(clientId) {
351
- const s = this.load()[clientId]?.markers;
357
+ /** 全局「快捷短语已 seed」标记(非 per-clientId)。
358
+ *
359
+ * 为什么全局:clientId sessionStorage(每标签页独立、关浏览器即失),按
360
+ * clientId 记 seed 会在每次新会话生成新 clientId 时误判为「从未 seed」,导致
361
+ * 用户删掉的默认短语又被填回默认。seed 只需一次(首次见空列表),之后即为用户
362
+ * 数据,增删改/恢复默认/关闭都走设置面板。存服务端而非浏览器 localStorage,
363
+ * 任何浏览器/标签页/清缓存都不受影响。 */
364
+ getQuickPhrasesSeeded() {
365
+ const meta = this.load()[ClientStateStore.GLOBAL_SETTINGS_KEY];
366
+ return !!meta?.quickPhrasesSeeded;
367
+ }
368
+ markQuickPhrasesSeeded() {
369
+ const all = this.load();
370
+ const meta = (all[ClientStateStore.GLOBAL_SETTINGS_KEY] ??= { projects: [] });
371
+ if (meta.quickPhrasesSeeded)
372
+ return;
373
+ meta.quickPhrasesSeeded = true;
374
+ this.save();
375
+ }
376
+ /** 内置标记工具开关(全局共享同一套 + 按 marker 禁用)。 */
377
+ getMarkerSettings(_clientId) {
378
+ const s = this.load()[ClientStateStore.GLOBAL_SETTINGS_KEY]?.markers;
352
379
  return {
353
380
  markersEnabled: s?.markersEnabled ?? true,
354
381
  disabledMarkers: s?.disabledMarkers ?? [],
355
382
  };
356
383
  }
357
- saveMarkerSettings(clientId, settings) {
384
+ saveMarkerSettings(_clientId, settings) {
358
385
  const all = this.load();
359
- const state = (all[clientId] ??= { projects: [] });
386
+ const state = (all[ClientStateStore.GLOBAL_SETTINGS_KEY] ??= { projects: [] });
360
387
  const cur = state.markers ?? { markersEnabled: true, disabledMarkers: [] };
361
388
  state.markers = {
362
389
  markersEnabled: settings.markersEnabled ?? cur.markersEnabled ?? true,
@@ -2141,10 +2141,13 @@ export class DshClientSession {
2141
2141
  subagentModels: [],
2142
2142
  quickPhrases: [...this.settings.quickPhrases],
2143
2143
  quickPhrasesEnabled: this.settings.quickPhrasesEnabled,
2144
+ quickPhrasesSeeded: this.stateStore.getQuickPhrasesSeeded(),
2144
2145
  };
2145
2146
  this.emit({ type: "settings_state", settings });
2146
2147
  }
2147
2148
  async setSettings(partial) {
2149
+ if (partial.quickPhrasesSeeded)
2150
+ this.stateStore.markQuickPhrasesSeeded();
2148
2151
  if (partial.promptMode !== undefined)
2149
2152
  this.settings.promptMode = partial.promptMode;
2150
2153
  if (partial.customSystemPrompt !== undefined)
@@ -947,6 +947,7 @@ wss.on("connection", (ws) => {
947
947
  disabledMarkers: msg.disabledMarkers,
948
948
  quickPhrases: msg.quickPhrases,
949
949
  quickPhrasesEnabled: msg.quickPhrasesEnabled,
950
+ quickPhrasesSeeded: msg.quickPhrasesSeeded,
950
951
  });
951
952
  break;
952
953
  case "extensions_reload":
@@ -44,15 +44,18 @@ export class MarkerService {
44
44
  const lines = collectGuidance(disabled, this.lang());
45
45
  if (lines.length === 0)
46
46
  return "";
47
+ // The dynamic lines ride the `{lines}` slot so translator packs can
48
+ // place them (a static table value could never inline them).
49
+ const linesText = lines.join("\n");
47
50
  return pick(this.lang(), "\n\n────────── 内联标记工具(重要)──────────\n" +
48
51
  "状态类操作(任务、标记位、通知)请直接写在回答正文里,采用内联标记语法,而不要调用对应工具。\n" +
49
52
  "插件会替你执行并把标记从展示文本中移除,不会中断你的回答。\n\n" +
50
- lines.join("\n") +
53
+ linesText +
51
54
  "\n──────────\n", "\n\n────────── Inline marker tools (important) ──────────\n" +
52
55
  "Write status operations (tasks, marker bits, notifications) directly in your reply text using inline marker syntax — do not call a tool for them.\n" +
53
56
  "The plugin executes them and strips the markers from the displayed text without interrupting your reply.\n\n" +
54
- lines.join("\n") +
55
- "\n──────────\n", "markers.service.guidance.frame");
57
+ linesText +
58
+ "\n──────────\n", "markers.service.guidance.frame", { lines: linesText });
56
59
  }
57
60
  setEnabled(enabled) {
58
61
  this.settings.markersEnabled = !!enabled;
@@ -205,6 +208,18 @@ export class MarkerService {
205
208
  this.pushOverlay(conversationId);
206
209
  }
207
210
  pushOverlay(conversationId) {
211
+ const lines = this.overlayLines(conversationId);
212
+ const key = `markers:${conversationId}`;
213
+ const prev = this.overlayCache.get(key);
214
+ const next = lines.length ? lines : [];
215
+ if (prev && prev.join("\n") === next.join("\n"))
216
+ return;
217
+ this.overlayCache.set(key, next);
218
+ // 通过宿主的 widget 合并(跟随当前活动会话,不覆盖扩展 widget)。
219
+ this.host.refreshMarkers();
220
+ }
221
+ /** 计算某个会话的标记 overlay 行(供 UI widget 动态渲染当前活动会话)。 */
222
+ overlayLines(conversationId) {
208
223
  const lines = [];
209
224
  for (const m of allMarkers()) {
210
225
  if (this.settings.disabledMarkers.includes(m.name))
@@ -230,18 +245,7 @@ export class MarkerService {
230
245
  lines.push(`[${ov.tool}]`);
231
246
  lines.push(...ov.lines.map((l) => ` ${l}`));
232
247
  }
233
- const key = `markers:${conversationId}`;
234
- const prev = this.overlayCache.get(key);
235
- const next = lines.length ? lines : [];
236
- if (prev && prev.join("\n") === next.join("\n"))
237
- return;
238
- this.overlayCache.set(key, next);
239
- if (lines.length > 0) {
240
- this.host.emit({ type: "widgets", widgets: [{ key: "markers", lines }] });
241
- }
242
- else {
243
- this.host.emit({ type: "widgets", widgets: [] });
244
- }
248
+ return lines;
245
249
  }
246
250
  describe(conversationId, tool, includeDeleted = false) {
247
251
  const st = this.getState(conversationId, TODO_NAMESPACE, initTodoState);
@@ -232,6 +232,7 @@ export class SettingsService {
232
232
  subagentModels: this.collectSubagentModels(),
233
233
  quickPhrases: [...this.settings.quickPhrases],
234
234
  quickPhrasesEnabled: this.settings.quickPhrasesEnabled,
235
+ quickPhrasesSeeded: this.host.stateStore.getQuickPhrasesSeeded(),
235
236
  },
236
237
  });
237
238
  }
@@ -126,18 +126,25 @@ export function makeSubagentTools(host, lang) {
126
126
  const convId = await host.spawnSubagent(p.prompt, p.type ?? "general", p.cwd ?? ctx.cwd, p.template, p.model);
127
127
  const subagentType = p.type ?? "general";
128
128
  const subagentTitleText = subagentTitle(p.prompt);
129
- return text(pick(getLang(), `子代理已启动(运行列表可见):${convId}\n类型:${subagentType} · 标题:${subagentTitleText}` +
130
- (p.template ? `\n模板:${p.template}` : "") +
131
- (p.model ? `\n模型:${p.model}` : "") +
132
- `\n用 subagent_wait_all 一次等全部完成(不用轮询),subagent_get_result 取单个结果,subagent_list 看运行态,subagent_steer 改向,subagent_stop 停止。`, `Subagent started (visible in the running list): ${convId}\nType: ${subagentType} · Title: ${subagentTitleText}` +
133
- (p.template ? `\nTemplate: ${p.template}` : "") +
134
- (p.model ? `\nModel: ${p.model}` : "") +
129
+ // Optional segments are pre-rendered per language (translators pick
130
+ // the Zh/En variant through the vars table; inline ternaries would
131
+ // leak source syntax into packs that copy them verbatim).
132
+ const templateLineZh = p.template ? `\n模板:${p.template}` : "";
133
+ const templateLineEn = p.template ? `\nTemplate: ${p.template}` : "";
134
+ const modelLineZh = p.model ? `\n模型:${p.model}` : "";
135
+ const modelLineEn = p.model ? `\nModel: ${p.model}` : "";
136
+ return text(pick(getLang(), `子代理已启动(运行列表可见):${convId}\n类型:${subagentType} · 标题:${subagentTitleText}${templateLineZh}${modelLineZh}` +
137
+ `\n用 subagent_wait_all 一次等全部完成(不用轮询),subagent_get_result 取单个结果,subagent_list 看运行态,subagent_steer 改向,subagent_stop 停止。`, `Subagent started (visible in the running list): ${convId}\nType: ${subagentType} · Title: ${subagentTitleText}${templateLineEn}${modelLineEn}` +
135
138
  `\nUse subagent_wait_all to wait for all at once (no polling), subagent_get_result for a single result, subagent_list for live status, subagent_steer to redirect, subagent_stop to stop.`, "subagents.spawn.started", {
136
139
  convId: convId,
137
140
  subagentType: subagentType,
138
141
  subagentTitleText: subagentTitleText,
139
142
  "p.template": p.template,
140
143
  "p.model": p.model,
144
+ templateLineZh: templateLineZh,
145
+ templateLineEn: templateLineEn,
146
+ modelLineZh: modelLineZh,
147
+ modelLineEn: modelLineEn,
141
148
  }), { convId, template: p.template, model: p.model });
142
149
  },
143
150
  }),
@@ -1009,6 +1009,17 @@ export class TerminalManager {
1009
1009
  list() {
1010
1010
  return [...this.terms.values(), ...this.history.values()].map((entry) => this.info(entry));
1011
1011
  }
1012
+ /** Count of LIVE PTYs only — exited terminals that merely retain their
1013
+ * output for review (history) do NOT count. Conversations are kept in the
1014
+ * running list while a live terminal exists; exited leftovers must not
1015
+ * pin an idle conversation forever. */
1016
+ countLive() {
1017
+ let n = 0;
1018
+ for (const entry of this.terms.values())
1019
+ if (!entry.exited)
1020
+ n++;
1021
+ return n;
1022
+ }
1012
1023
  emitList() {
1013
1024
  this.emit({ type: "terminal_list", terminals: this.list() });
1014
1025
  }
@@ -1539,7 +1550,7 @@ function backgroundResult(terminals, opts, command, partialText, silentSeconds,
1539
1550
  text: pick(lang, `命令仍在持久终端 ai-bash 中运行(已连续 ${silentSeconds} 秒无输出,未结束)。` +
1540
1551
  `本次调用不阻塞——命令继续在后台执行,结束时你会收到自动通知。\n` +
1541
1552
  `已有输出:\n${partialZh}\n` +
1542
- `要重新阻塞等它结束就用 terminal_wait(terminalId="ai-bash")(无需反复轮询);需要交互用 terminal_input / terminal_key(Ctrl+C 可终止)。`, `Command still running in the persistent terminal ai-bash (no output for ${silentSeconds}s, not finished).` +
1553
+ `要重新阻塞等它结束就用 terminal_wait(terminalId="ai-bash")(无需反复轮询);需要交互用 terminal_input / terminal_key(Ctrl+C 可终止)。`, `Command still running in the persistent terminal ai-bash (no output for ${silentSeconds}s, not finished). ` +
1543
1554
  `This call does not block — the command keeps running in the background and you will be notified automatically when it finishes.\n` +
1544
1555
  `Partial output:\n${partialEn}\n` +
1545
1556
  `To block until it finishes, use terminal_wait(terminalId="ai-bash") (no polling needed); use terminal_input / terminal_key to interact (Ctrl+C aborts).`, "terminals.bash.background.running", { silentSeconds, partialZh, partialEn }),
@@ -45,6 +45,15 @@ export class WebUIContext {
45
45
  this.emit = emit;
46
46
  }
47
47
  // -- widgets -------------------------------------------------------------
48
+ /** Register a widget whose lines come from a plain getter, re-evaluated on
49
+ * every render/refresh (no SDK Component required). Used for
50
+ * conversation-scoped overlays (e.g. markers) so switching conversations
51
+ * re-renders them without re-registering. */
52
+ setDynamicWidget(key, lines) {
53
+ this.widgets.set(key, { render: () => lines() });
54
+ this.lastLines.delete(key);
55
+ this.push();
56
+ }
48
57
  /** Matches ExtensionUIContext's overloaded setWidget exactly. */
49
58
  setWidget = (key, content, options) => {
50
59
  void options;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.69.0",
3
+ "version": "0.70.0",
4
4
  "description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,81 +1,88 @@
1
- /* theme-name: 赛博朋克 */
2
- /* theme-name-en: Cyberpunk */
3
- :root {
4
- color-scheme: dark;
5
- --bg: #0a0a0f;
6
- --bg-elev: #12121e;
7
- --bg-elev2: #1a1a2e;
8
- --border: #2b2b4a;
9
- --border-soft: #20203a;
10
- --text: #e6e6ff;
11
- --text-dim: #9a9ac4;
12
- --text-faint: #6a6a8e;
13
- --accent: #00d4ff;
14
- --accent-soft: rgba(0, 212, 255, 0.14);
15
- --green: #00ff41;
16
- --green-soft: rgba(0, 255, 65, 0.12);
17
- --red: #ff006e;
18
- --red-soft: rgba(255, 0, 110, 0.12);
19
- --amber: #ffd700;
20
- --term-bg: #0a0a0f;
21
- --term-fg: #e6e6ff;
22
- --term-cursor: #00d4ff;
23
- --term-cursor-accent: #0a0a0f;
24
- --term-selection: rgba(0, 212, 255, 0.35);
25
- --term-black: #1a1a2e;
26
- --term-red: #ff006e;
27
- --term-green: #00ff41;
28
- --term-yellow: #ffd700;
29
- --term-blue: #00d4ff;
30
- --term-magenta: #ff00ff;
31
- --term-cyan: #00f5ff;
32
- --term-white: #e6e6ff;
33
- --term-bright-black: #6a6a8e;
34
- --term-bright-red: #ff006e;
35
- --term-bright-green: #00ff41;
36
- --term-bright-yellow: #ffd700;
37
- --term-bright-blue: #00d4ff;
38
- --term-bright-magenta: #ff00ff;
39
- --term-bright-cyan: #00f5ff;
40
- --term-bright-white: #ffffff;
41
- --mono: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
42
- --tooltip-bg: #232733;
43
- --code-bg: #0b0d12;
44
- --code-text: #c9d1d9;
45
- --err-text: #f0a5a5;
46
- --red-text: #fca5a5;
47
- --amber-text: #fcd34d;
48
- --info-blue: #00d4ff;
49
- --link: #a78bfa;
50
- --link-hover: #c4b5fd;
51
- --link-soft: #ddd6fe;
52
- --md-strong: #f3f4f6;
53
- --skill-blue: #5eb3ff;
54
- --plugin-purple: #ff00ff;
55
- --auth-green: #6ee7a0;
56
- --scroll-thumb: #2a2f3d;
57
- --scroll-thumb-hover: #3a4152;
58
- --notice-err-bg: #401d23;
59
- --notice-warn-bg: #38251f;
60
- --notice-info-bg: #1b2544;
61
- --notice-err-border: #ef4444;
62
- --notice-warn-border: #f59e0b;
63
- --notice-info-border: #3b82f6;
64
- --send-blue: #00d4ff;
65
- --send-blue-hover: #00b8d4;
66
- --brand-grad-a: #00d4ff;
67
- --brand-grad-b: #ff006e;
68
- --on-amber: #1a1a1a;
69
- --search-active-fg: #1a1d26;
70
- --switch-knob: #ffffff;
71
- --bg-elev3: rgba(255, 255, 255, 0.06);
72
- --glow-015: rgba(255, 255, 255, 0.015);
73
- --glow-025: rgba(255, 255, 255, 0.025);
74
- --glow-03: rgba(255, 255, 255, 0.03);
75
- --glow-04: rgba(255, 255, 255, 0.04);
76
- --glow-05: rgba(255, 255, 255, 0.05);
77
- --glow-12: rgba(255, 255, 255, 0.12);
78
- --glow-18: rgba(255, 255, 255, 0.18);
79
- --glow-22: rgba(255, 255, 255, 0.22);
80
- --glow-38: rgba(255, 255, 255, 0.38);
81
- }
1
+ /* theme-name: 赛博朋克 */
2
+ /* theme-name-en: Cyberpunk */
3
+ :root {
4
+ color-scheme: dark;
5
+ --bg: #0a0a0f;
6
+ --bg-elev: #12121e;
7
+ --bg-elev2: #1a1a2e;
8
+ --border: #2b2b4a;
9
+ --border-soft: #20203a;
10
+ --text: #e6e6ff;
11
+ --text-dim: #9a9ac4;
12
+ --text-faint: #6a6a8e;
13
+ --accent: #00d4ff;
14
+ --accent-soft: rgba(0, 212, 255, 0.14);
15
+ --green: #00ff41;
16
+ --green-soft: rgba(0, 255, 65, 0.12);
17
+ --red: #ff006e;
18
+ --red-soft: rgba(255, 0, 110, 0.12);
19
+ --amber: #ffd700;
20
+ --term-bg: #0a0a0f;
21
+ --term-fg: #e6e6ff;
22
+ --term-cursor: #00d4ff;
23
+ --term-cursor-accent: #0a0a0f;
24
+ --term-selection: rgba(0, 212, 255, 0.35);
25
+ --term-black: #1a1a2e;
26
+ --term-red: #ff006e;
27
+ --term-green: #00ff41;
28
+ --term-yellow: #ffd700;
29
+ --term-blue: #00d4ff;
30
+ --term-magenta: #ff00ff;
31
+ --term-cyan: #00f5ff;
32
+ --term-white: #e6e6ff;
33
+ --term-bright-black: #6a6a8e;
34
+ --term-bright-red: #ff006e;
35
+ --term-bright-green: #00ff41;
36
+ --term-bright-yellow: #ffd700;
37
+ --term-bright-blue: #00d4ff;
38
+ --term-bright-magenta: #ff00ff;
39
+ --term-bright-cyan: #00f5ff;
40
+ --term-bright-white: #ffffff;
41
+ --mono: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
42
+ --mermaid-font-size: 12px;
43
+ --tooltip-bg: #232733;
44
+ --code-bg: #0b0d12;
45
+ --code-text: #c9d1d9;
46
+ --err-text: #f0a5a5;
47
+ --red-text: #fca5a5;
48
+ --amber-text: #fcd34d;
49
+ --info-blue: #00d4ff;
50
+ --link: #a78bfa;
51
+ --link-hover: #c4b5fd;
52
+ --link-soft: #ddd6fe;
53
+ --md-strong: #f3f4f6;
54
+ --skill-blue: #5eb3ff;
55
+ --plugin-purple: #ff00ff;
56
+ --auth-green: #6ee7a0;
57
+ --scroll-thumb: #2a2f3d;
58
+ --scroll-thumb-hover: #3a4152;
59
+ --notice-err-bg: #401d23;
60
+ --notice-warn-bg: #38251f;
61
+ --notice-info-bg: #1b2544;
62
+ --notice-err-border: #ef4444;
63
+ --notice-warn-border: #f59e0b;
64
+ --notice-info-border: #3b82f6;
65
+ --send-blue: #00d4ff;
66
+ --send-blue-hover: #00b8d4;
67
+ --brand-grad-a: #00d4ff;
68
+ --brand-grad-b: #ff006e;
69
+ --on-amber: #1a1a1a;
70
+ --search-active-fg: #1a1d26;
71
+ --switch-knob: #ffffff;
72
+ --bg-elev3: rgba(255, 255, 255, 0.06);
73
+ --glow-015: rgba(255, 255, 255, 0.015);
74
+ --glow-025: rgba(255, 255, 255, 0.025);
75
+ --glow-03: rgba(255, 255, 255, 0.03);
76
+ --glow-04: rgba(255, 255, 255, 0.04);
77
+ --glow-05: rgba(255, 255, 255, 0.05);
78
+ --glow-12: rgba(255, 255, 255, 0.12);
79
+ --glow-18: rgba(255, 255, 255, 0.18);
80
+ --glow-22: rgba(255, 255, 255, 0.22);
81
+ --glow-38: rgba(255, 255, 255, 0.38);
82
+ --control-fg: #9aa1b4;
83
+ --control-bg: #1a1d26;
84
+ --control-border: #262a35;
85
+ --bg-image: none;
86
+ --bg-image-dim: 0.78;
87
+ --bg-image-blur: 0px;
88
+ }