pi-web-ui 0.94.0 → 0.94.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,19 @@
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ 暂无未发布内容。
14
+
15
+ ## [0.94.1] — 2026-09-22
16
+
17
+ ### Fixed
18
+
19
+ - **HTTP 代理的 `undici` 提升为直接依赖 + 懒加载** — `undici` 从传递依赖提升为 `dependencies` 直接依赖(`^8.9.0`),版本漂移不再悄悄改变代理行为;`server/http-proxy.ts` 改为运行时懒加载,缺失时只告警并停用代理,不再崩溃服务端启动。
20
+
21
+ ### Changed
22
+
23
+ - **深青(dark-teal)主题微调(PR #277,感谢 @A5Kush)** — 深色下粗体改用主题青(`--text-strong`)更显眼,`--bg-elev2` 提亮,品牌渐变与部分描边换成符合主题的青色。
24
+ - **主题机制文档修正** — 主题是完整样式表(可覆盖 `:root` 变量 / 任意选择器 / 布局改动 / 自带新 token),`styles.css` 只是共享基线;之前文档里「主题 = 纯 `:root` 调色板、改布局永不碰主题」的说法已过时(`AGENTS.md` / `docs/architecture-core.md` 同步)。
25
+
13
26
  ## [0.94.0] — 2026-09-22
14
27
 
15
28
  ### Added
@@ -1194,6 +1207,7 @@ when?, children?}`,也收 `topbar` / `settings` 这类简写别名);宿主
1194
1207
  - 0.29.0(2026-08-23):全局搜索弹窗(Ctrl+K)+ 消息列表惰性窗口化。
1195
1208
 
1196
1209
  [Unreleased]: https://github.com/xing-shuyin/pi-web-ui/compare/v0.93.0...main
1210
+ [0.94.1]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.94.1
1197
1211
  [0.94.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.94.0
1198
1212
  [0.93.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.93.0
1199
1213
  [0.92.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.92.0
@@ -9,7 +9,7 @@
9
9
  import { existsSync, readFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { EventEmitter } from "node:events";
12
- import * as undici from "undici";
12
+ import { createRequire } from "node:module";
13
13
  /** Read proxy settings from settings.json and environment. */
14
14
  export function resolveProxySettings(agentDir) {
15
15
  let httpProxy;
@@ -49,6 +49,16 @@ export function initHttpProxy(agentDir) {
49
49
  return { active: false };
50
50
  }
51
51
  try {
52
+ // Lazy-load undici so a missing optional dep never crashes server startup;
53
+ // proxy simply stays inactive with a warning.
54
+ let undici;
55
+ try {
56
+ undici = createRequire(import.meta.url)("undici");
57
+ }
58
+ catch (err) {
59
+ console.warn("[proxy] undici not installed, HTTP proxy support disabled:", err);
60
+ return { active: false, proxyUrl: effectiveProxy };
61
+ }
52
62
  const dispatcher = withUndiciErrorListener(new undici.EnvHttpProxyAgent({
53
63
  allowH2: false,
54
64
  proxyTunnel: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.94.0",
3
+ "version": "0.94.1",
4
4
  "description": "Just open your browser to get all your work done — browser cockpit for AI coding agents (chat, code, files, terminal, Git in one tab)",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -106,6 +106,7 @@
106
106
  "remark-math": "^6.0.0",
107
107
  "tar": "^7.5.22",
108
108
  "typebox": "^1.3.34",
109
+ "undici": "^8.9.0",
109
110
  "ws": "^8.21.3",
110
111
  "yauzl": "^3.4.0"
111
112
  },
@@ -4,10 +4,11 @@
4
4
  color-scheme: dark;
5
5
  --bg: #0a0a0a;
6
6
  --bg-elev: #17171c;
7
- --bg-elev2: #1c1c22;
7
+ --bg-elev2: #222228;
8
8
  --border: #26262e;
9
9
  --border-soft: #1f1f26;
10
10
  --text: #dadada;
11
+ --text-strong: #3c9185; /* 深色主题下,用主题色使粗体更明显 */
11
12
  --text-dim: #a8a8b3;
12
13
  --text-faint: #6e6e7a;
13
14
  --accent: #31ac9d;
@@ -53,8 +54,8 @@
53
54
  --notice-info-border: #2dd4bf;
54
55
  --send-blue: #0f766e;
55
56
  --send-blue-hover: #0b5a54;
56
- --brand-grad-a: #28a696;
57
- --brand-grad-b: #38e6f8;
57
+ --brand-grad-a: #00ffe7;
58
+ --brand-grad-b: #202020;
58
59
  --on-amber: #12121a;
59
60
  --search-active-fg: #0f0f12;
60
61
  --switch-knob: #f5f5f7;
@@ -169,12 +170,12 @@
169
170
  color: #0f0f12;
170
171
  }
171
172
 
172
-
173
173
  /* 模板卡悬停阴影 */
174
174
  .empty-template:hover {
175
175
  box-shadow: 0 8px 22px #5DC7B040;
176
176
  }
177
177
 
178
+ /* 描边 */
178
179
  /* 内置几处写死的紫描边,换成符合主题的青 */
179
180
  .empty-template:hover,
180
181
  .empty-template.add:hover,
@@ -187,7 +188,8 @@
187
188
  .attach-chip,
188
189
  .attachcard-mode.bridged,
189
190
  .browser-control-modal .bc-cite,
190
- .notice-info {
191
+ .notice-info,
192
+ .set-catalog-custom {
191
193
  border-color: #5DC7B080;
192
194
  }
193
195
  .chip.newchat {
@@ -215,6 +217,11 @@
215
217
  .fp-html-bar {
216
218
  background: var(--accent-soft);
217
219
  }
220
+ /* 侧栏 Hover */
221
+ .lp-section-title:hover {
222
+ background: var(--chip-bg);
223
+ }
224
+
218
225
 
219
226
  /* 输入框发送\停止按钮阴影 */
220
227
  .inputbox .btn.send,
@@ -228,10 +235,15 @@
228
235
  box-shadow: 0 8px 22px #5dc7b040;
229
236
  }
230
237
 
231
- /* 使正文行内代码更显眼 */
238
+ /* 正文 */
239
+ /* 行内代码更显眼 */
232
240
  .msg-text code {
233
241
  background: #303038;
234
242
  }
243
+ /* 粗体用 --text-strong */
244
+ strong {
245
+ color: var(--text-strong);
246
+ }
235
247
 
236
248
  /* 调整 MD 文件预览样式 */
237
249
  .fp-markdown :not(pre)>code,
@@ -250,3 +262,5 @@
250
262
  .fp-markdown {
251
263
  background: radial-gradient(circle at 10% 0%, #5CCBF60D, transparent 34%), #00000038;
252
264
  }
265
+
266
+
@@ -1,3 +1,3 @@
1
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{i as t,t as n}from"./react-CR5VJ85Q.js";import{a as r,c as i,d as a,f as o,g as s,h as c,i as l,l as u,m as d,o as f,p,r as m,s as h,t as g,u as _}from"./index-18jK6al2.js";import{n as v,t as y}from"./xterm-Cf5xxe4g.js";var b=e(t(),1);function x(e,t){return Math.abs(t)>10&&Math.abs(t)>Math.abs(e)*1.2}function S(e,t){return e-t}var C=n();function w(e){let t=null;return{clean:e.replace(/\r?\n?\[pi-term-exit:(-?\d+)\]\r?\n?/g,(e,n)=>(t=Number(n),`\r
1
+ import{r as e}from"./rolldown-runtime-hePW80VL.js";import{i as t,t as n}from"./react-CR5VJ85Q.js";import{a as r,c as i,d as a,f as o,g as s,h as c,i as l,l as u,m as d,o as f,p,r as m,s as h,t as g,u as _}from"./index-DVLrHI2E.js";import{n as v,t as y}from"./xterm-Cf5xxe4g.js";var b=e(t(),1);function x(e,t){return Math.abs(t)>10&&Math.abs(t)>Math.abs(e)*1.2}function S(e,t){return e-t}var C=n();function w(e){let t=null;return{clean:e.replace(/\r?\n?\[pi-term-exit:(-?\d+)\]\r?\n?/g,(e,n)=>(t=Number(n),`\r
2
2
  `)).replace(/\r?\n?\x1b\[90m\[(?:进程已退出,退出码 |Process exited with code )-?\d+\]\x1b\[0m\r?\n?/g,`\r
3
3
  `),exitCode:t}}function T({conversationId:e,terminalId:t,command:n,cwd:i,title:a,agentBash:o,active:s,running:c,exitCode:u,register:d}){let p=(0,b.useRef)(null),h=(0,b.useRef)(null),{locale:g}=f(),_=(0,b.useRef)(g);_.current=g;let T=n?JSON.stringify(n):``,E=(0,b.useRef)(c===!1);(0,b.useEffect)(()=>{let c=p.current;if(!c)return;let u=new v({theme:l(),fontFamily:`"SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace`,fontSize:13,cursorBlink:!0,scrollback:8e3}),f=new y;u.loadAddon(f),u.open(c),h.current={term:u,fit:f},s&&u.focus();let g=()=>{u.options.theme=l()};window.addEventListener(m,g),u.attachCustomKeyEventHandler(e=>{if(e.type!==`keydown`)return!0;let t=e.key?.toLowerCase();if((e.ctrlKey||e.metaKey)&&t===`v`)return!1;if(e.ctrlKey&&!e.shiftKey&&!e.altKey&&t===`c`&&u.hasSelection()){let e=u.textarea;return e&&(e.value=u.getSelection(),e.select()),!1}return!0});let b=d(e,t,{write:e=>u.write(w(e).clean),dispose:()=>u.dispose()}),C=()=>{try{f.fit(),r({type:`terminal_resize`,terminalId:t,conversationId:e,cols:u.cols,rows:u.rows})}catch{}},T=requestAnimationFrame(()=>{try{f.fit()}catch{}E.current||r(n?{type:`run_command`,terminalId:t,conversationId:e,command:n,cols:u.cols,rows:u.rows}:{type:`terminal_create`,terminalId:t,title:a,locale:_.current,agentBash:o,conversationId:e,cwd:i,cols:u.cols,rows:u.rows})}),D=u.onData(n=>{r({type:`terminal_input`,terminalId:t,conversationId:e,data:n})}),O=u.element,k=null,A=0,j=0,M=0,N=!1,P=e=>{if(e.touches.length!==1){k=null,N=!1;return}let t=e.touches[0];k=t.identifier,A=t.clientX,j=t.clientY,M=t.clientY,N=!1},F=e=>{if(k===null||e.touches.length!==1)return;let t=e.touches[0];if(t.identifier!==k)return;if(!N){if(!x(t.clientX-A,t.clientY-j))return;N=!0}e.preventDefault();let n=S(M,t.clientY);M=t.clientY,n!==0&&typeof WheelEvent<`u`&&(c.querySelector(`.xterm-viewport`)??O)?.dispatchEvent(new WheelEvent(`wheel`,{bubbles:!0,cancelable:!0,deltaY:n,deltaMode:0,clientX:t.clientX,clientY:t.clientY}))},I=e=>{if(k!==null){if(e.touches.length>0){k=null,N=!1;return}k=null,N=!1}};O?.addEventListener(`touchstart`,P,{passive:!0}),O?.addEventListener(`touchmove`,F,{passive:!1}),O?.addEventListener(`touchend`,I),O?.addEventListener(`touchcancel`,I);let L=null;return typeof ResizeObserver<`u`&&(L=new ResizeObserver(()=>{c.offsetWidth>0&&c.offsetHeight>0&&C()}),L.observe(c)),()=>{cancelAnimationFrame(T),D.dispose(),O?.removeEventListener(`touchstart`,P),O?.removeEventListener(`touchmove`,F),O?.removeEventListener(`touchend`,I),O?.removeEventListener(`touchcancel`,I),window.removeEventListener(m,g),L?.disconnect(),b(),u.dispose(),h.current=null}},[e,t,T,d]),(0,b.useEffect)(()=>{if(!s)return;let n=requestAnimationFrame(()=>{let n=h.current;if(n){try{n.fit.fit(),r({type:`terminal_resize`,terminalId:t,conversationId:e,cols:n.term.cols,rows:n.term.rows})}catch{}n.term.focus()}});return()=>cancelAnimationFrame(n)},[s]);let{t:D}=f(),O=(0,b.useRef)(void 0);return(0,b.useEffect)(()=>{if(c===O.current||(O.current=c,c!==!1))return;let e=h.current;e&&e.term.write(`\r\n\x1b[90m${D(`exitBanner`,{code:u??``})}\x1b[0m\r\n`)},[c,t,u]),(0,C.jsx)(`div`,{ref:p,className:`term-xterm ${s?``:`hidden`}`})}var E={name:``,command:``,cwd:"${pwd}"};function D({chat:e,terminal:t,uiTerminalToolbar:n,onUiAction:l}){let f=h(),[m,v]=(0,b.useState)(null),[y,x]=(0,b.useState)(!1),[S,w]=(0,b.useState)(!1),[D,O]=(0,b.useState)(null),[k,A]=(0,b.useState)(E),[j,M]=(0,b.useState)(null),N=(0,b.useRef)(null),[P,F]=(0,b.useState)(!0),[I,L]=(0,b.useState)(null),[R,z]=(0,b.useState)(``);(0,b.useEffect)(()=>{e.terminals.length===0?v(null):e.terminals.some(e=>e.id===m)||v(e.terminals[e.terminals.length-1].id)},[e.terminals,m]),(0,b.useEffect)(()=>{e.terminalActiveId&&(v(e.terminalActiveId),x(!1))},[e.terminalActiveId]),(0,b.useEffect)(()=>()=>{N.current&&clearTimeout(N.current)},[]);let B=e.terminals.filter(e=>!e.agentBash),V=e.terminals.filter(e=>e.agentBash),H=n=>{if(!e.ready)return;let r=i(),a=e.activeConversationId||e.state?.conversationId||``;t.create({...n,id:r,conversationId:a,cols:n.cols??80,rows:n.rows??24,running:!0,exitCode:null}),v(r),x(!1)},U=()=>H({title:f(`terminalTitle`,{n:B.length+1}),cwd:e.state?.cwd??``}),W=n=>{let i=n.name||n.command,a=e.terminals.find(e=>e.title===i);if(a){t.restart(a.id),v(a.id),r({type:`run_command`,terminalId:a.id,conversationId:a.conversationId,command:n,cols:80,rows:24});return}H({title:i,cwd:e.state?.cwd??``,command:n})},G=n=>{let i=e.terminals.find(e=>e.id===n);if(i&&r({type:`terminal_kill`,terminalId:n,conversationId:i.conversationId}),t.close(n),m===n){let t=e.terminals.filter(e=>e.id!==n);v(t.length>0?t[t.length-1].id:null)}},K=e=>(0,C.jsxs)(`div`,{className:`term-tab ${e.id===m?`active`:``}`,children:[(0,C.jsxs)(`button`,{type:`button`,className:`term-tab-main`,title:`${e.cwd}${e.command?`\n> ${e.command.command}`:``}`,onClick:()=>{I||(v(e.id),x(!1))},children:[(0,C.jsx)(`span`,{className:`term-tab-dot ${e.running?`run`:`exit`}`}),(0,C.jsxs)(`span`,{className:`term-tab-title`,children:[I===e.id?(0,C.jsx)(`input`,{autoFocus:!0,className:`term-tab-rename-input`,value:R,placeholder:e.title,onClick:e=>e.stopPropagation(),onChange:e=>z(e.target.value),onKeyDown:t=>{if(t.stopPropagation(),t.key===`Enter`&&!t.nativeEvent.isComposing){let t=R.trim();t&&r({type:`rename_terminal`,terminalId:e.id,conversationId:e.conversationId,title:t}),L(null)}else t.key===`Escape`&&L(null)},onBlur:()=>L(null)}):e.title,!e.running&&(0,C.jsx)(`span`,{className:`term-tab-exit`,children:f(`exited`,{code:e.exitCode===null?``:` ${e.exitCode}`})})]})]}),(0,C.jsx)(`button`,{type:`button`,className:`term-tab-close term-tab-rename`,title:f(`renameTerminal`),onClick:t=>{t.stopPropagation(),z(e.title),L(e.id)},children:(0,C.jsx)(u,{})}),(0,C.jsx)(`button`,{type:`button`,className:`term-tab-close`,title:f(`closeTerminal`),onClick:()=>G(e.id),children:(0,C.jsx)(s,{})})]},e.id),q=()=>{w(!0),O(null),A(E)},J=t=>{let n=e.commands[t];n&&(w(!1),O(t),A({name:n.name,command:n.command,cwd:n.cwd??``}))},Y=()=>{w(!1),O(null)},X=()=>{let t=k.name.trim(),n=k.command.trim();if(!t||!n)return;let i={name:t,command:n,cwd:k.cwd.trim()||void 0},a=S?[...e.commands,i]:D===null?e.commands:e.commands.map((e,t)=>t===D?i:e);r({type:`save_commands`,commands:a}),Y()},Z=t=>{if(j===t){let n=e.commands.filter((e,n)=>n!==t);r({type:`save_commands`,commands:n}),M(null),N.current&&clearTimeout(N.current)}else M(t),N.current&&clearTimeout(N.current),N.current=setTimeout(()=>M(null),2500)},ee=S||D!==null,te=n===void 0?[`host:term-cmd-refresh`,`host:term-cmd-new`,`host:term-tab-new`].map(e=>({id:e,source:`host`})):n.filter(e=>!e.hidden),Q=(e,t)=>{let n=new Set(e);return te.filter(e=>n.has(e.id)||t&&e.source!==`host`)},$={"host:term-cmd-refresh":(0,C.jsx)(`button`,{type:`button`,className:`panel-refresh`,title:f(`rerun`),onClick:()=>r({type:`list_commands`}),children:(0,C.jsx)(p,{})}),"host:term-cmd-new":(0,C.jsx)(`button`,{type:`button`,className:`panel-new`,title:f(`newCommand`),onClick:q,children:(0,C.jsx)(o,{})}),"host:term-tab-new":(0,C.jsx)(`button`,{type:`button`,className:`panel-new`,title:f(`newTerminal`),onClick:U,children:(0,C.jsx)(o,{})})};return(0,C.jsxs)(`div`,{className:`terminal-view`,children:[(0,C.jsxs)(`aside`,{className:`term-side term-commands ${y?`open`:``}`,children:[(0,C.jsxs)(`div`,{className:`panel-header`,children:[(0,C.jsx)(`span`,{className:`panel-title`,children:f(`commands`)}),(0,C.jsx)(`div`,{className:`panel-header-actions`,children:g(Q([`host:term-cmd-refresh`,`host:term-cmd-new`],!1),$,l)})]}),(0,C.jsx)(`div`,{className:`panel-body`,children:ee?(0,C.jsxs)(`div`,{className:`cmd-form`,children:[(0,C.jsx)(`label`,{htmlFor:`cmd-name`,children:f(`name`)}),(0,C.jsx)(`input`,{id:`cmd-name`,className:`cmd-input`,value:k.name,placeholder:f(`exampleName`),autoFocus:!0,onChange:e=>A({...k,name:e.target.value})}),(0,C.jsx)(`label`,{htmlFor:`cmd-command`,children:f(`command`)}),(0,C.jsx)(`input`,{id:`cmd-command`,className:`cmd-input`,value:k.command,placeholder:f(`exampleCommand`),onChange:e=>A({...k,command:e.target.value}),onKeyDown:e=>{e.key===`Enter`&&!e.nativeEvent.isComposing&&X()}}),(0,C.jsxs)(`label`,{htmlFor:`cmd-cwd`,children:[f(`directory`),` `,(0,C.jsx)(`span`,{className:`cmd-hint`,children:f(`cwdHint`)})]}),(0,C.jsx)(`input`,{id:`cmd-cwd`,className:`cmd-input`,value:k.cwd,placeholder:"${pwd}",onChange:e=>A({...k,cwd:e.target.value}),onKeyDown:e=>{e.key===`Enter`&&!e.nativeEvent.isComposing&&X()}}),(0,C.jsxs)(`div`,{className:`cmd-form-actions`,children:[(0,C.jsx)(`button`,{type:`button`,className:`btn`,onClick:Y,children:f(`cancel`)}),(0,C.jsx)(`button`,{type:`button`,className:`btn primary`,disabled:!k.name.trim()||!k.command.trim(),onClick:X,children:f(`save`)})]})]}):(0,C.jsxs)(C.Fragment,{children:[e.commands.length===0&&(0,C.jsx)(`div`,{className:`panel-empty`,children:f(`noCommands`)}),e.commands.map((e,t)=>(0,C.jsxs)(`div`,{className:`cmd-item`,children:[(0,C.jsx)(`button`,{type:`button`,className:`cmd-run`,title:f(`clickToRun`),onClick:()=>W(e),children:(0,C.jsx)(a,{})}),(0,C.jsxs)(`button`,{type:`button`,className:`cmd-main`,title:f(`clickToRun`),onClick:()=>W(e),children:[(0,C.jsx)(`span`,{className:`cmd-name`,children:e.name}),(0,C.jsx)(`span`,{className:`cmd-command`,children:e.command}),e.cwd&&(0,C.jsx)(`span`,{className:`cmd-cwd`,children:e.cwd})]}),(0,C.jsx)(`button`,{type:`button`,className:`cmd-act`,title:f(`edit`),onClick:()=>J(t),children:(0,C.jsx)(u,{})}),(0,C.jsx)(`button`,{type:`button`,className:`cmd-act del ${j===t?`confirm`:``}`,title:f(`delete`),onClick:()=>Z(t),children:j===t?f(`confirmQ`):(0,C.jsx)(c,{})})]},t))]})}),(0,C.jsxs)(`div`,{className:`term-tabs-block`,children:[(0,C.jsxs)(`div`,{className:`panel-header`,children:[(0,C.jsx)(`span`,{className:`panel-title`,children:f(`terminal`)}),g(Q([`host:term-tab-new`],!0),$,l)]}),(0,C.jsxs)(`div`,{className:`panel-body`,children:[e.terminals.length===0&&(0,C.jsx)(`div`,{className:`panel-empty`,children:f(`noTerminal`)}),B.map(K),V.length>0&&(0,C.jsxs)(`div`,{className:`term-folder`,children:[(0,C.jsxs)(`button`,{type:`button`,className:`term-folder-header ${P?`open`:``}`,title:f(`aiBashGroup`),onClick:()=>F(e=>!e),children:[(0,C.jsx)(`span`,{className:`term-folder-caret`,children:P?`▾`:`▸`}),(0,C.jsx)(`span`,{className:`term-folder-title`,children:f(`aiBashGroup`)}),(0,C.jsx)(`span`,{className:`term-folder-count`,children:V.length})]}),P&&(0,C.jsx)(`div`,{className:`term-folder-body`,children:V.map(K)})]})]})]})]}),(0,C.jsxs)(`div`,{className:`term-main`,children:[y&&(0,C.jsx)(`div`,{className:`drawer-backdrop`,onClick:()=>x(!1)}),(0,C.jsx)(`button`,{type:`button`,className:`term-side-toggle`,title:f(`commands`),onClick:()=>x(e=>!e),children:(0,C.jsx)(_,{})}),e.terminals.length===0?(0,C.jsxs)(`div`,{className:`term-empty`,children:[(0,C.jsx)(d,{className:`term-empty-icon`}),(0,C.jsx)(`div`,{className:`term-empty-title`,children:f(`builtinTerminal`)}),(0,C.jsx)(`div`,{className:`term-empty-sub`,children:f(`termEmptySub`)})]}):e.terminals.map(e=>(0,C.jsx)(T,{conversationId:e.conversationId,terminalId:e.id,command:e.command,cwd:e.cwd,title:e.title,agentBash:e.agentBash,active:e.id===m,running:e.running,exitCode:e.exitCode,register:t.register},`${e.conversationId}:${e.id}`))]})]})}export{D as TerminalPanel};