From bbab143dc1988bb130474178e1f3035381237f5e Mon Sep 17 00:00:00 2001 From: lgxwool Date: Tue, 21 Apr 2026 21:34:32 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(webview):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E6=A8=AA=E5=B9=85=E7=BB=84=E4=BB=B6=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E9=94=99=E8=AF=AF=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +- .../com/github/codeplangui/ChatService.kt | 6 + src/main/resources/webview/index.html | 174 +++++++++--------- webview/src/App.tsx | 28 ++- webview/src/components/ErrorBanner.css | 161 ++++++++++++++++ webview/src/components/ErrorBanner.tsx | 101 ++++++++-- webview/src/types/bridge.d.ts | 2 +- 7 files changed, 363 insertions(+), 113 deletions(-) create mode 100644 webview/src/components/ErrorBanner.css diff --git a/build.gradle b/build.gradle index c43519a..9cede7e 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ test { tasks.register('installWebviewDependencies', Exec) { workingDir 'webview' - commandLine 'npm', 'ci' + commandLine 'npm.cmd', 'ci' inputs.file('webview/package.json') inputs.file('webview/package-lock.json') outputs.dir('webview/node_modules') @@ -60,7 +60,7 @@ tasks.register('installWebviewDependencies', Exec) { tasks.register('buildWebview', Exec) { dependsOn 'installWebviewDependencies' workingDir 'webview' - commandLine 'npm', 'run', 'build' + commandLine 'npm.cmd', 'run', 'build' inputs.file('webview/index.html') inputs.dir('webview/src') inputs.file('webview/package.json') diff --git a/src/main/kotlin/com/github/codeplangui/ChatService.kt b/src/main/kotlin/com/github/codeplangui/ChatService.kt index 034f772..11e2ebb 100644 --- a/src/main/kotlin/com/github/codeplangui/ChatService.kt +++ b/src/main/kotlin/com/github/codeplangui/ChatService.kt @@ -733,6 +733,12 @@ $selection lowerMsg.contains("api key") || lowerMsg.contains("unauthorized") -> BridgeErrorPayload(type = "config", message = message, action = "openSettings") + lowerMsg.contains("insufficient_quota") || lowerMsg.contains("quota") || + lowerMsg.contains("billing") || lowerMsg.contains("credit") || + lowerMsg.contains("payment") || lowerMsg.contains("余额不足") || + lowerMsg.contains("超出限额") || lowerMsg.contains("额度不足") || lowerMsg.contains("欠费") -> + BridgeErrorPayload(type = "quota", message = message, action = "openSettings") + lowerMsg.contains("timeout") || lowerMsg.contains("超时") || lowerMsg.contains("无法连接") || lowerMsg.contains("connectexception") || lowerMsg.contains("http 5") || lowerMsg.contains("http 429") -> diff --git a/src/main/resources/webview/index.html b/src/main/resources/webview/index.html index fab51ca..e046fa0 100644 --- a/src/main/resources/webview/index.html +++ b/src/main/resources/webview/index.html @@ -1,10 +1,10 @@ - - - - - - CodePlanGUI - +`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${Wa(e,!0)}`}br(e){return"
"}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){const a=this.parser.parseInline(n),r=FA(e);if(r===null)return a;e=r;let i='",i}image({href:e,title:t,text:n,tokens:a}){a&&(n=this.parser.parseInline(a,this.parser.textRenderer));const r=FA(e);if(r===null)return Wa(n);e=r;let i=`${n}{const o=r[i].flat(1/0);n=n.concat(this.walkTokens(o,t))}):r.tokens&&(n=n.concat(this.walkTokens(r.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{const a={...n};if(a.async=this.defaults.async||a.async||!1,n.extensions&&(n.extensions.forEach(r=>{if(!r.name)throw new Error("extension name required");if("renderer"in r){const i=t.renderers[r.name];i?t.renderers[r.name]=function(...o){let s=r.renderer.apply(this,o);return s===!1&&(s=i.apply(this,o)),s}:t.renderers[r.name]=r.renderer}if("tokenizer"in r){if(!r.level||r.level!=="block"&&r.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");const i=t[r.level];i?i.unshift(r.tokenizer):t[r.level]=[r.tokenizer],r.start&&(r.level==="block"?t.startBlock?t.startBlock.push(r.start):t.startBlock=[r.start]:r.level==="inline"&&(t.startInline?t.startInline.push(r.start):t.startInline=[r.start]))}"childTokens"in r&&r.childTokens&&(t.childTokens[r.name]=r.childTokens)}),a.extensions=t),n.renderer){const r=this.defaults.renderer||new Ad(this.defaults);for(const i in n.renderer){if(!(i in r))throw new Error(`renderer '${i}' does not exist`);if(["options","parser"].includes(i))continue;const o=i,s=n.renderer[o],l=r[o];r[o]=(...c)=>{let u=s.apply(r,c);return u===!1&&(u=l.apply(r,c)),u||""}}a.renderer=r}if(n.tokenizer){const r=this.defaults.tokenizer||new Nd(this.defaults);for(const i in n.tokenizer){if(!(i in r))throw new Error(`tokenizer '${i}' does not exist`);if(["options","rules","lexer"].includes(i))continue;const o=i,s=n.tokenizer[o],l=r[o];r[o]=(...c)=>{let u=s.apply(r,c);return u===!1&&(u=l.apply(r,c)),u}}a.tokenizer=r}if(n.hooks){const r=this.defaults.hooks||new ku;for(const i in n.hooks){if(!(i in r))throw new Error(`hook '${i}' does not exist`);if(["options","block"].includes(i))continue;const o=i,s=n.hooks[o],l=r[o];ku.passThroughHooks.has(i)?r[o]=c=>{if(this.defaults.async)return Promise.resolve(s.call(r,c)).then(d=>l.call(r,d));const u=s.call(r,c);return l.call(r,u)}:r[o]=(...c)=>{let u=s.apply(r,c);return u===!1&&(u=l.apply(r,c)),u}}a.hooks=r}if(n.walkTokens){const r=this.defaults.walkTokens,i=n.walkTokens;a.walkTokens=function(o){let s=[];return s.push(i.call(this,o)),r&&(s=s.concat(r.call(this,o))),s}}this.defaults={...this.defaults,...a}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return br.lex(e,t??this.defaults)}parser(e,t){return vr.parse(e,t??this.defaults)}parseMarkdown(e){return(n,a)=>{const r={...a},i={...this.defaults,...r},o=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&r.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));i.hooks&&(i.hooks.options=i,i.hooks.block=e);const s=i.hooks?i.hooks.provideLexer():e?br.lex:br.lexInline,l=i.hooks?i.hooks.provideParser():e?vr.parse:vr.parseInline;if(i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then(c=>s(c,i)).then(c=>i.hooks?i.hooks.processAllTokens(c):c).then(c=>i.walkTokens?Promise.all(this.walkTokens(c,i.walkTokens)).then(()=>c):c).then(c=>l(c,i)).then(c=>i.hooks?i.hooks.postprocess(c):c).catch(o);try{i.hooks&&(n=i.hooks.preprocess(n));let c=s(n,i);i.hooks&&(c=i.hooks.processAllTokens(c)),i.walkTokens&&this.walkTokens(c,i.walkTokens);let u=l(c,i);return i.hooks&&(u=i.hooks.postprocess(u)),u}catch(c){return o(c)}}}onError(e,t){return n=>{if(n.message+=` +Please report this to https://github.com/markedjs/marked.`,e){const a="

An error occurred:

"+Wa(n.message+"",!0)+"
";return t?Promise.resolve(a):a}if(t)return Promise.reject(n);throw n}}},Vi=new lL;function gt(e,t){return Vi.parse(e,t)}gt.options=gt.setOptions=function(e){return Vi.setOptions(e),gt.defaults=Vi.defaults,Xw(gt.defaults),gt};gt.getDefaults=mh;gt.defaults=no;gt.use=function(...e){return Vi.use(...e),gt.defaults=Vi.defaults,Xw(gt.defaults),gt};gt.walkTokens=function(e,t){return Vi.walkTokens(e,t)};gt.parseInline=Vi.parseInline;gt.Parser=vr;gt.parser=vr.parse;gt.Renderer=Ad;gt.TextRenderer=hh;gt.Lexer=br;gt.lexer=br.lex;gt.Tokenizer=Nd;gt.Hooks=ku;gt.parse=gt;gt.options;gt.setOptions;gt.use;gt.walkTokens;gt.parseInline;vr.parse;br.lex;function j5(e){if(typeof e=="function"&&(e={highlight:e}),!e||typeof e.highlight!="function")throw new Error("Must provide highlight function");return typeof e.langPrefix!="string"&&(e.langPrefix="language-"),typeof e.emptyLangClass!="string"&&(e.emptyLangClass=""),{async:!!e.async,walkTokens(t){if(t.type!=="code")return;const n=zA(t.lang);if(e.async)return Promise.resolve(e.highlight(t.text,n,t.lang||"")).then(YA(t));const a=e.highlight(t.text,n,t.lang||"");if(a instanceof Promise)throw new Error("markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.");YA(t)(a)},useNewRenderer:!0,renderer:{code(t,n,a){typeof t=="object"&&(a=t.escaped,n=t.lang,t=t.text);const r=zA(n),i=r?e.langPrefix+qA(r):e.emptyLangClass,o=i?` class="${i}"`:"";return t=t.replace(/\n$/,""),`
${a?t:qA(t,!0)}
+
`}}}}function zA(e){return(e||"").match(/\S*/)[0]}function YA(e){return t=>{typeof t=="string"&&t!==e.text&&(e.escaped=!0,e.text=t)}}const cL=/[&<>"']/,X5=new RegExp(cL.source,"g"),uL=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,Z5=new RegExp(uL.source,"g"),J5={"&":"&","<":"<",">":">",'"':""","'":"'"},HA=e=>J5[e];function qA(e,t){if(t){if(cL.test(e))return e.replace(X5,HA)}else if(uL.test(e))return e.replace(Z5,HA);return e}const eK=new lL(j5({langPrefix:"hljs language-",highlight(e,t){const n=kA.getLanguage(t)?t:"plaintext";return kA.highlight(e,{language:n}).value}}));async function tK(e){if(navigator.clipboard?.writeText)try{return await navigator.clipboard.writeText(e),!0}catch{}const t=document.createElement("textarea");t.value=e,t.style.position="fixed",t.style.opacity="0",document.body.appendChild(t),t.focus(),t.select();try{return document.execCommand("copy")}finally{document.body.removeChild(t)}}function nK({content:e}){const t=m.useRef(null);return m.useEffect(()=>{if(!t.current)return;const n=eK.parse(e);t.current.innerHTML=HV.sanitize(n),t.current.querySelectorAll("pre").forEach(a=>{const r=a.querySelector("code");if(!r||a.querySelector("button"))return;a.classList.add("assistant-code-block");const i=document.createElement("div");i.className="bubble-copy-anchor",a.appendChild(i);const o=document.createElement("div");i.appendChild(o);const s=document.createElement("button");s.className="bubble-copy-fallback",s.textContent="Copy",s.onclick=async()=>{await tK(r.textContent||"")&&(s.textContent="Copied",window.setTimeout(()=>{s.textContent="Copy"},2e3))},o.appendChild(s)})},[e]),$.jsx("div",{ref:t,className:"assistant-markdown"})}function aK({logs:e,isStreaming:t}){const[n,a]=m.useState(!1),r=m.useRef(null),i=m.useRef(t);return m.useEffect(()=>{t?i.current=!0:i.current&&(i.current=!1,a(!0))},[t]),m.useEffect(()=>{r.current&&!n&&(r.current.scrollTop=r.current.scrollHeight)},[e.length,n]),e.length===0?null:$.jsxs("div",{className:"exec-log-panel",children:[$.jsxs("div",{className:"exec-log-header",onClick:()=>a(!n),children:[n?$.jsx(LU,{}):$.jsx(lU,{}),$.jsxs("span",{className:"exec-log-title",children:["Output (",e.length,")"]})]}),!n&&$.jsxs("div",{ref:r,className:"exec-log-body",children:[e.map((o,s)=>$.jsx("div",{className:`exec-log-line exec-log-${o.type}`,children:o.text},s)),t&&$.jsx("span",{className:"stream-cursor"})]})]})}function rK({data:e}){const{command:t,status:n,result:a,logs:r}=e,i=n==="running",o=r&&r.length>0,s=()=>{switch(n){case"waiting":return $.jsxs($.Fragment,{children:[$.jsx(AU,{style:{marginRight:6}}),"等待审批"]});case"running":return $.jsxs($.Fragment,{children:[$.jsx(Xd,{style:{marginRight:6}}),"执行中"]});case"blocked":return $.jsxs($.Fragment,{children:[$.jsx(sC,{style:{marginRight:6,color:"#ff4d4f"}}),"已拦截 · ",a?.reason]});case"denied":return $.jsxs($.Fragment,{children:[$.jsx(sC,{style:{marginRight:6,color:"#ff4d4f"}}),"用户拒绝"]});case"timeout":return $.jsxs($.Fragment,{children:[$.jsx(jk,{style:{marginRight:6,color:"#faad14"}}),"超时 · ",a?.timeout_seconds,"s"]});case"done":{if(!a)return null;const l=a.status==="ok",c=a.duration_ms?`${(a.duration_ms/1e3).toFixed(1)}s`:"";return l?$.jsxs($.Fragment,{children:[$.jsx(qk,{style:{marginRight:6,color:"#52c41a"}}),"完成 · exit ",a.exit_code," · ",c]}):$.jsxs($.Fragment,{children:[$.jsx(tU,{style:{marginRight:6,color:"#ff4d4f"}}),"失败 · exit ",a.exit_code," · ",c]})}}};return $.jsxs("div",{className:"exec-card",children:[$.jsx("div",{className:"exec-card-header",children:s()}),$.jsxs(An.Text,{code:!0,className:"exec-card-command",children:["$ ",t]}),o&&$.jsx(aK,{logs:r,isStreaming:i}),!o&&a?.stdout&&$.jsx(VA,{text:a.stdout,label:"stdout"}),!o&&a?.stderr&&$.jsx(VA,{text:a.stderr,label:"stderr"}),a?.truncated&&$.jsx(An.Text,{type:"secondary",style:{fontSize:11},children:"[output truncated]"})]})}const ME=5;function VA({text:e,label:t}){const n=e.split(` +`),[a,r]=m.useState(n.length<=ME),i=a?n:n.slice(0,ME);return $.jsxs("div",{style:{marginTop:8},children:[t&&$.jsx(An.Text,{type:"secondary",style:{fontSize:11},children:t}),$.jsx("pre",{style:{margin:"4px 0",fontSize:12,overflowX:"auto",background:"rgba(0,0,0,0.04)",padding:"6px 10px",borderRadius:4},children:i.join(` +`)}),!a&&$.jsxs(An.Link,{style:{fontSize:12},onClick:()=>r(!0),children:["▼ show ",n.length-ME," more lines"]})]})}async function iK(e){if(navigator.clipboard?.writeText)try{return await navigator.clipboard.writeText(e),!0}catch{}const t=document.createElement("textarea");t.value=e,t.style.position="fixed",t.style.opacity="0",document.body.appendChild(t),t.focus(),t.select();try{return document.execCommand("copy")}finally{document.body.removeChild(t)}}function oK({text:e}){const[t,n]=m.useState(!1),a=async()=>{await iK(e)&&(n(!0),setTimeout(()=>n(!1),2e3))};return $.jsx(qn,{type:"text",size:"small",icon:t?$.jsx(dM,{}):$.jsx(mM,{}),onClick:a,className:"bubble-copy-button"})}const sK=m.memo(function({group:t}){return $.jsx("div",{className:"assistant-group",children:t.children.map(n=>n.kind==="execution"?$.jsx(rK,{data:n.data},n.data.requestId):$.jsx("div",{className:"message-row message-row-assistant",children:$.jsxs("div",{className:"message-bubble message-bubble-assistant",children:[$.jsxs("div",{className:"assistant-bubble-header",children:[$.jsx("span",{className:"assistant-bubble-label",children:"assistant"}),$.jsx(oK,{text:n.content})]}),$.jsx(nK,{content:n.content}),n.isStreaming&&$.jsx("span",{className:"stream-cursor"})]})},n.id))})});function lK(e){const n=e.trimStart().split(/\s+|[|;><&]/)[0]?.trim()??"";return n.substring(n.lastIndexOf("/")+1)}function cK({open:e,command:t,description:n,onAllow:a,onDeny:r}){const[i,o]=m.useState(!1),s=()=>{a(i),o(!1)},l=()=>{r(),o(!1)},c=lK(t),u=c?`允许所有 ${c} 命令自动执行`:"记住此命令,以后自动执行";return $.jsxs(Ya,{open:e,getContainer:!1,title:$.jsxs("span",{children:[$.jsx(qU,{style:{color:"#faad14",marginRight:8}}),"AI 请求执行命令"]}),footer:[$.jsx(qn,{onClick:l,children:"拒绝"},"deny"),$.jsx(qn,{type:"primary",danger:!0,onClick:s,children:"允许执行"},"allow")],closable:!1,maskClosable:!1,children:[$.jsx("div",{style:{marginBottom:12},children:$.jsxs(An.Text,{code:!0,style:{fontSize:13,display:"block",padding:"8px 12px",background:"rgba(0,0,0,0.06)",borderRadius:6},children:["$ ",t]})}),n&&$.jsx(An.Text,{type:"secondary",children:n}),$.jsx("div",{style:{marginTop:12},children:$.jsx(sh,{checked:i,onChange:d=>o(d.target.checked),children:u})})]})}const WA={config:{label:"配置错误",icon:$.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[$.jsx("rect",{x:"3",y:"11",width:"18",height:"11",rx:"2",ry:"2"}),$.jsx("path",{d:"M7 11V7a5 5 0 0 1 10 0v4"})]}),cssClass:"error-banner-config"},quota:{label:"配额不足",icon:$.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[$.jsx("line",{x1:"12",y1:"1",x2:"12",y2:"23"}),$.jsx("path",{d:"M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"})]}),cssClass:"error-banner-quota"},network:{label:"网络错误",icon:$.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[$.jsx("circle",{cx:"12",cy:"12",r:"10"}),$.jsx("polyline",{points:"12 6 12 12 16 14"})]}),cssClass:"error-banner-network"},runtime:{label:"操作失败",icon:$.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[$.jsx("circle",{cx:"12",cy:"12",r:"10"}),$.jsx("line",{x1:"15",y1:"9",x2:"9",y2:"15"}),$.jsx("line",{x1:"9",y1:"9",x2:"15",y2:"15"})]}),cssClass:"error-banner-runtime"}};function uK({error:e,onClose:t,onAction:n}){const a=WA[e.type]??WA.runtime;return $.jsxs("div",{className:`error-banner ${a.cssClass}`,children:[$.jsx("div",{className:"error-banner-icon",children:a.icon}),$.jsxs("div",{className:"error-banner-body",children:[$.jsxs("div",{className:"error-banner-header",children:[$.jsx("span",{className:"error-banner-label",children:a.label}),$.jsx("button",{className:"error-banner-close",onClick:t,"aria-label":"关闭",children:$.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",children:[$.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),$.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]}),$.jsx("div",{className:"error-banner-message",children:e.message})]}),e.action==="openSettings"&&$.jsx(qn,{size:"small",className:"error-banner-btn-settings",onClick:()=>n?.("openSettings"),children:"打开设置"}),e.action==="retry"&&$.jsx(qn,{size:"small",className:"error-banner-btn-retry",onClick:()=>n?.("retry"),children:"重试"})]})}const dK={unconfigured:"Provider not configured",ready:"Ready",streaming:"Streaming response",error:"API key missing"};function _K(e){return dK[e]}function mK({inputText:e,isLoading:t,isBridgeReady:n,connectionState:a}){const r=e.trim();return n?a==="unconfigured"?{canSend:!1,reason:"请先在 Settings 中配置 Provider",text:r}:a==="error"?{canSend:!1,reason:"API Key 未设置或未保存,请在 Settings 中重新配置并应用",text:r}:t?{canSend:!1,reason:"请等待当前响应完成",text:r}:r?{canSend:!0,reason:null,text:r}:{canSend:!1,reason:"请输入问题",text:r}:{canSend:!1,reason:"IDE bridge 正在连接,请稍后",text:r}}function pK({onNewChat:e,onOpenSettings:t,status:n,bridgeReady:a}){const r=n.providerName||"CodePlanGUI",i=n.model||(a?"Select a provider in Settings":"Connecting bridge");return $.jsxs("div",{className:"provider-bar",children:[$.jsxs("div",{children:[$.jsx(An.Title,{level:5,className:"provider-title",children:r}),$.jsxs(An.Text,{className:"provider-meta",children:[i," · ",_K(n.connectionState)]}),n.contextFile&&$.jsxs(An.Text,{className:"provider-context",title:n.contextFile,children:[$.jsx(vU,{})," ",n.contextFile]})]}),$.jsxs("div",{className:"provider-actions",children:[$.jsx(qn,{type:"text",size:"small",icon:$.jsx(GU,{}),onClick:t,title:"Open Settings",className:"provider-action"}),$.jsx(qn,{type:"text",size:"small",icon:$.jsx(xU,{}),onClick:e,title:"New Chat",className:"provider-action"})]})]})}function gK(e,t){return e?t?{label:"context on",title:t}:{label:"no open file",title:"当前没有可附加的文件上下文"}:{label:"context off",title:"不附加文件上下文"}}function dL(e){if(typeof e=="string")return e;if(e==null)return"{}";try{return JSON.stringify(e)}catch{return"{}"}}function fK(e){const t=dL(e);try{return JSON.parse(t)}catch{return}}function EK(e,t){return{...e,...t,contextFile:t.contextFile??e.contextFile??""}}function SK(e,t){return{...e,contextFile:t}}function bK(e){for(let t=e.length-1;t>=0;t--)if(e[t].type==="assistant")return t;return-1}function vo(e,t){const n=bK(e.groups);if(n===-1)return e;const a=[...e.groups];return a[n]=t(a[n]),{...e,groups:a}}function vK(e){const t=[];let n=null;for(const a of e)a.role!=="user"&&a.role!=="assistant"||a.role==="assistant"&&a.content.trim().length===0||(a.role==="user"?(n&&(t.push(n),n=null),t.push({type:"human",id:a.id,message:{id:a.id,content:a.content}})):(n||(n={type:"assistant",id:a.id,children:[],isStreaming:!1}),n.children.push({kind:"text",id:`text-${a.id}`,content:a.content,isStreaming:!1})));return n&&t.push(n),t}function hK(e,t,n){switch(t){case"start":{const a=e.groups[e.groups.length-1];return a?.type==="assistant"&&a.isStreaming?{...e,isLoading:!0,error:null,currentRoundTextIndex:null}:{...e,isLoading:!0,error:null,currentRoundTextIndex:null,groups:[...e.groups,{type:"assistant",id:n.msgId,children:[],isStreaming:!0}]}}case"token":{const a=e.groups[e.groups.length-1];if(a?.type!=="assistant")return e;const r=a,i=e.currentRoundTextIndex;if(i!==null&&r.children[i]?.kind==="text"){const c=r.children[i],u=[...r.children];u[i]={kind:"text",id:c.id,content:c.content+n.text,isStreaming:c.isStreaming};const d=[...e.groups];return d[d.length-1]={...r,children:u},{...e,groups:d}}const o={kind:"text",id:`text-${crypto.randomUUID()}`,content:n.text,isStreaming:!0},s=r.children.length,l=[...e.groups];return l[l.length-1]={...r,children:[...r.children,o]},{...e,groups:l,currentRoundTextIndex:s}}case"execution_card":return vo(e,a=>({...a,children:[...a.children,{kind:"execution",data:{requestId:n.requestId,command:n.command,status:"running"}}]}));case"log":return vo(e,a=>({...a,children:a.children.map(r=>r.kind==="execution"&&r.data.requestId===n.requestId?{...r,data:{...r.data,logs:[...r.data.logs||[],{text:n.line,type:n.type}]}}:r)}));case"execution_status":{const a=fK(n.result);return vo(e,r=>({...r,children:r.children.map(i=>i.kind==="execution"&&i.data.requestId===n.requestId?{...i,data:{...i.data,status:n.status,result:a}}:i)}))}case"approval_request":return{...vo(e,a=>({...a,children:a.children.map(r=>r.kind==="execution"&&r.data.requestId===n.requestId?{...r,data:{...r.data,status:"waiting"}}:r)})),approvalRequestId:n.requestId,approvalCommand:n.command,approvalDescription:n.description,approvalOpen:!0};case"round_end":return e.currentRoundTextIndex!==null?vo({...e,currentRoundTextIndex:null},a=>({...a,children:a.children.filter((r,i)=>i!==e.currentRoundTextIndex)})):e;case"end":return vo({...e,isLoading:!1,continuationInfo:null,currentRoundTextIndex:null},a=>({...a,isStreaming:!1,children:a.children.map(r=>r.kind==="text"?{...r,isStreaming:!1}:r)}));case"error":{const a=e.groups.map(r=>r.type==="assistant"&&r.isStreaming?{...r,isStreaming:!1,children:r.children.map(i=>i.kind==="text"?{...i,isStreaming:!1}:i)}:r);return{...e,isLoading:!1,continuationInfo:null,currentRoundTextIndex:null,groups:a,error:{type:"runtime",message:n.message}}}case"structured_error":{const a=e.groups.map(r=>r.type==="assistant"&&r.isStreaming?{...r,isStreaming:!1,children:r.children.map(i=>i.kind==="text"?{...i,isStreaming:!1}:i)}:r);return{...e,isLoading:!1,continuationInfo:null,currentRoundTextIndex:null,groups:a,error:{type:n.type,message:n.message,action:n.action}}}case"continuation":return{...e,continuationInfo:{current:n.current,max:n.max}};case"restore_messages":return{...e,groups:vK(JSON.parse(n.messages))};case"status":return{...e,status:EK(e.status,n)};case"context_file":return{...e,status:SK(e.status,n.fileName)};case"theme":return{...e,themeMode:n.mode};default:return e}}function TK(e){const[t,n]=m.useState(()=>window.__bridge?.isReady===!0),a=m.useRef(!1),r=m.useRef(e);return r.current=e,m.useEffect(()=>{const i=()=>{window.__bridge||(window.__bridge={isReady:!1,sendMessage:()=>{},newChat:()=>{},openSettings:()=>{},cancelStream:()=>{},frontendReady:()=>{},debugLog:()=>{},onEvent:(s,l)=>{},approvalResponse:()=>{}}),window.__bridge.onEvent=(s,l)=>{try{const c=JSON.parse(l);r.current(s,c)}catch(c){console.warn(`[CodePlanGUI] Failed to parse event payload: type=${s}`,c)}};const o=window.__bridge.isReady===!0;n(o),o&&!a.current&&(a.current=!0,window.__bridge.frontendReady())};return i(),document.addEventListener("bridge_ready",i),()=>document.removeEventListener("bridge_ready",i)},[]),t}function CK(e,t,n){const a=e.trim();return!n||t||!a?null:{text:a}}const yK={groups:[],isLoading:!1,error:null,status:{providerName:"",model:"",connectionState:"unconfigured",contextFile:""},themeMode:"dark",approvalOpen:!1,approvalRequestId:"",approvalCommand:"",approvalDescription:"",continuationInfo:null,currentRoundTextIndex:null};function RK(){const[e,t]=m.useState(yK),[n,a]=m.useState(""),r=m.useRef(!1),[i,o]=m.useState(!0),s=m.useRef(null),l=m.useRef(null),{groups:c,isLoading:u,error:d,status:_,themeMode:p,approvalOpen:E,approvalRequestId:v,approvalCommand:b,approvalDescription:f,continuationInfo:g}=e;m.useEffect(()=>{document.documentElement.classList.remove("theme-dark","theme-light"),document.documentElement.classList.add(`theme-${p}`)},[p]);const S=()=>{s.current?.scrollIntoView({behavior:"smooth"})};m.useEffect(()=>{S()},[c]);const h=m.useCallback(A=>{window.__bridge?.debugLog(A)},[]),C=m.useCallback((A,G)=>{if(A==="execution_card")h(`[approval-ui] received execution card requestId=${G.requestId} command=${G.command} description=${G.description}`);else if(A==="approval_request")h(`[approval-ui] received approval request requestId=${G.requestId} command=${G.command} description=${G.description}`);else if(A==="execution_status"){const z=dL(G.result);h(`[approval-ui] received execution status requestId=${G.requestId} status=${G.status} result=${z.slice(0,240)}`)}t(z=>hK(z,A,G))},[h]),y=m.useCallback(A=>{h(`[approval-ui] modal allow clicked requestId=${v} addToWhitelist=${A}`),t(G=>({...G,approvalOpen:!1})),window.__bridge?.approvalResponse(v,"allow",A)},[v,h]),T=m.useCallback(()=>{h(`[approval-ui] modal deny clicked requestId=${v}`),t(A=>({...A,approvalOpen:!1})),window.__bridge?.approvalResponse(v,"deny")},[v,h]),O=p==="dark"?Uy.darkAlgorithm:Uy.defaultAlgorithm,N=TK(C);m.useEffect(()=>{N&&t(A=>({...A,error:null}))},[N]);const D=mK({inputText:n,isLoading:u,isBridgeReady:N,connectionState:_.connectionState}),x=gK(i,_.contextFile||""),w=()=>{if(!D.canSend){if(D.reason&&D.text){const z=_.connectionState==="unconfigured"||_.connectionState==="error"?"config":"runtime",H=_.connectionState==="unconfigured"||_.connectionState==="error"?"openSettings":void 0;t(Y=>({...Y,error:{type:z,message:D.reason,action:H}}))}return}const A=CK(D.text,u,N);if(!A)return;const G=Hy();l.current={text:A.text,includeContext:i},t(z=>({...z,groups:[...z.groups,{type:"human",id:G,message:{id:G,content:A.text}}]})),a(""),window.__bridge?.sendMessage(A.text,i)},U=A=>{A.key==="Enter"&&!A.shiftKey&&!r.current&&(A.preventDefault(),w())},L=()=>{t(A=>({...A,groups:[],error:null,isLoading:!1,currentRoundTextIndex:null})),window.__bridge?.newChat()},k=m.useCallback(()=>{u&&window.__bridge?.cancelStream()},[u]),R=m.useCallback(A=>{if(A==="openSettings")window.__bridge?.openSettings(),t(G=>({...G,error:null}));else if(A==="retry"&&l.current){const G=l.current,z=Hy();t(H=>({...H,groups:[...H.groups,{type:"human",id:z,message:{id:z,content:G.text}}],error:null})),window.__bridge?.sendMessage(G.text,G.includeContext)}},[]);m.useEffect(()=>{const A=G=>{G.key==="Escape"&&u&&(G.preventDefault(),window.__bridge?.cancelStream())};return document.addEventListener("keydown",A),()=>document.removeEventListener("keydown",A)},[u]);const I=A=>{A.style.height="auto",A.style.height=`${Math.min(A.scrollHeight,120)}px`};return $.jsx(Mr,{theme:{algorithm:O,token:{colorPrimary:"#d2a15e",colorInfo:"#d2a15e",borderRadius:16,fontFamily:"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif"}},children:$.jsxs("div",{className:"app-shell",children:[$.jsx(cK,{open:E,command:b,description:f,onAllow:y,onDeny:T}),$.jsx(pK,{onNewChat:L,onOpenSettings:()=>window.__bridge?.openSettings(),status:_,bridgeReady:N}),d&&$.jsx(uK,{error:d,onClose:()=>t(A=>({...A,error:null})),onAction:R}),$.jsxs("div",{className:"messages-area",children:[c.length===0&&$.jsx("div",{className:"empty-state",children:$.jsxs("div",{className:"empty-card",children:[$.jsx("div",{className:"empty-icon",children:"✦"}),$.jsx("div",{className:"empty-kicker",children:"Ready for context"}),$.jsx(An.Title,{level:3,className:"empty-title",children:"向 AI 提问,或选中代码后右键 Ask AI"}),$.jsxs("div",{className:"empty-copy",children:["当前会话支持流式输出、上下文注入和 Markdown 代码块复制。输入区支持",$.jsx("strong",{children:" Enter 发送"}),",",$.jsx("strong",{children:"Shift+Enter 换行"}),"。"]}),_.connectionState==="unconfigured"&&$.jsx(qn,{type:"link",onClick:()=>window.__bridge?.openSettings(),children:"打开 Settings 配置 Provider"})]})}),c.map(A=>A.type==="human"?$.jsx("div",{className:"message-row message-row-user",children:$.jsx("div",{className:"message-bubble message-bubble-user",children:$.jsx(An.Text,{children:A.message.content})})},A.id):$.jsx(sK,{group:A},A.id)),u&&!c.some(A=>A.type==="assistant"&&A.children.some(G=>G.kind==="text"&&G.isStreaming))&&$.jsxs("div",{className:"continuation-indicator",children:[$.jsx("span",{className:"continuation-spinner"}),g&&$.jsxs("span",{className:"continuation-text",children:["续写中 ",g.current,"/",g.max]})]}),$.jsx("div",{ref:s})]}),$.jsxs("div",{className:"input-area",children:[$.jsx("div",{className:"input-meta",children:$.jsxs("div",{className:"context-toggle",children:[$.jsx(Cc,{title:x.title,children:$.jsx(Mw,{size:"small",checked:i,onChange:o})}),$.jsx("span",{className:"context-caption context-file-label",title:x.title,children:x.label})]})}),$.jsxs("div",{className:"composer-row",children:[$.jsx("textarea",{value:n,onChange:A=>{a(A.target.value),I(A.target)},onCompositionStart:()=>{r.current=!0},onCompositionEnd:A=>{r.current=!1,a(A.target.value)},onKeyDown:U,placeholder:"输入问题... (Enter 发送,Shift+Enter 换行)",disabled:u,rows:1,className:"composer-input"}),$.jsx(qn,{type:"primary",icon:u?$.jsx(Fk,{}):$.jsx(UU,{}),onClick:u?k:w,disabled:!u&&!D.canSend,title:u?"停止生成 (Esc)":D.reason??"Send",size:"small",className:"send-button"})]})]})]})})}uk.createRoot(document.getElementById("root")).render($.jsx(m.StrictMode,{children:$.jsx(RK,{})})); - - -
- - +*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#79c0ff}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-comment,.hljs-code,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}.error-banner{display:flex;align-items:center;gap:10px;margin:10px 16px 0;padding:10px 14px;border-radius:14px;border:1px solid;backdrop-filter:blur(12px);font-size:13px;line-height:1.5}.error-banner-icon{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:8px}.error-banner-body{flex:1;min-width:0}.error-banner-header{display:flex;align-items:center;gap:8px;margin-bottom:2px}.error-banner-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.1em;opacity:.85}.error-banner-close{display:flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;margin-left:auto;border:none;border-radius:6px;cursor:pointer;opacity:.5;transition:opacity .15s,background .15s}.error-banner-close:hover{opacity:1;background:#ffffff1a}.error-banner-message{color:var(--text);white-space:pre-wrap;word-break:break-word;opacity:.9}.error-banner-config{background:#d2a15e1a;border-color:#d2a15e4d}.error-banner-config .error-banner-icon{background:#d2a15e2e;color:#d2a15e}.error-banner-config .error-banner-label{color:#d2a15e}.error-banner-quota{background:#d4a0171a;border-color:#d4a0174d}.error-banner-quota .error-banner-icon{background:#d4a0172e;color:#d4a017}.error-banner-quota .error-banner-label{color:#d4a017}.error-banner-network{background:#4a90d91a;border-color:#4a90d94d}.error-banner-network .error-banner-icon{background:#4a90d92e;color:#4a90d9}.error-banner-network .error-banner-label{color:#4a90d9}.error-banner-runtime{background:#5b181473;border-color:#ff8a7540}.error-banner-runtime .error-banner-icon{background:#ff8a7526;color:#ff8a75}.error-banner-runtime .error-banner-label{color:#ff8a75}.error-banner-btn-settings,.error-banner-btn-retry{flex-shrink:0;height:28px!important;padding:0 12px!important;border-radius:10px!important;font-size:12px!important;font-weight:500!important;border:1px solid!important;cursor:pointer;transition:all .15s ease}.error-banner-btn-settings{background:linear-gradient(135deg,#b98346e6,#87552be6)!important;border-color:#f0c48866!important;color:#f2eadf!important;box-shadow:0 4px 12px #592f1140}.error-banner-btn-settings:hover{background:linear-gradient(135deg,#c8914ef2,#965f30f2)!important;border-color:#f0c4888c!important;box-shadow:0 6px 16px #592f114d;transform:translateY(-1px)}.error-banner-btn-retry{background:#4a90d91f!important;border-color:#4a90d959!important;color:#4a90d9!important}.error-banner-btn-retry:hover{background:#4a90d938!important;border-color:#4a90d980!important;transform:translateY(-1px)}:root,.theme-dark{color-scheme:dark;--bg: #0f1115;--bg-elevated: rgba(22, 24, 30, .86);--panel: rgba(29, 31, 38, .84);--panel-strong: rgba(40, 30, 20, .72);--border: rgba(232, 214, 190, .12);--border-strong: rgba(214, 164, 95, .28);--text: #f2eadf;--muted: #bcae9a;--accent: #d2a15e;--accent-strong: #f0c488;--danger: #ff8a75;--shadow: 0 18px 48px rgba(0, 0, 0, .35);--font-body: "Avenir Next", "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;--font-mono: "SFMono-Regular", "JetBrains Mono", "Menlo", monospace;--input-bg: rgba(22, 24, 30, .92);--input-border: rgba(232, 214, 190, .14);--input-placeholder: rgba(188, 174, 154, .68);--input-focus-border: rgba(240, 196, 136, .38);--input-focus-ring: rgba(210, 161, 94, .09);--gradient-bg: linear-gradient(180deg, #14171c 0%, #0f1115 100%);--bar-bg: linear-gradient(180deg, rgba(23, 25, 31, .92), rgba(16, 18, 24, .88));--input-area-bg: linear-gradient(180deg, rgba(16, 18, 24, .82), rgba(12, 14, 19, .94));--assistant-bubble-bg: linear-gradient(180deg, rgba(29, 31, 38, .94), rgba(17, 19, 24, .92));--pre-bg: rgba(7, 8, 11, .88);--pre-border: rgba(240, 196, 136, .12);--card-bg: linear-gradient(180deg, rgba(35, 28, 23, .54), rgba(16, 18, 24, .88));--send-button-bg: linear-gradient(135deg, #b98346, #87552b);--send-button-shadow: 0 10px 24px rgba(89, 47, 17, .28);--send-button-disabled-bg: rgba(70, 72, 80, .65);--send-button-disabled-border: rgba(255, 255, 255, .08)}.theme-light{color-scheme:light;--bg: #f5f5f5;--bg-elevated: rgba(255, 255, 255, .9);--panel: rgba(255, 255, 255, .85);--panel-strong: rgba(245, 240, 230, .8);--border: rgba(0, 0, 0, .08);--border-strong: rgba(210, 161, 94, .35);--text: #1a1a1a;--muted: #666666;--accent: #b98346;--accent-strong: #87552b;--danger: #d94e41;--shadow: 0 18px 48px rgba(0, 0, 0, .12);--input-bg: rgba(255, 255, 255, .95);--input-border: rgba(135, 85, 43, .18);--input-placeholder: rgba(102, 102, 102, .6);--input-focus-border: rgba(135, 85, 43, .35);--input-focus-ring: rgba(185, 131, 70, .14);--gradient-bg: linear-gradient(180deg, #fafafa 0%, #f5f5f5 100%);--bar-bg: linear-gradient(180deg, rgba(250, 250, 252, .97), rgba(242, 242, 246, .95));--input-area-bg: linear-gradient(180deg, rgba(248, 248, 250, .95), rgba(240, 240, 244, .98));--assistant-bubble-bg: linear-gradient(180deg, rgba(255, 255, 255, .95), rgba(248, 248, 250, .92));--pre-bg: rgba(236, 236, 240, .9);--pre-border: rgba(180, 130, 70, .15);--card-bg: linear-gradient(180deg, rgba(255, 255, 255, .85), rgba(242, 242, 246, .9));--send-button-bg: linear-gradient(135deg, #d5ae77, #b98346);--send-button-shadow: 0 10px 20px rgba(185, 131, 70, .18);--send-button-disabled-bg: rgba(205, 205, 210, .88);--send-button-disabled-border: rgba(120, 120, 128, .12)}*{box-sizing:border-box}html,body,#root{margin:0;min-height:100%;height:100%}body{overflow:hidden;font-family:var(--font-body);font-size:13px;color:var(--text);background:radial-gradient(circle at top left,rgba(210,161,94,.12),transparent 32%),radial-gradient(circle at bottom right,rgba(125,72,43,.14),transparent 30%),var(--gradient-bg)}body:before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.1;background-image:linear-gradient(rgba(255,255,255,.04) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.04) 1px,transparent 1px);background-size:36px 36px;mask-image:linear-gradient(180deg,rgba(255,255,255,.75),transparent)}.app-shell{display:flex;flex-direction:column;height:100vh;position:relative}.provider-bar{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:18px 18px 14px;border-bottom:1px solid var(--border);backdrop-filter:blur(16px);background:var(--bar-bg)}.provider-eyebrow,.assistant-bubble-label,.context-caption,.empty-kicker{text-transform:uppercase;letter-spacing:.18em;font-size:10px;color:var(--muted)}.provider-title{margin:4px 0 0!important;color:var(--text)!important;font-size:18px!important;font-weight:600!important}.provider-meta{display:block;margin-top:6px;color:var(--muted)!important}.provider-context{display:block;margin-top:4px;color:var(--accent)!important;font-size:11px}.provider-actions{display:flex;gap:8px}.provider-action.ant-btn{color:var(--accent)!important;border:1px solid var(--border-strong);border-radius:999px;background:#d2a15e14}.provider-action.ant-btn:hover{color:var(--accent-strong)!important;border-color:#f0c48873!important;background:#d2a15e24!important}.error-banner.ant-alert{margin:12px 16px 0;border-radius:16px;background:#5b1814c7;border:1px solid rgba(255,138,117,.25)}.messages-area{flex:1;overflow-y:auto;padding:16px 18px 12px;scroll-behavior:smooth}.empty-state{min-height:100%;display:flex;align-items:center;justify-content:center;text-align:center;padding:24px}.empty-card{width:min(100%,420px);padding:28px 24px;border-radius:28px;border:1px solid var(--border);background:var(--card-bg),var(--bg-elevated);box-shadow:var(--shadow)}.empty-icon{width:74px;height:74px;margin:0 auto 16px;display:grid;place-items:center;border-radius:50%;border:1px solid rgba(210,161,94,.28);background:radial-gradient(circle,#d2a15e33,#d2a15e08);color:var(--accent-strong);font-size:28px}.empty-title{margin:8px 0!important;color:var(--text)!important}.empty-copy{color:var(--muted);line-height:1.8}.message-row{display:flex;margin-bottom:16px}.message-row-user{justify-content:flex-end}.message-row-assistant{justify-content:flex-start}.message-bubble{max-width:min(88%,720px);border-radius:22px;border:1px solid var(--border);box-shadow:var(--shadow)}.message-bubble-user{padding:13px 15px;background:linear-gradient(135deg,#d2a15e3d,#7345266b),var(--panel-strong);border-radius:22px 22px 8px}.message-bubble-user .ant-typography{color:var(--text);white-space:pre-wrap;word-break:break-word}.message-bubble-assistant{width:100%;padding:14px 16px 16px;background:var(--assistant-bubble-bg),var(--panel)}.assistant-bubble-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.bubble-copy-button.ant-btn{color:var(--muted)!important}.bubble-copy-button.ant-btn:hover{color:var(--accent-strong)!important;background:#d2a15e14!important}.assistant-markdown{line-height:1.75;color:var(--text);word-break:break-word}.assistant-markdown h1,.assistant-markdown h2,.assistant-markdown h3{font-weight:600;letter-spacing:.02em}.assistant-markdown p,.assistant-markdown ul,.assistant-markdown ol{margin:0 0 .9em}.assistant-markdown a{color:var(--accent-strong)}.assistant-markdown pre{margin:1.1em 0;padding:14px 14px 16px;overflow-x:auto;border-radius:18px;border:1px solid var(--pre-border);background:var(--pre-bg);position:relative}.assistant-markdown code{font-family:var(--font-mono)}.assistant-markdown code:not(pre code){padding:.15em .45em;border-radius:999px;background:#d2a15e1f;color:var(--accent-strong)}.bubble-copy-anchor{position:absolute;top:10px;right:10px}.bubble-copy-fallback{border:1px solid rgba(240,196,136,.18);border-radius:999px;background:#22242deb;color:var(--muted);font-family:var(--font-body);font-size:11px;padding:4px 9px;cursor:pointer}.bubble-copy-fallback:hover{color:var(--accent-strong)}.stream-cursor{display:inline-block;width:8px;height:14px;margin-left:4px;border-radius:3px;background:var(--accent);animation:blink .7s infinite;vertical-align:text-bottom}.continuation-indicator{display:flex;align-items:center;gap:8px;padding:8px 16px;color:var(--muted);font-size:12px}.continuation-text{color:var(--muted);font-size:12px}.continuation-spinner{display:inline-block;width:12px;height:12px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.input-area{display:flex;flex-direction:column;gap:10px;padding:14px 16px 18px;border-top:1px solid var(--border);background:var(--input-area-bg)}.input-meta{display:flex;align-items:center;justify-content:flex-start;gap:12px}.context-toggle{display:inline-flex;align-items:center;gap:10px}.context-caption{font-size:10px}.context-file-label{max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--accent)}.composer-row{display:flex;align-items:flex-end;gap:10px}.composer-input{flex:1;resize:none;min-height:44px;max-height:120px;padding:11px 13px;border-radius:18px;border:1px solid var(--input-border);background:var(--input-bg);color:var(--text);font:inherit;line-height:1.6;outline:none;box-shadow:inset 0 1px #ffffff0a;caret-color:var(--accent-strong);cursor:text}.composer-input:focus{border-color:var(--input-focus-border);box-shadow:0 0 0 4px var(--input-focus-ring)}.composer-input::placeholder{color:var(--input-placeholder)}.send-button.ant-btn{height:44px;border-radius:16px;border:1px solid rgba(240,196,136,.28);background:var(--send-button-bg)!important;box-shadow:var(--send-button-shadow)}.send-button.ant-btn:disabled{background:var(--send-button-disabled-bg)!important;border-color:var(--send-button-disabled-border)!important;box-shadow:none}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}.exec-card{max-width:min(88%,720px);border-radius:14px;border:1px solid var(--border);background:var(--assistant-bubble-bg),var(--panel);padding:10px 14px;font-size:13px}.exec-card-header{margin-bottom:6px}.exec-card-command{font-size:12px!important}.exec-log-panel{margin-top:8px;border-radius:10px;border:1px solid var(--border);background:var(--pre-bg);overflow:hidden}.exec-log-header{display:flex;align-items:center;gap:8px;padding:5px 12px;cursor:pointer;user-select:none;font-size:11px;color:var(--muted);border-bottom:1px solid var(--border)}.exec-log-header:hover{color:var(--accent-strong)}.exec-log-title{font-family:var(--font-mono);letter-spacing:.02em}.exec-log-body{max-height:220px;overflow-y:auto;padding:6px 12px;font-family:var(--font-mono);font-size:11px;line-height:1.6}.exec-log-line{white-space:pre-wrap;word-break:break-all;padding:1px 0}.exec-log-stdout{color:var(--text);opacity:.85}.exec-log-stderr{color:var(--danger)}.exec-log-info{color:var(--accent);opacity:.9} + + +
+ + diff --git a/webview/src/App.tsx b/webview/src/App.tsx index 67a84d6..8f0bb47 100644 --- a/webview/src/App.tsx +++ b/webview/src/App.tsx @@ -40,6 +40,7 @@ export default function App() { const isComposingRef = useRef(false) const [includeContext, setIncludeContext] = useState(true) const messagesEndRef = useRef(null) + const lastUserMessageRef = useRef<{ text: string; includeContext: boolean } | null>(null) const { groups, isLoading, error, status, themeMode, approvalOpen, approvalRequestId, approvalCommand, approvalDescription, continuationInfo } = appState @@ -109,7 +110,13 @@ export default function App() { const handleSend = () => { if (!composerReadiness.canSend) { if (composerReadiness.reason && composerReadiness.text) { - setAppState(prev => ({ ...prev, error: { type: 'runtime' as const, message: composerReadiness.reason! } })) + const errorType = (status.connectionState === 'unconfigured' || status.connectionState === 'error') + ? 'config' as const + : 'runtime' as const + const action = (status.connectionState === 'unconfigured' || status.connectionState === 'error') + ? 'openSettings' as 'openSettings' | undefined + : undefined + setAppState(prev => ({ ...prev, error: { type: errorType, message: composerReadiness.reason!, action } })) } return } @@ -118,6 +125,7 @@ export default function App() { if (!payload) return const userMsgId = uuidv4() + lastUserMessageRef.current = { text: payload.text, includeContext } setAppState(prev => ({ ...prev, groups: [...prev.groups, { type: 'human' as const, id: userMsgId, message: { id: userMsgId, content: payload.text } }], @@ -149,6 +157,22 @@ export default function App() { window.__bridge?.cancelStream() }, [isLoading]) + const handleErrorAction = useCallback((action: 'openSettings' | 'retry') => { + if (action === 'openSettings') { + window.__bridge?.openSettings() + setAppState(prev => ({ ...prev, error: null })) + } else if (action === 'retry' && lastUserMessageRef.current) { + const msg = lastUserMessageRef.current + const userMsgId = uuidv4() + setAppState(prev => ({ + ...prev, + groups: [...prev.groups, { type: 'human' as const, id: userMsgId, message: { id: userMsgId, content: msg.text } }], + error: null, + })) + window.__bridge?.sendMessage(msg.text, msg.includeContext) + } + }, []) + // ESC key to cancel streaming useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { @@ -194,7 +218,7 @@ export default function App() { bridgeReady={bridgeReady} /> - {error && setAppState(prev => ({ ...prev, error: null }))} />} + {error && setAppState(prev => ({ ...prev, error: null }))} onAction={handleErrorAction} />}
{groups.length === 0 && ( diff --git a/webview/src/components/ErrorBanner.css b/webview/src/components/ErrorBanner.css new file mode 100644 index 0000000..11c5cf1 --- /dev/null +++ b/webview/src/components/ErrorBanner.css @@ -0,0 +1,161 @@ +.error-banner { + display: flex; + align-items: center; + gap: 10px; + margin: 10px 16px 0; + padding: 10px 14px; + border-radius: 14px; + border: 1px solid; + backdrop-filter: blur(12px); + font-size: 13px; + line-height: 1.5; +} + +.error-banner-icon { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 8px; +} + +.error-banner-body { + flex: 1; + min-width: 0; +} + +.error-banner-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 2px; +} + +.error-banner-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + opacity: 0.85; +} + +.error-banner-close { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + padding: 0; + margin-left: auto; + border: none; + border-radius: 6px; + cursor: pointer; + opacity: 0.5; + transition: opacity 0.15s, background 0.15s; +} + +.error-banner-close:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.1); +} + +.error-banner-message { + color: var(--text); + white-space: pre-wrap; + word-break: break-word; + opacity: 0.9; +} + +/* ---- Config: warm amber ---- */ +.error-banner-config { + background: rgba(210, 161, 94, 0.1); + border-color: rgba(210, 161, 94, 0.3); +} +.error-banner-config .error-banner-icon { + background: rgba(210, 161, 94, 0.18); + color: #d2a15e; +} +.error-banner-config .error-banner-label { + color: #d2a15e; +} + +/* ---- Quota: gold ---- */ +.error-banner-quota { + background: rgba(212, 160, 23, 0.1); + border-color: rgba(212, 160, 23, 0.3); +} +.error-banner-quota .error-banner-icon { + background: rgba(212, 160, 23, 0.18); + color: #d4a017; +} +.error-banner-quota .error-banner-label { + color: #d4a017; +} + +/* ---- Network: blue ---- */ +.error-banner-network { + background: rgba(74, 144, 217, 0.1); + border-color: rgba(74, 144, 217, 0.3); +} +.error-banner-network .error-banner-icon { + background: rgba(74, 144, 217, 0.18); + color: #4a90d9; +} +.error-banner-network .error-banner-label { + color: #4a90d9; +} + +/* ---- Runtime: muted red ---- */ +.error-banner-runtime { + background: rgba(91, 24, 20, 0.45); + border-color: rgba(255, 138, 117, 0.25); +} +.error-banner-runtime .error-banner-icon { + background: rgba(255, 138, 117, 0.15); + color: #ff8a75; +} +.error-banner-runtime .error-banner-label { + color: #ff8a75; +} + +/* ---- Action Buttons ---- */ +.error-banner-btn-settings, +.error-banner-btn-retry { + flex-shrink: 0; + height: 28px !important; + padding: 0 12px !important; + border-radius: 10px !important; + font-size: 12px !important; + font-weight: 500 !important; + border: 1px solid !important; + cursor: pointer; + transition: all 0.15s ease; +} + +.error-banner-btn-settings { + background: linear-gradient(135deg, rgba(185, 131, 70, 0.9), rgba(135, 85, 43, 0.9)) !important; + border-color: rgba(240, 196, 136, 0.4) !important; + color: #f2eadf !important; + box-shadow: 0 4px 12px rgba(89, 47, 17, 0.25); +} + +.error-banner-btn-settings:hover { + background: linear-gradient(135deg, rgba(200, 145, 78, 0.95), rgba(150, 95, 48, 0.95)) !important; + border-color: rgba(240, 196, 136, 0.55) !important; + box-shadow: 0 6px 16px rgba(89, 47, 17, 0.3); + transform: translateY(-1px); +} + +.error-banner-btn-retry { + background: rgba(74, 144, 217, 0.12) !important; + border-color: rgba(74, 144, 217, 0.35) !important; + color: #4a90d9 !important; +} + +.error-banner-btn-retry:hover { + background: rgba(74, 144, 217, 0.22) !important; + border-color: rgba(74, 144, 217, 0.5) !important; + transform: translateY(-1px); +} diff --git a/webview/src/components/ErrorBanner.tsx b/webview/src/components/ErrorBanner.tsx index cf598c8..e4253c2 100644 --- a/webview/src/components/ErrorBanner.tsx +++ b/webview/src/components/ErrorBanner.tsx @@ -1,32 +1,93 @@ -import { Alert, Button, Space } from 'antd' +import { Button } from 'antd' import type { BridgeError } from '../types/bridge' +import './ErrorBanner.css' interface Props { error: BridgeError onClose: () => void + onAction?: (action: 'openSettings' | 'retry') => void } -export function ErrorBanner({ error, onClose }: Props) { - const alertType = error.type === 'config' ? 'warning' : 'error' +const ERROR_CONFIG = { + config: { + label: '配置错误', + icon: ( + + + + + ), + cssClass: 'error-banner-config', + }, + quota: { + label: '配额不足', + icon: ( + + + + + ), + cssClass: 'error-banner-quota', + }, + network: { + label: '网络错误', + icon: ( + + + + + ), + cssClass: 'error-banner-network', + }, + runtime: { + label: '操作失败', + icon: ( + + + + + + ), + cssClass: 'error-banner-runtime', + }, +} - const action = error.action === 'openSettings' ? ( - - ) : error.action === 'retry' ? ( - - ) : undefined +export function ErrorBanner({ error, onClose, onAction }: Props) { + const config = ERROR_CONFIG[error.type as keyof typeof ERROR_CONFIG] ?? ERROR_CONFIG.runtime return ( - {action} : undefined} - /> +
+
{config.icon}
+
+
+ {config.label} + +
+
{error.message}
+
+ {error.action === 'openSettings' && ( + + )} + {error.action === 'retry' && ( + + )} +
) } diff --git a/webview/src/types/bridge.d.ts b/webview/src/types/bridge.d.ts index 32c72b1..88dccf3 100644 --- a/webview/src/types/bridge.d.ts +++ b/webview/src/types/bridge.d.ts @@ -17,7 +17,7 @@ export interface ExecutionResult { } export interface BridgeError { - type: 'config' | 'network' | 'runtime' + type: 'config' | 'quota' | 'network' | 'runtime' message: string action?: 'openSettings' | 'retry' } From 48ebe891d750ebeb9b03a37231f26ee7a1b784e0 Mon Sep 17 00:00:00 2001 From: lgxwool Date: Tue, 21 Apr 2026 22:15:03 +0800 Subject: [PATCH 2/2] feat: fix build.gradle to use 'npm' instead of 'npm.cmd' Co-Authored-By: Claude Opus 4.6 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9cede7e..c43519a 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ test { tasks.register('installWebviewDependencies', Exec) { workingDir 'webview' - commandLine 'npm.cmd', 'ci' + commandLine 'npm', 'ci' inputs.file('webview/package.json') inputs.file('webview/package-lock.json') outputs.dir('webview/node_modules') @@ -60,7 +60,7 @@ tasks.register('installWebviewDependencies', Exec) { tasks.register('buildWebview', Exec) { dependsOn 'installWebviewDependencies' workingDir 'webview' - commandLine 'npm.cmd', 'run', 'build' + commandLine 'npm', 'run', 'build' inputs.file('webview/index.html') inputs.dir('webview/src') inputs.file('webview/package.json')