pi-web-ui 0.70.0 → 0.72.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.
@@ -177,6 +177,21 @@ export class ModelAdminService {
177
177
  const data = this.readProviderKeys();
178
178
  return data[provider]?.activeKeyName ?? null;
179
179
  }
180
+ /** Whether a named key still exists for a provider (no side effects, no
181
+ * notices). Restore paths use this to drop stale per-project references
182
+ * silently instead of going through activate (which notifies). */
183
+ hasProviderKey(provider, keyName) {
184
+ const pid = provider.trim();
185
+ const targetName = keyName.trim();
186
+ if (!pid || !targetName)
187
+ return false;
188
+ try {
189
+ return this.readProviderKeys()[pid]?.keys.some((k) => k.name === targetName) ?? false;
190
+ }
191
+ catch {
192
+ return false;
193
+ }
194
+ }
180
195
  /** Seed a provider's key list from an EXISTING auth.json credential (legacy
181
196
  * configs written before the multi-key store existed) so the store stays
182
197
  * authoritative and the UI shows the current active key immediately even
@@ -373,36 +388,44 @@ export class ModelAdminService {
373
388
  this.host.flushSnapshot();
374
389
  }
375
390
  /** Make a stored API key the ACTIVE one for a built-in provider by NAME (the
376
- * server resolves the stored value from the name). */
377
- async activateProviderKey(provider, keyName) {
391
+ * server resolves the stored value from the name). Returns true when the
392
+ * key is (now) active, false when it doesn't exist or the switch failed.
393
+ * `silent` suppresses all notices — for automatic project restores, which
394
+ * must self-heal stale references without spamming the user. */
395
+ async activateProviderKey(provider, keyName, opts) {
378
396
  const pid = provider.trim();
379
397
  const targetName = keyName.trim();
398
+ const silent = opts?.silent === true;
399
+ const notice = (msg) => {
400
+ if (!silent)
401
+ this.host.emit(msg);
402
+ };
380
403
  try {
381
404
  const data = this.readProviderKeys();
382
405
  const entry = data[pid];
383
406
  const target = entry?.keys.find((k) => k.name === targetName);
384
407
  if (!target) {
385
- this.host.emit({
408
+ notice({
386
409
  type: "notice",
387
410
  level: "error",
388
411
  text: `${pid} 的密钥「${targetName}」不存在`,
389
412
  textEn: `Key "${targetName}" for ${pid} does not exist`,
390
413
  });
391
- return;
414
+ return false;
392
415
  }
393
416
  if (entry.activeKeyName === targetName) {
394
- this.host.emit({
417
+ notice({
395
418
  type: "notice",
396
419
  level: "info",
397
420
  text: `「${targetName}」已是当前密钥`,
398
421
  textEn: `"${targetName}" is already the active key`,
399
422
  });
400
- return;
423
+ return true;
401
424
  }
402
425
  entry.activeKeyName = targetName;
403
426
  this.writeProviderKeys(data);
404
427
  await this.applyActiveKey(pid, target.apiKey);
405
- this.host.emit({
428
+ notice({
406
429
  type: "notice",
407
430
  level: "info",
408
431
  text: `⚡ 已切换到 ${pid} 的「${targetName}」`,
@@ -411,16 +434,21 @@ export class ModelAdminService {
411
434
  await this.host.pushModels();
412
435
  await this.listProviders();
413
436
  this.listProviderKeys();
437
+ return true;
414
438
  }
415
439
  catch (err) {
416
- this.host.emit({
440
+ notice({
417
441
  type: "notice",
418
442
  level: "error",
419
443
  text: `切换密钥失败:${err.message}`,
420
444
  textEn: `Failed to switch key: ${err.message}`,
421
445
  });
446
+ return false;
447
+ }
448
+ finally {
449
+ if (!silent)
450
+ this.host.flushSnapshot();
422
451
  }
423
- this.host.flushSnapshot();
424
452
  }
425
453
  /** Remove a stored API key by NAME. If it was active, the first remaining key
426
454
  * becomes active (or the provider returns to unconfigured when no key is left). */
@@ -8,4 +8,4 @@
8
8
  * its own copy in web/src/protocol-version.ts; scripts/check-protocol-sync.mjs
9
9
  * verifies the two never drift.
10
10
  */
11
- export const PROTOCOL_VERSION = 14;
11
+ export const PROTOCOL_VERSION = 15;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.70.0",
3
+ "version": "0.72.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": {
@@ -21,6 +21,7 @@
21
21
  "deploy/",
22
22
  "extensions/",
23
23
  "plugins/catalog.json",
24
+ "CHANGELOG.md",
24
25
  "README.md",
25
26
  "LICENSE"
26
27
  ],
@@ -62,6 +63,9 @@
62
63
  "test:unit": "vitest run",
63
64
  "test:smoke": "node tests/run-smoke.mjs",
64
65
  "check:protocol": "node scripts/check-protocol-sync.mjs",
66
+ "i18n:diff": "node scripts/i18n-diff.mjs",
67
+ "release:notes": "node scripts/release-notes.mjs",
68
+ "changelog:i18n": "node scripts/release-notes.mjs --unreleased",
65
69
  "test:freeze": "node tests/freeze-test.mjs",
66
70
  "start": "node dist/server/index.js",
67
71
  "format": "prettier --write server/ web/src/ extensions/ scripts/ bin/ tests/*.mjs tests/unit/ *.mjs",
@@ -85,4 +85,13 @@
85
85
  --bg-image: none;
86
86
  --bg-image-dim: 0.78;
87
87
  --bg-image-blur: 0px;
88
+ --wallpaper-panel-alpha: 62%;
89
+ --topbar-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
90
+ --statusbar-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
91
+ --panel-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
92
+ --card-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
93
+ --chip-bg: color-mix(in srgb, var(--bg-elev2) var(--wallpaper-panel-alpha, 62%), transparent);
94
+ --msgs-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
95
+ --inputbox-bg: color-mix(in srgb, var(--bg-elev2) var(--wallpaper-panel-alpha, 62%), transparent);
96
+ --menu-bg: var(--bg-elev2);
88
97
  }
package/themes/dazzle.css CHANGED
@@ -85,4 +85,13 @@
85
85
  --bg-image: none;
86
86
  --bg-image-dim: 0.78;
87
87
  --bg-image-blur: 0px;
88
+ --wallpaper-panel-alpha: 62%;
89
+ --topbar-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
90
+ --statusbar-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
91
+ --panel-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
92
+ --card-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
93
+ --chip-bg: color-mix(in srgb, var(--bg-elev2) var(--wallpaper-panel-alpha, 62%), transparent);
94
+ --msgs-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
95
+ --inputbox-bg: color-mix(in srgb, var(--bg-elev2) var(--wallpaper-panel-alpha, 62%), transparent);
96
+ --menu-bg: var(--bg-elev2);
88
97
  }
@@ -85,6 +85,15 @@
85
85
  --bg-image: none;
86
86
  --bg-image-dim: 0.78;
87
87
  --bg-image-blur: 0px;
88
+ --wallpaper-panel-alpha: 62%;
89
+ --topbar-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
90
+ --statusbar-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
91
+ --panel-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
92
+ --card-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
93
+ --chip-bg: color-mix(in srgb, var(--bg-elev2) var(--wallpaper-panel-alpha, 62%), transparent);
94
+ --msgs-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
95
+ --inputbox-bg: color-mix(in srgb, var(--bg-elev2) var(--wallpaper-panel-alpha, 62%), transparent);
96
+ --menu-bg: var(--bg-elev2);
88
97
  }
89
98
 
90
99
  /* ---- ambient gradient(镜像 .fp-markdown 预览底色,覆盖整个窗口)---- */
@@ -0,0 +1,78 @@
1
+ /* theme-name: 半透明 */
2
+ /* theme-name-en: Translucent */
3
+ /*
4
+ * 半透明主题——专为聊天背景图(壁纸,issue #100)设计:
5
+ * 调色板与默认暗色一致,但把所有底板统一降到半透明(顶栏/底栏/左右面板、
6
+ * 卡片/控件/代码面、输入框盒子、终端视图都走同一 --wallpaper-panel-alpha),
7
+ * 让壁纸从各处透出、整体融合成一张画。配壁纸用最佳;不配壁纸时这些区域会
8
+ * 透出纯色底(--bg),观感略偏暗沉,属预期。
9
+ *
10
+ * 统一透图率:--wallpaper-panel-alpha: 45%(45% 容器色 + 55% 透图)。
11
+ * 已覆盖的面:顶栏/底栏/左右面板、左右折叠按钮(.panel-collapse-btn /
12
+ * .panel-rail)、工具调用卡及其参数/输出块(.toolcall / .toolcall-args pre /
13
+ * .toolcall-output pre)、bash 块(.bashblock/.termline)、消息代码块
14
+ * (.codeblock pre)、全部消息气泡(.thinking/.toolcall/.bashblock/
15
+ * .skillcard/.attachcard/.queued-bubble/.retry-notice/.msg-collapsed(含 hover))、
16
+ * 快捷短语(.quick-chip)、新对话提示词模板卡(.empty-template)、顶栏
17
+ * chips / 视图切换(.chip/.view-switch)——走 styles.css
18
+ * 的 --card-bg/--chip-bg/--inputbox-bg,随 --wallpaper-panel-alpha 统一联动;
19
+ * 中间的聊天/消息区用 --msgs-bg(同式 45%),气泡立在统一玻璃面上略实
20
+ * 一层形成层次。**下拉弹窗保持实色**:声音/语言/主题/模型选择/思考强度等
21
+ * 点出来的 .dd-menu 背景用 --menu-bg(这里覆盖回 var(--bg-elev2) 实色,
22
+ * 弹窗内容多为密集列表,实色保可读)。终端视图(xterm canvas/容器)读
23
+ * --term-bg,这里同样设半透明。
24
+ * 各变量都可单独覆盖回实色(如 var(--bg-elev))即关闭对应面的透图;
25
+ * --msgs-bg 覆盖回 transparent 可恢复「消息区全透、壁纸从卡片间隙露出」。
26
+ */
27
+ :root {
28
+ color-scheme: dark;
29
+ --bg: #0d0e12;
30
+ --bg-elev: #14161c;
31
+ --bg-elev2: #1a1d26;
32
+ --border: #262a35;
33
+ --border-soft: #1e2230;
34
+ --text: #e6e8ef;
35
+ --text-dim: #9aa1b4;
36
+ --text-faint: #6b7284;
37
+ --accent: #8b5cf6;
38
+ --accent-soft: rgba(139, 92, 246, 0.14);
39
+ --green: #34d399;
40
+ --green-soft: rgba(52, 211, 153, 0.12);
41
+ --red: #f87171;
42
+ --red-soft: rgba(248, 113, 113, 0.12);
43
+ --amber: #fbbf24;
44
+ --term-bg: #0b0d12;
45
+ --term-fg: #e6e8ef;
46
+ --term-cursor: #8b5cf6;
47
+ --term-cursor-accent: #0b0d12;
48
+ --term-selection: rgba(139, 92, 246, 0.35);
49
+ --term-black: #1a1d26;
50
+ --term-red: #f87171;
51
+ --term-green: #34d399;
52
+ --term-yellow: #fbbf24;
53
+ --term-blue: #60a5fa;
54
+ --term-magenta: #c084fc;
55
+ --term-cyan: #22d3ee;
56
+ --term-white: #e6e8ef;
57
+ --term-bright-black: #6b7284;
58
+ --term-bright-red: #f87171;
59
+ --term-bright-green: #34d399;
60
+ --term-bright-yellow: #fbbf24;
61
+ --term-bright-blue: #60a5fa;
62
+ --term-bright-magenta: #c084fc;
63
+ --term-bright-cyan: #22d3ee;
64
+ --term-bright-white: #ffffff;
65
+
66
+ /* ---- 壁纸适配:三大区域(顶栏/底栏/左右面板)背景链式引用此变量,
67
+ 从默认 62% 降到 45% → 容器色更薄、图更透(45% 容器色 + 55% 透图)---- */
68
+ --wallpaper-panel-alpha: 45%;
69
+ /* 下拉弹窗(声音/语言/主题/模型/思考强度/版本更新):实色底保密集列表可读
70
+ (与 styles.css 默认一致,这里显式锁定,防止跟随壁纸半透)。 */
71
+ --menu-bg: var(--bg-elev2);
72
+ /* 压暗略放轻(默认 0.78):让图更鲜活,文字靠压暗与半透明底板仍可读 */
73
+ --bg-image-dim: 0.72;
74
+ /* 输入框盒子:styles.css 默认已与控件级底板同式跟随 --wallpaper-panel-alpha,
75
+ 这里不单独覆盖;想比整体厚/薄就设一个独立值 */
76
+ /* 终端视图:xterm canvas + 容器都读 --term-bg,改成半透明随整体透图 */
77
+ --term-bg: color-mix(in srgb, #0b0d12 45%, transparent);
78
+ }
@@ -0,0 +1,78 @@
1
+ /* theme-name: 全透明 */
2
+ /* theme-name-en: Transparent */
3
+ /*
4
+ * 全透明主题——「半透明」的极致版(专配壁纸,issue #100):
5
+ * 调色板与默认暗色一致,但把壁纸机制里所有表面变量的透图率压到 0
6
+ * (--wallpaper-panel-alpha: 0% → --topbar-bg / --statusbar-bg / --panel-bg /
7
+ * --card-bg / --chip-bg / --inputbox-bg / --msgs-bg 全部变成 transparent),
8
+ * 顶栏/底栏/左右面板/消息区/输入框/全部气泡/代码块/终端视图统统直接见壁纸,
9
+ * 界面只剩边框与文字定结构。代价是文字全靠 --bg-image-dim 压暗撑着——
10
+ * 这里压暗加重到 0.85(默认 0.78),亮区壁纸下文字仍可读。
11
+ *
12
+ * 全透后 hover/浮动反馈不能依赖容器色(已是 0%),本文件尾段把这些实色
13
+ * hover 统一改成 25% 半透底(折叠摘要、展开条、折叠键、复制键、滚到底按钮);
14
+ * 强调色 hover(accent-soft)与语义按钮(发送/停止/杀进程)不受影响。
15
+ * 下拉弹窗(声音/语言/主题/模型/思考强度等 .dd-menu)保持实色
16
+ * (--menu-bg: var(--bg-elev2)),密集列表不随界面透图。
17
+ * 配壁纸用最佳;不配壁纸时界面直接透出 --bg 纯色(= 默认暗色,无差别)。
18
+ */
19
+ :root {
20
+ color-scheme: dark;
21
+ --bg: #0d0e12;
22
+ --bg-elev: #14161c;
23
+ --bg-elev2: #1a1d26;
24
+ --border: #262a35;
25
+ --border-soft: #1e2230;
26
+ --text: #e6e8ef;
27
+ --text-dim: #9aa1b4;
28
+ --text-faint: #6b7284;
29
+ --accent: #8b5cf6;
30
+ --accent-soft: rgba(139, 92, 246, 0.14);
31
+ --green: #34d399;
32
+ --green-soft: rgba(52, 211, 153, 0.12);
33
+ --red: #f87171;
34
+ --red-soft: rgba(248, 113, 113, 0.12);
35
+ --amber: #fbbf24;
36
+ --term-bg: transparent;
37
+ --term-fg: #e6e8ef;
38
+ --term-cursor: #8b5cf6;
39
+ --term-cursor-accent: #0b0d12;
40
+ --term-selection: rgba(139, 92, 246, 0.35);
41
+ --term-black: #1a1d26;
42
+ --term-red: #f87171;
43
+ --term-green: #34d399;
44
+ --term-yellow: #fbbf24;
45
+ --term-blue: #60a5fa;
46
+ --term-magenta: #c084fc;
47
+ --term-cyan: #22d3ee;
48
+ --term-white: #e6e8ef;
49
+ --term-bright-black: #6b7284;
50
+ --term-bright-red: #f87171;
51
+ --term-bright-green: #34d399;
52
+ --term-bright-yellow: #fbbf24;
53
+ --term-bright-blue: #60a5fa;
54
+ --term-bright-magenta: #c084fc;
55
+ --term-bright-cyan: #22d3ee;
56
+ --term-bright-white: #ffffff;
57
+
58
+ /* ---- 全透明核心:0% 容器色 = 全部 color-mix 表面(三大区域 / 卡片 /
59
+ 控件 / 输入框 / 消息区)直接变 transparent;终端 --term-bg 上面已设
60
+ transparent。想留一点点玻璃感就把这里提到 10~15%。---- */
61
+ --wallpaper-panel-alpha: 0%;
62
+ /* 下拉弹窗(声音/语言/主题/模型/思考强度):实色底保密集列表可读 */
63
+ --menu-bg: var(--bg-elev2);
64
+ /* 压暗加重:全透后没有容器色兜底,文字只靠压暗保可读 */
65
+ --bg-image-dim: 0.85;
66
+ }
67
+
68
+ /* hover / 浮动静置反馈:0% 容器色下这些实色 hover 必消失,统一给 25% 半透
69
+ 底(仍透图、可辨反馈)。强调色 hover(accent-soft)与语义按钮不受影响;
70
+ 下拉弹窗已在实色菜单内,其 hover 保留 styles.css 默认实色。 */
71
+ .msg-collapsed:hover,
72
+ .panel-rail:hover,
73
+ .chead-toggle:hover,
74
+ .chead-copy:hover,
75
+ .empty-templates-reset:hover,
76
+ .scroll-bottom {
77
+ background: color-mix(in srgb, var(--bg-elev) 25%, transparent);
78
+ }
package/themes/white.css CHANGED
@@ -85,6 +85,15 @@
85
85
  --bg-image: none;
86
86
  --bg-image-dim: 0.78;
87
87
  --bg-image-blur: 0px;
88
+ --wallpaper-panel-alpha: 62%;
89
+ --topbar-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
90
+ --statusbar-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
91
+ --panel-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
92
+ --card-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
93
+ --chip-bg: color-mix(in srgb, var(--bg-elev2) var(--wallpaper-panel-alpha, 62%), transparent);
94
+ --msgs-bg: color-mix(in srgb, var(--bg-elev) var(--wallpaper-panel-alpha, 62%), transparent);
95
+ --inputbox-bg: color-mix(in srgb, var(--bg-elev2) var(--wallpaper-panel-alpha, 62%), transparent);
96
+ --menu-bg: var(--bg-elev2);
88
97
  }
89
98
 
90
99
  /* ---- syntax highlighting (overrides static github-dark import) ---- */
@@ -1,4 +1,4 @@
1
- import{a as l,j as n}from"./markdown-Cpo0pNcR.js";import{u as P,b as O,T as M,a as W,F as Y,c as z,d as Z,e as H,f as ee,g as ne,h as te,i as se,r as ae}from"./index-CaiIOwy7.js";import{D as re,o as ie}from"./xterm-B96xOxS9.js";import"./react-CtudoG1_.js";function le(t){let c=null;return{clean:t.replace(/\r?\n?\[pi-term-exit:(-?\d+)\]\r?\n?/g,(a,x)=>(c=Number(x),`\r
1
+ import{a as l,j as n}from"./markdown-Cpo0pNcR.js";import{u as P,b as O,T as M,a as W,F as Y,c as z,d as Z,e as H,f as ee,g as ne,h as te,i as se,r as ae}from"./index-CBZtMKV2.js";import{D as re,o as ie}from"./xterm-B96xOxS9.js";import"./react-CtudoG1_.js";function le(t){let c=null;return{clean:t.replace(/\r?\n?\[pi-term-exit:(-?\d+)\]\r?\n?/g,(a,x)=>(c=Number(x),`\r
2
2
  `)).replace(/\r?\n?\x1b\[90m\[(?:进程已退出,退出码 |Process exited with code )-?\d+\]\x1b\[0m\r?\n?/g,`\r
3
3
  `),exitCode:c}}function ce({conversationId:t,terminalId:c,command:p,cwd:a,title:x,active:u,running:v,exitCode:j,send:h,register:C}){const T=l.useRef(null),y=l.useRef(null),{locale:o}=P(),N=l.useRef(o);N.current=o;const E=p?JSON.stringify(p):"";l.useEffect(()=>{const m=T.current;if(!m)return;const r=new re({theme:O(),fontFamily:'"SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',fontSize:13,cursorBlink:!0,scrollback:8e3}),b=new ie;r.loadAddon(b),r.open(m),y.current={term:r,fit:b},u&&r.focus();const g=()=>{r.options.theme=O()};window.addEventListener(M,g),r.attachCustomKeyEventHandler(d=>{if(d.type!=="keydown")return!0;const S=d.key?.toLowerCase();if((d.ctrlKey||d.metaKey)&&S==="v")return!1;if(d.ctrlKey&&!d.shiftKey&&!d.altKey&&S==="c"&&r.hasSelection()){const D=r.textarea;return D&&(D.value=r.getSelection(),D.select()),!1}return!0});const _=C(t,c,{write:d=>r.write(le(d).clean),dispose:()=>r.dispose()}),$=()=>{try{b.fit(),h({type:"terminal_resize",terminalId:c,conversationId:t,cols:r.cols,rows:r.rows})}catch{}},B=requestAnimationFrame(()=>{try{b.fit()}catch{}h(p?{type:"run_command",terminalId:c,conversationId:t,command:p,cols:r.cols,rows:r.rows}:{type:"terminal_create",terminalId:c,title:x,locale:N.current,conversationId:t,cwd:a,cols:r.cols,rows:r.rows})}),R=r.onData(d=>{h({type:"terminal_input",terminalId:c,conversationId:t,data:d})});let k=null;return typeof ResizeObserver<"u"&&(k=new ResizeObserver(()=>{m.offsetWidth>0&&m.offsetHeight>0&&$()}),k.observe(m)),()=>{cancelAnimationFrame(B),R.dispose(),window.removeEventListener(M,g),k?.disconnect(),_(),r.dispose(),y.current=null}},[t,c,E,h,C]),l.useEffect(()=>{if(!u)return;const m=requestAnimationFrame(()=>{const r=y.current;if(r){try{r.fit.fit(),h({type:"terminal_resize",terminalId:c,conversationId:t,cols:r.term.cols,rows:r.term.rows})}catch{}r.term.focus()}});return()=>cancelAnimationFrame(m)},[u]);const{t:F}=P(),f=l.useRef(void 0);return l.useEffect(()=>{if(v===f.current||(f.current=v,v!==!1))return;const m=y.current;m&&m.term.write(`\r
4
4
  \x1B[90m${F("exitBanner",{code:j??""})}\x1B[0m\r