diff --git a/.github/agent-ci.Dockerfile b/.github/agent-ci.Dockerfile new file mode 100644 index 0000000..f5121e0 --- /dev/null +++ b/.github/agent-ci.Dockerfile @@ -0,0 +1,14 @@ +FROM ghcr.io/actions/actions-runner:latest + +RUN sudo apt-get update \ + && sudo apt-get install -y --no-install-recommends \ + composer \ + docker.io \ + php-cli \ + php-curl \ + php-mbstring \ + php-sqlite3 \ + php-xml \ + php-zip \ + xz-utils \ + && sudo rm -rf /var/lib/apt/lists/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 807172e..9cc62af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,10 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: shivammathur/setup-php@v2 - with: - php-version: '8.1' - tools: composer:v2 + - name: Ensure PHP toolchain + run: | + php -r "exit( version_compare( PHP_VERSION, '8.1', '>=' ) ? 0 : 1 );" + php -v + composer --version - name: Install Composer dependencies run: composer install --no-interaction --prefer-dist - name: Run PHP Unit tests @@ -31,14 +32,30 @@ jobs: with: fetch-depth: 0 - id: changed-js - uses: dorny/paths-filter@v3 - with: - filters: | - js: - - '**/*.js' - - '**/*.jsx' - - '**/*.mjs' - - '**/*.cjs' + name: Detect JS changes + run: | + if [ "${AGENT_CI_LOCAL:-}" = "true" ]; then + echo "js=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + base_ref="${GITHUB_BASE_REF:-}" + if [ -n "$base_ref" ]; then + git fetch --no-tags --depth=1 origin "$base_ref" + base="origin/$base_ref" + elif git rev-parse --verify HEAD^ >/dev/null 2>&1; then + base="HEAD^" + else + base="$(git rev-list --max-parents=0 HEAD)" + fi + + merge_base="$(git merge-base "$base" HEAD 2>/dev/null || echo "$base")" + + if git diff --name-only "$merge_base" HEAD | grep -Eq '\.(js|jsx|mjs|cjs)$'; then + echo "js=true" >> "$GITHUB_OUTPUT" + else + echo "js=false" >> "$GITHUB_OUTPUT" + fi - uses: actions/setup-node@v4 if: steps.changed-js.outputs.js == 'true' with: @@ -59,6 +76,8 @@ jobs: runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' || github.base_ref == 'main' timeout-minutes: 20 + env: + WP_ENV_CORE: WordPress/WordPress#7.0-branch steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -67,10 +86,11 @@ jobs: cache: npm - name: Install dependencies run: npm ci - - uses: shivammathur/setup-php@v2 - with: - php-version: '8.1' - tools: composer:v2 + - name: Ensure PHP toolchain + run: | + php -r "exit( version_compare( PHP_VERSION, '8.1', '>=' ) ? 0 : 1 );" + php -v + composer --version - name: Install Composer dependencies run: composer install --no-interaction --prefer-dist --no-progress - name: Run dist plugin standards check diff --git a/.gitignore b/.gitignore index 4e60e16..b5f3f53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ .DS_Store vendor/ -dist/ \ No newline at end of file +dist/ +.env.agent-ci diff --git a/AGENTS.md b/AGENTS.md index 76eb027..f77b8d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -201,6 +201,13 @@ When writing or refactoring PHPUnit tests: - Run the full PHPUnit suite when shared test support files (like `WordPressStubs.php`) are changed. - Prefer assertions that verify integration calls and state transitions happened (for example submenu registration/removal and metadata checks). +## Local CI With Agent CI + +- Install the `agent-ci` skill one time with `npx skills add redwoodjs/agent-ci --skill agent-ci`. +- Before completing substantial work, run `npm run ci:agent:ci` or `npm run ci:agent` and fix any failures before reporting the work as done. +- If Agent CI pauses on a failed step, fix the issue and resume with `npm run ci:agent:retry -- --name `. +- Keep local Agent CI secrets in `.env.agent-ci`. Never commit that file. + ## Important Notes - Asset files (`build/scripts/*.asset.php`) are auto-generated - never edit manually diff --git a/README.md b/README.md index 3222f2d..00f1ba6 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,35 @@ make test make lint make lint-changed npm run plugin:check +npm run ci:agent:ci +npm run ci:agent ``` +## Local GitHub Actions With Agent CI + +ClawPress is set up to run its GitHub Actions workflows locally with [Agent CI](https://github.com/redwoodjs/agent-ci). + +One-time agent skill setup: + +```bash +npx skills add redwoodjs/agent-ci --skill agent-ci +``` + +Local workflow commands: + +```bash +npm run ci:agent:ci +npm run ci:agent +npm run ci:agent:retry -- --name +``` + +Notes: + +- Agent CI needs Docker available locally. +- The current `agent-ci` CLI release expects Node.js 22+ for these local runner commands. +- Local secrets belong in `.env.agent-ci` and should never be committed. +- `.github/agent-ci.Dockerfile` adds the extra tools this repo needs for local workflow runs, including Docker CLI access for `wp-env`. + ## Key Features ### Admin Assistant MVP diff --git a/build/panel/panel.asset.php b/build/panel/panel.asset.php index 1ca01b4..3b095ad 100644 --- a/build/panel/panel.asset.php +++ b/build/panel/panel.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-element', 'wp-i18n'), 'version' => '2c16ed003bf137db32ce'); + array('react-jsx-runtime', 'wp-element', 'wp-i18n'), 'version' => 'fc1139b8633ce83d1ed5'); diff --git a/build/panel/panel.js b/build/panel/panel.js index c57ec61..44ef945 100644 --- a/build/panel/panel.js +++ b/build/panel/panel.js @@ -1,15 +1,15 @@ -(()=>{"use strict";const e=window.wp.element,t=window.wp.i18n,s=window.ReactJSXRuntime,r=e=>"online"===e?(0,t.__)("Online","clawpress"):"offline"===e?(0,t.__)("Offline","clawpress"):e,a=({onClose:e,onToggleTheme:a,statusMode:n,statusLabel:l,statusLoading:o})=>{const i=n||"offline";return(0,s.jsxs)("div",{className:"clawpress-header",children:[(0,s.jsxs)("div",{className:"clawpress-header-meta",children:[(0,s.jsx)("div",{className:"clawpress-title",children:(0,t.__)("ClawPress Agent","clawpress")}),(0,s.jsxs)("div",{className:`clawpress-status clawpress-status-${i}`,children:[(0,s.jsx)("span",{className:"clawpress-status-dot"}),(0,s.jsx)("span",{className:"clawpress-status-mode",children:o?(0,t.__)("Checking…","clawpress"):r(i)}),l?(0,s.jsx)("span",{className:"clawpress-status-label",children:l}):null]})]}),(0,s.jsx)("button",{className:"clawpress-theme-toggle",type:"button",onClick:a,"aria-label":(0,t.__)("Toggle theme","clawpress"),children:(0,s.jsx)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",focusable:"false",children:(0,s.jsx)("path",{d:"M12 18q2.484 0 4.242-1.758t1.758-4.242-1.758-4.242-4.242-1.758q-1.219 0-2.484 0.563 1.547 0.703 2.508 2.18t0.961 3.258-0.961 3.258-2.508 2.18q1.266 0.563 2.484 0.563zM20.016 8.672l3.281 3.328-3.281 3.328v4.688h-4.688l-3.328 3.281-3.328-3.281h-4.688v-4.688l-3.281-3.328 3.281-3.328v-4.688h4.688l3.328-3.281 3.328 3.281h4.688v4.688z"})})}),(0,s.jsx)("button",{className:"clawpress-close",onClick:e,type:"button","aria-label":(0,t.__)("Close panel","clawpress"),children:(0,s.jsx)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",focusable:"false",children:(0,s.jsx)("path",{d:"M18.984 6.422l-5.578 5.578 5.578 5.578-1.406 1.406-5.578-5.578-5.578 5.578-1.406-1.406 5.578-5.578-5.578-5.578 1.406-1.406 5.578 5.578 5.578-5.578z"})})})]})},n=e=>{const t=Number(e);if(!Number.isFinite(t)||t<=0)return"0";if(t<1e3)return String(Math.round(t));const s=[{threshold:1e9,suffix:"b"},{threshold:1e6,suffix:"m"},{threshold:1e3,suffix:"k"}];for(const e of s)if(t>=e.threshold){const s=t/e.threshold,r=s>=100?0:1;return`${s.toFixed(r).replace(/\.0$/,"")}${e.suffix}`}return String(Math.round(t))},l=({input:r,onInputChange:a,onSend:l,onStop:o,streaming:i,panelOpen:c,suggestions:u,contextUsage:d,onSendSuggestion:p,onHistoryUp:m,onHistoryDown:g})=>{const h=(0,e.useRef)(null),w=(0,e.useRef)(i),_=Number(d?.usedTokens),f=Number(d?.contextWindowTokens),y=Number(d?.percentUsed),b=Number.isFinite(_)&&_>=0?Math.round(_):0,x=Number.isFinite(f)&&f>0?Math.round(f):0;let N=null;Number.isFinite(y)&&y>=0?N=Math.max(0,Math.min(100,Math.round(y))):x>0&&(N=Math.max(0,Math.min(100,Math.round(b/x*100))));const v=null===N?null:Math.max(0,100-N),j=x>0&&null!==N&&b>=0,k=j&&null!==v?(0,t.sprintf)(/* translators: 1: percentage used, 2: percentage left */ /* translators: 1: percentage used, 2: percentage left */ +(()=>{"use strict";const e=window.wp.element,t=window.wp.i18n,s=window.ReactJSXRuntime,r=e=>"online"===e?(0,t.__)("Online","clawpress"):"offline"===e?(0,t.__)("Offline","clawpress"):e,a=({onClose:e,onToggleTheme:a,statusMode:n,statusLabel:l,statusLoading:o})=>{const i=n||"offline";return(0,s.jsxs)("div",{className:"clawpress-header",children:[(0,s.jsxs)("div",{className:"clawpress-header-meta",children:[(0,s.jsx)("div",{className:"clawpress-title",children:(0,t.__)("ClawPress Agent","clawpress")}),(0,s.jsxs)("div",{className:`clawpress-status clawpress-status-${i}`,children:[(0,s.jsx)("span",{className:"clawpress-status-dot"}),(0,s.jsx)("span",{className:"clawpress-status-mode",children:o?(0,t.__)("Checking…","clawpress"):r(i)}),l?(0,s.jsx)("span",{className:"clawpress-status-label",children:l}):null]})]}),(0,s.jsx)("button",{className:"clawpress-theme-toggle",type:"button",onClick:a,"aria-label":(0,t.__)("Toggle theme","clawpress"),children:(0,s.jsx)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",focusable:"false",children:(0,s.jsx)("path",{d:"M12 18q2.484 0 4.242-1.758t1.758-4.242-1.758-4.242-4.242-1.758q-1.219 0-2.484 0.563 1.547 0.703 2.508 2.18t0.961 3.258-0.961 3.258-2.508 2.18q1.266 0.563 2.484 0.563zM20.016 8.672l3.281 3.328-3.281 3.328v4.688h-4.688l-3.328 3.281-3.328-3.281h-4.688v-4.688l-3.281-3.328 3.281-3.328v-4.688h4.688l3.328-3.281 3.328 3.281h4.688v4.688z"})})}),(0,s.jsx)("button",{className:"clawpress-close",onClick:e,type:"button","aria-label":(0,t.__)("Close panel","clawpress"),children:(0,s.jsx)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",focusable:"false",children:(0,s.jsx)("path",{d:"M18.984 6.422l-5.578 5.578 5.578 5.578-1.406 1.406-5.578-5.578-5.578 5.578-1.406-1.406 5.578-5.578-5.578-5.578 1.406-1.406 5.578 5.578 5.578-5.578z"})})})]})},n=e=>{const t=Number(e);if(!Number.isFinite(t)||t<=0)return"0";if(t<1e3)return String(Math.round(t));const s=[{threshold:1e9,suffix:"b"},{threshold:1e6,suffix:"m"},{threshold:1e3,suffix:"k"}];for(const e of s)if(t>=e.threshold){const s=t/e.threshold,r=s>=100?0:1;return`${s.toFixed(r).replace(/\.0$/,"")}${e.suffix}`}return String(Math.round(t))},l=({input:r,onInputChange:a,onSend:l,onStop:o,streaming:i,panelOpen:c,suggestions:u,contextUsage:d,onSendSuggestion:p,onHistoryUp:m,onHistoryDown:g})=>{const h=(0,e.useRef)(null),w=(0,e.useRef)(i),y=Number(d?.usedTokens),_=Number(d?.contextWindowTokens),f=Number(d?.percentUsed),b=Number.isFinite(y)&&y>=0?Math.round(y):0,x=Number.isFinite(_)&&_>0?Math.round(_):0;let N=null;Number.isFinite(f)&&f>=0?N=Math.max(0,Math.min(100,Math.round(f))):x>0&&(N=Math.max(0,Math.min(100,Math.round(b/x*100))));const v=null===N?null:Math.max(0,100-N),j=x>0&&null!==N&&b>=0,k=j&&null!==v?(0,t.sprintf)(/* translators: 1: percentage used, 2: percentage left */ /* translators: 1: percentage used, 2: percentage left */ (0,t.__)("%1$d%% used (%2$d%% left)","clawpress"),N,v):"",S=j?(0,t.sprintf)(/* translators: 1: used tokens, 2: available context-window tokens */ /* translators: 1: used tokens, 2: available context-window tokens */ (0,t.__)("%1$s / %2$s tokens used","clawpress"),n(b),n(x)):"";return(0,e.useEffect)(()=>{const e=w.current;if(w.current=i,!e||i||!c)return;const t=h.current;if(!t)return;t.focus();const s=t.value.length;t.setSelectionRange(s,s)},[i,c]),(0,s.jsxs)("div",{className:"clawpress-input",children:[Array.isArray(u)&&u.length>0?(0,s.jsxs)("div",{className:"clawpress-suggestions","aria-label":(0,t.__)("Suggestions","clawpress"),children:[(0,s.jsx)("div",{className:"clawpress-suggestions-label",children:(0,t.__)("Suggestions","clawpress")}),(0,s.jsx)("div",{className:"clawpress-suggestions-list",children:u.map(e=>(0,s.jsx)("button",{className:"clawpress-suggestion button button-secondary button-small",onClick:()=>p?.(e),type:"button",disabled:i,children:e},e))})]}):null,(0,s.jsx)("textarea",{ref:h,value:r,onChange:a,onKeyDown:e=>{if("Enter"!==e.key||e.shiftKey||(e.preventDefault(),l()),"ArrowUp"===e.key){const t=h.current,s=t?.selectionStart??0,n=!r.slice(0,s).includes("\n");if((""===r.trim()||n)&&m){e.preventDefault();const t=m(r);if(null==t)return;a({target:{value:t}}),setTimeout(()=>{if(!h.current)return;const e=t.length;h.current.setSelectionRange(e,e)},0)}}if("ArrowDown"===e.key){const t=h.current,s=t?.selectionStart??0,n=!r.slice(s).includes("\n");if((""===r.trim()||n)&&g){e.preventDefault();const t=g();if(null==t)return;a({target:{value:t}}),setTimeout(()=>{if(!h.current)return;const e=t.length;h.current.setSelectionRange(e,e)},0)}}},placeholder:(0,t.__)("Ask me anything…","clawpress"),disabled:i}),(0,s.jsxs)("div",{className:"clawpress-input-footer",children:[(0,s.jsx)("div",{className:"clawpress-context-slot",children:j?(0,s.jsxs)("div",{className:"clawpress-context-indicator",role:"img",tabIndex:0,"aria-label":(0,t.sprintf)(/* translators: 1: context usage summary, 2: token usage summary */ /* translators: 1: context usage summary, 2: token usage summary */ -(0,t.__)("Context window: %1$s. %2$s.","clawpress"),k,S),children:[(0,s.jsx)("span",{className:"clawpress-context-pie",style:{"--clawpress-context-used":`${N}%`},"aria-hidden":"true"}),(0,s.jsxs)("div",{className:"clawpress-context-tooltip",role:"tooltip",children:[(0,s.jsx)("div",{className:"clawpress-context-tooltip-title",children:(0,t.__)("Context window:","clawpress")}),(0,s.jsx)("div",{className:"clawpress-context-tooltip-line",children:k}),(0,s.jsx)("div",{className:"clawpress-context-tooltip-line",children:S}),(0,s.jsx)("div",{className:"clawpress-context-tooltip-note",children:(0,t.__)("Codex automatically compacts its context","clawpress")})]})]}):null}),i?(0,s.jsx)("button",{className:"button",onClick:o,type:"button",children:(0,t.__)("Stop","clawpress")}):(0,s.jsx)("button",{className:"button button-primary",onClick:l,type:"button",children:(0,t.__)("Send","clawpress")})]})]})},o=e=>{if(!e||void 0===e.arguments||null===e.arguments)return{};if("string"==typeof e.arguments){if(""===e.arguments.trim())return{};try{return JSON.parse(e.arguments)}catch{return{raw:e.arguments}}}return e.arguments},i=({status:e,title:r,canRerun:a,policy:n,isOpen:l,error:o,children:i,onRun:c,onCancel:u,showActions:d,runLabelOverride:p})=>{const m=p||((e,s)=>"running"===e?(0,t.__)("Running…","clawpress"):"blocked"===e?(0,t.__)("Blocked","clawpress"):"done"===e&&s?(0,t.__)("Re-run","clawpress"):"error"===e?(0,t.__)("Retry","clawpress"):(0,t.__)("Run","clawpress"))(e,a),g="cancelled"===e,h="error"===e,w=void 0!==d?d:"running"!==e&&!g&&("done"!==e||a),_="blocked"===e,f=((e,s)=>"running"===e?(0,t.__)("Running…","clawpress"):"blocked"===e?"serial"===s?.concurrency?(0,t.__)("Blocked (waiting for earlier tool).","clawpress"):(0,t.__)("Blocked.","clawpress"):null)(e,n);return(0,s.jsxs)("details",{className:"clawpress-tool-dialog",open:!g&&l,children:[(0,s.jsxs)("summary",{className:"clawpress-tool-dialog-summary",children:[(0,s.jsxs)("span",{className:"clawpress-tool-dialog-heading",children:[(0,s.jsx)("span",{className:`clawpress-tool-dialog-status clawpress-tool-dialog-status-${e}`,"aria-hidden":"true"}),(0,s.jsx)("span",{className:"clawpress-tool-dialog-title",children:r})]}),w?(0,s.jsxs)("span",{className:"clawpress-tool-dialog-actions",children:[h?(0,s.jsx)("span",{className:"clawpress-tool-dialog-error-icon","aria-hidden":"true",children:(0,s.jsx)("svg",{viewBox:"0 0 24 24",role:"img",children:(0,s.jsx)("path",{d:"M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm0 6a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V9a1 1 0 0 1 1-1Zm0 10a1.25 1.25 0 1 1 0-2.5A1.25 1.25 0 0 1 12 18Z"})})}):null,(0,s.jsx)("button",{className:"button button-primary",type:"button",onClick:c,disabled:"running"===e||_||g,children:m}),(0,s.jsx)("button",{className:"button",type:"button",onClick:u,children:(0,t.__)("Cancel","clawpress")})]}):null]}),(0,s.jsxs)("div",{className:"clawpress-tool-dialog-body",children:[g?(0,s.jsx)("p",{children:(0,t.__)("Cancelled.","clawpress")}):null,f?(0,s.jsx)("p",{children:f}):null,"error"===e?(0,s.jsx)("p",{children:o}):null,i]})]})},c=({fields:r,initialValues:a,onSubmit:n,onRun:l,onCancel:o,disabled:i})=>{const[c,u]=(0,e.useState)(a);(0,e.useEffect)(()=>{u(a)},[a]);const d=(e,t)=>u(s=>({...s,[e]:t}));return(0,s.jsxs)("form",{className:"clawpress-tool-dialog-form",onSubmit:e=>{e.preventDefault(),i||n(c)},children:[r.map(e=>{const t=c[e.name],r={id:`clawpress-tool-field-${e.name}`,name:e.name,disabled:i};if("hidden"===e.type)return(0,s.jsx)("input",{...r,type:"hidden",value:t??""},e.name);let a=null;return a="textarea"===e.type?(0,s.jsx)("textarea",{...r,className:"clawpress-tool-dialog-input",rows:e.rows||3,value:t??"",onChange:t=>d(e.name,t.target.value)}):"select"===e.type?(0,s.jsx)("select",{...r,className:"clawpress-tool-dialog-input",value:t??"",onChange:t=>d(e.name,t.target.value),children:e.options?.map(e=>(0,s.jsx)("option",{value:e.value,children:e.label},e.value))}):"checkbox"===e.type?(0,s.jsx)("input",{...r,type:"checkbox",checked:Boolean(t),onChange:t=>d(e.name,t.target.checked)}):(0,s.jsx)("input",{...r,className:"clawpress-tool-dialog-input",type:e.type||"text",value:t??"",onChange:t=>d(e.name,t.target.value)}),(0,s.jsxs)("label",{className:"clawpress-tool-dialog-field",htmlFor:r.id,children:[(0,s.jsx)("span",{className:"clawpress-tool-dialog-label",children:e.label}),a,e.help?(0,s.jsx)("span",{className:"clawpress-tool-dialog-help",children:e.help}):null]},e.name)}),(0,s.jsxs)("div",{className:"clawpress-tool-dialog-form-actions",children:[(0,s.jsx)("button",{className:"button button-primary",type:"submit",disabled:i,children:(0,t.__)("Preview","clawpress")}),l?(0,s.jsx)("button",{className:"button",type:"button",onClick:e=>{e.preventDefault(),i||l&&l(c)},disabled:i,children:(0,t.__)("Run","clawpress")}):null,o?(0,s.jsx)("button",{className:"button",type:"button",onClick:o,disabled:i,children:(0,t.__)("Cancel","clawpress")}):null]})]})},u="update_posts_find_replace",d=({toolName:e,args:r,toolDialog:a,runTool:n,onCancel:l,policy:o,isOpen:c})=>{const u=a.status||"idle",d=a.error||null,p=a.result||null,m=Boolean(o?.canRerun);return(0,s.jsxs)(i,{status:u,title:e||(0,t.__)("Unknown tool","clawpress"),canRerun:m,policy:o,isOpen:c,error:d,onRun:e=>{e.preventDefault(),e.stopPropagation(),n(a.id,a.args||r)},onCancel:e=>{e.preventDefault(),e.stopPropagation(),l(a.id)},children:["done"===u?(0,s.jsx)("syntax-highlight",{language:"json",children:JSON.stringify(p,null,2)}):(0,s.jsx)("syntax-highlight",{language:"json",children:JSON.stringify(r,null,2)}),a.diff?(0,s.jsxs)("div",{className:"clawpress-tool-dialog-diff",children:[(0,s.jsx)("h4",{children:(0,t.__)("Changes:","clawpress")}),(0,s.jsx)("syntax-highlight",{language:"json",children:JSON.stringify(a.diff,null,2)})]}):null]})},p={[u]:{renderer:({args:e,toolDialog:r,runTool:a,onCancel:n,policy:l,isOpen:o})=>{const u=e.search??"",d=e.replace??"",p=e.post_status??"any",m=r.status||"idle",g=r.error||null,h=r.result||null,w="done"===m?Boolean(h?.dry_run):Boolean(l?.canRerun),_="done"===m&&h?.dry_run,f="error"===m,y="blocked"===m,b=h?.total??0,x=Array.isArray(h?.changed)?h.changed:[],N=e=>{a(r.id,e)},v=[{name:"search",label:(0,t.__)("Find","clawpress"),type:"text",help:(0,t.__)("The text to search for.","clawpress")},{name:"replace",label:(0,t.__)("Replace","clawpress"),type:"text",help:(0,t.__)("The text to replace with.","clawpress")},{name:"post_status",label:(0,t.__)("Post status","clawpress"),type:"select",options:[{value:"any",label:(0,t.__)("Any","clawpress")},{value:"publish",label:(0,t.__)("Published","clawpress")},{value:"draft",label:(0,t.__)("Draft","clawpress")}]},{name:"dry_run",label:(0,t.__)("Dry run","clawpress"),type:"hidden"}],j="cancelled"===m?(0,t.__)("Update Posts - Find and Replace (Cancelled)","clawpress"):(0,t.__)("Update Posts - Find and Replace","clawpress");let k=null;return"cancelled"!==m&&(k=f?(0,s.jsxs)("div",{className:"clawpress-tool-result-actions",children:[(0,s.jsx)("button",{className:"button button-primary",type:"button",onClick:()=>N({...r.args||e,dry_run:!1!==r.args?.dry_run}),children:(0,t.__)("Retry","clawpress")}),(0,s.jsx)("button",{className:"button",type:"button",onClick:()=>n(r.id),children:(0,t.__)("Cancel","clawpress")})]}):"done"!==m?(0,s.jsx)(c,{fields:v,initialValues:{search:u,replace:d,post_status:p,dry_run:!0},disabled:"running"===m||y,onSubmit:e=>N({...e,dry_run:!0}),onRun:e=>N({...e,dry_run:!1}),onCancel:()=>n(r.id)}):(0,s.jsxs)("div",{className:"clawpress-tool-result",children:[(0,s.jsx)("div",{className:"clawpress-tool-result-summary",children:(0,s.jsx)("span",{children:_?(0,t.__)("Preview Results","clawpress"):(0,t.__)("Results","clawpress")})}),0===b?(0,s.jsx)("p",{children:(0,t.__)("No matches found.","clawpress")}):(0,s.jsxs)("details",{className:"clawpress-tool-result-list",open:!0,children:[(0,s.jsx)("summary",{children:(0,t.sprintf)(/* translators: %d: number of changed posts */ /* translators: %d: number of changed posts */ +(0,t.__)("Context window: %1$s. %2$s.","clawpress"),k,S),children:[(0,s.jsx)("span",{className:"clawpress-context-pie",style:{"--clawpress-context-used":`${N}%`},"aria-hidden":"true"}),(0,s.jsxs)("div",{className:"clawpress-context-tooltip",role:"tooltip",children:[(0,s.jsx)("div",{className:"clawpress-context-tooltip-title",children:(0,t.__)("Context window:","clawpress")}),(0,s.jsx)("div",{className:"clawpress-context-tooltip-line",children:k}),(0,s.jsx)("div",{className:"clawpress-context-tooltip-line",children:S}),(0,s.jsx)("div",{className:"clawpress-context-tooltip-note",children:(0,t.__)("Codex automatically compacts its context","clawpress")})]})]}):null}),i?(0,s.jsx)("button",{className:"button",onClick:o,type:"button",children:(0,t.__)("Stop","clawpress")}):(0,s.jsx)("button",{className:"button button-primary",onClick:l,type:"button",children:(0,t.__)("Send","clawpress")})]})]})},o=e=>{if(!e||void 0===e.arguments||null===e.arguments)return{};if("string"==typeof e.arguments){if(""===e.arguments.trim())return{};try{return JSON.parse(e.arguments)}catch{return{raw:e.arguments}}}return e.arguments},i=({status:e,title:r,canRerun:a,policy:n,isOpen:l,error:o,children:i,onRun:c,onCancel:u,showActions:d,runLabelOverride:p})=>{const m=p||((e,s)=>"running"===e?(0,t.__)("Running…","clawpress"):"blocked"===e?(0,t.__)("Blocked","clawpress"):"done"===e&&s?(0,t.__)("Re-run","clawpress"):"error"===e?(0,t.__)("Retry","clawpress"):(0,t.__)("Run","clawpress"))(e,a),g="cancelled"===e,h="error"===e,w=void 0!==d?d:"running"!==e&&!g&&("done"!==e||a),y="blocked"===e,_=((e,s)=>"running"===e?(0,t.__)("Running…","clawpress"):"blocked"===e?"serial"===s?.concurrency?(0,t.__)("Blocked (waiting for earlier tool).","clawpress"):(0,t.__)("Blocked.","clawpress"):null)(e,n);return(0,s.jsxs)("details",{className:"clawpress-tool-dialog",open:!g&&l,children:[(0,s.jsxs)("summary",{className:"clawpress-tool-dialog-summary",children:[(0,s.jsxs)("span",{className:"clawpress-tool-dialog-heading",children:[(0,s.jsx)("span",{className:`clawpress-tool-dialog-status clawpress-tool-dialog-status-${e}`,"aria-hidden":"true"}),(0,s.jsx)("span",{className:"clawpress-tool-dialog-title",children:r})]}),w?(0,s.jsxs)("span",{className:"clawpress-tool-dialog-actions",children:[h?(0,s.jsx)("span",{className:"clawpress-tool-dialog-error-icon","aria-hidden":"true",children:(0,s.jsx)("svg",{viewBox:"0 0 24 24",role:"img",children:(0,s.jsx)("path",{d:"M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm0 6a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V9a1 1 0 0 1 1-1Zm0 10a1.25 1.25 0 1 1 0-2.5A1.25 1.25 0 0 1 12 18Z"})})}):null,(0,s.jsx)("button",{className:"button button-primary",type:"button",onClick:c,disabled:"running"===e||y||g,children:m}),(0,s.jsx)("button",{className:"button",type:"button",onClick:u,children:(0,t.__)("Cancel","clawpress")})]}):null]}),(0,s.jsxs)("div",{className:"clawpress-tool-dialog-body",children:[g?(0,s.jsx)("p",{children:(0,t.__)("Cancelled.","clawpress")}):null,_?(0,s.jsx)("p",{children:_}):null,"error"===e?(0,s.jsx)("p",{children:o}):null,i]})]})},c=({fields:r,initialValues:a,onSubmit:n,onRun:l,onCancel:o,disabled:i})=>{const[c,u]=(0,e.useState)(a);(0,e.useEffect)(()=>{u(a)},[a]);const d=(e,t)=>u(s=>({...s,[e]:t}));return(0,s.jsxs)("form",{className:"clawpress-tool-dialog-form",onSubmit:e=>{e.preventDefault(),i||n(c)},children:[r.map(e=>{const t=c[e.name],r={id:`clawpress-tool-field-${e.name}`,name:e.name,disabled:i};if("hidden"===e.type)return(0,s.jsx)("input",{...r,type:"hidden",value:t??""},e.name);let a=null;return a="textarea"===e.type?(0,s.jsx)("textarea",{...r,className:"clawpress-tool-dialog-input",rows:e.rows||3,value:t??"",onChange:t=>d(e.name,t.target.value)}):"select"===e.type?(0,s.jsx)("select",{...r,className:"clawpress-tool-dialog-input",value:t??"",onChange:t=>d(e.name,t.target.value),children:e.options?.map(e=>(0,s.jsx)("option",{value:e.value,children:e.label},e.value))}):"checkbox"===e.type?(0,s.jsx)("input",{...r,type:"checkbox",checked:Boolean(t),onChange:t=>d(e.name,t.target.checked)}):(0,s.jsx)("input",{...r,className:"clawpress-tool-dialog-input",type:e.type||"text",value:t??"",onChange:t=>d(e.name,t.target.value)}),(0,s.jsxs)("label",{className:"clawpress-tool-dialog-field",htmlFor:r.id,children:[(0,s.jsx)("span",{className:"clawpress-tool-dialog-label",children:e.label}),a,e.help?(0,s.jsx)("span",{className:"clawpress-tool-dialog-help",children:e.help}):null]},e.name)}),(0,s.jsxs)("div",{className:"clawpress-tool-dialog-form-actions",children:[(0,s.jsx)("button",{className:"button button-primary",type:"submit",disabled:i,children:(0,t.__)("Preview","clawpress")}),l?(0,s.jsx)("button",{className:"button",type:"button",onClick:e=>{e.preventDefault(),i||l&&l(c)},disabled:i,children:(0,t.__)("Run","clawpress")}):null,o?(0,s.jsx)("button",{className:"button",type:"button",onClick:o,disabled:i,children:(0,t.__)("Cancel","clawpress")}):null]})]})},u="update_posts_find_replace",d=({toolName:e,args:r,toolDialog:a,runTool:n,onCancel:l,policy:o,isOpen:c})=>{const u=a.status||"idle",d=a.error||null,p=a.result||null,m=Boolean(o?.canRerun);return(0,s.jsxs)(i,{status:u,title:e||(0,t.__)("Unknown tool","clawpress"),canRerun:m,policy:o,isOpen:c,error:d,onRun:e=>{e.preventDefault(),e.stopPropagation(),n(a.id,a.args||r)},onCancel:e=>{e.preventDefault(),e.stopPropagation(),l(a.id)},children:["done"===u?(0,s.jsx)("syntax-highlight",{language:"json",children:JSON.stringify(p,null,2)}):(0,s.jsx)("syntax-highlight",{language:"json",children:JSON.stringify(r,null,2)}),a.diff?(0,s.jsxs)("div",{className:"clawpress-tool-dialog-diff",children:[(0,s.jsx)("h4",{children:(0,t.__)("Changes:","clawpress")}),(0,s.jsx)("syntax-highlight",{language:"json",children:JSON.stringify(a.diff,null,2)})]}):null]})},p={[u]:{renderer:({args:e,toolDialog:r,runTool:a,onCancel:n,policy:l,isOpen:o})=>{const u=e.search??"",d=e.replace??"",p=e.post_status??"any",m=r.status||"idle",g=r.error||null,h=r.result||null,w="done"===m?Boolean(h?.dry_run):Boolean(l?.canRerun),y="done"===m&&h?.dry_run,_="error"===m,f="blocked"===m,b=h?.total??0,x=Array.isArray(h?.changed)?h.changed:[],N=e=>{a(r.id,e)},v=[{name:"search",label:(0,t.__)("Find","clawpress"),type:"text",help:(0,t.__)("The text to search for.","clawpress")},{name:"replace",label:(0,t.__)("Replace","clawpress"),type:"text",help:(0,t.__)("The text to replace with.","clawpress")},{name:"post_status",label:(0,t.__)("Post status","clawpress"),type:"select",options:[{value:"any",label:(0,t.__)("Any","clawpress")},{value:"publish",label:(0,t.__)("Published","clawpress")},{value:"draft",label:(0,t.__)("Draft","clawpress")}]},{name:"dry_run",label:(0,t.__)("Dry run","clawpress"),type:"hidden"}],j="cancelled"===m?(0,t.__)("Update Posts - Find and Replace (Cancelled)","clawpress"):(0,t.__)("Update Posts - Find and Replace","clawpress");let k=null;return"cancelled"!==m&&(k=_?(0,s.jsxs)("div",{className:"clawpress-tool-result-actions",children:[(0,s.jsx)("button",{className:"button button-primary",type:"button",onClick:()=>N({...r.args||e,dry_run:!1!==r.args?.dry_run}),children:(0,t.__)("Retry","clawpress")}),(0,s.jsx)("button",{className:"button",type:"button",onClick:()=>n(r.id),children:(0,t.__)("Cancel","clawpress")})]}):"done"!==m?(0,s.jsx)(c,{fields:v,initialValues:{search:u,replace:d,post_status:p,dry_run:!0},disabled:"running"===m||f,onSubmit:e=>N({...e,dry_run:!0}),onRun:e=>N({...e,dry_run:!1}),onCancel:()=>n(r.id)}):(0,s.jsxs)("div",{className:"clawpress-tool-result",children:[(0,s.jsx)("div",{className:"clawpress-tool-result-summary",children:(0,s.jsx)("span",{children:y?(0,t.__)("Preview Results","clawpress"):(0,t.__)("Results","clawpress")})}),0===b?(0,s.jsx)("p",{children:(0,t.__)("No matches found.","clawpress")}):(0,s.jsxs)("details",{className:"clawpress-tool-result-list",open:!0,children:[(0,s.jsx)("summary",{children:(0,t.sprintf)(/* translators: %d: number of changed posts */ /* translators: %d: number of changed posts */ (0,t.__)("Changed posts (%d)","clawpress"),b)}),(0,s.jsx)("ul",{children:x.map(e=>(0,s.jsxs)("li",{children:[(0,s.jsx)("span",{className:"clawpress-tool-result-title",children:e.title||(0,t.__)("Untitled","clawpress")}),(0,s.jsx)("span",{className:"clawpress-tool-result-meta",children:(0,t.sprintf)(/* translators: %d: post ID */ /* translators: %d: post ID */ (0,t.__)("ID %d","clawpress"),e.id)}),(0,s.jsx)("span",{className:"clawpress-tool-result-meta",children:(0,t.sprintf)(/* translators: %d: number of replacements in a post */ /* translators: %d: number of replacements in a post */ -(0,t._n)("%d change","%d changes",e.count,"clawpress"),e.count)})]},e.id))})]}),_?(0,s.jsxs)("div",{className:"clawpress-tool-result-actions",children:[(0,s.jsx)("button",{className:"button button-primary",type:"button",onClick:()=>N({search:u,replace:d,post_status:p,dry_run:!1}),children:(0,t.__)("Run","clawpress")}),(0,s.jsx)("button",{className:"button",type:"button",onClick:()=>n(r.id),children:(0,t.__)("Cancel","clawpress")})]}):null]})),(0,s.jsx)(i,{status:m,title:j,canRerun:w,policy:l,isOpen:o,error:g,showActions:!1,onRun:e=>{e.preventDefault(),e.stopPropagation(),N({search:u,replace:d,post_status:p,dry_run:!1})},onCancel:e=>{e.preventDefault(),e.stopPropagation(),n(r.id)},children:k})},policy:{concurrency:"serial",canRerun:!1}}},m=e=>p[e]?.policy||{},g=({toolDialog:e,isOpen:t,onRunTool:r,onCancel:a})=>{if(!e)return null;const n=o(e.function),l=e.function?.name||"",i=(e=>p[e]?.renderer||d)(l),c=m(l);return(0,s.jsx)(i,{toolName:l,args:n,toolDialog:e,runTool:r,onCancel:a,policy:c,isOpen:t})},h=e=>Array.isArray(e?.data?.actions)?e.data.actions.filter(e=>e&&"object"==typeof e).map((e,t)=>{const s="string"==typeof e.label?e.label.trim():"",r="string"==typeof e.type&&e.type.trim()?e.type.trim().toLowerCase():"send_prompt";if(!s)return null;if("open_url"===r){const r=[e.url,e.href].map(e=>"string"==typeof e?e.trim():"").find(e=>e.length>0);return r?{id:"string"==typeof e.id&&e.id.trim()?e.id.trim():`action-${t}`,label:s,type:"open_url",url:r}:null}if("run_tool"===r){const r=[e.tool,e.tool_name,e.name].map(e=>"string"==typeof e?e.trim():"").find(e=>e.length>0);if(!r)return null;let a={};if(e.args&&"object"==typeof e.args&&!Array.isArray(e.args))a=e.args;else if("string"==typeof e.arguments)try{const t=JSON.parse(e.arguments);t&&"object"==typeof t&&!Array.isArray(t)&&(a=t)}catch{a={}}else e.arguments&&"object"==typeof e.arguments&&!Array.isArray(e.arguments)&&(a=e.arguments);return{id:"string"==typeof e.id&&e.id.trim()?e.id.trim():`action-${t}`,label:s,type:"run_tool",tool:r,args:a}}const a=[e.prompt,e.message,e.command].map(e=>"string"==typeof e?e.trim():"").find(e=>e.length>0);return a?{id:"string"==typeof e.id&&e.id.trim()?e.id.trim():`action-${t}`,label:s,type:"send_prompt",prompt:a}:null}).filter(e=>Boolean(e)):[],w=({card:e,onSendAction:r,isBusy:a=!1})=>{const n="string"==typeof e?.data?.title&&e.data.title.trim()?e.data.title:(0,t.__)("Welcome to ClawPress","clawpress"),l="string"==typeof e?.data?.message&&e.data.message.trim()?e.data.message:(0,t.__)("Hello! I am ready to help with your WordPress tasks.","clawpress"),o="string"==typeof e?.data?.subtitle&&e.data.subtitle.trim()?e.data.subtitle:"",i="string"==typeof e?.data?.emoji&&e.data.emoji.trim()?e.data.emoji:"👋",c=h(e);return(0,s.jsx)("div",{className:"clawpress-card clawpress-card-welcome",children:(0,s.jsxs)("div",{className:"clawpress-card-body",children:[(0,s.jsx)("div",{className:"clawpress-card-section-title",children:(0,s.jsxs)("div",{className:"clawpress-card-welcome-title-line",children:[(0,s.jsx)("span",{className:"clawpress-card-emoji","aria-hidden":"true",children:i}),(0,s.jsx)("div",{className:"clawpress-card-title",children:n})]})}),o?(0,s.jsx)("div",{className:"clawpress-card-section-subtitle",children:(0,s.jsx)("div",{className:"clawpress-card-subtitle",children:o})}):null,(0,s.jsx)("div",{className:"clawpress-card-section-content",children:(0,s.jsx)("div",{className:"clawpress-card-text",children:l})}),c.length>0?(0,s.jsx)("div",{className:"clawpress-card-section-buttons",children:(0,s.jsx)("div",{className:"clawpress-card-actions",children:c.map(e=>(0,s.jsx)("button",{type:"button",className:"button button-secondary button-small",onClick:()=>r?.(e),disabled:a,children:e.label},e.id))})}):null]})})},_="__clawpress_custom_model__",f=(e,t)=>e.filter(e=>"send_prompt"===e?.type&&"string"==typeof e.prompt&&e.prompt.startsWith(t)).map(e=>({...e,value:e.prompt.slice(t.length).trim()})).filter(e=>e.value.length>0),y=e=>"string"!=typeof e?"":e.replace(/\r\n/g,"\n").replace(//gi,"\n"),b=({card:r,onSendAction:a,isBusy:n=!1})=>{const l=r?.data&&"object"==typeof r.data&&!Array.isArray(r.data)?r.data:{},o="string"==typeof l.title&&l.title.trim()?l.title:(0,t.__)("Setup Wizard","clawpress"),i="string"==typeof l.emoji&&l.emoji.trim()?l.emoji:"🧙",c="string"==typeof l.message&&l.message.trim()?y(l.message):(0,t.__)("Follow these steps to finish setup.","clawpress"),u="string"==typeof l.detail&&l.detail.trim()?l.detail:"",d="string"==typeof l.error&&l.error.trim()?l.error:"",p=y(u),m=y(d),g="string"==typeof l.step?l.step.trim():"",w="string"==typeof l.step_label&&l.step_label.trim()?l.step_label:g,b=Number.isFinite(Number(l.step_index))?Number(l.step_index):null,x=Number.isFinite(Number(l.step_total))?Number(l.step_total):null,N=Array.isArray(l.steps)?l.steps:[],v=h(r),j=(0,e.useMemo)(()=>f(v,"/setup provider "),[v]),k=(0,e.useMemo)(()=>f(v,"/setup model "),[v]),S="string"==typeof l.selected_model&&l.selected_model.trim()?l.selected_model.trim():"",A=(0,e.useMemo)(()=>"model"!==g?k:[...k,{id:_,label:(0,t.__)("Use Custom Model ID","clawpress"),type:"send_prompt",prompt:""}],[k,g]),E=(0,e.useMemo)(()=>{const e=new Set([...j.map(e=>e.id),...k.map(e=>e.id)]);return v.filter(t=>!e.has(t.id))},[v,j,k]),C=(0,e.useMemo)(()=>{const e=E.some(e=>"send_prompt"===e?.type&&"string"==typeof e.prompt&&"/setup back"===e.prompt.trim());return"provider"===g||e?E:[{id:"wizard-back-fallback",label:(0,t.__)("Back","clawpress"),type:"send_prompt",prompt:"/setup back"},...E]},[E,g]),[R,M]=(0,e.useState)(j[0]?.id||""),[$,T]=(0,e.useState)(A[0]?.id||""),[D,P]=(0,e.useState)("");(0,e.useEffect)(()=>{const e=j.map(e=>e.id);0!==e.length?e.includes(R)||M(e[0]):M("")},[j,R]),(0,e.useEffect)(()=>{const e=A.map(e=>e.id);0!==e.length?e.includes($)||T(e[0]):T("")},[A,$]),(0,e.useEffect)(()=>{if("model"!==g||!S)return;const e=k.find(e=>e.value===S);e?T(e.id):(T(_),P(S))},[k,S,g]);const q=j.find(e=>e.id===R)||j[0]||null,I=k.find(e=>e.id===$)||k[0]||null,L="model"===g&&$===_,F=D.trim(),O="string"==typeof l.settings_url&&l.settings_url.trim()?l.settings_url.trim():"",B=(0,e.useMemo)(()=>{if(!O||"undefined"==typeof window)return!1;try{const e=new URL(window.location.href),t=new URL(O,window.location.origin);return e.pathname===t.pathname&&e.search===t.search}catch{return!1}},[O]),U=b&&x?(0,t.sprintf)(/* translators: 1: current step number, 2: total step count, 3: step label. */ /* translators: 1: current step number, 2: total step count, 3: step label. */ -(0,t.__)("Step %1$d OF %2$d : %3$s","clawpress"),b,x,w||""):"",W="workspace"===g&&"string"==typeof l.workspace_path&&l.workspace_path.trim()?(e=>{if("string"!=typeof e)return"";const t=e.trim().replace(/\\/g,"/");if(!t)return"";const s=t.indexOf("/wp-content");return-1===s?t:t.slice(s)})(l.workspace_path):"",H="workspace"!==g?"":"string"==typeof l.workspace_exists&&l.workspace_exists.trim()?l.workspace_exists.trim():"string"==typeof l.workspace_exists_line&&l.workspace_exists_line.trim()?l.workspace_exists_line.replace(/^Exists\s*:\s*/i,"").trim():"",J="provider"===g&&j.length>1,z="model"===g&&A.length>0,G="provider"===g&&1===j.length,K="model"===g&&!z&&1===k.length,X=G||K||C.length>0,V=J||z||X;let Z="";Z=L?F?`/setup model ${F}`:"":I?.prompt||"";const Y="provider"===g?q?.prompt||"":Z,Q=n||"provider"===g&&!q||"model"===g&&(L?0===F.length:!I);return(0,s.jsx)("div",{className:"clawpress-card clawpress-card-setup",children:(0,s.jsxs)("div",{className:"clawpress-card-body",children:[(0,s.jsx)("div",{className:"clawpress-card-section-title",children:(0,s.jsxs)("div",{className:"clawpress-card-setup-title-line",children:[(0,s.jsx)("span",{className:"clawpress-card-setup-emoji","aria-hidden":"true",children:i}),(0,s.jsx)("div",{className:"clawpress-card-title",children:o})]})}),U||N.length>0?(0,s.jsxs)("div",{className:"clawpress-card-section-subtitle",children:[U?(0,s.jsx)("div",{className:"clawpress-card-subtitle clawpress-card-setup-step-line",children:U}):null,N.length>0?(0,s.jsx)("div",{className:"clawpress-card-setup-progress","aria-hidden":"true",children:N.map((e,t)=>{const r="string"==typeof e?.status?e.status:"pending",a=["pending","done","current","completed"].includes(r)?r:"pending";return(0,s.jsxs)("div",{className:`clawpress-card-setup-progress-item clawpress-card-setup-progress-${a}`,children:[(0,s.jsx)("span",{className:"clawpress-card-setup-progress-dot",children:t+1}),tM(e.target.value),disabled:n,children:j.map(e=>(0,s.jsx)("option",{value:e.id,children:e.label},e.id))}):null,z?(0,s.jsx)("select",{className:"clawpress-card-setup-select",value:$,onChange:e=>T(e.target.value),disabled:n,children:A.map(e=>(0,s.jsx)("option",{value:e.id,children:e.label},e.id))}):null,L?(0,s.jsx)("input",{type:"text",className:"clawpress-card-setup-input",value:D,onChange:e=>P(e.target.value),placeholder:(0,t.__)("Enter custom model ID","clawpress"),disabled:n}):null,(0,s.jsx)("button",{type:"button",className:"button button-primary button-small",disabled:Q,onClick:()=>a?.(Y),children:"provider"===g?(0,t.__)("Use Selected Provider","clawpress"):(0,t.__)("Use Selected Model","clawpress")})]}):null,X?(0,s.jsxs)("div",{className:"clawpress-card-actions",children:[G?(0,s.jsx)("button",{type:"button",className:"button button-primary button-small",onClick:()=>a?.(j[0]),disabled:n,children:j[0].label}):null,K?(0,s.jsx)("button",{type:"button",className:"button button-primary button-small",onClick:()=>a?.(k[0]),disabled:n,children:k[0].label}):null,C.map(e=>(0,s.jsx)("button",{type:"button",className:"button button-secondary button-small",onClick:()=>a?.(e),disabled:n,children:e.label},e.id))]}):null]}):null]})})},x=({card:e})=>{const r="string"==typeof e?.data?.title&&e.data.title.trim()?e.data.title:(0,t.__)("Request Error","clawpress"),a="string"==typeof e?.data?.message&&e.data.message.trim()?e.data.message:(0,t.__)("An unknown error occurred.","clawpress"),n="string"==typeof e?.data?.subtitle&&e.data.subtitle.trim()?e.data.subtitle:"";return(0,s.jsx)("div",{className:"clawpress-card clawpress-card-error",role:"alert","aria-live":"polite",children:(0,s.jsxs)("div",{className:"clawpress-card-body",children:[(0,s.jsx)("div",{className:"clawpress-card-section-title",children:(0,s.jsx)("div",{className:"clawpress-card-title",children:r})}),n?(0,s.jsx)("div",{className:"clawpress-card-section-subtitle",children:(0,s.jsx)("div",{className:"clawpress-card-subtitle",children:n})}):null,(0,s.jsx)("div",{className:"clawpress-card-section-content",children:(0,s.jsx)("div",{className:"clawpress-card-text",children:a})})]})})},N=({card:e,onSendAction:r,isBusy:a=!1})=>{const n="string"==typeof e?.data?.title&&e.data.title.trim()?e.data.title:(0,t.__)("User Confirmation Required","clawpress"),l="string"==typeof e?.data?.subtitle&&e.data.subtitle.trim()?e.data.subtitle:(0,t.__)("Destructive action pending","clawpress"),o="string"==typeof e?.data?.message&&e.data.message.trim()?e.data.message:(0,t.__)("Please confirm or decline this action.","clawpress"),i=h(e);return(0,s.jsx)("div",{className:"clawpress-card clawpress-card-user-confirmation",role:"alert","aria-live":"polite",children:(0,s.jsxs)("div",{className:"clawpress-card-body",children:[(0,s.jsx)("div",{className:"clawpress-card-section-title",children:(0,s.jsx)("div",{className:"clawpress-card-title",children:n})}),l?(0,s.jsx)("div",{className:"clawpress-card-section-subtitle",children:(0,s.jsx)("div",{className:"clawpress-card-subtitle",children:l})}):null,(0,s.jsx)("div",{className:"clawpress-card-section-content",children:(0,s.jsx)("div",{className:"clawpress-card-text",children:o})}),i.length>0?(0,s.jsx)("div",{className:"clawpress-card-section-buttons",children:(0,s.jsx)("div",{className:"clawpress-card-actions",children:i.map((e,t)=>(0,s.jsx)("button",{type:"button",className:"button button-small "+(0===t?"button-primary":"button-secondary"),onClick:()=>r?.(e),disabled:a,children:e.label},e.id))})}):null]})})},v=({card:e,fallbackText:r,onSendAction:a,isBusy:n=!1})=>{if(!e||"object"!=typeof e)return(0,s.jsx)("div",{className:"clawpress-msg-content",children:r||""});switch(e.type){case"welcome":return(0,s.jsx)(w,{card:e,onSendAction:a,isBusy:n});case"setup":return(0,s.jsx)(b,{card:e,onSendAction:a,isBusy:n});case"error":return(0,s.jsx)(x,{card:e});case"user_confirmation":return(0,s.jsx)(N,{card:e,onSendAction:a,isBusy:n})}const l="string"==typeof e?.data?.title&&e.data.title.trim()?e.data.title:(0,t.__)("Card","clawpress"),o="string"==typeof e?.data?.message&&e.data.message.trim()?e.data.message:r||"",i="string"==typeof e?.data?.subtitle&&e.data.subtitle.trim()?e.data.subtitle:"",c=h(e);return o||0!==c.length?(0,s.jsx)("div",{className:"clawpress-card clawpress-card-generic",children:(0,s.jsxs)("div",{className:"clawpress-card-body",children:[l?(0,s.jsx)("div",{className:"clawpress-card-section-title",children:(0,s.jsx)("div",{className:"clawpress-card-title",children:l})}):null,i?(0,s.jsx)("div",{className:"clawpress-card-section-subtitle",children:(0,s.jsx)("div",{className:"clawpress-card-subtitle",children:i})}):null,o?(0,s.jsx)("div",{className:"clawpress-card-section-content",children:(0,s.jsx)("div",{className:"clawpress-card-text",children:o})}):null,c.length>0?(0,s.jsx)("div",{className:"clawpress-card-section-buttons",children:(0,s.jsx)("div",{className:"clawpress-card-actions",children:c.map(e=>(0,s.jsx)("button",{type:"button",className:"button button-secondary button-small",onClick:()=>a?.(e),disabled:n,children:e.label},e.id))})}):null]})}):(0,s.jsx)("div",{className:"clawpress-msg-content",children:r||""})},j=e=>"string"==typeof e&&e?e.split(/(`[^`\n]+`|\*\*[^*\n]+?\*\*)/g).map((e,t)=>{const r=e.match(/^\*\*([^*\n]+?)\*\*$/);if(r)return(0,s.jsx)("strong",{children:r[1]},`strong-${t}`);const a=e.match(/^`([^`\n]+)`$/);return a?(0,s.jsx)("code",{children:a[1]},`code-${t}`):e}):e||"",k=({messages:r,streaming:a,currentStreamText:n,waitingForResponse:l,toolDialogs:o,onRunToolDialog:i,onCancelToolDialog:c,onSendCardAction:u})=>{const d=(0,e.useRef)(null),p=new Set([(0,t.__)("I am still working on this","clawpress"),(0,t.__)("Yes, still working on it.","clawpress"),(0,t.__)("Ok, this is taking long now. Still on it.","clawpress"),(0,t.__)("Plot twist: still working on it. My keyboard is sweating.","clawpress"),(0,t.__)("At this point even my coffee is worried, but I am still on it.","clawpress")]),h=e=>"string"==typeof e&&p.has(e.trim());(0,e.useEffect)(()=>{const e=d.current;e&&(e.scrollTop=e.scrollHeight)},[r,o,a,n,l]);const w=o.filter(e=>"serial"===m(e.function?.name||"").concurrency&&"done"!==e.status&&"cancelled"!==e.status).sort((e,t)=>e.createdAt-t.createdAt),_=w[0]?.id||o[o.length-1]?.id||null,f=r.length-1,y=f>=0?r[f]:null,b=h(y?.content||""),x=[...r.map((e,t)=>({type:"message",createdAt:e.createdAt??0,messageIndex:t,data:e})),...o.map(e=>({type:"tool",createdAt:e.createdAt??0,data:e}))].sort((e,t)=>{const s=e.createdAt-t.createdAt;return 0!==s?s:"message"===e.type&&"message"===t.type?(e.messageIndex??0)-(t.messageIndex??0):"message"===e.type?-1:"message"===t.type?1:0});return(0,s.jsxs)("div",{className:"clawpress-messages",ref:d,children:[x.map(e=>"message"===e.type?(()=>{const r=e.data.content||"",n=h(r);if(n&&e.messageIndex!==f)return null;const o=n?"system":e.data.role,i="system"===o,c="assistant"===o,d=e.data.card&&"object"==typeof e.data.card,p=d&&e.messageIndex===f,m=/(\.\.\.|…)\s*$/.test(r),g=i&&m||n,w=g?r.replace(/\s*(\.\.\.|…)\s*$/,""):r;return(0,s.jsxs)("div",{className:`clawpress-msg clawpress-${o}`,children:[!i&&!c||n?null:(0,s.jsx)("div",{className:"clawpress-msg-label",children:i?(0,t.__)("System","clawpress"):(0,t.__)("AGENT","clawpress")}),d?(0,s.jsx)(v,{card:e.data.card,fallbackText:w,onSendAction:u,isBusy:a||l||!p}):(0,s.jsx)("div",{className:"clawpress-msg-content"+(g?" clawpress-thinking":""),children:j(w)})]},e.data.id||e.data.content)})():(0,s.jsx)(g,{toolDialog:e.data,isOpen:e.data.id===_,onRunTool:i,onCancel:c},e.data.id)),!l||n||b?null:(0,s.jsx)("div",{className:"clawpress-msg clawpress-system",children:(0,s.jsx)("div",{className:"clawpress-msg-content clawpress-thinking",children:(0,t.__)("Thinking","clawpress")})}),a&&n?(0,s.jsxs)("div",{className:"clawpress-msg clawpress-assistant",children:[(0,s.jsx)("div",{className:"clawpress-msg-label",children:(0,t.__)("AGENT","clawpress")}),(0,s.jsx)("div",{className:"clawpress-msg-content",children:j(n||"...")})]}):null]})},S=({onToggle:e})=>(0,s.jsx)("button",{className:"button button-primary clawpress-toggle",onClick:e,type:"button",children:(0,t.__)("ClawPress","clawpress")}),A=[(0,t.__)("I am still working on this","clawpress"),(0,t.__)("Yes, still working on it.","clawpress"),(0,t.__)("Ok, this is taking long now. Still on it.","clawpress"),(0,t.__)("Plot twist: still working on it. My keyboard is sweating.","clawpress"),(0,t.__)("At this point even my coffee is worried, but I am still on it.","clawpress")],E=new Set(["done","success","error","timeout","requires_confirmation","failed","cancelled","canceled"]),C=async({url:e,method:s="GET",nonce:r,body:a,signal:n})=>{const l=await fetch(e,{method:s,credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":r},body:a?JSON.stringify(a):void 0,signal:n}),o=await l.text();let i={};if(o)try{i=JSON.parse(o)}catch{i={message:o}}if(!l.ok){const e=i?.message||i?.error||(0,t.sprintf)(/* translators: %d: HTTP status code */ /* translators: %d: HTTP status code */ +(0,t._n)("%d change","%d changes",e.count,"clawpress"),e.count)})]},e.id))})]}),y?(0,s.jsxs)("div",{className:"clawpress-tool-result-actions",children:[(0,s.jsx)("button",{className:"button button-primary",type:"button",onClick:()=>N({search:u,replace:d,post_status:p,dry_run:!1}),children:(0,t.__)("Run","clawpress")}),(0,s.jsx)("button",{className:"button",type:"button",onClick:()=>n(r.id),children:(0,t.__)("Cancel","clawpress")})]}):null]})),(0,s.jsx)(i,{status:m,title:j,canRerun:w,policy:l,isOpen:o,error:g,showActions:!1,onRun:e=>{e.preventDefault(),e.stopPropagation(),N({search:u,replace:d,post_status:p,dry_run:!1})},onCancel:e=>{e.preventDefault(),e.stopPropagation(),n(r.id)},children:k})},policy:{concurrency:"serial",canRerun:!1}}},m=e=>p[e]?.policy||{},g=({toolDialog:e,isOpen:t,onRunTool:r,onCancel:a})=>{if(!e)return null;const n=o(e.function),l=e.function?.name||"",i=(e=>p[e]?.renderer||d)(l),c=m(l);return(0,s.jsx)(i,{toolName:l,args:n,toolDialog:e,runTool:r,onCancel:a,policy:c,isOpen:t})},h=e=>Array.isArray(e?.data?.actions)?e.data.actions.filter(e=>e&&"object"==typeof e).map((e,t)=>{const s="string"==typeof e.label?e.label.trim():"",r="string"==typeof e.type&&e.type.trim()?e.type.trim().toLowerCase():"send_prompt";if(!s)return null;if("open_url"===r){const r=[e.url,e.href].map(e=>"string"==typeof e?e.trim():"").find(e=>e.length>0);return r?{id:"string"==typeof e.id&&e.id.trim()?e.id.trim():`action-${t}`,label:s,type:"open_url",url:r}:null}if("run_tool"===r){const r=[e.tool,e.tool_name,e.name].map(e=>"string"==typeof e?e.trim():"").find(e=>e.length>0);if(!r)return null;let a={};if(e.args&&"object"==typeof e.args&&!Array.isArray(e.args))a=e.args;else if("string"==typeof e.arguments)try{const t=JSON.parse(e.arguments);t&&"object"==typeof t&&!Array.isArray(t)&&(a=t)}catch{a={}}else e.arguments&&"object"==typeof e.arguments&&!Array.isArray(e.arguments)&&(a=e.arguments);return{id:"string"==typeof e.id&&e.id.trim()?e.id.trim():`action-${t}`,label:s,type:"run_tool",tool:r,args:a}}const a=[e.prompt,e.message,e.command].map(e=>"string"==typeof e?e.trim():"").find(e=>e.length>0);return a?{id:"string"==typeof e.id&&e.id.trim()?e.id.trim():`action-${t}`,label:s,type:"send_prompt",prompt:a}:null}).filter(e=>Boolean(e)):[],w=({card:e,onSendAction:r,isBusy:a=!1})=>{const n="string"==typeof e?.data?.title&&e.data.title.trim()?e.data.title:(0,t.__)("Welcome to ClawPress","clawpress"),l="string"==typeof e?.data?.message&&e.data.message.trim()?e.data.message:(0,t.__)("Hello! I am ready to help with your WordPress tasks.","clawpress"),o="string"==typeof e?.data?.subtitle&&e.data.subtitle.trim()?e.data.subtitle:"",i="string"==typeof e?.data?.emoji&&e.data.emoji.trim()?e.data.emoji:"👋",c=h(e);return(0,s.jsx)("div",{className:"clawpress-card clawpress-card-welcome",children:(0,s.jsxs)("div",{className:"clawpress-card-body",children:[(0,s.jsx)("div",{className:"clawpress-card-section-title",children:(0,s.jsxs)("div",{className:"clawpress-card-welcome-title-line",children:[(0,s.jsx)("span",{className:"clawpress-card-emoji","aria-hidden":"true",children:i}),(0,s.jsx)("div",{className:"clawpress-card-title",children:n})]})}),o?(0,s.jsx)("div",{className:"clawpress-card-section-subtitle",children:(0,s.jsx)("div",{className:"clawpress-card-subtitle",children:o})}):null,(0,s.jsx)("div",{className:"clawpress-card-section-content",children:(0,s.jsx)("div",{className:"clawpress-card-text",children:l})}),c.length>0?(0,s.jsx)("div",{className:"clawpress-card-section-buttons",children:(0,s.jsx)("div",{className:"clawpress-card-actions",children:c.map(e=>(0,s.jsx)("button",{type:"button",className:"button button-secondary button-small",onClick:()=>r?.(e),disabled:a,children:e.label},e.id))})}):null]})})},y="__clawpress_custom_model__",_=(e,t)=>e.filter(e=>"send_prompt"===e?.type&&"string"==typeof e.prompt&&e.prompt.startsWith(t)).map(e=>({...e,value:e.prompt.slice(t.length).trim()})).filter(e=>e.value.length>0),f=e=>"string"!=typeof e?"":e.replace(/\r\n/g,"\n").replace(//gi,"\n"),b=({card:r,onSendAction:a,isBusy:n=!1})=>{const l=r?.data&&"object"==typeof r.data&&!Array.isArray(r.data)?r.data:{},o="string"==typeof l.title&&l.title.trim()?l.title:(0,t.__)("Setup Wizard","clawpress"),i="string"==typeof l.emoji&&l.emoji.trim()?l.emoji:"🧙",c="string"==typeof l.message&&l.message.trim()?f(l.message):(0,t.__)("Follow these steps to finish setup.","clawpress"),u="string"==typeof l.detail&&l.detail.trim()?l.detail:"",d="string"==typeof l.error&&l.error.trim()?l.error:"",p=f(u),m=f(d),g="string"==typeof l.step?l.step.trim():"",w="string"==typeof l.step_label&&l.step_label.trim()?l.step_label:g,b=Number.isFinite(Number(l.step_index))?Number(l.step_index):null,x=Number.isFinite(Number(l.step_total))?Number(l.step_total):null,N=Array.isArray(l.steps)?l.steps:[],v=h(r),j=(0,e.useMemo)(()=>_(v,"/setup provider "),[v]),k=(0,e.useMemo)(()=>_(v,"/setup model "),[v]),S="string"==typeof l.selected_model&&l.selected_model.trim()?l.selected_model.trim():"",A=(0,e.useMemo)(()=>"model"!==g?k:[...k,{id:y,label:(0,t.__)("Use Custom Model ID","clawpress"),type:"send_prompt",prompt:""}],[k,g]),E=(0,e.useMemo)(()=>{const e=new Set([...j.map(e=>e.id),...k.map(e=>e.id)]);return v.filter(t=>!e.has(t.id))},[v,j,k]),C=(0,e.useMemo)(()=>{const e=E.some(e=>"send_prompt"===e?.type&&"string"==typeof e.prompt&&"/setup back"===e.prompt.trim());return"provider"===g||e?E:[{id:"wizard-back-fallback",label:(0,t.__)("Back","clawpress"),type:"send_prompt",prompt:"/setup back"},...E]},[E,g]),[R,M]=(0,e.useState)(j[0]?.id||""),[$,T]=(0,e.useState)(A[0]?.id||""),[P,D]=(0,e.useState)("");(0,e.useEffect)(()=>{const e=j.map(e=>e.id);0!==e.length?e.includes(R)||M(e[0]):M("")},[j,R]),(0,e.useEffect)(()=>{const e=A.map(e=>e.id);0!==e.length?e.includes($)||T(e[0]):T("")},[A,$]),(0,e.useEffect)(()=>{if("model"!==g||!S)return;const e=k.find(e=>e.value===S);e?T(e.id):(T(y),D(S))},[k,S,g]);const q=j.find(e=>e.id===R)||j[0]||null,I=k.find(e=>e.id===$)||k[0]||null,L="model"===g&&$===y,F=P.trim(),O="string"==typeof l.settings_url&&l.settings_url.trim()?l.settings_url.trim():"",B=(0,e.useMemo)(()=>{if(!O||"undefined"==typeof window)return!1;try{const e=new URL(window.location.href),t=new URL(O,window.location.origin);return e.pathname===t.pathname&&e.search===t.search}catch{return!1}},[O]),U=b&&x?(0,t.sprintf)(/* translators: 1: current step number, 2: total step count, 3: step label. */ /* translators: 1: current step number, 2: total step count, 3: step label. */ +(0,t.__)("Step %1$d OF %2$d : %3$s","clawpress"),b,x,w||""):"",W="workspace"===g&&"string"==typeof l.workspace_path&&l.workspace_path.trim()?(e=>{if("string"!=typeof e)return"";const t=e.trim().replace(/\\/g,"/");if(!t)return"";const s=t.indexOf("/wp-content");return-1===s?t:t.slice(s)})(l.workspace_path):"",H="workspace"!==g?"":"string"==typeof l.workspace_exists&&l.workspace_exists.trim()?l.workspace_exists.trim():"string"==typeof l.workspace_exists_line&&l.workspace_exists_line.trim()?l.workspace_exists_line.replace(/^Exists\s*:\s*/i,"").trim():"",J="provider"===g&&j.length>1,z="model"===g&&A.length>0,G="provider"===g&&1===j.length,K="model"===g&&!z&&1===k.length,X=G||K||C.length>0,V=J||z||X;let Z="";Z=L?F?`/setup model ${F}`:"":I?.prompt||"";const Y="provider"===g?q?.prompt||"":Z,Q=n||"provider"===g&&!q||"model"===g&&(L?0===F.length:!I);return(0,s.jsx)("div",{className:"clawpress-card clawpress-card-setup",children:(0,s.jsxs)("div",{className:"clawpress-card-body",children:[(0,s.jsx)("div",{className:"clawpress-card-section-title",children:(0,s.jsxs)("div",{className:"clawpress-card-setup-title-line",children:[(0,s.jsx)("span",{className:"clawpress-card-setup-emoji","aria-hidden":"true",children:i}),(0,s.jsx)("div",{className:"clawpress-card-title",children:o})]})}),U||N.length>0?(0,s.jsxs)("div",{className:"clawpress-card-section-subtitle",children:[U?(0,s.jsx)("div",{className:"clawpress-card-subtitle clawpress-card-setup-step-line",children:U}):null,N.length>0?(0,s.jsx)("div",{className:"clawpress-card-setup-progress","aria-hidden":"true",children:N.map((e,t)=>{const r="string"==typeof e?.status?e.status:"pending",a=["pending","done","current","completed"].includes(r)?r:"pending";return(0,s.jsxs)("div",{className:`clawpress-card-setup-progress-item clawpress-card-setup-progress-${a}`,children:[(0,s.jsx)("span",{className:"clawpress-card-setup-progress-dot",children:t+1}),tM(e.target.value),disabled:n,children:j.map(e=>(0,s.jsx)("option",{value:e.id,children:e.label},e.id))}):null,z?(0,s.jsx)("select",{className:"clawpress-card-setup-select",value:$,onChange:e=>T(e.target.value),disabled:n,children:A.map(e=>(0,s.jsx)("option",{value:e.id,children:e.label},e.id))}):null,L?(0,s.jsx)("input",{type:"text",className:"clawpress-card-setup-input",value:P,onChange:e=>D(e.target.value),placeholder:(0,t.__)("Enter custom model ID","clawpress"),disabled:n}):null,(0,s.jsx)("button",{type:"button",className:"button button-primary button-small",disabled:Q,onClick:()=>a?.(Y),children:"provider"===g?(0,t.__)("Use Selected Provider","clawpress"):(0,t.__)("Use Selected Model","clawpress")})]}):null,X?(0,s.jsxs)("div",{className:"clawpress-card-actions",children:[G?(0,s.jsx)("button",{type:"button",className:"button button-primary button-small",onClick:()=>a?.(j[0]),disabled:n,children:j[0].label}):null,K?(0,s.jsx)("button",{type:"button",className:"button button-primary button-small",onClick:()=>a?.(k[0]),disabled:n,children:k[0].label}):null,C.map(e=>(0,s.jsx)("button",{type:"button",className:"button button-secondary button-small",onClick:()=>a?.(e),disabled:n,children:e.label},e.id))]}):null]}):null]})})},x=({card:e})=>{const r="string"==typeof e?.data?.title&&e.data.title.trim()?e.data.title:(0,t.__)("Request Error","clawpress"),a="string"==typeof e?.data?.message&&e.data.message.trim()?e.data.message:(0,t.__)("An unknown error occurred.","clawpress"),n="string"==typeof e?.data?.subtitle&&e.data.subtitle.trim()?e.data.subtitle:"";return(0,s.jsx)("div",{className:"clawpress-card clawpress-card-error",role:"alert","aria-live":"polite",children:(0,s.jsxs)("div",{className:"clawpress-card-body",children:[(0,s.jsx)("div",{className:"clawpress-card-section-title",children:(0,s.jsx)("div",{className:"clawpress-card-title",children:r})}),n?(0,s.jsx)("div",{className:"clawpress-card-section-subtitle",children:(0,s.jsx)("div",{className:"clawpress-card-subtitle",children:n})}):null,(0,s.jsx)("div",{className:"clawpress-card-section-content",children:(0,s.jsx)("div",{className:"clawpress-card-text",children:a})})]})})},N=({card:e,onSendAction:r,isBusy:a=!1})=>{const n="string"==typeof e?.data?.title&&e.data.title.trim()?e.data.title:(0,t.__)("User Confirmation Required","clawpress"),l="string"==typeof e?.data?.subtitle&&e.data.subtitle.trim()?e.data.subtitle:(0,t.__)("Destructive action pending","clawpress"),o="string"==typeof e?.data?.message&&e.data.message.trim()?e.data.message:(0,t.__)("Please confirm or decline this action.","clawpress"),i=h(e);return(0,s.jsx)("div",{className:"clawpress-card clawpress-card-user-confirmation",role:"alert","aria-live":"polite",children:(0,s.jsxs)("div",{className:"clawpress-card-body",children:[(0,s.jsx)("div",{className:"clawpress-card-section-title",children:(0,s.jsx)("div",{className:"clawpress-card-title",children:n})}),l?(0,s.jsx)("div",{className:"clawpress-card-section-subtitle",children:(0,s.jsx)("div",{className:"clawpress-card-subtitle",children:l})}):null,(0,s.jsx)("div",{className:"clawpress-card-section-content",children:(0,s.jsx)("div",{className:"clawpress-card-text",children:o})}),i.length>0?(0,s.jsx)("div",{className:"clawpress-card-section-buttons",children:(0,s.jsx)("div",{className:"clawpress-card-actions",children:i.map((e,t)=>(0,s.jsx)("button",{type:"button",className:"button button-small "+(0===t?"button-primary":"button-secondary"),onClick:()=>r?.(e),disabled:a,children:e.label},e.id))})}):null]})})},v=({card:e,fallbackText:r,onSendAction:a,isBusy:n=!1})=>{if(!e||"object"!=typeof e)return(0,s.jsx)("div",{className:"clawpress-msg-content",children:r||""});switch(e.type){case"welcome":return(0,s.jsx)(w,{card:e,onSendAction:a,isBusy:n});case"setup":return(0,s.jsx)(b,{card:e,onSendAction:a,isBusy:n});case"error":return(0,s.jsx)(x,{card:e});case"user_confirmation":return(0,s.jsx)(N,{card:e,onSendAction:a,isBusy:n})}const l="string"==typeof e?.data?.title&&e.data.title.trim()?e.data.title:(0,t.__)("Card","clawpress"),o="string"==typeof e?.data?.message&&e.data.message.trim()?e.data.message:r||"",i="string"==typeof e?.data?.subtitle&&e.data.subtitle.trim()?e.data.subtitle:"",c=h(e);return o||0!==c.length?(0,s.jsx)("div",{className:"clawpress-card clawpress-card-generic",children:(0,s.jsxs)("div",{className:"clawpress-card-body",children:[l?(0,s.jsx)("div",{className:"clawpress-card-section-title",children:(0,s.jsx)("div",{className:"clawpress-card-title",children:l})}):null,i?(0,s.jsx)("div",{className:"clawpress-card-section-subtitle",children:(0,s.jsx)("div",{className:"clawpress-card-subtitle",children:i})}):null,o?(0,s.jsx)("div",{className:"clawpress-card-section-content",children:(0,s.jsx)("div",{className:"clawpress-card-text",children:o})}):null,c.length>0?(0,s.jsx)("div",{className:"clawpress-card-section-buttons",children:(0,s.jsx)("div",{className:"clawpress-card-actions",children:c.map(e=>(0,s.jsx)("button",{type:"button",className:"button button-secondary button-small",onClick:()=>a?.(e),disabled:n,children:e.label},e.id))})}):null]})}):(0,s.jsx)("div",{className:"clawpress-msg-content",children:r||""})},j=e=>"string"==typeof e&&e?e.split(/(`[^`\n]+`|\*\*[^*\n]+?\*\*)/g).map((e,t)=>{const r=e.match(/^\*\*([^*\n]+?)\*\*$/);if(r)return(0,s.jsx)("strong",{children:r[1]},`strong-${t}`);const a=e.match(/^`([^`\n]+)`$/);return a?(0,s.jsx)("code",{children:a[1]},`code-${t}`):e}):e||"",k=({messages:r,streaming:a,currentStreamText:n,waitingForResponse:l,toolDialogs:o,onRunToolDialog:i,onCancelToolDialog:c,onSendCardAction:u})=>{const d=(0,e.useRef)(null),p=new Set([(0,t.__)("I am still working on this","clawpress"),(0,t.__)("Yes, still working on it.","clawpress"),(0,t.__)("Ok, this is taking long now. Still on it.","clawpress"),(0,t.__)("Plot twist: still working on it. My keyboard is sweating.","clawpress"),(0,t.__)("At this point even my coffee is worried, but I am still on it.","clawpress")]),h=e=>"string"==typeof e&&p.has(e.trim());(0,e.useEffect)(()=>{const e=d.current;e&&(e.scrollTop=e.scrollHeight)},[r,o,a,n,l]);const w=o.filter(e=>"serial"===m(e.function?.name||"").concurrency&&"done"!==e.status&&"cancelled"!==e.status).sort((e,t)=>e.createdAt-t.createdAt),y=w[0]?.id||o[o.length-1]?.id||null,_=r.length-1,f=_>=0?r[_]:null,b=h(f?.content||""),x=[...r.map((e,t)=>({type:"message",createdAt:e.createdAt??0,messageIndex:t,data:e})),...o.map(e=>({type:"tool",createdAt:e.createdAt??0,data:e}))].sort((e,t)=>{const s=e.createdAt-t.createdAt;return 0!==s?s:"message"===e.type&&"message"===t.type?(e.messageIndex??0)-(t.messageIndex??0):"message"===e.type?-1:"message"===t.type?1:0});return(0,s.jsxs)("div",{className:"clawpress-messages",ref:d,children:[x.map(e=>"message"===e.type?(()=>{const r=e.data.content||"",n=h(r);if(n&&e.messageIndex!==_)return null;const o=n?"system":e.data.role,i="system"===o,c="assistant"===o,d=e.data.card&&"object"==typeof e.data.card,p=d&&e.messageIndex===_,m=/(\.\.\.|…)\s*$/.test(r),g=i&&m||n,w=g?r.replace(/\s*(\.\.\.|…)\s*$/,""):r;return(0,s.jsxs)("div",{className:`clawpress-msg clawpress-${o}`,children:[!i&&!c||n?null:(0,s.jsx)("div",{className:"clawpress-msg-label",children:i?(0,t.__)("System","clawpress"):(0,t.__)("AGENT","clawpress")}),d?(0,s.jsx)(v,{card:e.data.card,fallbackText:w,onSendAction:u,isBusy:a||l||!p}):(0,s.jsx)("div",{className:"clawpress-msg-content"+(g?" clawpress-thinking":""),children:j(w)})]},e.data.id||e.data.content)})():(0,s.jsx)(g,{toolDialog:e.data,isOpen:e.data.id===y,onRunTool:i,onCancel:c},e.data.id)),!l||n||b?null:(0,s.jsx)("div",{className:"clawpress-msg clawpress-system",children:(0,s.jsx)("div",{className:"clawpress-msg-content clawpress-thinking",children:(0,t.__)("Thinking","clawpress")})}),a&&n?(0,s.jsxs)("div",{className:"clawpress-msg clawpress-assistant",children:[(0,s.jsx)("div",{className:"clawpress-msg-label",children:(0,t.__)("AGENT","clawpress")}),(0,s.jsx)("div",{className:"clawpress-msg-content",children:j(n||"...")})]}):null]})},S=({onToggle:e})=>(0,s.jsx)("button",{className:"button button-primary clawpress-toggle",onClick:e,type:"button",children:(0,t.__)("ClawPress","clawpress")}),A=[(0,t.__)("I am still working on this","clawpress"),(0,t.__)("Yes, still working on it.","clawpress"),(0,t.__)("Ok, this is taking long now. Still on it.","clawpress"),(0,t.__)("Plot twist: still working on it. My keyboard is sweating.","clawpress"),(0,t.__)("At this point even my coffee is worried, but I am still on it.","clawpress")],E=new Set(["done","success","error","timeout","requires_confirmation","failed","cancelled","canceled"]),C=async({url:e,method:s="GET",nonce:r,body:a,signal:n})=>{const l=await fetch(e,{method:s,credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":r},body:a?JSON.stringify(a):void 0,signal:n}),o=await l.text();let i={};if(o)try{i=JSON.parse(o)}catch{i={message:o}}if(!l.ok){const e=i?.message||i?.error||(0,t.sprintf)(/* translators: %d: HTTP status code */ /* translators: %d: HTTP status code */ (0,t.__)("Request failed (%d)","clawpress"),l.status);throw new Error(e)}return i},R=e=>Boolean(e)&&"object"==typeof e&&!Array.isArray(e),M=({mockEnabled:e,mockScenario:s,mockDelay:r,restBase:a,streamNonce:n,nonce:l,onEvent:o,onDone:i,onError:c})=>e?(({mockScenario:e,mockDelay:s,onEvent:r,onDone:a,onError:n})=>({stream:l=>(({prompt:e,mode:s="normal",delayMode:r="normal",onEvent:a,onDone:n,onError:l})=>{const o=[];let i=!1;const c="slow"===r?3e3:0,u=(e,t)=>{const s=setTimeout(()=>{i||e()},t+c);o.push(s)},d=(e,t)=>{i||a({type:e,payload:t})},p=()=>{i=!0,o.forEach(clearTimeout),n?.({aborted:!0})};if("infinite"===r)return{stop:p};if("error"===s)return u(()=>l?.({error:(0,t.__)("Mock error: something went wrong.","clawpress")}),300),u(()=>n?.({aborted:!1}),600),{stop:p};"tool"!==s&&"tool_error"!==s||(u(()=>d("tool_call",{function:{name:"update_posts_find_replace",arguments:"{}"}}),200),u(()=>d("tool_plan",{function:{name:"update_posts_find_replace",arguments:{search:"tool_error"===s?(0,t.sprintf)(/* translators: %s: mock text prefixed with ERROR to trigger an error path */ /* translators: %s: mock text prefixed with ERROR to trigger an error path */ (0,t.__)("ERROR: %s","clawpress"),(0,t.__)("Old Phrase","clawpress")):(0,t.__)("Old Phrase","clawpress"),replace:"tool_error"===s?(0,t.sprintf)(/* translators: %s: mock text prefixed with ERROR to trigger an error path */ /* translators: %s: mock text prefixed with ERROR to trigger an error path */ (0,t.__)("ERROR: %s","clawpress"),(0,t.__)("New Phrase","clawpress")):(0,t.__)("New Phrase","clawpress"),post_status:"tool_error"===s?"draft":"publish",dry_run:!0}}}),700));const m=(0,t.__)("Here is a longer mock response to help you test streaming behavior.","clawpress")+(0,t.__)("It should keep streaming long enough for you to press Stop and see the UI react.","clawpress")+(0,t.__)("We can include multiple sentences, line breaks, and a bit of variety.","clawpress")+(0,t.__)("Chunk one: The quick brown fox jumps over the lazy dog.","clawpress")+(0,t.__)("Chunk two: Sphinx of black quartz, judge my vow.","clawpress")+(0,t.__)("Chunk three: Pack my box with five dozen liquor jugs.","clawpress")+(0,t.__)("Final chunk: This should be enough to test canceling a long stream.","clawpress");let g=(0,t.sprintf)(/* translators: %s: user prompt */ /* translators: %s: user prompt */ -(0,t.__)('Here is a mock response for: "%s"',"clawpress"),e);"tool"===s||"tool_error"===s?g=(0,t.__)("I found a tool that can update posts. Here is the proposed change.","clawpress"):"long"===s&&(g=m);const h=g.split("");let w=150;return h.forEach(e=>{u(()=>d("delta",{text:e}),w),w+=12}),u(()=>n?.({aborted:!1}),w+200),{stop:p}})({prompt:l,mode:e,delayMode:s,onEvent:({type:e,payload:t})=>r(e,t),onDone:()=>a?.({aborted:!1}),onError:n}),runTool:(e,r)=>(async(e,s,{mockDelay:r="normal"}={})=>{if("infinite"===r)await new Promise(()=>{});else{const e="slow"===r?3e3:300;await new Promise(t=>setTimeout(t,e))}if(("string"==typeof s?.search?s.search:"").includes("ERROR"))throw new Error((0,t.__)("Mock tool error: execution failed.","clawpress"));return{step:{data:{result:{dry_run:!0,total:2,changed:[{id:123,title:(0,t.__)("Hello World","clawpress"),count:1},{id:456,title:(0,t.__)("Sample Post","clawpress"),count:2}],tool:e,args:s}}}}})(e,r,{mockDelay:s}),getHistory:async()=>({items:[]}),getStatus:async()=>({mode:"offline",provider:{id:null,configured:!1},model:{id:null,configured:!1},setup:{completed:!1},memory:{enabled:!1},agent_user:{id:null,configured:!1},suggestions:["/help","/clear","/status","/setup resume","/memory list","/site info","/tools list"]}),getPanelState:async()=>({open:!1,width:420,last_history_id:""}),setPanelState:async e=>({open:Boolean(e?.open),width:Number(e?.width)||420,last_history_id:"string"==typeof e?.last_history_id?e.last_history_id:""})}))({mockScenario:s,mockDelay:r,onEvent:o,onDone:i,onError:c}):(({restBase:e,nonce:s,onEvent:r,onDone:a,onError:n})=>{const l=(t,r)=>C({url:`${e}/agent/runs/${t}`,method:"GET",nonce:s,signal:r}),o=(t,r,a)=>C({url:`${e}/agent/runs/${t}/events?after=${r}&limit=100`,method:"GET",nonce:s,signal:a}),i=e=>new Promise((t,s)=>{const r=setTimeout(()=>{e?.removeEventListener("abort",a),t()},1e3),a=()=>{clearTimeout(r),e?.removeEventListener("abort",a);const t=new Error("Aborted");t.name="AbortError",s(t)};e?.addEventListener("abort",a,{once:!0})}),c=e=>{const t=Number.isFinite(Number(e))?Math.max(0,Number(e)):0,s=Math.min(Math.floor(t/8),A.length-1);return A[s]||A[0]},u=e=>{if(!e||"object"!=typeof e)return null;const t="string"==typeof e.type?e.type.trim():"";return t?{type:t,data:e.data&&"object"==typeof e.data&&!Array.isArray(e.data)?e.data:{}}:null},d=e=>{if(!e||"object"!=typeof e)return null;const t=e=>{const t=Number(e);return!Number.isFinite(t)||t<0?null:Math.round(t)},s=e=>{if(null==e)return null;const t=Number(e);return Number.isFinite(t)?Math.max(0,Math.min(100,Math.round(t))):null},r=t(e.prompt_tokens)??0,a=t(e.completion_tokens)??0,n=t(e.total_tokens)??0,l=t(e.used_tokens)??(r>0?r:n),o=t(e.context_window_tokens)??null;return 0===r&&0===a&&0===n&&0===l&&null===o?null:{promptTokens:r,completionTokens:a,totalTokens:n,usedTokens:l,contextWindowTokens:o,percentUsed:s(e.percent_used),percentLeft:s(e.percent_left),windowIsEstimated:"boolean"==typeof e.window_is_estimated?e.window_is_estimated:null}},p=e=>{if(!e||"object"!=typeof e)return null;const t=[e.name,e.tool_name,e.ability_name].map(e=>"string"==typeof e?e.trim():"").find(e=>e.length>0);if(!t)return null;let s="";"string"==typeof e.ability?s=e.ability.trim():"string"==typeof e.ability_name&&(s=e.ability_name.trim());const r=e.args&&"object"==typeof e.args&&!Array.isArray(e.args)?e.args:{},a=(e=>{const t="string"==typeof e?e.trim().toLowerCase():"";return"success"===t||"error"===t||"requires_confirmation"===t?t:"success"})(e.status);return{name:t,ability:s||null,args:r,status:a,message:("string"==typeof e.message&&e.message.trim()?e.message.trim():"")||null,round:Number.isFinite(Number(e.round))?Math.max(1,Math.round(Number(e.round))):1,sequence:Number.isFinite(Number(e.sequence))?Math.max(1,Math.round(Number(e.sequence))):1,requiresConfirmation:"boolean"==typeof e.requires_confirmation?e.requires_confirmation:"requires_confirmation"===a}},m=e=>"string"==typeof e?e.trim().toLowerCase():"",g=(e,t,s,a)=>{const n=(e=>{if(!e||"object"!=typeof e)return"";const t="string"==typeof e.name?e.name.trim():"";if(!t)return"";const s="string"==typeof e.status?e.status.trim().toLowerCase():"",r="string"==typeof e.message?e.message.trim():"";return`${t}|${s}|${Number.isFinite(Number(e.round))?Math.max(1,Math.round(Number(e.round))):1}|${Number.isFinite(Number(e.sequence))?Math.max(1,Math.round(Number(e.sequence))):1}|${r}`})(e);if(n&&a instanceof Set){if(a.has(n))return;a.add(n)}r("tool_call",{call:e,index:t,total:s})},h=(e,t)=>{if(!Array.isArray(e)||0===e.length)return;const s=e.filter(e=>R(e)&&"string"==typeof e.event_type&&"agent.tool_call"===e.event_type);if(s.length>0)return void s.forEach((e,r)=>{const a=R(e.payload)?e.payload:{},n=m(a.status);let l="tool_call";"string"==typeof a.tool_name&&a.tool_name.trim()?l=a.tool_name.trim():"string"==typeof a.ability_name&&a.ability_name.trim()&&(l=a.ability_name.trim());let o="success";"error"!==n&&"requires_confirmation"!==n||(o=n);const i="string"==typeof a.message&&a.message.trim()?a.message.trim():"",c=p({name:l,tool_name:a.tool_name,ability_name:a.ability_name,args:{},status:o,message:i||null,round:Number.isFinite(Number(a.round))?Math.max(1,Math.round(Number(a.round))):1,sequence:Number.isFinite(Number(a.sequence))?Math.max(1,Math.round(Number(a.sequence))):r+1,requires_confirmation:"requires_confirmation"===n});c&&g(c,r+1,s.length,t)});const r=e.filter(e=>R(e)&&"string"==typeof e.event_type&&"tool_call"===e.event_type);0!==r.length&&r.forEach((e,s)=>{const a=R(e.payload)?e.payload:{},n=m(a.status),l=R(a.result)?a.result:{},o=R(a.error)?a.error:{};let i="";"string"==typeof o.message&&o.message.trim()?i=o.message.trim():"string"==typeof l.message&&l.message.trim()&&(i=l.message.trim());let c="tool_call";"string"==typeof a.tool_name&&a.tool_name.trim()?c=a.tool_name.trim():"string"==typeof a.ability_name&&a.ability_name.trim()&&(c=a.ability_name.trim());let u="success";"error"!==n&&"requires_confirmation"!==n||(u=n);const d=p({name:c,tool_name:a.tool_name,ability_name:a.ability_name,args:{},status:u,message:i||null,round:Number.isFinite(Number(a.round))?Math.max(1,Math.round(Number(a.round))):1,sequence:Number.isFinite(Number(a.sequence))?Math.max(1,Math.round(Number(a.sequence))):s+1,requires_confirmation:"requires_confirmation"===n});d&&g(d,s+1,r.length,t)})},w=(e,t)=>{const s=R(e?.meta)?e.meta:{};let a=null;if(R(s.last_result)?a=s.last_result:R(s.result)&&(a=s.result),!a)return;const n=d(a.context);n&&r("context_usage",{context:n});const l=Array.isArray(a.tool_calls)?a.tool_calls.map(e=>p(e)).filter(Boolean):[];l.forEach((e,s)=>{g(e,s+1,l.length,t)})},_=(e,s,a)=>{const n=R(e?.meta)?e.meta:{};let l=null;if(R(n.result)?l=n.result:R(n.last_result)&&(l=n.last_result),((e,s,a)=>{if(!R(e))return!1;const n=d(e.context);n&&r("context_usage",{context:n});const l=Array.isArray(e.tool_calls)?e.tool_calls.map(e=>p(e)).filter(Boolean):[];if(l.forEach((e,t)=>{g(e,t+1,l.length,a)}),R(e.error)){const s=e.error,a="string"==typeof s.message&&s.message.trim()?s.message.trim():(0,t.__)("Run failed.","clawpress");return r("error",{error:a,type:"string"==typeof s.type&&s.type.trim()?s.type.trim():"provider",card:u(e.card)}),!0}const o="string"==typeof e.assistant_text?e.assistant_text.trim():"",i=u(e.card);return i?(r("response_card",{card:i,text:o,role:"assistant"}),!0):!(!o||o===s||(r("response_message",{text:o,role:"assistant"}),0))})(l,s,a))return;const o=m(e?.status);if("requires_confirmation"===o)return void r("response_message",{text:(0,t.__)("Action requires confirmation before continuing.","clawpress"),role:"assistant"});if("done"===o||"success"===o)return void r("response_message",{text:(0,t.__)("I finished the background steps, but I did not receive a final text response. Please tell me to continue and I will pick up from here.","clawpress"),role:"assistant"});const i="string"==typeof e?.error_message&&e.error_message.trim()?e.error_message.trim():(0,t.__)("Run ended with an error.","clawpress");r("error",{error:i,type:"timeout"===o?"timeout":"provider"})};return{stream:f=>{const y=new AbortController,b=new Set;return(async()=>{try{const n=await(x=f,N=y.signal,C({url:`${e}/chat/message`,method:"POST",nonce:s,body:{message:x},signal:N}));n?.meta?.command?.effects&&!0===n.meta.command.effects.clear_history&&r("history_reset",{}),Array.isArray(n?.meta?.suggestions)&&r("suggestions",{items:n.meta.suggestions});const v=d(n?.meta?.context);v&&r("context_usage",{context:v});const j=Array.isArray(n?.meta?.tool_calls)?n.meta.tool_calls.map(e=>p(e)).filter(Boolean):[];j.forEach((e,t)=>{g(e,t+1,j.length,b)});const k=n?.meta?.error&&"object"==typeof n.meta.error?n.meta.error:null,S=n?.meta?.card&&"object"==typeof n.meta.card?u(n.meta.card):null;if(k){const e="string"==typeof k.message&&k.message.trim()?k.message.trim():(0,t.__)("Chat request failed.","clawpress");return r("error",{error:e,type:"string"==typeof k.type&&k.type.trim()?k.type.trim():"provider",card:S}),void a?.({aborted:!1})}const A="string"==typeof n?.reply?n.reply.trim():"",R=Boolean(n?.meta?.command?.name),M=u(n?.meta?.card);M?r("response_card",{card:M,text:A,role:R?"system":"assistant"}):A&&r("response_message",{text:A,role:R?"system":"assistant"});const $=m(n?.meta?.status),T=Number(n?.meta?.run_id),D=Number(n?.meta?.events_cursor);"in_progress"===$&&(Number.isFinite(T)&&T>0?await(async({runId:e,initialReply:s,signal:a,initialEventsCursor:n=0,seenToolCallKeys:u=null})=>{let d=Number.isFinite(Number(n))?Math.max(0,Math.round(Number(n))):0;const p=Date.now();let g="";for(;;){if(a?.aborted){const e=new Error("Aborted");throw e.name="AbortError",e}const n=(Date.now()-p)/1e3;if(n>=180)return void r("error",{error:(0,t.__)("Run polling timed out before a terminal status was received.","clawpress"),type:"timeout"});try{const t=await o(e,d,a),s=Array.isArray(t?.events)?t.events:[];h(s,u);const r=Number(t?.next_cursor);Number.isFinite(r)&&r>0&&(d=Math.max(d,r))}catch(e){if("AbortError"===e?.name)throw e}const f=await l(e,a);w(f,u);const y=m(f?.status);if(E.has(y))return void _(f,s,u);const b=c(n);b&&b!==g&&(r("run_progress",{text:b}),g=b),await i(a)}})({runId:T,initialReply:A,signal:y.signal,initialEventsCursor:D,seenToolCallKeys:b}):r("error",{error:(0,t.__)("Run entered progress mode without a valid run ID.","clawpress"),type:"request"})),a?.({aborted:!1})}catch(e){if("AbortError"===e?.name)return void a?.({aborted:!0});n?.({error:e?.message||(0,t.__)("Chat request failed.","clawpress"),type:"request"}),a?.({aborted:!1})}var x,N})(),{stop:()=>y.abort()}},runTool:async()=>{throw new Error((0,t.__)("Tool execution is not available in chat mode.","clawpress"))},getHistory:()=>C({url:`${e}/chat/history`,method:"GET",nonce:s}),getStatus:()=>C({url:`${e}/status`,method:"GET",nonce:s}),getPanelState:()=>C({url:`${e}/panel/state`,method:"GET",nonce:s}),setPanelState:t=>C({url:`${e}/panel/state`,method:"POST",nonce:s,body:t})}})({restBase:a,streamNonce:n,nonce:l,onEvent:o,onDone:i,onError:c}),$=({mockEnabled:e,mockScenario:r,mockDelay:a,onSelectScenario:n,onSelectDelay:l,onRunScenario:o,themeMode:i})=>{if(!e)return null;const c=[{key:"normal",label:(0,t.__)("Normal","clawpress")},{key:"long",label:(0,t.__)("Long","clawpress")},{key:"tool",label:(0,t.__)("Tool","clawpress")},{key:"tool_error",label:(0,t.__)("Tool Error","clawpress")},{key:"error",label:(0,t.__)("Error","clawpress")}],u=[{key:"normal",label:(0,t.__)("Normal","clawpress")},{key:"slow",label:(0,t.__)("Slow (3s)","clawpress")},{key:"infinite",label:(0,t.__)("Infinite","clawpress")}];return(0,s.jsxs)("div",{className:"clawpress-mock-panel","data-theme":i,children:[(0,s.jsx)("div",{className:"clawpress-mock-badge",children:(0,t.__)("Mock","clawpress")}),(0,s.jsxs)("fieldset",{className:"clawpress-mock-fieldset",children:[(0,s.jsx)("legend",{children:(0,t.__)("Scenario","clawpress")}),(0,s.jsx)("div",{className:"clawpress-mock-buttons",children:c.map(e=>(0,s.jsx)("button",{className:"button "+(r===e.key?"button-primary":""),type:"button",onClick:()=>n(e.key),children:e.label},e.key))})]}),(0,s.jsxs)("fieldset",{className:"clawpress-mock-fieldset",children:[(0,s.jsx)("legend",{children:(0,t.__)("Response delay","clawpress")}),(0,s.jsx)("div",{className:"clawpress-mock-buttons",children:u.map(e=>(0,s.jsx)("button",{className:"button "+(a===e.key?"button-primary":""),type:"button",onClick:()=>l(e.key),children:e.label},e.key))})]}),(0,s.jsx)("button",{className:"button",type:"button",onClick:o,children:(0,t.__)("Run","clawpress")})]})},T=()=>{const[r,n]=(0,e.useState)(JSON.parse(localStorage.getItem("clawpress_open")||"false")),[i,c]=(0,e.useState)(Number(localStorage.getItem("clawpress_width")||CLAWPRESS_PANEL.defaultWidth)),[u,d]=(0,e.useState)([]),[p,g]=(0,e.useState)(""),[h,w]=(0,e.useState)(()=>{const e=localStorage.getItem("clawpress_input_history");if(!e)return[];try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(e=>"string"==typeof e):[]}catch{return[]}}),[_,f]=(0,e.useState)(-1),[y,b]=(0,e.useState)(""),[x,N]=(0,e.useState)(!1),[v,j]=(0,e.useState)(""),[A,E]=(0,e.useState)(!1),[C,R]=(0,e.useState)([]),[T,D]=(0,e.useState)([]),[P,q]=(0,e.useState)(!1),[I,L]=(0,e.useState)(null),[F,O]=(0,e.useState)(!0),[B,U]=(0,e.useState)([]),[W,H]=(0,e.useState)(null),[J,z]=(0,e.useState)(!1),G=Boolean(CLAWPRESS_PANEL.mockEnabled),[K,X]=(0,e.useState)(localStorage.getItem("clawpress_theme")||"light"),[V,Z]=(0,e.useState)("normal"),[Y,Q]=(0,e.useState)("normal"),[ee,te]=(0,e.useState)(!1),se=(0,e.useRef)(null),re=(0,e.useRef)(0),ae=(0,e.useRef)([]),ne=(0,e.useRef)(!1),le=(0,e.useRef)(null),oe=(0,e.useRef)(null),ie=(0,e.useRef)(!1),ce=(0,e.useRef)(null),ue=(0,e.useRef)(null),de=(0,e.useRef)(null),pe=(0,e.useRef)(null),me=(0,e.useRef)(v),ge=(0,e.useRef)(C);(0,e.useEffect)(()=>{me.current=v},[v]),(0,e.useEffect)(()=>{},[]),(0,e.useEffect)(()=>{ge.current=C},[C]);const he=(0,e.useRef)(!1),we=(0,e.useRef)(0),_e=(0,e.useRef)(i),fe=e=>{if(!e||"object"!=typeof e)return null;const t="string"==typeof e.type?e.type.trim():"";return t?{type:t,data:e.data&&"object"==typeof e.data&&!Array.isArray(e.data)?e.data:{}}:null},ye=(e,t,s=null)=>d(r=>[...r,{id:`msg-${Date.now()}-${Math.random()}`,role:e,content:t,card:fe(s),createdAt:++re.current}]),be=(e,s=null,r=null)=>{if(!e||"object"!=typeof e)return"";const a=[e.name,e.tool_name,e.ability_name].map(e=>"string"==typeof e?e.trim():"").find(e=>e.length>0);if(!a)return"";const n="string"==typeof e.status?e.status.trim().toLowerCase():"success",l="string"==typeof e.message?e.message.trim():"";let o=(0,t.__)("success","clawpress");"error"===n?o=(0,t.__)("error","clawpress"):"requires_confirmation"===n&&(o=(0,t.__)("confirmation required","clawpress"));let i=(0,t.sprintf)(/* translators: 1: tool name, 2: tool call status */ /* translators: 1: tool name, 2: tool call status */ +(0,t.__)('Here is a mock response for: "%s"',"clawpress"),e);"tool"===s||"tool_error"===s?g=(0,t.__)("I found a tool that can update posts. Here is the proposed change.","clawpress"):"long"===s&&(g=m);const h=g.split("");let w=150;return h.forEach(e=>{u(()=>d("delta",{text:e}),w),w+=12}),u(()=>n?.({aborted:!1}),w+200),{stop:p}})({prompt:l,mode:e,delayMode:s,onEvent:({type:e,payload:t})=>r(e,t),onDone:()=>a?.({aborted:!1}),onError:n}),runTool:(e,r)=>(async(e,s,{mockDelay:r="normal"}={})=>{if("infinite"===r)await new Promise(()=>{});else{const e="slow"===r?3e3:300;await new Promise(t=>setTimeout(t,e))}if(("string"==typeof s?.search?s.search:"").includes("ERROR"))throw new Error((0,t.__)("Mock tool error: execution failed.","clawpress"));return{step:{data:{result:{dry_run:!0,total:2,changed:[{id:123,title:(0,t.__)("Hello World","clawpress"),count:1},{id:456,title:(0,t.__)("Sample Post","clawpress"),count:2}],tool:e,args:s}}}}})(e,r,{mockDelay:s}),getHistory:async()=>({items:[]}),getStatus:async()=>({mode:"offline",provider:{id:null,configured:!1},model:{id:null,configured:!1},setup:{completed:!1},memory:{enabled:!1},agent_user:{id:null,configured:!1},suggestions:["/help","/clear","/status","/setup resume","/memory list","/site info","/tools list"]}),getPanelState:async()=>({open:!1,width:420,last_history_id:""}),setPanelState:async e=>({open:Boolean(e?.open),width:Number(e?.width)||420,last_history_id:"string"==typeof e?.last_history_id?e.last_history_id:""})}))({mockScenario:s,mockDelay:r,onEvent:o,onDone:i,onError:c}):(({restBase:e,nonce:s,streamNonce:r,onEvent:a,onDone:n,onError:l})=>{const o=e=>{const t=e.split("\n");let s="message";const r=[];if(t.forEach(e=>{e&&!e.startsWith(":")&&(e.startsWith("event:")?s=e.slice(6).trim()||s:e.startsWith("data:")&&r.push(e.slice(5).trimStart()))}),0===r.length)return null;try{return JSON.parse(r.join("\n"))}catch{return null}},i=(t,r)=>C({url:`${e}/agent/runs/${t}`,method:"GET",nonce:s,signal:r}),c=(t,r,a)=>C({url:`${e}/agent/runs/${t}/events?after=${r}&limit=100`,method:"GET",nonce:s,signal:a}),u=e=>new Promise((t,s)=>{const r=setTimeout(()=>{e?.removeEventListener("abort",a),t()},1e3),a=()=>{clearTimeout(r),e?.removeEventListener("abort",a);const t=new Error("Aborted");t.name="AbortError",s(t)};e?.addEventListener("abort",a,{once:!0})}),d=e=>{const t=Number.isFinite(Number(e))?Math.max(0,Number(e)):0,s=Math.min(Math.floor(t/8),A.length-1);return A[s]||A[0]},p=e=>{if(!e||"object"!=typeof e)return null;const t="string"==typeof e.type?e.type.trim():"";return t?{type:t,data:e.data&&"object"==typeof e.data&&!Array.isArray(e.data)?e.data:{}}:null},m=e=>{if(!e||"object"!=typeof e)return null;const t=e=>{const t=Number(e);return!Number.isFinite(t)||t<0?null:Math.round(t)},s=e=>{if(null==e)return null;const t=Number(e);return Number.isFinite(t)?Math.max(0,Math.min(100,Math.round(t))):null},r=t(e.prompt_tokens)??0,a=t(e.completion_tokens)??0,n=t(e.total_tokens)??0,l=t(e.used_tokens)??(r>0?r:n),o=t(e.context_window_tokens)??null;return 0===r&&0===a&&0===n&&0===l&&null===o?null:{promptTokens:r,completionTokens:a,totalTokens:n,usedTokens:l,contextWindowTokens:o,percentUsed:s(e.percent_used),percentLeft:s(e.percent_left),windowIsEstimated:"boolean"==typeof e.window_is_estimated?e.window_is_estimated:null}},g=e=>{if(!e||"object"!=typeof e)return null;const t=[e.name,e.tool_name,e.ability_name].map(e=>"string"==typeof e?e.trim():"").find(e=>e.length>0);if(!t)return null;let s="";"string"==typeof e.ability?s=e.ability.trim():"string"==typeof e.ability_name&&(s=e.ability_name.trim());const r=e.args&&"object"==typeof e.args&&!Array.isArray(e.args)?e.args:{},a=(e=>{const t="string"==typeof e?e.trim().toLowerCase():"";return"success"===t||"error"===t||"requires_confirmation"===t?t:"success"})(e.status);return{name:t,ability:s||null,args:r,status:a,message:("string"==typeof e.message&&e.message.trim()?e.message.trim():"")||null,round:Number.isFinite(Number(e.round))?Math.max(1,Math.round(Number(e.round))):1,sequence:Number.isFinite(Number(e.sequence))?Math.max(1,Math.round(Number(e.sequence))):1,requiresConfirmation:"boolean"==typeof e.requires_confirmation?e.requires_confirmation:"requires_confirmation"===a}},h=e=>"string"==typeof e?e.trim().toLowerCase():"",w=(e,t,s,r)=>{const n=(e=>{if(!e||"object"!=typeof e)return"";const t="string"==typeof e.name?e.name.trim():"";if(!t)return"";const s="string"==typeof e.status?e.status.trim().toLowerCase():"",r="string"==typeof e.message?e.message.trim():"";return`${t}|${s}|${Number.isFinite(Number(e.round))?Math.max(1,Math.round(Number(e.round))):1}|${Number.isFinite(Number(e.sequence))?Math.max(1,Math.round(Number(e.sequence))):1}|${r}`})(e);if(n&&r instanceof Set){if(r.has(n))return;r.add(n)}a("tool_call",{call:e,index:t,total:s})},y=(e,t)=>{if(!Array.isArray(e)||0===e.length)return;const s=e.filter(e=>R(e)&&"string"==typeof e.event_type&&"agent.tool_call"===e.event_type);if(s.length>0)return void s.forEach((e,r)=>{const a=R(e.payload)?e.payload:{},n=h(a.status);let l="tool_call";"string"==typeof a.tool_name&&a.tool_name.trim()?l=a.tool_name.trim():"string"==typeof a.ability_name&&a.ability_name.trim()&&(l=a.ability_name.trim());let o="success";"error"!==n&&"requires_confirmation"!==n||(o=n);const i="string"==typeof a.message&&a.message.trim()?a.message.trim():"",c=g({name:l,tool_name:a.tool_name,ability_name:a.ability_name,args:{},status:o,message:i||null,round:Number.isFinite(Number(a.round))?Math.max(1,Math.round(Number(a.round))):1,sequence:Number.isFinite(Number(a.sequence))?Math.max(1,Math.round(Number(a.sequence))):r+1,requires_confirmation:"requires_confirmation"===n});c&&w(c,r+1,s.length,t)});const r=e.filter(e=>R(e)&&"string"==typeof e.event_type&&"tool_call"===e.event_type);0!==r.length&&r.forEach((e,s)=>{const a=R(e.payload)?e.payload:{},n=h(a.status),l=R(a.result)?a.result:{},o=R(a.error)?a.error:{};let i="";"string"==typeof o.message&&o.message.trim()?i=o.message.trim():"string"==typeof l.message&&l.message.trim()&&(i=l.message.trim());let c="tool_call";"string"==typeof a.tool_name&&a.tool_name.trim()?c=a.tool_name.trim():"string"==typeof a.ability_name&&a.ability_name.trim()&&(c=a.ability_name.trim());let u="success";"error"!==n&&"requires_confirmation"!==n||(u=n);const d=g({name:c,tool_name:a.tool_name,ability_name:a.ability_name,args:{},status:u,message:i||null,round:Number.isFinite(Number(a.round))?Math.max(1,Math.round(Number(a.round))):1,sequence:Number.isFinite(Number(a.sequence))?Math.max(1,Math.round(Number(a.sequence))):s+1,requires_confirmation:"requires_confirmation"===n});d&&w(d,s+1,r.length,t)})},_=(e,t)=>{const s=R(e?.meta)?e.meta:{};let r=null;if(R(s.last_result)?r=s.last_result:R(s.result)&&(r=s.result),!r)return;const n=m(r.context);n&&a("context_usage",{context:n});const l=Array.isArray(r.tool_calls)?r.tool_calls.map(e=>g(e)).filter(Boolean):[];l.forEach((e,s)=>{w(e,s+1,l.length,t)})},f=(e,s,r)=>{const n=R(e?.meta)?e.meta:{};let l=null;if(R(n.result)?l=n.result:R(n.last_result)&&(l=n.last_result),((e,s,r)=>{if(!R(e))return!1;const n=m(e.context);n&&a("context_usage",{context:n});const l=Array.isArray(e.tool_calls)?e.tool_calls.map(e=>g(e)).filter(Boolean):[];if(l.forEach((e,t)=>{w(e,t+1,l.length,r)}),R(e.error)){const s=e.error,r="string"==typeof s.message&&s.message.trim()?s.message.trim():(0,t.__)("Run failed.","clawpress");return a("error",{error:r,type:"string"==typeof s.type&&s.type.trim()?s.type.trim():"provider",card:p(e.card)}),!0}const o="string"==typeof e.assistant_text?e.assistant_text.trim():"",i=p(e.card);return i?(a("response_card",{card:i,text:o,role:"assistant"}),!0):!(!o||o===s||(a("response_message",{text:o,role:"assistant"}),0))})(l,s,r))return;const o=h(e?.status);if("requires_confirmation"===o)return void a("response_message",{text:(0,t.__)("Action requires confirmation before continuing.","clawpress"),role:"assistant"});if("done"===o||"success"===o)return void a("response_message",{text:(0,t.__)("I finished the background steps, but I did not receive a final text response. Please tell me to continue and I will pick up from here.","clawpress"),role:"assistant"});const i="string"==typeof e?.error_message&&e.error_message.trim()?e.error_message.trim():(0,t.__)("Run ended with an error.","clawpress");a("error",{error:i,type:"timeout"===o?"timeout":"provider"})};return{stream:b=>{const x=new AbortController,N=new Set;return(async()=>{try{const l=await(async(n,l,i)=>{const c=await fetch(`${e}/chat/stream`,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":r||s,Accept:"text/event-stream"},body:JSON.stringify({message:n}),signal:l});if(!c.ok){const e=await c.text();let s=e;if(e)try{const t=JSON.parse(e);s=t?.message||t?.error||e}catch{s=e}throw new Error(s||(0,t.__)("Streaming request failed.","clawpress"))}const u=c.headers.get("content-type")||"";if(!c.body||!u.toLowerCase().includes("text/event-stream"))throw new Error((0,t.__)("Streaming endpoint returned a non-streaming response.","clawpress"));const d=c.body.getReader(),h=new TextDecoder;let y="",_="",f=null;const b=e=>{const s=e&&"string"==typeof e.type?e.type:"",r=e?.payload&&"object"==typeof e.payload?e.payload:{};switch(s){case"delta":"string"==typeof r.text&&r.text.length>0&&(_+=r.text,a("delta",{text:r.text}));break;case"tool_call":{const e=g(r.call);e&&w(e,Number.isFinite(Number(r.index))?Number(r.index):void 0,Number.isFinite(Number(r.total))?Number(r.total):void 0,i);break}case"history_reset":a("history_reset",{});break;case"suggestions":Array.isArray(r.items)&&a("suggestions",{items:r.items});break;case"context_usage":{const e=m(r.context);e&&a("context_usage",{context:e});break}case"response_card":{const e=p(r.card);e&&a("response_card",{card:e,text:"string"==typeof r.text?r.text:"",role:"system"===r.role?"system":"assistant"});break}case"response_message":"string"==typeof r.text&&r.text.trim()&&a("response_message",{text:r.text.trim(),role:"system"===r.role?"system":"assistant"});break;case"error":a("error",{error:"string"==typeof r.error&&r.error.trim()?r.error.trim():(0,t.__)("Chat request failed.","clawpress"),type:"string"==typeof r.type&&r.type.trim()?r.type.trim():"request",card:p(r.card)});break;case"in_progress":{const e=Number(r.run_id);Number.isFinite(e)&&e>0&&(f={runId:e,initialEventsCursor:Number(r.events_cursor),initialReply:_||("string"==typeof r.initial_reply?r.initial_reply.trim():"")});break}}};for(;;){const{done:e,value:t}=await d.read();y+=h.decode(t||new Uint8Array,{stream:!e});const s=y.split("\n\n");if(y=s.pop()||"",s.forEach(e=>{const t=o(e);t&&b(t)}),e){if(y.trim()){const e=o(y);e&&b(e)}break}}return{initialReply:_,continuation:f}})(b,x.signal,N);l?.continuation&&await(async({runId:e,initialReply:s,signal:r,initialEventsCursor:n=0,seenToolCallKeys:l=null})=>{let o=Number.isFinite(Number(n))?Math.max(0,Math.round(Number(n))):0;const p=Date.now();let m="";for(;;){if(r?.aborted){const e=new Error("Aborted");throw e.name="AbortError",e}const n=(Date.now()-p)/1e3;if(n>=180)return void a("error",{error:(0,t.__)("Run polling timed out before a terminal status was received.","clawpress"),type:"timeout"});try{const t=await c(e,o,r),s=Array.isArray(t?.events)?t.events:[];y(s,l);const a=Number(t?.next_cursor);Number.isFinite(a)&&a>0&&(o=Math.max(o,a))}catch(e){if("AbortError"===e?.name)throw e}const g=await i(e,r);_(g,l);const w=h(g?.status);if(E.has(w))return void f(g,s,l);const b=d(n);b&&b!==m&&(a("run_progress",{text:b}),m=b),await u(r)}})({runId:l.continuation.runId,initialReply:l.continuation.initialReply||"",signal:x.signal,initialEventsCursor:l.continuation.initialEventsCursor,seenToolCallKeys:N}),n?.({aborted:!1})}catch(e){if("AbortError"===e?.name)return void n?.({aborted:!0});l?.({error:e?.message||(0,t.__)("Chat request failed.","clawpress"),type:"request"}),n?.({aborted:!1})}})(),{stop:()=>x.abort()}},runTool:async()=>{throw new Error((0,t.__)("Tool execution is not available in chat mode.","clawpress"))},getHistory:()=>C({url:`${e}/chat/history`,method:"GET",nonce:s}),getStatus:()=>C({url:`${e}/status`,method:"GET",nonce:s}),getPanelState:()=>C({url:`${e}/panel/state`,method:"GET",nonce:s}),setPanelState:t=>C({url:`${e}/panel/state`,method:"POST",nonce:s,body:t})}})({restBase:a,streamNonce:n,nonce:l,onEvent:o,onDone:i,onError:c}),$=({mockEnabled:e,mockScenario:r,mockDelay:a,onSelectScenario:n,onSelectDelay:l,onRunScenario:o,themeMode:i})=>{if(!e)return null;const c=[{key:"normal",label:(0,t.__)("Normal","clawpress")},{key:"long",label:(0,t.__)("Long","clawpress")},{key:"tool",label:(0,t.__)("Tool","clawpress")},{key:"tool_error",label:(0,t.__)("Tool Error","clawpress")},{key:"error",label:(0,t.__)("Error","clawpress")}],u=[{key:"normal",label:(0,t.__)("Normal","clawpress")},{key:"slow",label:(0,t.__)("Slow (3s)","clawpress")},{key:"infinite",label:(0,t.__)("Infinite","clawpress")}];return(0,s.jsxs)("div",{className:"clawpress-mock-panel","data-theme":i,children:[(0,s.jsx)("div",{className:"clawpress-mock-badge",children:(0,t.__)("Mock","clawpress")}),(0,s.jsxs)("fieldset",{className:"clawpress-mock-fieldset",children:[(0,s.jsx)("legend",{children:(0,t.__)("Scenario","clawpress")}),(0,s.jsx)("div",{className:"clawpress-mock-buttons",children:c.map(e=>(0,s.jsx)("button",{className:"button "+(r===e.key?"button-primary":""),type:"button",onClick:()=>n(e.key),children:e.label},e.key))})]}),(0,s.jsxs)("fieldset",{className:"clawpress-mock-fieldset",children:[(0,s.jsx)("legend",{children:(0,t.__)("Response delay","clawpress")}),(0,s.jsx)("div",{className:"clawpress-mock-buttons",children:u.map(e=>(0,s.jsx)("button",{className:"button "+(a===e.key?"button-primary":""),type:"button",onClick:()=>l(e.key),children:e.label},e.key))})]}),(0,s.jsx)("button",{className:"button",type:"button",onClick:o,children:(0,t.__)("Run","clawpress")})]})},T=()=>{const[r,n]=(0,e.useState)(JSON.parse(localStorage.getItem("clawpress_open")||"false")),[i,c]=(0,e.useState)(Number(localStorage.getItem("clawpress_width")||CLAWPRESS_PANEL.defaultWidth)),[u,d]=(0,e.useState)([]),[p,g]=(0,e.useState)(""),[h,w]=(0,e.useState)(()=>{const e=localStorage.getItem("clawpress_input_history");if(!e)return[];try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(e=>"string"==typeof e):[]}catch{return[]}}),[y,_]=(0,e.useState)(-1),[f,b]=(0,e.useState)(""),[x,N]=(0,e.useState)(!1),[v,j]=(0,e.useState)(""),[A,E]=(0,e.useState)(!1),[C,R]=(0,e.useState)([]),[T,P]=(0,e.useState)([]),[D,q]=(0,e.useState)(!1),[I,L]=(0,e.useState)(null),[F,O]=(0,e.useState)(!0),[B,U]=(0,e.useState)([]),[W,H]=(0,e.useState)(null),[J,z]=(0,e.useState)(!1),G=Boolean(CLAWPRESS_PANEL.mockEnabled),[K,X]=(0,e.useState)(localStorage.getItem("clawpress_theme")||"light"),[V,Z]=(0,e.useState)("normal"),[Y,Q]=(0,e.useState)("normal"),[ee,te]=(0,e.useState)(!1),se=(0,e.useRef)(null),re=(0,e.useRef)(0),ae=(0,e.useRef)(null),ne=(0,e.useRef)(!1),le=(0,e.useRef)(null),oe=(0,e.useRef)(null),ie=(0,e.useRef)(null),ce=(0,e.useRef)(null),ue=(0,e.useRef)(v),de=(0,e.useRef)(C);(0,e.useEffect)(()=>{ue.current=v},[v]),(0,e.useEffect)(()=>{},[]),(0,e.useEffect)(()=>{de.current=C},[C]);const pe=(0,e.useRef)(!1),me=(0,e.useRef)(0),ge=(0,e.useRef)(i),he=e=>{if(!e||"object"!=typeof e)return null;const t="string"==typeof e.type?e.type.trim():"";return t?{type:t,data:e.data&&"object"==typeof e.data&&!Array.isArray(e.data)?e.data:{}}:null},we=(e,t,s=null)=>d(r=>[...r,{id:`msg-${Date.now()}-${Math.random()}`,role:e,content:t,card:he(s),createdAt:++re.current}]),ye=(e,s=null,r=null)=>{if(!e||"object"!=typeof e)return"";const a=[e.name,e.tool_name,e.ability_name].map(e=>"string"==typeof e?e.trim():"").find(e=>e.length>0);if(!a)return"";const n="string"==typeof e.status?e.status.trim().toLowerCase():"success",l="string"==typeof e.message?e.message.trim():"";let o=(0,t.__)("success","clawpress");"error"===n?o=(0,t.__)("error","clawpress"):"requires_confirmation"===n&&(o=(0,t.__)("confirmation required","clawpress"));let i=(0,t.sprintf)(/* translators: 1: tool name, 2: tool call status */ /* translators: 1: tool name, 2: tool call status */ (0,t.__)("Tool call `%1$s` (%2$s)","clawpress"),a,o);return Number.isFinite(Number(s))&&Number.isFinite(Number(r))&&(i=(0,t.sprintf)(/* translators: 1: tool call position, 2: total tool calls, 3: tool summary text */ /* translators: 1: tool call position, 2: total tool calls, 3: tool summary text */ -(0,t.__)("[%1$d/%2$d] %3$s","clawpress"),Math.max(1,Math.round(Number(s))),Math.max(1,Math.round(Number(r))),i)),l?`${i}\n${l}`:i},xe=e=>{if(!Array.isArray(e))return[];const t=new Set;return e.map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0).filter(e=>!t.has(e)&&(t.add(e),!0)).slice(0,8)},Ne=(0,e.useRef)(null),ve=e=>{const t="string"==typeof e&&e.trim()?e:"";if(!t)return;const s=Ne.current;if(s)return void d(e=>e.map(e=>e.id===s?{...e,content:t}:e));const r=`status-${Date.now()}-${Math.random()}`;Ne.current=r,d(e=>[...e,{id:r,role:"system",content:t,createdAt:++re.current}])},je=()=>{const e=Ne.current;e&&(d(t=>t.filter(t=>t.id!==e)),Ne.current=null)};(0,e.useEffect)(()=>localStorage.setItem("clawpress_open",JSON.stringify(r)),[r]),(0,e.useEffect)(()=>localStorage.setItem("clawpress_width",String(i)),[i]),(0,e.useEffect)(()=>{document.body.classList.toggle("clawpress-panel-open",r),document.body.style.setProperty("--clawpress-panel-width",`${i}px`);const e=document.querySelector("#wp-admin-bar-clawpress-toggle > a");e&&e.setAttribute("aria-expanded",r?"true":"false")},[r,i]),(0,e.useEffect)(()=>{localStorage.setItem("clawpress_theme",K)},[K]),(0,e.useEffect)(()=>{localStorage.setItem("clawpress_input_history",JSON.stringify(h))},[h]),(0,e.useEffect)(()=>{const e=e=>{if(!e.metaKey&&!e.ctrlKey||e.altKey||"k"!==e.key.toLowerCase())return;const t=e.target?.ownerDocument?.activeElement,s=t?.tagName?.toLowerCase();"input"===s||"textarea"===s||"select"===s||t?.isContentEditable||(e.preventDefault(),n(e=>!e))};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[]),(0,e.useEffect)(()=>{const e=e=>{if(!he.current)return;const t=we.current-e.clientX,s=Math.max(320,_e.current+t);c(s)},t=()=>{he.current&&(he.current=!1,document.body.classList.remove("clawpress-resizing"))};return window.addEventListener("mousemove",e),window.addEventListener("mouseup",t),()=>{window.removeEventListener("mousemove",e),window.removeEventListener("mouseup",t)}},[]);const ke=(e=null)=>{const t="string"==typeof e?e.trim():p.trim();t&&(g(""),w(e=>{const s=[...e,t],r=CLAWPRESS_PANEL.historyLimit??20;return s.length>r?s.slice(s.length-r):s}),f(-1),b(""),ye("user",t),E(!0),$e(t))},Se=e=>m(e).concurrency||"parallel",Ae=(e,t=[])=>{const s=e?.name||"",r="serial"===Se(s)&&t.some(e=>e.function?.name===s&&"done"!==e.status&&"error"!==e.status&&"cancelled"!==e.status);return{id:`tool-${Date.now()}-${Math.random()}`,function:e,args:o(e),status:r?"blocked":"idle",result:null,error:null,diff:null,createdAt:++re.current}},Ee=()=>{if(ne.current)return;const e=ae.current;if(!e.length)return;const{type:s,payload:r}=e.shift();"assistant_message"===s&&r?.content?(e=>{if(!e)return;ne.current=!0,j(""),me.current="";let t=0;const s=()=>{if(t>=e.length)return ye("assistant",e),j(""),me.current="",ne.current=!1,le.current=null,void Ee();const r=e.slice(t,t+2);t+=2,j(e=>{const t=e+r;return me.current=t,t}),le.current=setTimeout(s,30)};s()})(r.content):(((e,s)=>{switch(e){case"delta":if(je(),s.text){const e=`${me.current||""}${s.text}`;me.current=e,j(e)}break;case"response_message":je(),s?.text&&ye("system"===s?.role?"system":"assistant",s.text);break;case"response_card":je(),s?.card&&ye("system"===s?.role?"system":"assistant","string"==typeof s?.text?s.text:"",s.card);break;case"history_reset":je(),d([]),D([]),R([]),H(null),re.current=0;break;case"suggestions":{const e=xe(s?.items);U(e);break}case"context_usage":s?.context&&H(s.context);break;case"tool_call":if(s?.call&&"object"==typeof s.call){const e=be(s.call,s?.index,s?.total);e&&ye("system",e);break}P||(ve((0,t.__)("Preparing tool plan…","clawpress")),q(!0));break;case"tool_plan":s?.function&&"object"==typeof s.function&&R(e=>[...e,s]),q(!1);break;case"run_progress":ve("string"==typeof s?.text&&s.text.trim()?s.text:(0,t.__)("I am still working on this","clawpress"));break;case"error":je(),ye("system",s?.error||(0,t.__)("Stream error.","clawpress"),s?.card||((e,s="")=>{const r={timeout:(0,t.__)("Request timed out","clawpress"),request:(0,t.__)("Network or API request error","clawpress"),provider:(0,t.__)("Provider error","clawpress")},a="string"==typeof s&&r[s]?r[s]:(0,t.__)("Error","clawpress");return{type:"error",data:{title:(0,t.__)("Request Error","clawpress"),subtitle:a,message:"string"==typeof e&&e.trim()?e:(0,t.__)("Chat request failed.","clawpress")}}})(s?.error,s?.type));break;case"done":je(),(()=>{N(!1),se.current=null,E(!1),je();const e=me.current;e&&e.trim()?(ye("assistant",e),j(""),me.current=""):(j(""),me.current="");const t=Array.isArray(ge.current)?ge.current:[];t.length>0&&D(e=>{const s=[...e];return t.forEach(e=>{e?.function&&"object"==typeof e.function&&s.push(Ae(e.function,s))}),s}),R([]),Me(!0)})()}})(s,r),Ee())},Ce=(e,t)=>{E(!1),ae.current.push({type:e,payload:t}),Ee()},Re=()=>M({mockEnabled:G,mockScenario:V,mockDelay:Y,restBase:CLAWPRESS_PANEL.restBase,streamNonce:CLAWPRESS_PANEL.streamNonce,nonce:CLAWPRESS_PANEL.nonce,onEvent:Ce,onDone:()=>Ce("done",{}),onError:e=>Ce("error",e)}),Me=async(e=!1)=>{e||O(!0);try{const e=await(Re().getStatus?.());e&&(L(e),U(t=>t.length>0?t:xe(e?.suggestions)))}catch{L(null)}finally{O(!1)}};ce.current=ye,ue.current=e=>Array.isArray(e)?e.filter(e=>e&&"object"==typeof e).reduce((e,t,s)=>{const r="user"===t.role||"assistant"===t.role||"system"===t.role?t.role:"system",a="string"==typeof t.content?t.content:"",n=Number.isFinite(Number(t.createdAt))?Number(t.createdAt):s+1,l="string"==typeof t.id&&t.id?t.id:`history-${n}-${s}`,o=fe(t.card),i=Array.isArray(t.tool_calls)?t.tool_calls.filter(e=>e&&"object"==typeof e):[];return i.forEach((t,s)=>{const r=be(t,s+1,i.length);r&&e.push({id:`${l}-tool-${s+1}`,role:"system",content:r,card:null,createdAt:n})}),e.push({id:l,role:r,content:a,card:o,createdAt:n}),e},[]):[],de.current=Re,pe.current=Me,(0,e.useEffect)(()=>{let e=!0;return(async()=>{const s=de.current?.();if(s){try{const t=await(s.getPanelState?.());if(!e)return;const a=(r=t)&&"object"==typeof r?{open:"boolean"==typeof r.open?r.open:null,width:Number.isFinite(Number(r.width))?Number(r.width):null,lastHistoryId:"string"==typeof r.last_history_id?r.last_history_id:"",welcomeCardSeen:"boolean"==typeof r.welcome_card_seen&&r.welcome_card_seen}:{open:null,width:null,lastHistoryId:"",welcomeCardSeen:!1};"boolean"==typeof a.open&&n(a.open),Number.isFinite(a.width)&&a.width>0&&c(a.width),ie.current=!0===a.welcomeCardSeen}catch{}var r;try{const t=await(s.getStatus?.());if(!e)return;L(t||null),U(e=>e.length>0?e:xe(t?.suggestions))}catch{if(!e)return;L(null)}finally{e&&O(!1)}try{const r=await(s.getHistory?.());if(!e)return;const a=ue.current?.(r?.items||[])||[];if(0!==a.length||ie.current)d(a),re.current=a.reduce((e,t)=>Math.max(e,Number(t.createdAt)||0),0);else{const e=Date.now(),r={id:`welcome-${e}`,role:"assistant",content:"",card:{type:"welcome",data:{title:(0,t.__)("Welcome to ClawPress","clawpress"),message:(0,t.__)("Hello! I am ready to help with your WordPress tasks.","clawpress"),emoji:"👋",actions:[{id:"start-setup",label:(0,t.__)("Start Setup","clawpress"),prompt:"/setup start"}]}},createdAt:e};d([r]),re.current=e,s.setPanelState?.({welcome_card_seen:!0}).catch(()=>{})}}catch{if(!e)return;ce.current?.("system",(0,t.__)("Unable to load chat history.","clawpress"))}e&&z(!0)}})(),()=>{e=!1}},[]),(0,e.useEffect)(()=>{if(!r)return;pe.current?.(!0);const e=setInterval(()=>{pe.current?.(!0)},15e3);return()=>clearInterval(e)},[r]),(0,e.useEffect)(()=>{if(!J)return;const e=u[u.length-1],t={open:r,width:i,last_history_id:"string"==typeof e?.id?e.id:""};return oe.current&&clearTimeout(oe.current),oe.current=setTimeout(()=>{const e=de.current?.();e?.setPanelState?.(t).catch(()=>{})},350),()=>{oe.current&&clearTimeout(oe.current)}},[J,r,i,u]);const $e=async e=>{N(!0),j(""),me.current="",R([]);const t=Re();se.current=t.stream(e)},Te=(e,t)=>{D(s=>s.map(s=>s.id===e?{...s,...t}:s))};return(0,e.useEffect)(()=>{D(e=>{let t=!1;const s=e.map(s=>{const r=s.function?.name||"";if("serial"!==Se(r))return s;if("running"===s.status||"error"===s.status||"done"===s.status||"cancelled"===s.status)return s;const a=e.some(e=>e.function?.name===r&&e.createdAt{const e=document.querySelector("#wp-admin-bar-clawpress-toggle > a");if(!e)return void te(!0);te(!1);const t=e=>{e.preventDefault(),n(e=>!e)};return e.addEventListener("click",t),()=>e.removeEventListener("click",t)},[]),(0,s.jsxs)(e.Fragment,{children:[ee?(0,s.jsx)(S,{onToggle:()=>n(e=>!e)}):null,(0,s.jsxs)("div",{className:"clawpress-panel","data-theme":K,style:{width:`${i}px`},children:[(0,s.jsx)(a,{onClose:()=>n(!1),onToggleTheme:()=>X(e=>"light"===e?"dark":"light"),statusMode:I?.mode||"offline",statusLabel:(e=>{if(!e||"object"!=typeof e)return"";const t=e?.provider?.id,s=e?.model?.id;return t&&s?`${t} · ${s}`:t||""})(I),statusLoading:F}),(0,s.jsx)("button",{type:"button",className:"clawpress-drag-handle",onMouseDown:e=>{he.current=!0,we.current=e.clientX,_e.current=i,document.body.classList.add("clawpress-resizing")},"aria-label":(0,t.__)("Resize panel","clawpress")}),(0,s.jsx)(k,{messages:u,streaming:x,currentStreamText:v,waitingForResponse:A,toolDialogs:T,onRunToolDialog:(e,s)=>{const r=T.find(t=>t.id===e);if(!r)return;const a=r.function?.name||"";if("serial"===Se(a)){const e=T.filter(e=>e.function?.name===a).sort((e,t)=>e.createdAt-t.createdAt),t=e.some(e=>"running"===e.status),n=e.some(e=>e.id!==r.id&&e.createdAt{const s=e.function?.name||"",r=e.args||o(e.function);Te(e.id,{status:"running",error:null});try{const t=await(async(e,t)=>{try{return await Re().runTool(e,t)}catch(e){throw e}})(s,r),a=t.step?.data?.result??null,n=Array.isArray(a?.changed)?a.changed:null;Te(e.id,{status:"done",result:a,diff:n})}catch(s){Te(e.id,{status:"error",error:s?.message||(0,t.__)("Tool execution failed.","clawpress")})}})(s?{...r,args:s}:r)},onCancelToolDialog:e=>{Te(e,{status:"cancelled"})},onSendCardAction:e=>{if("string"==typeof e)return void ke(e);if(!e||"object"!=typeof e)return;if("open_url"===e.type){const s="string"==typeof e.url?e.url.trim():"";if(!s)return void ye("system",(0,t.__)("Invalid card action.","clawpress"));try{const e=new URL(s,window.location.origin);window.location.assign(e.toString())}catch{ye("system",(0,t.__)("Invalid card URL.","clawpress"))}return}if("run_tool"===e.type){const s="string"==typeof e.tool?e.tool.trim():"";if(!s)return void ye("system",(0,t.__)("Invalid card action.","clawpress"));const r=e.args&&"object"==typeof e.args&&!Array.isArray(e.args)?e.args:{};return void D(e=>{const t=[...e];return t.push(Ae({name:s,arguments:r},t)),t})}const s="string"==typeof e.prompt?e.prompt.trim():"";s&&ke(s)}}),(0,s.jsx)(l,{input:p,onInputChange:e=>g(e.target.value),onSend:ke,panelOpen:r,suggestions:B,contextUsage:W,onSendSuggestion:e=>ke(e),onStop:()=>{se.current?.stop?.(),ye("system",(0,t.__)("Stream stopped.","clawpress"))},streaming:x,onHistoryUp:e=>{if(0===h.length)return null;if(-1===_){b(e);const t=h.length-1;return f(t),h[t]}const t=Math.max(0,_-1);return f(t),h[t]},onHistoryDown:()=>{if(-1===_)return null;const e=_+1;if(e>=h.length){f(-1);const e=y;return b(""),e}return f(e),h[e]}})]}),(0,s.jsx)($,{mockEnabled:G,mockScenario:V,mockDelay:Y,onSelectScenario:Z,onSelectDelay:Q,onRunScenario:()=>{if(!G)return;g("");const e=(0,t.sprintf)(/* translators: %s: selected mock scenario */ /* translators: %s: selected mock scenario */ -(0,t.__)("Mock: %s","clawpress"),V);ye("user",e),$e(e)},themeMode:K})]})};let D=document.getElementById("clawpress-floating-panel-root");D||(D=document.createElement("div"),D.id="clawpress-floating-panel-root",document.body.appendChild(D)),(0,e.createRoot)(D).render((0,s.jsx)(T,{}))})(); \ No newline at end of file +(0,t.__)("[%1$d/%2$d] %3$s","clawpress"),Math.max(1,Math.round(Number(s))),Math.max(1,Math.round(Number(r))),i)),l?`${i}\n${l}`:i},_e=e=>{if(!Array.isArray(e))return[];const t=new Set;return e.map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0).filter(e=>!t.has(e)&&(t.add(e),!0)).slice(0,8)},fe=(0,e.useRef)(null),be=e=>{const t="string"==typeof e&&e.trim()?e:"";if(!t)return;const s=fe.current;if(s)return void d(e=>e.map(e=>e.id===s?{...e,content:t}:e));const r=`status-${Date.now()}-${Math.random()}`;fe.current=r,d(e=>[...e,{id:r,role:"system",content:t,createdAt:++re.current}])},xe=()=>{const e=fe.current;e&&(d(t=>t.filter(t=>t.id!==e)),fe.current=null)};(0,e.useEffect)(()=>localStorage.setItem("clawpress_open",JSON.stringify(r)),[r]),(0,e.useEffect)(()=>localStorage.setItem("clawpress_width",String(i)),[i]),(0,e.useEffect)(()=>{document.body.classList.toggle("clawpress-panel-open",r),document.body.style.setProperty("--clawpress-panel-width",`${i}px`);const e=document.querySelector("#wp-admin-bar-clawpress-toggle > a");e&&e.setAttribute("aria-expanded",r?"true":"false")},[r,i]),(0,e.useEffect)(()=>{localStorage.setItem("clawpress_theme",K)},[K]),(0,e.useEffect)(()=>{localStorage.setItem("clawpress_input_history",JSON.stringify(h))},[h]),(0,e.useEffect)(()=>{const e=e=>{if(!e.metaKey&&!e.ctrlKey||e.altKey||"k"!==e.key.toLowerCase())return;const t=e.target?.ownerDocument?.activeElement,s=t?.tagName?.toLowerCase();"input"===s||"textarea"===s||"select"===s||t?.isContentEditable||(e.preventDefault(),n(e=>!e))};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[]),(0,e.useEffect)(()=>{const e=e=>{if(!pe.current)return;const t=me.current-e.clientX,s=Math.max(320,ge.current+t);c(s)},t=()=>{pe.current&&(pe.current=!1,document.body.classList.remove("clawpress-resizing"))};return window.addEventListener("mousemove",e),window.addEventListener("mouseup",t),()=>{window.removeEventListener("mousemove",e),window.removeEventListener("mouseup",t)}},[]);const Ne=(e=null)=>{const t="string"==typeof e?e.trim():p.trim();t&&(g(""),w(e=>{const s=[...e,t],r=CLAWPRESS_PANEL.historyLimit??20;return s.length>r?s.slice(s.length-r):s}),_(-1),b(""),we("user",t),E(!0),Ce(t))},ve=e=>m(e).concurrency||"parallel",je=(e,t=[])=>{const s=e?.name||"",r="serial"===ve(s)&&t.some(e=>e.function?.name===s&&"done"!==e.status&&"error"!==e.status&&"cancelled"!==e.status);return{id:`tool-${Date.now()}-${Math.random()}`,function:e,args:o(e),status:r?"blocked":"idle",result:null,error:null,diff:null,createdAt:++re.current}},ke=(e,t={})=>{"suggestions"!==e&&"context_usage"!==e&&E(!1),Se(e,t)},Se=(e,s)=>{switch(e){case"delta":if(xe(),s.text){const e=`${ue.current||""}${s.text}`;ue.current=e,j(e)}break;case"response_message":xe(),s?.text&&we("system"===s?.role?"system":"assistant",s.text);break;case"response_card":if(xe(),s?.card){const e=ue.current||"";if("system"!==s?.role&&e.trim()){we("assistant",e,s.card),j(""),ue.current="";break}we("system"===s?.role?"system":"assistant","string"==typeof s?.text?s.text:"",s.card)}break;case"history_reset":xe(),d([]),P([]),R([]),H(null),re.current=0;break;case"suggestions":{const e=_e(s?.items);U(e);break}case"context_usage":s?.context&&H(s.context);break;case"tool_call":if(s?.call&&"object"==typeof s.call){const e=ye(s.call,s?.index,s?.total);e&&we("system",e);break}D||(be((0,t.__)("Preparing tool plan…","clawpress")),q(!0));break;case"tool_plan":s?.function&&"object"==typeof s.function&&R(e=>[...e,s]),q(!1);break;case"run_progress":be("string"==typeof s?.text&&s.text.trim()?s.text:(0,t.__)("I am still working on this","clawpress"));break;case"error":xe(),we("system",s?.error||(0,t.__)("Stream error.","clawpress"),s?.card||((e,s="")=>{const r={timeout:(0,t.__)("Request timed out","clawpress"),request:(0,t.__)("Network or API request error","clawpress"),provider:(0,t.__)("Provider error","clawpress")},a="string"==typeof s&&r[s]?r[s]:(0,t.__)("Error","clawpress");return{type:"error",data:{title:(0,t.__)("Request Error","clawpress"),subtitle:a,message:"string"==typeof e&&e.trim()?e:(0,t.__)("Chat request failed.","clawpress")}}})(s?.error,s?.type));break;case"done":xe(),(()=>{N(!1),se.current=null,E(!1),xe();const e=ue.current;e&&e.trim()?(we("assistant",e),j(""),ue.current=""):(j(""),ue.current="");const t=Array.isArray(de.current)?de.current:[];t.length>0&&P(e=>{const s=[...e];return t.forEach(e=>{e?.function&&"object"==typeof e.function&&s.push(je(e.function,s))}),s}),R([]),Ee(!0)})()}},Ae=()=>M({mockEnabled:G,mockScenario:V,mockDelay:Y,restBase:CLAWPRESS_PANEL.restBase,streamNonce:CLAWPRESS_PANEL.streamNonce,nonce:CLAWPRESS_PANEL.nonce,onEvent:ke,onDone:()=>ke("done",{}),onError:e=>ke("error",e)}),Ee=async(e=!1)=>{e||O(!0);try{const e=await(Ae().getStatus?.());e&&(L(e),U(t=>t.length>0?t:_e(e?.suggestions)))}catch{L(null)}finally{O(!1)}};le.current=we,oe.current=e=>Array.isArray(e)?e.filter(e=>e&&"object"==typeof e).reduce((e,t,s)=>{const r="user"===t.role||"assistant"===t.role||"system"===t.role?t.role:"system",a="string"==typeof t.content?t.content:"",n=Number.isFinite(Number(t.createdAt))?Number(t.createdAt):s+1,l="string"==typeof t.id&&t.id?t.id:`history-${n}-${s}`,o=he(t.card),i=Array.isArray(t.tool_calls)?t.tool_calls.filter(e=>e&&"object"==typeof e):[];return i.forEach((t,s)=>{const r=ye(t,s+1,i.length);r&&e.push({id:`${l}-tool-${s+1}`,role:"system",content:r,card:null,createdAt:n})}),e.push({id:l,role:r,content:a,card:o,createdAt:n}),e},[]):[],ie.current=Ae,ce.current=Ee,(0,e.useEffect)(()=>{let e=!0;return(async()=>{const s=ie.current?.();if(s){try{const t=await(s.getPanelState?.());if(!e)return;const a=(r=t)&&"object"==typeof r?{open:"boolean"==typeof r.open?r.open:null,width:Number.isFinite(Number(r.width))?Number(r.width):null,lastHistoryId:"string"==typeof r.last_history_id?r.last_history_id:"",welcomeCardSeen:"boolean"==typeof r.welcome_card_seen&&r.welcome_card_seen}:{open:null,width:null,lastHistoryId:"",welcomeCardSeen:!1};"boolean"==typeof a.open&&n(a.open),Number.isFinite(a.width)&&a.width>0&&c(a.width),ne.current=!0===a.welcomeCardSeen}catch{}var r;try{const t=await(s.getStatus?.());if(!e)return;L(t||null),U(e=>e.length>0?e:_e(t?.suggestions))}catch{if(!e)return;L(null)}finally{e&&O(!1)}try{const r=await(s.getHistory?.());if(!e)return;const a=oe.current?.(r?.items||[])||[];if(0!==a.length||ne.current)d(a),re.current=a.reduce((e,t)=>Math.max(e,Number(t.createdAt)||0),0);else{const e=Date.now(),r={id:`welcome-${e}`,role:"assistant",content:"",card:{type:"welcome",data:{title:(0,t.__)("Welcome to ClawPress","clawpress"),message:(0,t.__)("Hello! I am ready to help with your WordPress tasks.","clawpress"),emoji:"👋",actions:[{id:"start-setup",label:(0,t.__)("Start Setup","clawpress"),prompt:"/setup start"}]}},createdAt:e};d([r]),re.current=e,s.setPanelState?.({welcome_card_seen:!0}).catch(()=>{})}}catch{if(!e)return;le.current?.("system",(0,t.__)("Unable to load chat history.","clawpress"))}e&&z(!0)}})(),()=>{e=!1}},[]),(0,e.useEffect)(()=>{if(!r)return;ce.current?.(!0);const e=setInterval(()=>{ce.current?.(!0)},15e3);return()=>clearInterval(e)},[r]),(0,e.useEffect)(()=>{if(!J)return;const e=u[u.length-1],t={open:r,width:i,last_history_id:"string"==typeof e?.id?e.id:""};return ae.current&&clearTimeout(ae.current),ae.current=setTimeout(()=>{const e=ie.current?.();e?.setPanelState?.(t).catch(()=>{})},350),()=>{ae.current&&clearTimeout(ae.current)}},[J,r,i,u]);const Ce=async e=>{N(!0),j(""),ue.current="",R([]);const t=Ae();se.current=t.stream(e)},Re=(e,t)=>{P(s=>s.map(s=>s.id===e?{...s,...t}:s))};return(0,e.useEffect)(()=>{P(e=>{let t=!1;const s=e.map(s=>{const r=s.function?.name||"";if("serial"!==ve(r))return s;if("running"===s.status||"error"===s.status||"done"===s.status||"cancelled"===s.status)return s;const a=e.some(e=>e.function?.name===r&&e.createdAt{const e=document.querySelector("#wp-admin-bar-clawpress-toggle > a");if(!e)return void te(!0);te(!1);const t=e=>{e.preventDefault(),n(e=>!e)};return e.addEventListener("click",t),()=>e.removeEventListener("click",t)},[]),(0,s.jsxs)(e.Fragment,{children:[ee?(0,s.jsx)(S,{onToggle:()=>n(e=>!e)}):null,(0,s.jsxs)("div",{className:"clawpress-panel","data-theme":K,style:{width:`${i}px`},children:[(0,s.jsx)(a,{onClose:()=>n(!1),onToggleTheme:()=>X(e=>"light"===e?"dark":"light"),statusMode:I?.mode||"offline",statusLabel:(e=>{if(!e||"object"!=typeof e)return"";const t=e?.provider?.id,s=e?.model?.id;return t&&s?`${t} · ${s}`:t||""})(I),statusLoading:F}),(0,s.jsx)("button",{type:"button",className:"clawpress-drag-handle",onMouseDown:e=>{pe.current=!0,me.current=e.clientX,ge.current=i,document.body.classList.add("clawpress-resizing")},"aria-label":(0,t.__)("Resize panel","clawpress")}),(0,s.jsx)(k,{messages:u,streaming:x,currentStreamText:v,waitingForResponse:A,toolDialogs:T,onRunToolDialog:(e,s)=>{const r=T.find(t=>t.id===e);if(!r)return;const a=r.function?.name||"";if("serial"===ve(a)){const e=T.filter(e=>e.function?.name===a).sort((e,t)=>e.createdAt-t.createdAt),t=e.some(e=>"running"===e.status),n=e.some(e=>e.id!==r.id&&e.createdAt{const s=e.function?.name||"",r=e.args||o(e.function);Re(e.id,{status:"running",error:null});try{const t=await(async(e,t)=>{try{return await Ae().runTool(e,t)}catch(e){throw e}})(s,r),a=t.step?.data?.result??null,n=Array.isArray(a?.changed)?a.changed:null;Re(e.id,{status:"done",result:a,diff:n})}catch(s){Re(e.id,{status:"error",error:s?.message||(0,t.__)("Tool execution failed.","clawpress")})}})(s?{...r,args:s}:r)},onCancelToolDialog:e=>{Re(e,{status:"cancelled"})},onSendCardAction:e=>{if("string"==typeof e)return void Ne(e);if(!e||"object"!=typeof e)return;if("open_url"===e.type){const s="string"==typeof e.url?e.url.trim():"";if(!s)return void we("system",(0,t.__)("Invalid card action.","clawpress"));try{const e=new URL(s,window.location.origin);window.location.assign(e.toString())}catch{we("system",(0,t.__)("Invalid card URL.","clawpress"))}return}if("run_tool"===e.type){const s="string"==typeof e.tool?e.tool.trim():"";if(!s)return void we("system",(0,t.__)("Invalid card action.","clawpress"));const r=e.args&&"object"==typeof e.args&&!Array.isArray(e.args)?e.args:{};return void P(e=>{const t=[...e];return t.push(je({name:s,arguments:r},t)),t})}const s="string"==typeof e.prompt?e.prompt.trim():"";s&&Ne(s)}}),(0,s.jsx)(l,{input:p,onInputChange:e=>g(e.target.value),onSend:Ne,panelOpen:r,suggestions:B,contextUsage:W,onSendSuggestion:e=>Ne(e),onStop:()=>{se.current?.stop?.(),we("system",(0,t.__)("Stream stopped.","clawpress"))},streaming:x,onHistoryUp:e=>{if(0===h.length)return null;if(-1===y){b(e);const t=h.length-1;return _(t),h[t]}const t=Math.max(0,y-1);return _(t),h[t]},onHistoryDown:()=>{if(-1===y)return null;const e=y+1;if(e>=h.length){_(-1);const e=f;return b(""),e}return _(e),h[e]}})]}),(0,s.jsx)($,{mockEnabled:G,mockScenario:V,mockDelay:Y,onSelectScenario:Z,onSelectDelay:Q,onRunScenario:()=>{if(!G)return;g("");const e=(0,t.sprintf)(/* translators: %s: selected mock scenario */ /* translators: %s: selected mock scenario */ +(0,t.__)("Mock: %s","clawpress"),V);we("user",e),Ce(e)},themeMode:K})]})};let P=document.getElementById("clawpress-floating-panel-root");P||(P=document.createElement("div"),P.id="clawpress-floating-panel-root",document.body.appendChild(P)),(0,e.createRoot)(P).render((0,s.jsx)(T,{}))})(); \ No newline at end of file diff --git a/composer.json b/composer.json index a84517a..1f2893a 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,8 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=8.1", - "woocommerce/action-scheduler": "^3.9" + "woocommerce/action-scheduler": "^3.9", + "bradvin/wp-ai-client-streaming": "^0.1.1" }, "require-dev": { "wp-coding-standards/wpcs": "^3.0", @@ -20,15 +21,20 @@ "includes/functions.php" ] }, + "autoload-dev": { + "classmap": [ + "tests/Support/AiClientStubs.php" + ] + }, "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true } }, "scripts": { - "lint": "phpcs", - "lint:fix": "phpcbf", - "test": "vendor/bin/phpunit --configuration phpunit.xml.dist", - "test:coverage": "vendor/bin/phpunit --configuration phpunit.xml.dist --coverage-text" + "lint": "php -d auto_prepend_file=tests/Support/AiClientStubs.php vendor/bin/phpcs", + "lint:fix": "php -d auto_prepend_file=tests/Support/AiClientStubs.php vendor/bin/phpcbf", + "test": "php -d auto_prepend_file=tests/Support/AiClientStubs.php vendor/bin/phpunit --configuration phpunit.xml.dist", + "test:coverage": "php -d auto_prepend_file=tests/Support/AiClientStubs.php vendor/bin/phpunit --configuration phpunit.xml.dist --coverage-text" } } diff --git a/composer.lock b/composer.lock index 847a08c..2b49eb8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,60 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "03149318cc2c29186e7cb96a6484a810", + "content-hash": "fa33624e61f29e15ed17d85899b26e65", "packages": [ + { + "name": "bradvin/wp-ai-client-streaming", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/bradvin/wp-ai-client-streaming.git", + "reference": "2825b743aef690145236f9a8392fd3b1bcade796" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bradvin/wp-ai-client-streaming/zipball/2825b743aef690145236f9a8392fd3b1bcade796", + "reference": "2825b743aef690145236f9a8392fd3b1bcade796", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "suggest": { + "ext-curl": "Required for true incremental streaming; without cURL the package falls back to the stock HTTP client." + }, + "type": "library", + "autoload": { + "files": [ + "load.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "bradvin", + "homepage": "https://github.com/bradvin", + "role": "Developer" + } + ], + "description": "WordPress AI streaming adapter package that mirrors WordPress 7 AI integration patterns.", + "homepage": "https://github.com/bradvin/wp-ai-client-streaming", + "keywords": [ + "ai", + "composer", + "streaming", + "wordpress", + "wp-ai-client" + ], + "support": { + "issues": "https://github.com/bradvin/wp-ai-client-streaming/issues", + "source": "https://github.com/bradvin/wp-ai-client-streaming" + }, + "time": "2026-04-18T15:36:28+00:00" + }, { "name": "woocommerce/action-scheduler", "version": "3.9.3", diff --git a/docs/agent-loop-spec-final.md b/docs/agent-loop-spec-final.md index 51ac005..e2b56fb 100644 --- a/docs/agent-loop-spec-final.md +++ b/docs/agent-loop-spec-final.md @@ -1,7 +1,7 @@ ## Agent Loop Spec (Final) ## Summary -Refactor the current chat-bound agent execution into a reusable, transport-agnostic **Agent Loop runtime** that can be called from: +Refactor the current chat-bound agent execution into a reusable **Agent Loop runtime** that can be called from: 1. synchronous chat requests, 2. heartbeat/background jobs, diff --git a/docs/agent-loop.md b/docs/agent-loop.md index aa4ecc5..035fb0e 100644 --- a/docs/agent-loop.md +++ b/docs/agent-loop.md @@ -11,7 +11,7 @@ The runtime spans: - `Agent_Run_Controller`: REST API for creating, spawning, enqueueing, and polling runs. - `Agent_Run_Helper` + `Agent_Session_Helper`: lifecycle/state helpers. - `Agent_Run_Store` + `Agent_Session_Store` + `Agent_Event_Store`: persistence. -- `Polling_Transport` / `Null_Transport`: event delivery. +- `Agent_Event_Sink`: runtime event delivery to live callbacks and/or persisted event logs. Shared runtime utilities: @@ -177,7 +177,7 @@ Invokable object reflection uses `ReflectionMethod(__invoke)` and catches both ` ## Eventing -- `Polling_Transport` appends runtime events via `Agent_Event_Helper`. +- `Agent_Event_Sink` can append runtime events via `Agent_Event_Helper`. - Runner emits operational events (slice started/paused/completed). - Polling endpoint returns incremental cursor (`next_cursor`) for client-side streaming. diff --git a/docs/streaming-support.md b/docs/streaming-support.md index b16d358..19bf585 100644 --- a/docs/streaming-support.md +++ b/docs/streaming-support.md @@ -1,50 +1,38 @@ -# Streaming Support Plan +# Streaming Support Notes -This document explains how ClawPress will adopt true streaming once the WP AI Client exposes stable streaming APIs. +This document captures the simplified streaming shape now that ClawPress uses the WordPress streaming client package. ## Current State -ClawPress already has the runtime layering needed for streaming with minimal core changes: +ClawPress now streams the main chat panel through: -- Transport contract: `includes/transports/interface-agent-transport.php` -- Polling transport: `includes/transports/class-polling-transport.php` -- Null transport: `includes/transports/class-null-transport.php` -- Loop runtime integration point: `includes/helpers/class-agent-loop-helper.php` +- `includes/helpers/class-agent-loop-helper.php` +- `includes/transports/class-agent-event-sink.php` +- `includes/rest/class-chat-controller.php` +- `src/panel/services/realClient.js` -Today, `transport_mode=streaming` is accepted but intentionally routed through polling transport in `Agent_Loop_Helper::create_transport()`. This means there is no true live token streaming yet. +`transport_mode=streaming` now uses a single `Agent_Event_Sink` that can: + +- emit live SSE callback events, +- persist non-delta runtime events for polling/resume flows, +- keep high-frequency `agent.llm.delta` events out of the event log by default. ## Why This Is Low-Risk -The execution control plane is already decoupled from delivery transport: +The execution control plane remains decoupled from delivery details: - Run/session state machine lives in helpers/stores. -- Runner logic (claim, pause, retry, complete) is transport-agnostic. +- Runner logic (claim, pause, retry, complete) is delivery-mode agnostic. - Event persistence and polling APIs remain valid as fallback. -As a result, adding streaming should not require redesigning retries, leases, resumability, or DB schemas. - -## Minimal Changes Needed When WP AI Client Adds Streaming - -1. Add `Streaming_Transport` implementation. - - New file: `includes/transports/class-streaming-transport.php` - - Implement `Agent_Transport` (`emit()`, `close()`). - - Emit live deltas to the connected client channel (SSE/WebSocket). - - Optionally mirror selected events to `Agent_Event_Helper` for observability and fallback polling. +As a result, streaming support did not require redesigning retries, leases, resumability, or DB schemas. -2. Switch transport selection in loop runtime. - - Update `Agent_Loop_Helper::create_transport()` in `includes/helpers/class-agent-loop-helper.php`. - - Return `Streaming_Transport` for `transport_mode=streaming`. - - Keep polling as default/fallback. +## Current Design -3. Integrate streaming model-call path. - - Update model invocation in `includes/helpers/class-agent-loop-helper.php`. - - Consume WP AI Client chunk/delta callbacks. - - Emit incremental `agent.llm.delta`/`agent.llm.response` style events through transport. - - Preserve existing normalized `TurnResult` semantics at stream end. - -4. Add/extend delivery adapter endpoint. - - Add SSE/WebSocket endpoint in REST/controller layer for clients to subscribe to stream events. - - Keep `/agent/runs/{run_id}/events` polling endpoint as backup path. +1. `Chat_Controller` exposes `/chat/stream` and forwards live runtime events as SSE frames. +2. `Agent_Loop_Helper` enables provider streaming when `wp_ai_client_stream_prompt()` is available. +3. `Agent_Event_Sink` handles both immediate callback delivery and persisted run events. +4. Polling remains the fallback path for `in_progress` continuations and background slices. ## Components Expected To Stay Unchanged @@ -81,11 +69,10 @@ The final event should include enough metadata for the client to reconcile again ## Testing Guidance for Future Streaming Work -When implementing true streaming, add tests for: +Streaming-sensitive tests should cover: -- transport selection (`streaming` -> `Streaming_Transport`); +- event-sink selection for `transport_mode=streaming`; - chunk/delta emission ordering and finalization; - fallback behavior when streaming channel disconnects; - parity of final `TurnResult` fields between polling and streaming modes; -- runner/background behavior unaffected by streaming transport. - +- runner/background behavior unaffected by streaming delivery. diff --git a/includes/helpers/class-abilities-helper.php b/includes/helpers/class-abilities-helper.php index 9f14c96..14669d8 100644 --- a/includes/helpers/class-abilities-helper.php +++ b/includes/helpers/class-abilities-helper.php @@ -128,23 +128,218 @@ public function get_tool_declarations(): array { continue; } - $parameters = $ability->get_input_schema(); - if ( [] === $parameters ) { - $parameters = [ - 'type' => 'object', - 'properties' => new \stdClass(), - 'additionalProperties' => false, - ]; + $declarations[] = $this->normalize_function_declaration( + new FunctionDeclaration( + $tool_name, + (string) $ability->get_description(), + $this->normalize_tool_input_schema( $ability->get_input_schema() ) + ) + ); + } + + return $declarations; + } + + /** + * Normalize a function declaration for provider-safe JSON schema encoding. + * + * @param FunctionDeclaration $declaration Raw function declaration. + */ + public function normalize_function_declaration( FunctionDeclaration $declaration ): FunctionDeclaration { + $parameters = $declaration->getParameters(); + $normalized = null; + + if ( is_array( $parameters ) && ! $this->should_omit_function_parameters( $parameters ) ) { + $normalized = $this->normalize_tool_input_schema( $parameters ); + } + + return new FunctionDeclaration( $declaration->getName(), $declaration->getDescription(), $normalized ); + } + + /** + * Normalize one ability input schema for function-declaration compatibility. + * + * The AI client expects JSON-object positions like `properties` and + * `additionalProperties` to serialize as `{}` instead of `[]`. + * + * @param array $schema Raw ability schema. + * @return array + */ + private function normalize_tool_input_schema( array $schema ): array { + if ( [] === $schema ) { + return [ + 'type' => 'object', + 'properties' => new \stdClass(), + 'additionalProperties' => false, + ]; + } + + return $this->normalize_schema_node( $schema ); + } + + /** + * Determine whether a function declaration should omit `parameters`. + * + * For no-argument tools, OpenAI-compatible providers accept omitted + * `parameters`, which is safer than emitting an empty-object schema. + * + * @param array $schema Raw or normalized schema. + */ + private function should_omit_function_parameters( array $schema ): bool { + if ( [] === $schema ) { + return true; + } + + if ( ! isset( $schema['type'] ) || 'object' !== $schema['type'] ) { + return false; + } + + $required = $schema['required'] ?? []; + if ( is_array( $required ) && [] !== $required ) { + return false; + } + + if ( ! array_key_exists( 'properties', $schema ) ) { + return true; + } + + $properties = $schema['properties']; + + if ( $properties instanceof \stdClass ) { + return [] === get_object_vars( $properties ); + } + + return is_array( $properties ) && [] === $properties; + } + + /** + * Normalize one JSON schema node recursively. + * + * @param array $schema Raw schema node. + * @return array + */ + private function normalize_schema_node( array $schema ): array { + if ( + isset( $schema['type'], $schema['default'] ) && + 'object' === $schema['type'] && + is_array( $schema['default'] ) && + [] === $schema['default'] + ) { + $schema['default'] = (object) []; + } + + foreach ( [ 'properties', 'patternProperties', 'definitions', '$defs' ] as $keyword ) { + if ( ! array_key_exists( $keyword, $schema ) ) { + continue; } - $declarations[] = new FunctionDeclaration( - $tool_name, - (string) $ability->get_description(), - $parameters + $schema[ $keyword ] = $this->normalize_schema_map( $schema[ $keyword ] ); + } + + if ( array_key_exists( 'dependencies', $schema ) ) { + $schema['dependencies'] = $this->normalize_dependencies_keyword( $schema['dependencies'] ); + } + + foreach ( [ 'additionalProperties', 'items', 'not' ] as $keyword ) { + if ( ! array_key_exists( $keyword, $schema ) ) { + continue; + } + + $schema[ $keyword ] = $this->normalize_schema_value( $schema[ $keyword ] ); + } + + foreach ( [ 'allOf', 'anyOf', 'oneOf' ] as $keyword ) { + if ( ! isset( $schema[ $keyword ] ) || ! is_array( $schema[ $keyword ] ) ) { + continue; + } + + $schema[ $keyword ] = array_values( + array_map( + fn( $item ) => $this->normalize_schema_value( $item ), + $schema[ $keyword ] + ) ); } - return $declarations; + return $schema; + } + + /** + * Normalize a schema-map keyword like `properties`. + * + * @param mixed $value Raw schema-map value. + * @return mixed + */ + private function normalize_schema_map( $value ) { + if ( ! is_array( $value ) ) { + return $value; + } + + if ( [] === $value ) { + return (object) []; + } + + $normalized = []; + foreach ( $value as $key => $schema ) { + $normalized[ $key ] = $this->normalize_schema_value( $schema ); + } + + return $normalized; + } + + /** + * Normalize the `dependencies` keyword. + * + * @param mixed $value Raw dependencies value. + * @return mixed + */ + private function normalize_dependencies_keyword( $value ) { + if ( ! is_array( $value ) ) { + return $value; + } + + if ( [] === $value ) { + return (object) []; + } + + $normalized = []; + foreach ( $value as $key => $dependency ) { + if ( is_array( $dependency ) && ! array_is_list( $dependency ) ) { + $normalized[ $key ] = $this->normalize_schema_node( $dependency ); + continue; + } + + $normalized[ $key ] = $dependency; + } + + return $normalized; + } + + /** + * Normalize a schema-bearing value. + * + * @param mixed $value Raw schema value. + * @return mixed + */ + private function normalize_schema_value( $value ) { + if ( ! is_array( $value ) ) { + return $value; + } + + if ( [] === $value ) { + return (object) []; + } + + if ( array_is_list( $value ) ) { + return array_values( + array_map( + fn( $item ) => $this->normalize_schema_value( $item ), + $value + ) + ); + } + + return $this->normalize_schema_node( $value ); } /** diff --git a/includes/helpers/class-agent-loop-helper.php b/includes/helpers/class-agent-loop-helper.php index 5d756c1..a368e1d 100644 --- a/includes/helpers/class-agent-loop-helper.php +++ b/includes/helpers/class-agent-loop-helper.php @@ -10,9 +10,7 @@ namespace ClawPress\Helpers; use ClawPress\Commands\Command_Confirmation_Store; -use ClawPress\Transports\Agent_Transport; -use ClawPress\Transports\Null_Transport; -use ClawPress\Transports\Polling_Transport; +use ClawPress\Transports\Agent_Event_Sink; use Throwable; use WordPress\AiClient\AiClient; use WordPress\AiClient\Builders\MessageBuilder; @@ -28,7 +26,7 @@ defined( 'ABSPATH' ) || exit; /** - * Reusable transport-agnostic agent loop runtime. + * Reusable agent loop runtime. */ final class Agent_Loop_Helper { /** @@ -181,7 +179,7 @@ private function run_internal( array $turn_request, bool $is_slice ): array { ]; } - $transport = $this->create_transport( $transport_mode, $run_id, $session_id ); + $event_sink = $this->create_event_sink( $transport_mode, $run_id, $session_id, $turn_request ); $online_reply_generator = isset( $turn_request['online_reply_generator'] ) && is_callable( $turn_request['online_reply_generator'] ) ? $turn_request['online_reply_generator'] : [ $this, 'generate_online_reply' ]; @@ -198,7 +196,7 @@ private function run_internal( array $turn_request, bool $is_slice ): array { $context['request_timeout'] = $this->settings_helper->get_request_timeout( $settings ); $context['generation_settings'] = $this->settings_helper->get_generation_settings( $settings ); - $transport->emit( + $event_sink->emit( [ 'type' => 'agent.run.started', 'payload' => [ @@ -219,7 +217,7 @@ private function run_internal( array $turn_request, bool $is_slice ): array { $provider, $model, $turn_request, - $transport, + $event_sink, $is_slice ) ); @@ -247,7 +245,7 @@ private function run_internal( array $turn_request, bool $is_slice ): array { $result['mode'] = 'error'; } - $transport->emit( + $event_sink->emit( [ 'type' => 'agent.run.finished', 'payload' => [ @@ -257,8 +255,9 @@ private function run_internal( array $turn_request, bool $is_slice ): array { ] ); - if ( $transport instanceof Polling_Transport ) { - $result['events_cursor'] = $transport->get_last_event_id(); + $last_event_id = $event_sink->get_last_event_id(); + if ( $last_event_id > 0 ) { + $result['events_cursor'] = $last_event_id; } return $result; @@ -269,7 +268,7 @@ private function run_internal( array $turn_request, bool $is_slice ): array { } $error_type = $this->classify_provider_error_type( $throwable, $error_message ); - $transport->emit( + $event_sink->emit( [ 'type' => 'agent.run.error', 'payload' => [ @@ -297,13 +296,14 @@ private function run_internal( array $turn_request, bool $is_slice ): array { ], ]; - if ( $transport instanceof Polling_Transport ) { - $result['events_cursor'] = $transport->get_last_event_id(); + $last_event_id = $event_sink->get_last_event_id(); + if ( $last_event_id > 0 ) { + $result['events_cursor'] = $last_event_id; } return $result; } finally { - $transport->close(); + $event_sink->close(); } } @@ -352,11 +352,11 @@ private function ensure_terminal_assistant_text( array $result ): array { * @param string $provider Provider identifier. * @param string $model Model identifier. * @param array $turn_request Turn request metadata. - * @param Agent_Transport|null $transport Transport implementation. + * @param Agent_Event_Sink|null $event_sink Event sink implementation. * @param bool $is_slice Whether slice execution mode is enabled. * @return array */ - private function generate_online_reply( array $context, string $provider, string $model, array $turn_request = [], ?Agent_Transport $transport = null, bool $is_slice = false ): array { + private function generate_online_reply( array $context, string $provider, string $model, array $turn_request = [], ?Agent_Event_Sink $event_sink = null, bool $is_slice = false ): array { $current_message = isset( $context['message'] ) ? trim( (string) $context['message'] ) : ''; $system_prompt = isset( $context['system_prompt'] ) ? trim( (string) $context['system_prompt'] ) : ''; $request_timeout = isset( $context['request_timeout'] ) ? (int) $context['request_timeout'] : 45; @@ -382,7 +382,8 @@ private function generate_online_reply( array $context, string $provider, string $slice_budget_ms = $is_slice ? max( 1, (int) ( $turn_request['slice_budget_ms'] ?? 1500 ) ) : 0; $max_steps_per_slice = $is_slice ? max( 1, (int) ( $turn_request['max_steps_per_slice'] ?? 1 ) ) : PHP_INT_MAX; $resume_state = $this->normalize_resume_cursor( $turn_request['resume_cursor'] ?? null ); - $transport = $transport ?? new Null_Transport(); + $event_sink = $event_sink ?? new Agent_Event_Sink(); + $stream_generation_args = $this->build_stream_generation_args( $turn_request, $event_sink ); $this->confirmation_store->clear_tool_batch( $requesting_user_id > 0 ? $requesting_user_id : null ); @@ -413,7 +414,7 @@ private function generate_online_reply( array $context, string $provider, string : ( isset( $resume_state['steps_completed'] ) ? max( 0, (int) $resume_state['steps_completed'] ) : 0 ); if ( $is_slice && $round_start > 0 ) { - $transport->emit( + $event_sink->emit( [ 'type' => 'agent.slice.resumed', 'payload' => [ @@ -436,7 +437,7 @@ private function generate_online_reply( array $context, string $provider, string $latest_context_usage ); - $transport->emit( + $event_sink->emit( [ 'type' => 'agent.slice.paused', 'payload' => [ @@ -464,7 +465,8 @@ private function generate_online_reply( array $context, string $provider, string $system_prompt, $request_timeout, $generation_settings, - $tool_declarations + $tool_declarations, + $stream_generation_args ); $assistant_message = $result->toMessage(); $conversation[] = $assistant_message; @@ -473,7 +475,7 @@ private function generate_online_reply( array $context, string $provider, string ++$steps_completed; $function_calls = $this->extract_function_calls( $assistant_message ); - $transport->emit( + $event_sink->emit( [ 'type' => 'agent.llm.response', 'payload' => [ @@ -520,7 +522,7 @@ private function generate_online_reply( array $context, string $provider, string $function_responses ); - $transport->emit( + $event_sink->emit( [ 'type' => 'agent.tool_calls.deferred', 'payload' => [ @@ -566,7 +568,7 @@ private function generate_online_reply( array $context, string $provider, string $tool_call_payload['message'] = $latest_tool_trace['message']; } - $transport->emit( + $event_sink->emit( [ 'type' => 'agent.tool_call', 'payload' => $tool_call_payload, @@ -606,7 +608,7 @@ private function generate_online_reply( array $context, string $provider, string $requesting_user_id > 0 ? $requesting_user_id : null ); - $transport->emit( + $event_sink->emit( [ 'type' => 'agent.confirmation.required', 'payload' => [ @@ -643,7 +645,7 @@ private function generate_online_reply( array $context, string $provider, string $latest_context_usage ); - $transport->emit( + $event_sink->emit( [ 'type' => 'agent.slice.paused', 'payload' => [ @@ -729,19 +731,23 @@ private function normalize_online_reply_payload( $raw_output ): array { } /** - * Create transport from transport mode. + * Create an event sink from the requested delivery mode. * * @param string $transport_mode Transport mode. * @param int $run_id Run identifier. * @param int $session_id Session identifier. */ - private function create_transport( string $transport_mode, int $run_id, int $session_id ): Agent_Transport { + private function create_event_sink( string $transport_mode, int $run_id, int $session_id, array $turn_request = [] ): Agent_Event_Sink { $transport_mode = strtolower( trim( $transport_mode ) ); + if ( 'streaming' === $transport_mode && isset( $turn_request['stream_event_callback'] ) && is_callable( $turn_request['stream_event_callback'] ) ) { + return new Agent_Event_Sink( $turn_request['stream_event_callback'], $run_id, $session_id ); + } + if ( in_array( $transport_mode, [ 'polling', 'streaming' ], true ) && ( $run_id > 0 || $session_id > 0 ) ) { - return new Polling_Transport( $run_id, $session_id ); + return new Agent_Event_Sink( null, $run_id, $session_id ); } - return new Null_Transport(); + return new Agent_Event_Sink(); } /** @@ -1304,7 +1310,7 @@ private function normalize_tool_declarations( array $context ): array { foreach ( $context['tool_declarations'] as $declaration ) { if ( $declaration instanceof FunctionDeclaration ) { - $declarations[] = $declaration; + $declarations[] = $this->abilities_helper->normalize_function_declaration( $declaration ); } } @@ -1322,6 +1328,260 @@ private function build_request_options( int $request_timeout ): RequestOptions { return $request_options; } + /** + * Build streaming generation args for a turn when live transport delivery is enabled. + * + * @param array $turn_request Turn request payload. + * @return array + */ + private function build_stream_generation_args( array $turn_request, Agent_Event_Sink $event_sink ): array { + $transport_mode = isset( $turn_request['transport_mode'] ) ? strtolower( trim( (string) $turn_request['transport_mode'] ) ) : 'polling'; + if ( 'streaming' !== $transport_mode || ! function_exists( 'wp_ai_client_stream_prompt' ) ) { + return []; + } + + if ( class_exists( '\WP_AI_Client_Streaming_Discovery_Strategy' ) ) { + \WP_AI_Client_Streaming_Discovery_Strategy::init(); + } + + return [ + 'streaming_enabled' => true, + 'on_event' => function ( \WP_AI_Client_SSE_Event $event ) use ( $event_sink ): void { + $this->emit_transport_stream_delta( $event, $event_sink ); + }, + 'should_continue' => static function (): bool { + return ! connection_aborted(); + }, + ]; + } + + /** + * Mirror one streamed provider event to the live transport. + */ + private function emit_transport_stream_delta( \WP_AI_Client_SSE_Event $event, Agent_Event_Sink $event_sink ): void { + if ( $event->is_done() ) { + return; + } + + $text = $this->extract_stream_event_text( $event ); + if ( '' === $text ) { + return; + } + + $event_sink->emit( + [ + 'type' => 'agent.llm.delta', + 'payload' => [ + 'text' => $text, + ], + ] + ); + } + + /** + * Extract incremental text content from a streamed provider event. + */ + private function extract_stream_event_text( \WP_AI_Client_SSE_Event $event ): string { + $data = $event->get_json_data(); + if ( ! is_array( $data ) ) { + return ''; + } + + $type = $this->resolve_stream_event_type( $event, $data ); + + if ( 'response.output_text.delta' === $type && isset( $data['delta'] ) ) { + return $this->normalize_stream_event_text_value( $data['delta'] ); + } + + if ( 'response.content_part.added' === $type && isset( $data['part'] ) ) { + return $this->normalize_stream_event_text_value( $data['part'] ); + } + + if ( + $this->is_non_text_stream_event_type( $type ) || + $this->contains_stream_tool_call_payload( $data ) + ) { + return ''; + } + + if ( isset( $data['choices'][0]['delta']['content'] ) ) { + return $this->normalize_stream_event_text_value( $data['choices'][0]['delta']['content'] ); + } + + if ( isset( $data['choices'][0]['delta']['text'] ) ) { + return $this->normalize_stream_event_text_value( $data['choices'][0]['delta']['text'] ); + } + + if ( isset( $data['choices'][0]['text'] ) ) { + return $this->normalize_stream_event_text_value( $data['choices'][0]['text'] ); + } + + if ( '' === $type && isset( $data['delta'] ) ) { + return $this->normalize_stream_event_text_value( $data['delta'] ); + } + + if ( '' === $type && isset( $data['text'] ) ) { + return $this->normalize_stream_event_text_value( $data['text'] ); + } + + return ''; + } + + /** + * Resolve the canonical stream event type. + * + * @param \WP_AI_Client_SSE_Event $event Stream event. + * @param array $data Decoded event payload. + */ + private function resolve_stream_event_type( \WP_AI_Client_SSE_Event $event, array $data ): string { + if ( isset( $data['type'] ) && is_string( $data['type'] ) ) { + return strtolower( trim( $data['type'] ) ); + } + + $event_name = trim( $event->get_event() ); + return '' !== $event_name ? strtolower( $event_name ) : ''; + } + + /** + * Determine whether a streamed event type should never be rendered as assistant text. + * + * @param string $type Normalized stream event type. + */ + private function is_non_text_stream_event_type( string $type ): bool { + if ( '' === $type ) { + return false; + } + + if ( + false !== strpos( $type, 'function_call' ) || + false !== strpos( $type, 'tool_call' ) || + false !== strpos( $type, 'function.arguments' ) || + false !== strpos( $type, 'arguments' ) + ) { + return true; + } + + return in_array( + $type, + [ + 'response.created', + 'response.in_progress', + 'response.output_text.done', + 'response.content_part.done', + 'response.output_item.added', + 'response.output_item.done', + 'response.completed', + ], + true + ); + } + + /** + * Detect tool-call payloads that should never be mirrored into assistant text. + * + * @param array $data Decoded stream event payload. + */ + private function contains_stream_tool_call_payload( array $data ): bool { + if ( isset( $data['tool_calls'] ) || isset( $data['function_call'] ) ) { + return true; + } + + if ( + isset( $data['choices'][0]['delta'] ) && + is_array( $data['choices'][0]['delta'] ) && + ( + isset( $data['choices'][0]['delta']['tool_calls'] ) || + isset( $data['choices'][0]['delta']['function_call'] ) + ) + ) { + return true; + } + + if ( isset( $data['item'] ) && is_array( $data['item'] ) && $this->is_stream_tool_item( $data['item'] ) ) { + return true; + } + + if ( isset( $data['delta'] ) && is_array( $data['delta'] ) && $this->is_stream_tool_item( $data['delta'] ) ) { + return true; + } + + return false; + } + + /** + * Determine whether an event fragment represents a tool/function call item. + * + * @param array $item Event fragment payload. + */ + private function is_stream_tool_item( array $item ): bool { + if ( isset( $item['tool_calls'] ) || isset( $item['function_call'] ) ) { + return true; + } + + $item_type = isset( $item['type'] ) && is_string( $item['type'] ) + ? strtolower( trim( $item['type'] ) ) + : ''; + + return '' !== $item_type && + ( + false !== strpos( $item_type, 'function_call' ) || + false !== strpos( $item_type, 'tool_call' ) || + false !== strpos( $item_type, 'tool_use' ) || + false !== strpos( $item_type, 'input_json' ) + ); + } + + /** + * Normalize streamed text values into a flat string. + * + * @param mixed $value Raw streamed text value. + */ + private function normalize_stream_event_text_value( $value ): string { + if ( is_string( $value ) ) { + return $value; + } + + if ( ! is_array( $value ) ) { + return ''; + } + + if ( isset( $value['text'] ) && is_string( $value['text'] ) ) { + return $value['text']; + } + + $text = ''; + foreach ( $value as $item ) { + if ( is_string( $item ) ) { + $text .= $item; + continue; + } + + if ( is_array( $item ) && isset( $item['text'] ) && is_string( $item['text'] ) ) { + $text .= $item['text']; + } + } + + return $text; + } + + /** + * Generate a result from a streaming builder. + * + * @param object $builder Streaming prompt builder. + */ + private function generate_result_from_streaming_builder( object $builder ): GenerativeAiResult { + $result = $builder->generate_result(); + if ( is_wp_error( $result ) ) { + throw new \RuntimeException( $result->get_error_message() ); + } + + if ( ! $result instanceof GenerativeAiResult ) { + throw new \RuntimeException( __( 'AI client did not return a generative result.', 'clawpress' ) ); + } + + return $result; + } + /** * Generate a result and retry once with explicit model binding when provider metadata matching fails. * @@ -1332,6 +1592,7 @@ private function build_request_options( int $request_timeout ): RequestOptions { * @param int $request_timeout Request timeout in seconds. * @param array $generation_settings Generation settings. * @param array $tool_declarations Tool declarations. + * @param array $stream_generation_args Optional streaming generation args. */ private function generate_result_with_explicit_model_fallback( array $conversation, @@ -1340,8 +1601,22 @@ private function generate_result_with_explicit_model_fallback( string $system_prompt, int $request_timeout, array $generation_settings, - array $tool_declarations + array $tool_declarations, + array $stream_generation_args = [] ): GenerativeAiResult { + if ( [] !== $stream_generation_args ) { + return $this->generate_streaming_result_with_explicit_model_fallback( + $conversation, + $provider, + $model, + $system_prompt, + $request_timeout, + $generation_settings, + $tool_declarations, + $stream_generation_args + ); + } + $builder = $this->build_prompt_builder_for_round( $conversation, $provider, @@ -1375,6 +1650,63 @@ private function generate_result_with_explicit_model_fallback( } } + /** + * Generate a streaming-aware result and retry once with explicit model binding when needed. + * + * @param array $conversation Conversation messages. + * @param string $provider Provider identifier. + * @param string $model Model identifier. + * @param string $system_prompt System prompt. + * @param int $request_timeout Request timeout in seconds. + * @param array $generation_settings Generation settings. + * @param array $tool_declarations Tool declarations. + * @param array $stream_generation_args Streaming generation args. + */ + private function generate_streaming_result_with_explicit_model_fallback( + array $conversation, + string $provider, + string $model, + string $system_prompt, + int $request_timeout, + array $generation_settings, + array $tool_declarations, + array $stream_generation_args + ): GenerativeAiResult { + $builder = $this->build_streaming_prompt_builder_for_round( + $conversation, + $provider, + $model, + $system_prompt, + $request_timeout, + $generation_settings, + $tool_declarations, + false, + $stream_generation_args + ); + + try { + return $this->generate_result_from_streaming_builder( $builder ); + } catch ( Throwable $throwable ) { + if ( ! $this->should_retry_with_explicit_model( $throwable, $provider, $model ) ) { + throw $throwable; + } + + $fallback_builder = $this->build_streaming_prompt_builder_for_round( + $conversation, + $provider, + $model, + $system_prompt, + $request_timeout, + $generation_settings, + $tool_declarations, + true, + $stream_generation_args + ); + + return $this->generate_result_from_streaming_builder( $fallback_builder ); + } + } + /** * Build a configured prompt builder for the current round. * @@ -1424,6 +1756,58 @@ private function build_prompt_builder_for_round( return $builder; } + /** + * Build a configured streaming prompt builder for the current round. + * + * @param array $conversation Conversation messages. + * @param string $provider Provider identifier. + * @param string $model Model identifier. + * @param string $system_prompt System prompt. + * @param int $request_timeout Request timeout in seconds. + * @param array $generation_settings Generation settings. + * @param array $tool_declarations Tool declarations. + * @param bool $use_explicit_model Whether to bind the selected model explicitly. + * @param array $stream_generation_args Streaming generation args. + * @return object + */ + private function build_streaming_prompt_builder_for_round( + array $conversation, + string $provider, + string $model, + string $system_prompt, + int $request_timeout, + array $generation_settings, + array $tool_declarations, + bool $use_explicit_model, + array $stream_generation_args + ): object { + $builder = wp_ai_client_stream_prompt( $conversation, $stream_generation_args )->using_provider( $provider ); + if ( '' !== $system_prompt ) { + $builder = $builder->using_system_instruction( $system_prompt ); + } + + if ( '' !== $model ) { + if ( $use_explicit_model ) { + $selected_model = AiClient::defaultRegistry()->getProviderModel( + $provider, + $model, + ModelConfig::fromArray( [] ) + ); + $builder = $builder->using_model( $selected_model ); + } else { + $builder = $builder->using_model_preference( [ $provider, $model ] ); + } + } + + $builder = $builder->using_request_options( $this->build_request_options( $request_timeout ) ); + $builder = $this->apply_generation_settings_to_streaming_prompt_builder( $builder, $generation_settings, $provider, $model ); + if ( [] !== $tool_declarations ) { + $builder = $builder->using_function_declarations( ...$tool_declarations ); + } + + return $builder; + } + /** * Determine whether generation should retry with explicit model binding. * @@ -1517,6 +1901,59 @@ private function apply_generation_settings_to_prompt_builder( PromptBuilder $bui return $builder; } + /** + * Apply generation settings to a streaming prompt builder. + * + * @param object $builder Streaming prompt builder. + * @param array $generation_settings Generation settings. + * @return object + */ + private function apply_generation_settings_to_streaming_prompt_builder( object $builder, array $generation_settings, string $provider, string $model ): object { + $option_setters = [ + fn ( object $current ): object => $this->apply_temperature_to_streaming_prompt_builder( + $current, + (float) $generation_settings['temperature'], + $provider, + $model + ), + fn ( object $current ): object => $this->apply_top_p_to_streaming_prompt_builder( + $current, + (float) $generation_settings['top_p'], + $provider, + $model + ), + fn ( object $current ): object => $this->apply_max_output_tokens_to_streaming_prompt_builder( + $current, + (int) $generation_settings['max_output_tokens'], + $provider, + $model + ), + fn ( object $current ): object => $this->apply_frequency_penalty_to_streaming_prompt_builder( + $current, + (float) $generation_settings['frequency_penalty'], + $provider, + $model + ), + fn ( object $current ): object => $this->apply_presence_penalty_to_streaming_prompt_builder( + $current, + (float) $generation_settings['presence_penalty'], + $provider, + $model + ), + ]; + + foreach ( $option_setters as $setter ) { + try { + $builder = $setter( $builder ); + } catch ( Throwable $throwable ) { + unset( $throwable ); + continue; + } + } + + return $builder; + } + /** * Apply max output token setting to prompt builder. * @@ -1541,6 +1978,28 @@ private function apply_max_output_tokens_to_prompt_builder( PromptBuilder $build return $builder->usingMaxTokens( $max_output_tokens ); } + /** + * Apply max output token setting to a streaming prompt builder. + * + * @param object $builder Streaming prompt builder. + * @return object + */ + private function apply_max_output_tokens_to_streaming_prompt_builder( object $builder, int $max_output_tokens, string $provider, string $model ): object { + if ( $this->provider_helper->should_use_max_output_tokens( $provider, $model ) ) { + $model_config = ModelConfig::fromArray( + [ + ModelConfig::KEY_CUSTOM_OPTIONS => [ + 'max_output_tokens' => $max_output_tokens, + ], + ] + ); + + return $builder->using_model_config( $model_config ); + } + + return $builder->using_max_tokens( $max_output_tokens ); + } + /** * Apply temperature when supported by the provider/model pair. * @@ -1557,6 +2016,20 @@ private function apply_temperature_to_prompt_builder( PromptBuilder $builder, fl return $builder->usingTemperature( $temperature ); } + /** + * Apply temperature when supported to a streaming prompt builder. + * + * @param object $builder Streaming prompt builder. + * @return object + */ + private function apply_temperature_to_streaming_prompt_builder( object $builder, float $temperature, string $provider, string $model ): object { + if ( ! $this->provider_helper->should_use_temperature( $provider, $model ) ) { + return $builder; + } + + return $builder->using_temperature( $temperature ); + } + /** * Apply top-p sampling when supported by the provider/model pair. * @@ -1573,6 +2046,20 @@ private function apply_top_p_to_prompt_builder( PromptBuilder $builder, float $t return $builder->usingTopP( $top_p ); } + /** + * Apply top-p when supported to a streaming prompt builder. + * + * @param object $builder Streaming prompt builder. + * @return object + */ + private function apply_top_p_to_streaming_prompt_builder( object $builder, float $top_p, string $provider, string $model ): object { + if ( ! $this->provider_helper->should_use_top_p( $provider, $model ) ) { + return $builder; + } + + return $builder->using_top_p( $top_p ); + } + /** * Apply frequency penalty when supported by the provider/model pair. * @@ -1589,6 +2076,20 @@ private function apply_frequency_penalty_to_prompt_builder( PromptBuilder $build return $builder->usingFrequencyPenalty( $frequency_penalty ); } + /** + * Apply frequency penalty when supported to a streaming prompt builder. + * + * @param object $builder Streaming prompt builder. + * @return object + */ + private function apply_frequency_penalty_to_streaming_prompt_builder( object $builder, float $frequency_penalty, string $provider, string $model ): object { + if ( ! $this->provider_helper->should_use_frequency_penalty( $provider, $model ) ) { + return $builder; + } + + return $builder->using_frequency_penalty( $frequency_penalty ); + } + /** * Apply presence penalty when supported by the provider/model pair. * @@ -1605,6 +2106,20 @@ private function apply_presence_penalty_to_prompt_builder( PromptBuilder $builde return $builder->usingPresencePenalty( $presence_penalty ); } + /** + * Apply presence penalty when supported to a streaming prompt builder. + * + * @param object $builder Streaming prompt builder. + * @return object + */ + private function apply_presence_penalty_to_streaming_prompt_builder( object $builder, float $presence_penalty, string $provider, string $model ): object { + if ( ! $this->provider_helper->should_use_presence_penalty( $provider, $model ) ) { + return $builder; + } + + return $builder->using_presence_penalty( $presence_penalty ); + } + /** * Extract function calls from a message. * @@ -1684,7 +2199,7 @@ private function now_ms(): int { * @param string $provider Provider ID. * @param string $model Model ID. * @param array $turn_request Turn request payload. - * @param Agent_Transport $transport Runtime transport. + * @param Agent_Event_Sink $event_sink Runtime event sink. * @param bool $is_slice Whether this is a slice execution. * @return mixed */ @@ -1694,10 +2209,10 @@ private function invoke_online_reply_generator( string $provider, string $model, array $turn_request, - Agent_Transport $transport, + Agent_Event_Sink $event_sink, bool $is_slice ) { - $args = [ $context, $provider, $model, $turn_request, $transport, $is_slice ]; + $args = [ $context, $provider, $model, $turn_request, $event_sink, $is_slice ]; $arity = $this->resolve_callable_arity( $online_reply_generator ); if ( $arity > 0 && $arity < count( $args ) ) { $args = array_slice( $args, 0, $arity ); diff --git a/includes/helpers/class-chat-helper.php b/includes/helpers/class-chat-helper.php index 07caed0..d043955 100644 --- a/includes/helpers/class-chat-helper.php +++ b/includes/helpers/class-chat-helper.php @@ -111,14 +111,18 @@ public static function create_for_testing( /** * Generate a model reply payload. * - * @param string $message User message. + * @param string $message User message. + * @param array $options Optional generation options. * @return array */ - public function generate_ai_reply( string $message ): array { + public function generate_ai_reply( string $message, array $options = [] ): array { $settings = $this->settings_helper->get_settings(); $resolved = call_user_func( $this->provider_model_resolver, $settings ); $provider = isset( $resolved['provider'] ) ? trim( (string) $resolved['provider'] ) : ''; $model = isset( $resolved['model'] ) ? trim( (string) $resolved['model'] ) : ''; + $transport_mode = isset( $options['transport_mode'] ) && 'streaming' === strtolower( trim( (string) $options['transport_mode'] ) ) + ? 'streaming' + : 'polling'; if ( '' === $provider ) { return [ @@ -143,7 +147,7 @@ public function generate_ai_reply( string $message ): array { $turn_request = [ 'message' => $message, 'trigger' => 'chat', - 'transport_mode' => 'polling', + 'transport_mode' => $transport_mode, 'slice_budget_ms' => $slice_budget_ms, 'max_steps_per_slice' => 2, 'requesting_user_id' => $requesting_user_id, @@ -165,7 +169,7 @@ public function generate_ai_reply( string $message ): array { $session_id, [ 'trigger_type' => 'chat', - 'transport_mode' => 'polling', + 'transport_mode' => $transport_mode, 'status' => 'queued', 'meta' => [ 'message' => $message, @@ -204,6 +208,10 @@ public function generate_ai_reply( string $message ): array { $turn_request['online_reply_generator'] = $this->online_reply_generator; } + if ( isset( $options['stream_event_callback'] ) && is_callable( $options['stream_event_callback'] ) ) { + $turn_request['stream_event_callback'] = $options['stream_event_callback']; + } + $runtime_result = $this->agent_loop_helper->run_slice( $turn_request ); if ( 'in_progress' === (string) ( $runtime_result['status'] ?? '' ) ) { diff --git a/includes/rest/class-chat-controller.php b/includes/rest/class-chat-controller.php index 3afd808..61a0c30 100644 --- a/includes/rest/class-chat-controller.php +++ b/includes/rest/class-chat-controller.php @@ -88,6 +88,22 @@ public function register_routes(): void { ] ); + register_rest_route( + 'clawpress/v1', + '/chat/stream', + [ + 'methods' => 'POST', + 'callback' => [ $this, 'send_stream_message' ], + 'permission_callback' => 'clawpress_check_permissions', + 'args' => [ + 'message' => [ + 'required' => true, + 'sanitize_callback' => 'sanitize_text_field', + ], + ], + ] + ); + register_rest_route( 'clawpress/v1', '/chat/history', @@ -115,11 +131,105 @@ public function send_message( \WP_REST_Request $request ): \WP_REST_Response { ); } + return new \WP_REST_Response( + $this->build_chat_response_data( + $message, + $this->resolve_reply_payload( $message ) + ), + 200 + ); + } + + /** + * Handle a streamed chat request. + * + * @param \WP_REST_Request $request The request object. + */ + public function send_stream_message( \WP_REST_Request $request ): \WP_REST_Response { + $message = trim( (string) $request->get_param( 'message' ) ); + if ( '' === $message ) { + return new \WP_REST_Response( + [ + 'error' => __( 'Message is required.', 'clawpress' ), + ], + 400 + ); + } + + $saw_delta = false; + $stream_event_callback = function ( array $event ) use ( &$saw_delta ): void { + if ( $this->emit_stream_transport_event( $event ) ) { + $saw_delta = true; + } + }; + + $this->start_stream_response(); + + try { + $response_data = $this->build_chat_response_data( + $message, + $this->resolve_reply_payload( $message, true, $stream_event_callback ) + ); + + $this->emit_stream_response_data( $response_data, $saw_delta ); + } catch ( \Throwable $throwable ) { + self::send_stream_frame( + 'error', + [ + 'error' => trim( sanitize_text_field( $throwable->getMessage() ) ) ?: __( 'Chat request failed.', 'clawpress' ), + 'type' => 'request', + ] + ); + } + + exit; + } + + /** + * Resolve the reply payload for one chat message. + * + * @param string $message User message. + * @param bool $streaming Whether streaming transport is requested. + * @param callable|null $stream_event_callback Optional live stream event callback. + * @return array + */ + private function resolve_reply_payload( string $message, bool $streaming = false, ?callable $stream_event_callback = null ): array { $command_payload = $this->commands->maybe_dispatch( $message ); - $reply_payload = is_array( $command_payload ) - ? $command_payload - : call_user_func( $this->reply_generator, $message ); + if ( is_array( $command_payload ) ) { + return $command_payload; + } + + if ( $streaming && $this->is_default_reply_generator() ) { + return $this->chat_helper->generate_ai_reply( + $message, + [ + 'transport_mode' => 'streaming', + 'stream_event_callback' => $stream_event_callback, + ] + ); + } + + return call_user_func( $this->reply_generator, $message ); + } + /** + * Determine whether the controller is using the default chat-helper reply generator. + */ + private function is_default_reply_generator(): bool { + return is_array( $this->reply_generator ) + && isset( $this->reply_generator[0], $this->reply_generator[1] ) + && $this->reply_generator[0] === $this->chat_helper + && 'generate_ai_reply' === (string) $this->reply_generator[1]; + } + + /** + * Normalize, persist, and log one chat response payload. + * + * @param string $message User message. + * @param array $reply_payload Raw reply payload. + * @return array + */ + private function build_chat_response_data( string $message, array $reply_payload ): array { $reply = isset( $reply_payload['reply'] ) ? trim( (string) $reply_payload['reply'] ) : ''; if ( '' === $reply ) { $reply = $this->chat_helper->build_offline_reply( $message ); @@ -171,42 +281,246 @@ static function ( $tool_call ): array { isset( $reply_payload['model'] ) ? (string) $reply_payload['model'] : '' ); - return new \WP_REST_Response( + return [ + 'message' => $message, + 'reply' => $reply, + 'meta' => [ + 'mode' => isset( $reply_payload['mode'] ) ? (string) $reply_payload['mode'] : 'offline', + 'provider' => isset( $reply_payload['provider'] ) && '' !== (string) $reply_payload['provider'] + ? (string) $reply_payload['provider'] + : null, + 'model' => isset( $reply_payload['model'] ) && '' !== (string) $reply_payload['model'] + ? (string) $reply_payload['model'] + : null, + 'suggestions' => isset( $reply_payload['suggestions'] ) && is_array( $reply_payload['suggestions'] ) + ? array_values( $reply_payload['suggestions'] ) + : null, + 'card' => isset( $reply_payload['card'] ) && is_array( $reply_payload['card'] ) + ? $reply_payload['card'] + : null, + 'command' => isset( $reply_payload['command'] ) && is_array( $reply_payload['command'] ) + ? $reply_payload['command'] + : null, + 'error' => isset( $reply_payload['error'] ) && is_array( $reply_payload['error'] ) + ? $reply_payload['error'] + : null, + 'context' => isset( $reply_payload['context'] ) && is_array( $reply_payload['context'] ) + ? $reply_payload['context'] + : null, + 'tool_calls' => $tool_calls_meta, + 'run_id' => isset( $reply_payload['run_id'] ) ? (int) $reply_payload['run_id'] : null, + 'session_id' => isset( $reply_payload['session_id'] ) ? (int) $reply_payload['session_id'] : null, + 'events_cursor' => isset( $reply_payload['events_cursor'] ) ? (int) $reply_payload['events_cursor'] : null, + 'status' => isset( $reply_payload['status'] ) ? (string) $reply_payload['status'] : null, + ], + ]; + } + + /** + * Emit one transport event into the SSE response. + * + * @param array $event Transport event payload. + * @return bool Whether a delta frame was emitted. + */ + private function emit_stream_transport_event( array $event ): bool { + $event_type = isset( $event['type'] ) ? (string) $event['type'] : ''; + $payload = isset( $event['payload'] ) && is_array( $event['payload'] ) + ? $event['payload'] + : []; + + if ( 'agent.llm.delta' === $event_type ) { + $text = isset( $payload['text'] ) ? (string) $payload['text'] : ''; + if ( '' === $text ) { + return false; + } + + self::send_stream_frame( + 'delta', + [ + 'text' => $text, + ] + ); + + return true; + } + + if ( 'agent.tool_call' !== $event_type ) { + return false; + } + + $status = isset( $payload['status'] ) ? strtolower( trim( (string) $payload['status'] ) ) : 'success'; + if ( ! in_array( $status, [ 'success', 'error', 'requires_confirmation' ], true ) ) { + $status = 'success'; + } + + $tool_name = isset( $payload['tool_name'] ) ? sanitize_text_field( (string) $payload['tool_name'] ) : ''; + $ability = isset( $payload['ability_name'] ) ? sanitize_text_field( (string) $payload['ability_name'] ) : ''; + $message = isset( $payload['message'] ) ? sanitize_text_field( (string) $payload['message'] ) : ''; + + self::send_stream_frame( + 'tool_call', [ - 'message' => $message, - 'reply' => $reply, - 'meta' => [ - 'mode' => isset( $reply_payload['mode'] ) ? (string) $reply_payload['mode'] : 'offline', - 'provider' => isset( $reply_payload['provider'] ) && '' !== (string) $reply_payload['provider'] - ? (string) $reply_payload['provider'] - : null, - 'model' => isset( $reply_payload['model'] ) && '' !== (string) $reply_payload['model'] - ? (string) $reply_payload['model'] - : null, - 'suggestions' => isset( $reply_payload['suggestions'] ) && is_array( $reply_payload['suggestions'] ) - ? array_values( $reply_payload['suggestions'] ) - : null, - 'card' => isset( $reply_payload['card'] ) && is_array( $reply_payload['card'] ) - ? $reply_payload['card'] - : null, - 'command' => isset( $reply_payload['command'] ) && is_array( $reply_payload['command'] ) - ? $reply_payload['command'] - : null, - 'error' => isset( $reply_payload['error'] ) && is_array( $reply_payload['error'] ) - ? $reply_payload['error'] - : null, - 'context' => isset( $reply_payload['context'] ) && is_array( $reply_payload['context'] ) - ? $reply_payload['context'] - : null, - 'tool_calls' => $tool_calls_meta, - 'run_id' => isset( $reply_payload['run_id'] ) ? (int) $reply_payload['run_id'] : null, - 'session_id' => isset( $reply_payload['session_id'] ) ? (int) $reply_payload['session_id'] : null, - 'events_cursor' => isset( $reply_payload['events_cursor'] ) ? (int) $reply_payload['events_cursor'] : null, - 'status' => isset( $reply_payload['status'] ) ? (string) $reply_payload['status'] : null, + 'call' => [ + 'name' => '' !== $tool_name ? $tool_name : $ability, + 'tool_name' => '' !== $tool_name ? $tool_name : null, + 'ability_name' => '' !== $ability ? $ability : null, + 'status' => $status, + 'message' => '' !== $message ? $message : null, + 'round' => isset( $payload['round'] ) ? max( 1, (int) $payload['round'] ) : 1, + 'sequence' => isset( $payload['sequence'] ) ? max( 1, (int) $payload['sequence'] ) : 1, + 'requires_confirmation' => 'requires_confirmation' === $status, ], - ], - 200 + ] ); + + return false; + } + + /** + * Emit the normalized chat response through the SSE stream. + * + * @param array $response_data Normalized response payload. + * @param bool $saw_delta Whether live token deltas were emitted. + */ + private function emit_stream_response_data( array $response_data, bool $saw_delta ): void { + $meta = isset( $response_data['meta'] ) && is_array( $response_data['meta'] ) + ? $response_data['meta'] + : []; + $reply = isset( $response_data['reply'] ) ? trim( (string) $response_data['reply'] ) : ''; + $is_command_response = isset( $meta['command']['name'] ) && is_string( $meta['command']['name'] ) && '' !== trim( $meta['command']['name'] ); + $role = $is_command_response ? 'system' : 'assistant'; + + if ( isset( $meta['command']['effects']['clear_history'] ) && true === $meta['command']['effects']['clear_history'] ) { + self::send_stream_frame( 'history_reset', [] ); + } + + if ( isset( $meta['suggestions'] ) && is_array( $meta['suggestions'] ) ) { + self::send_stream_frame( + 'suggestions', + [ + 'items' => array_values( $meta['suggestions'] ), + ] + ); + } + + if ( isset( $meta['context'] ) && is_array( $meta['context'] ) ) { + self::send_stream_frame( + 'context_usage', + [ + 'context' => $meta['context'], + ] + ); + } + + if ( isset( $meta['error'] ) && is_array( $meta['error'] ) ) { + self::send_stream_frame( + 'error', + [ + 'error' => isset( $meta['error']['message'] ) && is_string( $meta['error']['message'] ) && '' !== trim( $meta['error']['message'] ) + ? trim( $meta['error']['message'] ) + : __( 'Chat request failed.', 'clawpress' ), + 'type' => isset( $meta['error']['type'] ) && is_string( $meta['error']['type'] ) && '' !== trim( $meta['error']['type'] ) + ? trim( $meta['error']['type'] ) + : 'provider', + 'card' => isset( $meta['card'] ) && is_array( $meta['card'] ) ? $meta['card'] : null, + ] + ); + } elseif ( isset( $meta['card'] ) && is_array( $meta['card'] ) ) { + self::send_stream_frame( + 'response_card', + [ + 'card' => $meta['card'], + 'text' => $reply, + 'role' => $role, + ] + ); + } elseif ( '' !== $reply && ( ! $saw_delta || 'system' === $role ) ) { + self::send_stream_frame( + 'response_message', + [ + 'text' => $reply, + 'role' => $role, + ] + ); + } + + if ( 'in_progress' === (string) ( $meta['status'] ?? '' ) ) { + self::send_stream_frame( + 'in_progress', + [ + 'run_id' => isset( $meta['run_id'] ) ? (int) $meta['run_id'] : 0, + 'events_cursor' => isset( $meta['events_cursor'] ) ? (int) $meta['events_cursor'] : 0, + 'status' => 'in_progress', + 'initial_reply' => $reply, + ] + ); + } + } + + /** + * Start the streaming response. + */ + private function start_stream_response(): void { + ignore_user_abort( true ); + set_time_limit( 0 ); + + while ( ob_get_level() ) { + ob_end_clean(); + } + + nocache_headers(); + header( 'Content-Type: text/event-stream; charset=' . get_option( 'blog_charset' ) ); + header( 'Cache-Control: no-cache, no-transform' ); + header( 'X-Accel-Buffering: no' ); + header( 'Connection: keep-alive' ); + header( 'Content-Encoding: identity' ); + header( 'X-Content-Type-Options: nosniff' ); + + @ini_set( 'zlib.output_compression', '0' ); + @ini_set( 'output_buffering', '0' ); + @ini_set( 'implicit_flush', '1' ); + @ini_set( 'output_handler', '' ); + + if ( function_exists( 'apache_setenv' ) ) { + @apache_setenv( 'no-gzip', '1' ); + @apache_setenv( 'dont-vary', '1' ); + } + + if ( function_exists( 'ob_implicit_flush' ) ) { + @ob_implicit_flush( true ); + } + + echo ':' . str_repeat( ' ', 4096 ) . "\n\n"; + flush(); + } + + /** + * Send one SSE frame to the browser. + * + * @param string $type Event type. + * @param array $payload Event payload. + */ + private static function send_stream_frame( string $type, array $payload ): void { + $frame = wp_json_encode( + [ + 'type' => $type, + 'payload' => $payload, + ] + ); + + if ( ! is_string( $frame ) ) { + return; + } + + echo "event: {$type}\n"; + echo 'data: ' . $frame . "\n\n"; + echo ':' . str_repeat( ' ', 2048 ) . "\n\n"; + + if ( function_exists( 'ob_flush' ) ) { + @ob_flush(); + } + + flush(); } /** diff --git a/includes/transports/class-agent-event-sink.php b/includes/transports/class-agent-event-sink.php new file mode 100644 index 0000000..dad8ae0 --- /dev/null +++ b/includes/transports/class-agent-event-sink.php @@ -0,0 +1,142 @@ +live_event_callback = $live_event_callback; + $this->run_id = $run_id; + $this->session_id = $session_id; + $this->persist_delta_events = $persist_delta_events; + + if ( $run_id > 0 || $session_id > 0 ) { + $this->event_helper = Agent_Event_Helper::get_instance(); + } + } + + /** + * Emit one runtime event. + * + * @param array $event Event payload. + */ + public function emit( array $event ): void { + if ( null !== $this->live_event_callback ) { + call_user_func( $this->live_event_callback, $event ); + } + + if ( ! $this->should_persist_event( $event ) ) { + return; + } + + $event_type = isset( $event['type'] ) ? (string) $event['type'] : 'agent.event'; + $payload = isset( $event['payload'] ) && is_array( $event['payload'] ) + ? $event['payload'] + : []; + + $event_id = $this->event_helper->emit( + $event_type, + [ + 'run_id' => $this->run_id, + 'session_id' => $this->session_id, + 'payload' => $payload, + ] + ); + + if ( $event_id > 0 ) { + $this->last_event_id = $event_id; + } + } + + /** + * Get the last persisted event id. + */ + public function get_last_event_id(): int { + return $this->last_event_id; + } + + /** + * Close the sink. + */ + public function close(): void {} + + /** + * Determine whether an event should be persisted. + * + * @param array $event Event payload. + */ + private function should_persist_event( array $event ): bool { + if ( null === $this->event_helper ) { + return false; + } + + if ( $this->persist_delta_events ) { + return true; + } + + return 'agent.llm.delta' !== (string) ( $event['type'] ?? '' ); + } +} diff --git a/includes/transports/class-null-transport.php b/includes/transports/class-null-transport.php deleted file mode 100644 index 008e1c7..0000000 --- a/includes/transports/class-null-transport.php +++ /dev/null @@ -1,31 +0,0 @@ - $event Event payload. - */ - public function emit( array $event ): void { - unset( $event ); - } - - /** - * Close transport. - */ - public function close(): void {} -} diff --git a/includes/transports/class-polling-transport.php b/includes/transports/class-polling-transport.php deleted file mode 100644 index e20efcc..0000000 --- a/includes/transports/class-polling-transport.php +++ /dev/null @@ -1,96 +0,0 @@ -event_helper = Agent_Event_Helper::get_instance(); - $this->run_id = $run_id; - $this->session_id = $session_id; - } - - /** - * Emit one event. - * - * @param array $event Event payload. - */ - public function emit( array $event ): void { - $event_type = isset( $event['type'] ) ? (string) $event['type'] : 'agent.event'; - $payload = isset( $event['payload'] ) && is_array( $event['payload'] ) - ? $event['payload'] - : []; - - $event_id = $this->event_helper->emit( - $event_type, - [ - 'run_id' => $this->run_id, - 'session_id' => $this->session_id, - 'payload' => $payload, - ] - ); - - if ( $event_id > 0 ) { - $this->last_event_id = $event_id; - } - } - - /** - * Close transport. - */ - public function close(): void {} - - /** - * Get last emitted event id. - */ - public function get_last_event_id(): int { - return $this->last_event_id; - } -} diff --git a/includes/transports/interface-agent-transport.php b/includes/transports/interface-agent-transport.php deleted file mode 100644 index ae20aab..0000000 --- a/includes/transports/interface-agent-transport.php +++ /dev/null @@ -1,29 +0,0 @@ - $event Event payload. - */ - public function emit( array $event ): void; - - /** - * Close transport resources. - */ - public function close(): void; -} diff --git a/languages/clawpress.pot b/languages/clawpress.pot index 85b1c8e..64990f7 100644 --- a/languages/clawpress.pot +++ b/languages/clawpress.pot @@ -9,7 +9,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-03-06T16:59:50+00:00\n" +"POT-Creation-Date: 2026-04-19T10:37:13+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: clawpress\n" @@ -238,19 +238,19 @@ msgid "Background follow-up slices are disabled for this trigger." msgstr "" #: includes/class-agent-runner.php:457 -#: includes/helpers/class-chat-helper.php:232 +#: includes/helpers/class-chat-helper.php:240 msgid "Agent run failed." msgstr "" #: includes/class-agent-runner.php:496 #: includes/helpers/class-agent-loop-helper.php:337 -#: src/panel/services/realClient.js:643 +#: src/panel/services/realClient.js:672 msgid "Action requires confirmation before continuing." msgstr "" #: includes/class-agent-runner.php:497 #: includes/helpers/class-agent-loop-helper.php:341 -#: src/panel/services/realClient.js:654 +#: src/panel/services/realClient.js:683 msgid "I finished the background steps, but I did not receive a final text response. Please tell me to continue and I will pick up from here." msgstr "" @@ -327,7 +327,7 @@ msgstr "" msgid "No agent memories found in Trash." msgstr "" -#: includes/class-security.php:74 +#: includes/class-security.php:76 msgid "The requesting user is not allowed to use ClawPress." msgstr "" @@ -355,12 +355,12 @@ msgid "Confirmed batch `%1$s`. Executing %2$d tool call(s):" msgstr "" #: includes/commands/class-commands.php:245 -#: src/panel/Panel.jsx:180 +#: src/panel/Panel.jsx:177 msgid "success" msgstr "" #: includes/commands/class-commands.php:246 -#: src/panel/Panel.jsx:182 +#: src/panel/Panel.jsx:179 msgid "error" msgstr "" @@ -699,9 +699,9 @@ msgstr "" #: includes/commands/handlers/class-setup-command-handler.php:395 #: includes/commands/handlers/class-test-command-handler.php:207 -#: includes/helpers/class-agent-loop-helper.php:268 -#: includes/helpers/class-chat-helper.php:537 -#: includes/helpers/class-chat-helper.php:590 +#: includes/helpers/class-agent-loop-helper.php:267 +#: includes/helpers/class-chat-helper.php:545 +#: includes/helpers/class-chat-helper.php:598 msgid "Unknown provider error." msgstr "" @@ -1127,133 +1127,137 @@ msgstr "" msgid "an unknown version" msgstr "" -#: includes/helpers/class-abilities-helper.php:345 +#: includes/helpers/class-abilities-helper.php:540 msgid "The requested tool is not registered." msgstr "" -#: includes/helpers/class-abilities-helper.php:358 +#: includes/helpers/class-abilities-helper.php:553 msgid "The requested ability is disabled in settings." msgstr "" -#: includes/helpers/class-abilities-helper.php:373 +#: includes/helpers/class-abilities-helper.php:568 msgid "The requested ability is not registered." msgstr "" -#: includes/helpers/class-abilities-helper.php:405 +#: includes/helpers/class-abilities-helper.php:600 msgid "Tool execution is blocked by runtime policy." msgstr "" -#: includes/helpers/class-abilities-helper.php:428 +#: includes/helpers/class-abilities-helper.php:623 msgid "Destructive tools are not allowed for this runtime trigger." msgstr "" -#: includes/helpers/class-abilities-helper.php:451 +#: includes/helpers/class-abilities-helper.php:646 msgid "File delete is blocked by runtime policy." msgstr "" -#: includes/helpers/class-abilities-helper.php:478 +#: includes/helpers/class-abilities-helper.php:673 msgid "This destructive action is pending batch confirmation." msgstr "" -#: includes/helpers/class-abilities-helper.php:503 +#: includes/helpers/class-abilities-helper.php:698 msgid "Explicit confirmation is required for this destructive action." msgstr "" #. translators: %d: maximum number of tool calls executed per round. -#: includes/helpers/class-agent-loop-helper.php:916 +#: includes/helpers/class-agent-loop-helper.php:922 #, php-format msgid "Tool call deferred because the per-round limit of %d was reached. Retry this call in the next round." msgstr "" #. translators: 1: tool name, 2: batch ID, 3: expiry time -#: includes/helpers/class-agent-loop-helper.php:1207 +#: includes/helpers/class-agent-loop-helper.php:1213 #, php-format msgid "Confirm batch `%2$s` to run `%1$s`. This batch expires at %3$s." msgstr "" #. translators: 1: total destructive calls, 2: batch ID, 3: comma-separated tool names, 4: expiry time -#: includes/helpers/class-agent-loop-helper.php:1215 +#: includes/helpers/class-agent-loop-helper.php:1221 #, php-format msgid "This reply queued %1$d destructive tool calls in batch `%2$s` (%3$s). Use Confirm All to execute the entire batch. This batch expires at %4$s." msgstr "" -#: includes/helpers/class-agent-loop-helper.php:1218 +#: includes/helpers/class-agent-loop-helper.php:1224 msgid "tools" msgstr "" -#: includes/helpers/class-agent-loop-helper.php:1226 +#: includes/helpers/class-agent-loop-helper.php:1232 #: src/panel/components/cards/UserConfirmationCard.jsx:8 msgid "User Confirmation Required" msgstr "" -#: includes/helpers/class-agent-loop-helper.php:1227 +#: includes/helpers/class-agent-loop-helper.php:1233 msgid "Destructive batch pending" msgstr "" -#: includes/helpers/class-agent-loop-helper.php:1233 +#: includes/helpers/class-agent-loop-helper.php:1239 msgid "Confirm All" msgstr "" -#: includes/helpers/class-agent-loop-helper.php:1234 +#: includes/helpers/class-agent-loop-helper.php:1240 msgid "Confirm Action" msgstr "" -#: includes/helpers/class-agent-loop-helper.php:1240 +#: includes/helpers/class-agent-loop-helper.php:1246 msgid "Decline" msgstr "" -#: includes/helpers/class-chat-helper.php:366 +#: includes/helpers/class-agent-loop-helper.php:1579 +msgid "AI client did not return a generative result." +msgstr "" + +#: includes/helpers/class-chat-helper.php:374 msgid "Unable to persist paused run state." msgstr "" -#: includes/helpers/class-chat-helper.php:376 +#: includes/helpers/class-chat-helper.php:384 msgid "Unable to release session after pausing run." msgstr "" -#: includes/helpers/class-chat-helper.php:388 +#: includes/helpers/class-chat-helper.php:396 #: src/panel/components/PanelMessages.jsx:45 -#: src/panel/Panel.jsx:636 +#: src/panel/Panel.jsx:649 #: src/panel/services/realClient.js:7 msgid "I am still working on this" msgstr "" -#: includes/helpers/class-chat-helper.php:462 +#: includes/helpers/class-chat-helper.php:470 msgid "A destructive action is waiting for your confirmation." msgstr "" -#: includes/helpers/class-chat-helper.php:465 +#: includes/helpers/class-chat-helper.php:473 msgid "Action required." msgstr "" #. translators: %s: the original user message -#: includes/helpers/class-chat-helper.php:476 +#: includes/helpers/class-chat-helper.php:484 #, php-format msgid "Offline mode: no configured AI provider was available. You said: \"%s\"" msgstr "" #. translators: %s: provider/transport error message -#: includes/helpers/class-chat-helper.php:553 -#: includes/helpers/class-chat-helper.php:602 +#: includes/helpers/class-chat-helper.php:561 +#: includes/helpers/class-chat-helper.php:610 #, php-format msgid "AI request failed: %s" msgstr "" -#: includes/helpers/class-chat-helper.php:569 -#: includes/helpers/class-chat-helper.php:618 +#: includes/helpers/class-chat-helper.php:577 +#: includes/helpers/class-chat-helper.php:626 #: src/panel/components/cards/ErrorCard.jsx:7 -#: src/panel/Panel.jsx:144 +#: src/panel/Panel.jsx:141 msgid "Request Error" msgstr "" -#: includes/helpers/class-chat-helper.php:571 -#: includes/helpers/class-chat-helper.php:620 -#: src/panel/Panel.jsx:131 +#: includes/helpers/class-chat-helper.php:579 +#: includes/helpers/class-chat-helper.php:628 +#: src/panel/Panel.jsx:128 msgid "Request timed out" msgstr "" -#: includes/helpers/class-chat-helper.php:572 -#: includes/helpers/class-chat-helper.php:621 -#: src/panel/Panel.jsx:133 +#: includes/helpers/class-chat-helper.php:580 +#: includes/helpers/class-chat-helper.php:629 +#: src/panel/Panel.jsx:130 msgid "Provider error" msgstr "" @@ -1310,23 +1314,17 @@ msgstr "" msgid "Unable to enqueue run." msgstr "" -#: includes/rest/class-chat-controller.php:112 +#: includes/rest/class-chat-controller.php:128 +#: includes/rest/class-chat-controller.php:153 msgid "Message is required." msgstr "" -#: includes/rest/class-settings-controller.php:147 -#: src/js/admin/components/views/SettingsView.js:103 -msgid "OpenAI" -msgstr "" - -#: includes/rest/class-settings-controller.php:151 -#: src/js/admin/components/views/SettingsView.js:107 -msgid "Anthropic" -msgstr "" - -#: includes/rest/class-settings-controller.php:155 -#: src/js/admin/components/views/SettingsView.js:111 -msgid "Google" +#: includes/rest/class-chat-controller.php:179 +#: includes/rest/class-chat-controller.php:421 +#: src/panel/Panel.jsx:146 +#: src/panel/services/realClient.js:917 +#: src/panel/services/realClient.js:1013 +msgid "Chat request failed." msgstr "" #: src/js/admin/components/App.js:24 @@ -1416,6 +1414,18 @@ msgstr "" msgid "Showing ClawPress Agent Files." msgstr "" +#: src/js/admin/components/views/SettingsView.js:103 +msgid "OpenAI" +msgstr "" + +#: src/js/admin/components/views/SettingsView.js:107 +msgid "Anthropic" +msgstr "" + +#: src/js/admin/components/views/SettingsView.js:111 +msgid "Google" +msgstr "" + #: src/js/admin/components/views/SettingsView.js:577 msgid "Unable to load settings." msgstr "" @@ -1664,12 +1674,12 @@ msgid "Please confirm or decline this action." msgstr "" #: src/panel/components/cards/WelcomeCard.jsx:8 -#: src/panel/Panel.jsx:829 +#: src/panel/Panel.jsx:787 msgid "Welcome to ClawPress" msgstr "" #: src/panel/components/cards/WelcomeCard.jsx:12 -#: src/panel/Panel.jsx:833 +#: src/panel/Panel.jsx:791 msgid "Hello! I am ready to help with your WordPress tasks." msgstr "" @@ -1691,7 +1701,7 @@ msgid "Tool Error" msgstr "" #: src/panel/components/MockPanel.jsx:21 -#: src/panel/Panel.jsx:139 +#: src/panel/Panel.jsx:136 msgid "Error" msgstr "" @@ -1893,72 +1903,66 @@ msgstr "" msgid "Sample Post" msgstr "" -#: src/panel/Panel.jsx:132 +#: src/panel/Panel.jsx:129 msgid "Network or API request error" msgstr "" -#: src/panel/Panel.jsx:149 -#: src/panel/services/realClient.js:810 -#: src/panel/services/realClient.js:881 -msgid "Chat request failed." -msgstr "" - -#: src/panel/Panel.jsx:184 +#: src/panel/Panel.jsx:181 msgid "confirmation required" msgstr "" #. translators: 1: tool name, 2: tool call status -#: src/panel/Panel.jsx:189 +#: src/panel/Panel.jsx:186 #, js-format msgid "Tool call `%1$s` (%2$s)" msgstr "" #. translators: 1: tool call position, 2: total tool calls, 3: tool summary text -#: src/panel/Panel.jsx:200 +#: src/panel/Panel.jsx:197 #, js-format msgid "[%1$d/%2$d] %3$s" msgstr "" -#: src/panel/Panel.jsx:621 +#: src/panel/Panel.jsx:634 msgid "Preparing tool plan…" msgstr "" -#: src/panel/Panel.jsx:643 +#: src/panel/Panel.jsx:656 msgid "Stream error." msgstr "" -#: src/panel/Panel.jsx:841 +#: src/panel/Panel.jsx:799 msgid "Start Setup" msgstr "" -#: src/panel/Panel.jsx:869 +#: src/panel/Panel.jsx:827 msgid "Unable to load chat history." msgstr "" -#: src/panel/Panel.jsx:940 +#: src/panel/Panel.jsx:898 msgid "Stream stopped." msgstr "" -#: src/panel/Panel.jsx:982 +#: src/panel/Panel.jsx:940 msgid "Tool execution failed." msgstr "" -#: src/panel/Panel.jsx:1049 -#: src/panel/Panel.jsx:1073 +#: src/panel/Panel.jsx:1007 +#: src/panel/Panel.jsx:1031 msgid "Invalid card action." msgstr "" -#: src/panel/Panel.jsx:1060 +#: src/panel/Panel.jsx:1018 msgid "Invalid card URL." msgstr "" #. translators: %s: selected mock scenario -#: src/panel/Panel.jsx:1116 +#: src/panel/Panel.jsx:1074 #, js-format msgid "Mock: %s" msgstr "" -#: src/panel/Panel.jsx:1206 +#: src/panel/Panel.jsx:1164 msgid "Resize panel" msgstr "" @@ -1968,23 +1972,27 @@ msgstr "" msgid "Request failed (%d)" msgstr "" -#: src/panel/services/realClient.js:580 +#: src/panel/services/realClient.js:609 msgid "Run failed." msgstr "" -#: src/panel/services/realClient.js:667 +#: src/panel/services/realClient.js:696 msgid "Run ended with an error." msgstr "" -#: src/panel/services/realClient.js:698 +#: src/panel/services/realClient.js:727 msgid "Run polling timed out before a terminal status was received." msgstr "" -#: src/panel/services/realClient.js:863 -msgid "Run entered progress mode without a valid run ID." +#: src/panel/services/realClient.js:806 +msgid "Streaming request failed." +msgstr "" + +#: src/panel/services/realClient.js:816 +msgid "Streaming endpoint returned a non-streaming response." msgstr "" -#: src/panel/services/realClient.js:893 +#: src/panel/services/realClient.js:1025 msgid "Tool execution is not available in chat mode." msgstr "" diff --git a/package.json b/package.json index 7b32d35..b684c70 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,11 @@ "LICENSE" ], "scripts": { + "agent-ci": "npx @redwoodjs/agent-ci", + "ci:agent": "npx @redwoodjs/agent-ci run --quiet --all --pause-on-failure", + "ci:agent:ci": "npx @redwoodjs/agent-ci run --quiet --pause-on-failure --workflow .github/workflows/ci.yml", + "ci:agent:retry": "npx @redwoodjs/agent-ci retry", + "ci:agent:abort": "npx @redwoodjs/agent-ci abort", "build": "npm run build:scripts && npm run build:panel && npm run build:pot", "build:scripts": "wp-scripts build --config webpack.scripts.config.js", "build:panel": "wp-scripts build --config webpack.panel.config.js", diff --git a/src/panel/Panel.jsx b/src/panel/Panel.jsx index 6db9795..7a031af 100644 --- a/src/panel/Panel.jsx +++ b/src/panel/Panel.jsx @@ -61,9 +61,6 @@ const Panel = () => { const [ showFloatingToggle, setShowFloatingToggle ] = useState( false ); const streamHandleRef = useRef( null ); const timelineRef = useRef( 0 ); - const eventQueueRef = useRef( [] ); - const isTypingRef = useRef( false ); - const typingTimerRef = useRef( null ); const panelStateSyncTimerRef = useRef( null ); const welcomeCardSeenRef = useRef( false ); const appendMessageRef = useRef( null ); @@ -554,6 +551,14 @@ const Panel = () => { requestStatus( true ); }; + const handleStreamEvent = ( eventType, parsed = {} ) => { + if ( 'suggestions' !== eventType && 'context_usage' !== eventType ) { + setWaitingForResponse( false ); + } + + processStreamEvent( eventType, parsed ); + }; + const processStreamEvent = ( eventType, parsed ) => { switch ( eventType ) { case 'delta': @@ -578,6 +583,14 @@ const Panel = () => { case 'response_card': clearEphemeralStatus(); if ( parsed?.card ) { + const streamedText = currentStreamTextRef.current || ''; + if ( parsed?.role !== 'system' && streamedText.trim() ) { + appendMessage( 'assistant', streamedText, parsed.card ); + setCurrentStreamText( '' ); + currentStreamTextRef.current = ''; + break; + } + appendMessage( parsed?.role === 'system' ? 'system' : 'assistant', typeof parsed?.text === 'string' ? parsed.text : '', @@ -652,61 +665,6 @@ const Panel = () => { } }; - const startTypingMessage = ( content ) => { - if ( ! content ) { - return; - } - isTypingRef.current = true; - setCurrentStreamText( '' ); - currentStreamTextRef.current = ''; - let index = 0; - const step = () => { - if ( index >= content.length ) { - appendMessage( 'assistant', content ); - setCurrentStreamText( '' ); - currentStreamTextRef.current = ''; - isTypingRef.current = false; - typingTimerRef.current = null; - processEventQueue(); - return; - } - const nextChunk = content.slice( index, index + 2 ); - index += 2; - setCurrentStreamText( ( prev ) => { - const next = prev + nextChunk; - currentStreamTextRef.current = next; - return next; - } ); - typingTimerRef.current = setTimeout( step, 30 ); - }; - step(); - }; - - const processEventQueue = () => { - if ( isTypingRef.current ) { - return; - } - const queue = eventQueueRef.current; - if ( ! queue.length ) { - return; - } - - const { type, payload } = queue.shift(); - if ( type === 'assistant_message' && payload?.content ) { - startTypingMessage( payload.content ); - return; - } - - processStreamEvent( type, payload ); - processEventQueue(); - }; - - const handleStreamEvent = ( eventType, parsed ) => { - setWaitingForResponse( false ); - eventQueueRef.current.push( { type: eventType, payload: parsed } ); - processEventQueue(); - }; - const buildClient = () => createAgentClient( { mockEnabled, diff --git a/src/panel/services/realClient.js b/src/panel/services/realClient.js index 822402c..e97c376 100644 --- a/src/panel/services/realClient.js +++ b/src/panel/services/realClient.js @@ -68,16 +68,45 @@ const requestJson = async ( { url, method = 'GET', nonce, body, signal } ) => { const isObjectRecord = ( value ) => Boolean( value ) && typeof value === 'object' && ! Array.isArray( value ); -const createRealClient = ( { restBase, nonce, onEvent, onDone, onError } ) => { - const sendMessage = ( message, signal ) => - requestJson( { - url: `${ restBase }/chat/message`, - method: 'POST', - nonce, - body: { message }, - signal, +const createRealClient = ( { + restBase, + nonce, + streamNonce, + onEvent, + onDone, + onError, +} ) => { + const parseSseBlock = ( block ) => { + const lines = block.split( '\n' ); + let type = 'message'; + const dataLines = []; + + lines.forEach( ( line ) => { + if ( ! line || line.startsWith( ':' ) ) { + return; + } + + if ( line.startsWith( 'event:' ) ) { + type = line.slice( 6 ).trim() || type; + return; + } + + if ( line.startsWith( 'data:' ) ) { + dataLines.push( line.slice( 5 ).trimStart() ); + } } ); + if ( dataLines.length === 0 ) { + return null; + } + + try { + return JSON.parse( dataLines.join( '\n' ) ); + } catch { + return null; + } + }; + const getRunStatus = ( runId, signal ) => requestJson( { url: `${ restBase }/agent/runs/${ runId }`, @@ -747,128 +776,231 @@ const createRealClient = ( { restBase, nonce, onEvent, onDone, onError } ) => { } }; - // Keep a stream-compatible interface for the existing panel flow. - const stream = ( prompt ) => { - const controller = new AbortController(); - const seenToolCallKeys = new Set(); - - ( async () => { - try { - const response = await sendMessage( prompt, controller.signal ); - const clearHistory = - response?.meta?.command?.effects && - response.meta.command.effects.clear_history === true; + const streamMessage = async ( message, signal, seenToolCallKeys ) => { + const response = await fetch( `${ restBase }/chat/stream`, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': streamNonce || nonce, + Accept: 'text/event-stream', + }, + body: JSON.stringify( { message } ), + signal, + } ); - if ( clearHistory ) { - onEvent( 'history_reset', {} ); - } + if ( ! response.ok ) { + const rawText = await response.text(); + let messageText = rawText; - if ( Array.isArray( response?.meta?.suggestions ) ) { - onEvent( 'suggestions', { - items: response.meta.suggestions, - } ); + if ( rawText ) { + try { + const parsed = JSON.parse( rawText ); + messageText = parsed?.message || parsed?.error || rawText; + } catch { + messageText = rawText; } + } - const contextUsage = normalizeContextUsage( - response?.meta?.context - ); - if ( contextUsage ) { - onEvent( 'context_usage', { context: contextUsage } ); - } + throw new Error( + messageText || __( 'Streaming request failed.', 'clawpress' ) + ); + } - const toolCalls = Array.isArray( response?.meta?.tool_calls ) - ? response.meta.tool_calls - .map( ( rawCall ) => normalizeToolCall( rawCall ) ) - .filter( Boolean ) - : []; + const contentType = response.headers.get( 'content-type' ) || ''; + if ( + ! response.body || + ! contentType.toLowerCase().includes( 'text/event-stream' ) + ) { + throw new Error( + __( + 'Streaming endpoint returned a non-streaming response.', + 'clawpress' + ) + ); + } - toolCalls.forEach( ( call, index ) => { - emitToolCallIfNew( - call, - index + 1, - toolCalls.length, - seenToolCallKeys - ); - } ); + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + let streamedText = ''; + let continuation = null; + + const handleParsedFrame = ( frame ) => { + const type = + frame && typeof frame.type === 'string' ? frame.type : ''; + const payload = + frame?.payload && typeof frame.payload === 'object' + ? frame.payload + : {}; - const responseError = - response?.meta?.error && - typeof response.meta.error === 'object' - ? response.meta.error - : null; - const responseCard = - response?.meta?.card && - typeof response.meta.card === 'object' - ? normalizeCard( response.meta.card ) - : null; - - if ( responseError ) { - const errorMessage = - typeof responseError.message === 'string' && - responseError.message.trim() - ? responseError.message.trim() - : __( 'Chat request failed.', 'clawpress' ); + switch ( type ) { + case 'delta': + if ( + typeof payload.text === 'string' && + payload.text.length > 0 + ) { + streamedText += payload.text; + onEvent( 'delta', { text: payload.text } ); + } + break; + case 'tool_call': { + const call = normalizeToolCall( payload.call ); + if ( call ) { + emitToolCallIfNew( + call, + Number.isFinite( Number( payload.index ) ) + ? Number( payload.index ) + : undefined, + Number.isFinite( Number( payload.total ) ) + ? Number( payload.total ) + : undefined, + seenToolCallKeys + ); + } + break; + } + case 'history_reset': + onEvent( 'history_reset', {} ); + break; + case 'suggestions': + if ( Array.isArray( payload.items ) ) { + onEvent( 'suggestions', { + items: payload.items, + } ); + } + break; + case 'context_usage': { + const context = normalizeContextUsage( payload.context ); + if ( context ) { + onEvent( 'context_usage', { context } ); + } + break; + } + case 'response_card': { + const card = normalizeCard( payload.card ); + if ( card ) { + onEvent( 'response_card', { + card, + text: + typeof payload.text === 'string' + ? payload.text + : '', + role: + payload.role === 'system' + ? 'system' + : 'assistant', + } ); + } + break; + } + case 'response_message': + if ( + typeof payload.text === 'string' && + payload.text.trim() + ) { + onEvent( 'response_message', { + text: payload.text.trim(), + role: + payload.role === 'system' + ? 'system' + : 'assistant', + } ); + } + break; + case 'error': onEvent( 'error', { - error: errorMessage, + error: + typeof payload.error === 'string' && + payload.error.trim() + ? payload.error.trim() + : __( 'Chat request failed.', 'clawpress' ), type: - typeof responseError.type === 'string' && - responseError.type.trim() - ? responseError.type.trim() - : 'provider', - card: responseCard, + typeof payload.type === 'string' && + payload.type.trim() + ? payload.type.trim() + : 'request', + card: normalizeCard( payload.card ), } ); - onDone?.( { aborted: false } ); - return; + break; + case 'in_progress': { + const runId = Number( payload.run_id ); + if ( Number.isFinite( runId ) && runId > 0 ) { + continuation = { + runId, + initialEventsCursor: Number( + payload.events_cursor + ), + initialReply: + streamedText || + ( typeof payload.initial_reply === 'string' + ? payload.initial_reply.trim() + : '' ), + }; + } + break; } + } + }; - const reply = - typeof response?.reply === 'string' - ? response.reply.trim() - : ''; + while ( true ) { + const { done, value } = await reader.read(); - const isCommandResponse = Boolean( - response?.meta?.command?.name - ); - const card = normalizeCard( response?.meta?.card ); + buffer += decoder.decode( value || new Uint8Array(), { + stream: ! done, + } ); - if ( card ) { - onEvent( 'response_card', { - card, - text: reply, - role: isCommandResponse ? 'system' : 'assistant', - } ); - } else if ( reply ) { - onEvent( 'response_message', { - text: reply, - role: isCommandResponse ? 'system' : 'assistant', - } ); + const blocks = buffer.split( '\n\n' ); + buffer = blocks.pop() || ''; + + blocks.forEach( ( block ) => { + const frame = parseSseBlock( block ); + if ( frame ) { + handleParsedFrame( frame ); } + } ); - const runStatus = normalizeRunStatus( response?.meta?.status ); - const runId = Number( response?.meta?.run_id ); - const initialEventsCursor = Number( - response?.meta?.events_cursor - ); - if ( runStatus === 'in_progress' ) { - if ( Number.isFinite( runId ) && runId > 0 ) { - await pollRunUntilTerminal( { - runId, - initialReply: reply, - signal: controller.signal, - initialEventsCursor, - seenToolCallKeys, - } ); - } else { - onEvent( 'error', { - error: __( - 'Run entered progress mode without a valid run ID.', - 'clawpress' - ), - type: 'request', - } ); + if ( done ) { + if ( buffer.trim() ) { + const frame = parseSseBlock( buffer ); + if ( frame ) { + handleParsedFrame( frame ); } } + break; + } + } + + return { + initialReply: streamedText, + continuation, + }; + }; + + const stream = ( prompt ) => { + const controller = new AbortController(); + const seenToolCallKeys = new Set(); + + ( async () => { + try { + const streamed = await streamMessage( + prompt, + controller.signal, + seenToolCallKeys + ); + + if ( streamed?.continuation ) { + await pollRunUntilTerminal( { + runId: streamed.continuation.runId, + initialReply: streamed.continuation.initialReply || '', + signal: controller.signal, + initialEventsCursor: + streamed.continuation.initialEventsCursor, + seenToolCallKeys, + } ); + } + onDone?.( { aborted: false } ); } catch ( err ) { if ( err?.name === 'AbortError' ) { diff --git a/tests/Support/AiClientStubs.php b/tests/Support/AiClientStubs.php new file mode 100644 index 0000000..2095746 --- /dev/null +++ b/tests/Support/AiClientStubs.php @@ -0,0 +1,688 @@ +text = $text; + $this->function_call = $function_call; + } + + public function getText(): ?string { + return $this->text; + } + + public function getFunctionCall(): ?\WordPress\AiClient\Tools\DTO\FunctionCall { + return $this->function_call; + } + + public function toArray(): array { + $data = []; + + if ( null !== $this->text ) { + $data['text'] = $this->text; + } + + if ( null !== $this->function_call ) { + $data['function_call'] = [ + 'id' => $this->function_call->getId(), + 'name' => $this->function_call->getName(), + 'args' => $this->function_call->getArgs(), + ]; + } + + return $data; + } + } + } + + if ( ! class_exists( __NAMESPACE__ . '\Message', false ) ) { + final class Message { + private MessageRole $role; + + /** @var array */ + private array $parts; + + /** + * @param array $parts Message parts. + */ + public function __construct( MessageRole $role, array $parts = [] ) { + $this->role = $role; + $this->parts = $parts; + } + + public function getRole(): MessageRole { + return $this->role; + } + + /** + * @return array + */ + public function getParts(): array { + return $this->parts; + } + + public function toArray(): array { + $content = []; + + foreach ( $this->parts as $part ) { + $content[] = $part->toArray(); + } + + return [ + 'role' => $this->role->value, + 'content' => $content, + ]; + } + } + } +} + +namespace WordPress\AiClient\Tools\DTO { + + if ( ! class_exists( __NAMESPACE__ . '\FunctionDeclaration', false ) ) { + final class FunctionDeclaration { + private string $name; + + private string $description; + + /** @var array|null */ + private ?array $parameters; + + /** + * @param array|null $parameters Parameters schema. + */ + public function __construct( string $name, string $description = '', ?array $parameters = null ) { + $this->name = $name; + $this->description = $description; + $this->parameters = $parameters; + } + + public function getName(): string { + return $this->name; + } + + public function getDescription(): string { + return $this->description; + } + + /** + * @return array|null + */ + public function getParameters(): ?array { + return $this->parameters; + } + } + } + + if ( ! class_exists( __NAMESPACE__ . '\FunctionCall', false ) ) { + final class FunctionCall { + private string $id; + + private string $name; + + /** @var array */ + private array $args; + + /** + * @param array $args Function call arguments. + */ + public function __construct( string $id = '', string $name = '', array $args = [] ) { + $this->id = $id; + $this->name = $name; + $this->args = $args; + } + + public function getId(): string { + return $this->id; + } + + public function getName(): string { + return $this->name; + } + + /** + * @return array + */ + public function getArgs(): array { + return $this->args; + } + } + } + + if ( ! class_exists( __NAMESPACE__ . '\FunctionResponse', false ) ) { + final class FunctionResponse { + private string $id; + + private string $name; + + /** @var array */ + private array $response; + + /** + * @param array $response Function response payload. + */ + public function __construct( string $id, string $name, array $response ) { + $this->id = $id; + $this->name = $name; + $this->response = $response; + } + + public function getId(): string { + return $this->id; + } + + public function getName(): string { + return $this->name; + } + + /** + * @return array + */ + public function getResponse(): array { + return $this->response; + } + } + } +} + +namespace WordPress\AiClient\Results\DTO { + + if ( ! class_exists( __NAMESPACE__ . '\TokenUsage', false ) ) { + final class TokenUsage { + private int $prompt_tokens; + + private int $completion_tokens; + + private int $total_tokens; + + public function __construct( int $prompt_tokens = 0, int $completion_tokens = 0, int $total_tokens = 0 ) { + $this->prompt_tokens = $prompt_tokens; + $this->completion_tokens = $completion_tokens; + $this->total_tokens = $total_tokens; + } + + public function getPromptTokens(): int { + return $this->prompt_tokens; + } + + public function getCompletionTokens(): int { + return $this->completion_tokens; + } + + public function getTotalTokens(): int { + return $this->total_tokens; + } + } + } + + if ( ! class_exists( __NAMESPACE__ . '\GenerativeAiResult', false ) ) { + final class GenerativeAiResult { + private \WordPress\AiClient\Messages\DTO\Message $message; + + private TokenUsage $token_usage; + + public function __construct( ?\WordPress\AiClient\Messages\DTO\Message $message = null, ?TokenUsage $token_usage = null ) { + $this->message = $message ?? new \WordPress\AiClient\Messages\DTO\Message( + \WordPress\AiClient\Messages\DTO\MessageRole::MODEL, + [ + new \WordPress\AiClient\Messages\DTO\MessagePart( '' ), + ] + ); + $this->token_usage = $token_usage ?? new TokenUsage(); + } + + public function toMessage(): \WordPress\AiClient\Messages\DTO\Message { + return $this->message; + } + + public function getTokenUsage(): TokenUsage { + return $this->token_usage; + } + } + } +} + +namespace WordPress\AiClient\Providers\Http\DTO { + + if ( ! class_exists( __NAMESPACE__ . '\RequestOptions', false ) ) { + final class RequestOptions { + private float $timeout = 0.0; + + private float $connect_timeout = 0.0; + + private int $max_redirects = 0; + + public function setTimeout( float $timeout ): self { + $this->timeout = $timeout; + return $this; + } + + public function setConnectTimeout( float $connect_timeout ): self { + $this->connect_timeout = $connect_timeout; + return $this; + } + + public function setMaxRedirects( int $max_redirects ): self { + $this->max_redirects = $max_redirects; + return $this; + } + + public function getTimeout(): float { + return $this->timeout; + } + + public function getConnectTimeout(): float { + return $this->connect_timeout; + } + + public function getMaxRedirects(): int { + return $this->max_redirects; + } + } + } +} + +namespace WordPress\AiClient\Providers\Models\DTO { + + if ( ! class_exists( __NAMESPACE__ . '\ModelConfig', false ) ) { + final class ModelConfig { + /** @var array */ + private array $data; + + /** + * @param array $data Config payload. + */ + private function __construct( array $data ) { + $this->data = $data; + } + + /** + * @param array $data Config payload. + */ + public static function fromArray( array $data ): self { + return new self( $data ); + } + + /** + * @return array + */ + public function toArray(): array { + return $this->data; + } + } + } + + if ( ! class_exists( __NAMESPACE__ . '\ModelMetadata', false ) ) { + final class ModelMetadata { + private string $id; + + private string $name; + + public function __construct( string $id, string $name = '' ) { + $this->id = $id; + $this->name = $name; + } + + public function getId(): string { + return $this->id; + } + + public function getName(): string { + return $this->name; + } + } + } +} + +namespace WordPress\AiClient\Providers\Contracts { + + if ( ! interface_exists( __NAMESPACE__ . '\ProviderAvailabilityInterface', false ) ) { + interface ProviderAvailabilityInterface { + public function isConfigured(): bool; + } + } + + if ( ! interface_exists( __NAMESPACE__ . '\ModelMetadataDirectoryInterface', false ) ) { + interface ModelMetadataDirectoryInterface { + /** + * @return array + */ + public function listModelMetadata(): array; + } + } +} + +namespace WordPress\AiClient\Providers\Http\Contracts { + + if ( ! interface_exists( __NAMESPACE__ . '\ClientWithOptionsInterface', false ) ) { + interface ClientWithOptionsInterface { + public function sendRequestWithOptions( \WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface $request, \WordPress\AiClient\Providers\Http\DTO\RequestOptions $options ): \WordPress\AiClientDependencies\Psr\Http\Message\ResponseInterface; + } + } +} + +namespace WordPress\AiClient\Providers\Http\Abstracts { + + if ( ! class_exists( __NAMESPACE__ . '\AbstractClientDiscoveryStrategy', false ) ) { + abstract class AbstractClientDiscoveryStrategy { + public static function init(): void {} + } + } +} + +namespace WordPress\AiClient\Providers\Http\Exception { + + if ( ! class_exists( __NAMESPACE__ . '\NetworkException', false ) ) { + class NetworkException extends \RuntimeException {} + } +} + +namespace WordPress\AiClient\Providers\Http { + + if ( ! class_exists( __NAMESPACE__ . '\HttpTransporter', false ) ) { + class HttpTransporter {} + } +} + +namespace WordPress\AiClient\Providers { + + if ( ! class_exists( __NAMESPACE__ . '\ProviderRegistry', false ) ) { + final class ProviderRegistry { + /** + * @return array + */ + public function getRegisteredProviderIds(): array { + return []; + } + + public function hasProvider( string $provider ): bool { + unset( $provider ); + return false; + } + + public function getProviderClassName( string $provider ): string { + unset( $provider ); + return ''; + } + + public function getHttpTransporter(): ?object { + return null; + } + + public function getProviderModel( string $provider, string $model, \WordPress\AiClient\Providers\Models\DTO\ModelConfig $config ): object { + unset( $provider, $model, $config ); + + return new class() { + public function metadata(): object { + return new class() { + /** + * @return array + */ + public function getSupportedOptions(): array { + return []; + } + }; + } + }; + } + } + } +} + +namespace WordPress\AiClient\Builders { + + if ( ! class_exists( __NAMESPACE__ . '\MessageBuilder', false ) ) { + final class MessageBuilder { + private string $content; + + private \WordPress\AiClient\Messages\DTO\MessageRole $role; + + private ?\WordPress\AiClient\Tools\DTO\FunctionResponse $function_response = null; + + public function __construct( string $content = '' ) { + $this->content = $content; + $this->role = \WordPress\AiClient\Messages\DTO\MessageRole::USER; + } + + public function usingUserRole(): self { + $this->role = \WordPress\AiClient\Messages\DTO\MessageRole::USER; + return $this; + } + + public function usingModelRole(): self { + $this->role = \WordPress\AiClient\Messages\DTO\MessageRole::MODEL; + return $this; + } + + public function usingSystemRole(): self { + $this->role = \WordPress\AiClient\Messages\DTO\MessageRole::SYSTEM; + return $this; + } + + public function withFunctionResponse( \WordPress\AiClient\Tools\DTO\FunctionResponse $function_response ): self { + $this->function_response = $function_response; + return $this; + } + + public function get(): \WordPress\AiClient\Messages\DTO\Message { + $parts = []; + + if ( '' !== $this->content ) { + $parts[] = new \WordPress\AiClient\Messages\DTO\MessagePart( $this->content ); + } + + if ( null !== $this->function_response ) { + $payload = wp_json_encode( $this->function_response->getResponse() ); + $parts[] = new \WordPress\AiClient\Messages\DTO\MessagePart( false === $payload ? '' : (string) $payload ); + } + + return new \WordPress\AiClient\Messages\DTO\Message( $this->role, $parts ); + } + } + } + + if ( ! class_exists( __NAMESPACE__ . '\PromptBuilder', false ) ) { + class PromptBuilder { + /** + * @param mixed ...$args Unused test-only arguments. + */ + public function __construct( ...$args ) { + unset( $args ); + } + + public function usingProvider( string $provider ): self { + unset( $provider ); + return $this; + } + + public function usingSystemInstruction( string $system_prompt ): self { + unset( $system_prompt ); + return $this; + } + + public function usingModel( object $model ): self { + unset( $model ); + return $this; + } + + public function usingModelPreference( array $preference ): self { + unset( $preference ); + return $this; + } + + public function usingRequestOptions( \WordPress\AiClient\Providers\Http\DTO\RequestOptions $request_options ): self { + unset( $request_options ); + return $this; + } + + public function usingFunctionDeclarations( \WordPress\AiClient\Tools\DTO\FunctionDeclaration ...$tool_declarations ): self { + unset( $tool_declarations ); + return $this; + } + + public function usingModelConfig( \WordPress\AiClient\Providers\Models\DTO\ModelConfig $model_config ): self { + unset( $model_config ); + return $this; + } + + public function generateResult(): \WordPress\AiClient\Results\DTO\GenerativeAiResult { + return new \WordPress\AiClient\Results\DTO\GenerativeAiResult(); + } + } + } +} + +namespace WordPress\AiClient { + + if ( ! class_exists( __NAMESPACE__ . '\AiClient', false ) ) { + final class AiClient { + private static ?\WordPress\AiClient\Providers\ProviderRegistry $registry = null; + + public static function defaultRegistry(): \WordPress\AiClient\Providers\ProviderRegistry { + if ( null === self::$registry ) { + self::$registry = new \WordPress\AiClient\Providers\ProviderRegistry(); + } + + return self::$registry; + } + + /** + * @param mixed ...$args Prompt payload. + */ + public static function prompt( ...$args ): \WordPress\AiClient\Builders\PromptBuilder { + return new \WordPress\AiClient\Builders\PromptBuilder( ...$args ); + } + } + } +} + +namespace WordPress\AiClientDependencies\Psr\Http\Client { + + if ( ! interface_exists( __NAMESPACE__ . '\ClientInterface', false ) ) { + interface ClientInterface { + public function sendRequest( \WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface $request ): \WordPress\AiClientDependencies\Psr\Http\Message\ResponseInterface; + } + } +} + +namespace WordPress\AiClientDependencies\Psr\Http\Message { + + if ( ! interface_exists( __NAMESPACE__ . '\RequestInterface', false ) ) { + interface RequestInterface {} + } + + if ( ! interface_exists( __NAMESPACE__ . '\ResponseInterface', false ) ) { + interface ResponseInterface {} + } + + if ( ! interface_exists( __NAMESPACE__ . '\ResponseFactoryInterface', false ) ) { + interface ResponseFactoryInterface {} + } + + if ( ! interface_exists( __NAMESPACE__ . '\StreamFactoryInterface', false ) ) { + interface StreamFactoryInterface {} + } +} + +namespace WordPress\AiClientDependencies\Nyholm\Psr7\Factory { + + if ( ! class_exists( __NAMESPACE__ . '\Psr17Factory', false ) ) { + final class Psr17Factory implements \WordPress\AiClientDependencies\Psr\Http\Message\ResponseFactoryInterface, \WordPress\AiClientDependencies\Psr\Http\Message\StreamFactoryInterface {} + } +} + +namespace { + + if ( ! class_exists( 'WP_AI_Client_HTTP_Client', false ) ) { + final class WP_AI_Client_HTTP_Client { + public function __construct( ...$args ) { + unset( $args ); + } + + public function sendRequest( $request ) { + unset( $request ); + return null; + } + + public function sendRequestWithOptions( $request, $options ) { + unset( $request, $options ); + return null; + } + } + } + + if ( ! class_exists( 'WP_AI_Client_Prompt_Builder', false ) ) { + class WP_AI_Client_Prompt_Builder { + private \WordPress\AiClient\Builders\PromptBuilder $builder; + + public function __construct( ...$args ) { + $this->builder = new \WordPress\AiClient\Builders\PromptBuilder( ...$args ); + } + + public function __call( string $name, array $arguments ) { + if ( method_exists( $this->builder, $name ) ) { + $result = $this->builder->$name( ...$arguments ); + return $result instanceof \WordPress\AiClient\Builders\PromptBuilder ? $this : $result; + } + + if ( 0 === strpos( $name, 'generate_' ) ) { + return new \WordPress\AiClient\Results\DTO\GenerativeAiResult(); + } + + return $this; + } + } + } +} diff --git a/tests/Unit/AbilitiesHelperTest.php b/tests/Unit/AbilitiesHelperTest.php index 9188e38..523a1a0 100644 --- a/tests/Unit/AbilitiesHelperTest.php +++ b/tests/Unit/AbilitiesHelperTest.php @@ -73,6 +73,21 @@ public function test_tool_declarations_are_built_from_registered_allowlist(): vo $this->assertContains( 'memory_long_term_delete', array_map( static fn( FunctionDeclaration $item ): string => $item->getName(), $declarations ) ); } + public function test_file_list_declaration_omits_parameters_for_no_argument_tool(): void { + $declarations = Abilities_Helper::get_instance()->get_tool_declarations(); + $file_list = null; + + foreach ( $declarations as $declaration ) { + if ( 'file_list' === $declaration->getName() ) { + $file_list = $declaration; + break; + } + } + + $this->assertInstanceOf( FunctionDeclaration::class, $file_list ); + $this->assertNull( $file_list->getParameters() ); + } + public function test_tool_declarations_respect_enabled_abilities_option(): void { update_option( Abilities_Helper::ENABLED_ABILITIES_OPTION, @@ -197,6 +212,45 @@ public function test_tool_declarations_include_enabled_external_registered_abili $this->assertSame( [ 'vendor__custom_tool' ], $names ); } + public function test_empty_registered_input_schema_omits_parameters(): void { + wp_register_ability( + 'vendor/custom-empty-input', + [ + 'label' => 'Custom Empty Input', + 'description' => 'External tool with empty input schema.', + 'input_schema' => [], + 'permission_callback' => static fn(): bool => true, + 'execute_callback' => static fn() => [ 'ok' => true ], + ] + ); + update_option( + Abilities_Helper::ENABLED_ABILITIES_OPTION, + [ + 'vendor/custom-empty-input', + ] + ); + + $declarations = Abilities_Helper::get_instance()->get_tool_declarations(); + + $this->assertCount( 1, $declarations ); + $this->assertNull( $declarations[0]->getParameters() ); + } + + public function test_normalize_function_declaration_omits_empty_object_parameter_schemas(): void { + $declaration = new FunctionDeclaration( + 'file_list', + 'List files.', + [ + 'type' => 'object', + 'properties' => [], + 'additionalProperties' => false, + ] + ); + + $normalized = Abilities_Helper::get_instance()->normalize_function_declaration( $declaration ); + $this->assertNull( $normalized->getParameters() ); + } + public function test_execute_tool_call_accepts_external_registered_ability_alias(): void { wp_register_ability( 'vendor/custom-tool', diff --git a/tests/Unit/AgentLoopHelperTest.php b/tests/Unit/AgentLoopHelperTest.php index 9c7cdc5..e547999 100644 --- a/tests/Unit/AgentLoopHelperTest.php +++ b/tests/Unit/AgentLoopHelperTest.php @@ -10,6 +10,7 @@ namespace ClawPress\Tests\Unit; use ClawPress\Helpers\Agent_Loop_Helper; +use ClawPress\Transports\Agent_Event_Sink; use ClawPress\Tests\Support\Agent_Runtime_Wpdb; use ClawPress\Tests\Support\TestCase; @@ -113,6 +114,72 @@ public function test_run_turn_streaming_mode_uses_polling_transport_events_curso $this->assertNotEmpty( $GLOBALS['wpdb']->events ); } + public function test_run_turn_streaming_mode_emits_live_delta_without_persisting_delta_events(): void { + $stream_events = []; + + $result = Agent_Loop_Helper::get_instance()->run_turn( + [ + 'run_id' => 155, + 'session_id' => 177, + 'message' => 'Stream live delta', + 'transport_mode' => 'streaming', + 'stream_event_callback' => static function ( array $event ) use ( &$stream_events ): void { + $stream_events[] = $event; + }, + 'provider_model_resolver' => static fn( array $settings ): array => [ + 'provider' => 'openai', + 'model' => 'gpt-4.1-mini', + ], + 'online_reply_generator' => static function ( array $context, string $provider, string $model, array $turn_request, \ClawPress\Transports\Agent_Event_Sink $event_sink ): array { + unset( $context, $provider, $model, $turn_request ); + + $event_sink->emit( + [ + 'type' => 'agent.llm.delta', + 'payload' => [ + 'text' => 'Hello ', + ], + ] + ); + $event_sink->emit( + [ + 'type' => 'agent.tool_call', + 'payload' => [ + 'tool_name' => 'file_read', + 'status' => 'success', + 'round' => 1, + 'sequence' => 1, + ], + ] + ); + + return [ + 'status' => 'success', + 'next_action' => 'stop', + 'reply' => 'Hello world', + ]; + }, + ] + ); + + $this->assertSame( 'success', $result['status'] ); + $this->assertSame( 'Hello world', $result['assistant_text'] ); + $this->assertCount( 4, $stream_events ); + $this->assertSame( 'agent.run.started', $stream_events[0]['type'] ); + $this->assertSame( 'agent.llm.delta', $stream_events[1]['type'] ); + $this->assertSame( 'agent.tool_call', $stream_events[2]['type'] ); + $this->assertSame( 'agent.run.finished', $stream_events[3]['type'] ); + + $persisted_event_types = array_values( + array_map( + static fn( array $event ): string => (string) ( $event['event_type'] ?? '' ), + $GLOBALS['wpdb']->events + ) + ); + $this->assertContains( 'agent.tool_call', $persisted_event_types ); + $this->assertNotContains( 'agent.llm.delta', $persisted_event_types ); + } + public function test_run_turn_fills_terminal_empty_assistant_text_with_friendly_message(): void { $result = Agent_Loop_Helper::get_instance()->run_turn( [ @@ -150,6 +217,65 @@ public function test_run_turn_fills_terminal_empty_assistant_text_with_friendly_ ); } + public function test_emit_transport_stream_delta_ignores_function_call_argument_payloads(): void { + $stream_events = []; + $helper = Agent_Loop_Helper::get_instance(); + $reflection = new \ReflectionClass( Agent_Loop_Helper::class ); + $method = $reflection->getMethod( 'emit_transport_stream_delta' ); + $method->setAccessible( true ); + $event_sink = new Agent_Event_Sink( + static function ( array $event ) use ( &$stream_events ): void { + $stream_events[] = $event; + } + ); + + $method->invoke( + $helper, + new \WP_AI_Client_SSE_Event( + 'response.function_call_arguments.delta', + '{"type":"response.function_call_arguments.delta","delta":"{}"}' + ), + $event_sink + ); + $method->invoke( + $helper, + new \WP_AI_Client_SSE_Event( + 'response.output_text.delta', + '{"type":"response.output_text.delta","delta":"Files:"}' + ), + $event_sink + ); + + $this->assertCount( 1, $stream_events ); + $this->assertSame( 'agent.llm.delta', $stream_events[0]['type'] ); + $this->assertSame( 'Files:', $stream_events[0]['payload']['text'] ); + } + + public function test_emit_transport_stream_delta_keeps_legacy_top_level_text_deltas(): void { + $stream_events = []; + $helper = Agent_Loop_Helper::get_instance(); + $reflection = new \ReflectionClass( Agent_Loop_Helper::class ); + $method = $reflection->getMethod( 'emit_transport_stream_delta' ); + $method->setAccessible( true ); + $event_sink = new Agent_Event_Sink( + static function ( array $event ) use ( &$stream_events ): void { + $stream_events[] = $event; + } + ); + + $method->invoke( + $helper, + new \WP_AI_Client_SSE_Event( + '', + '{"delta":"Files:"}' + ), + $event_sink + ); + + $this->assertCount( 1, $stream_events ); + $this->assertSame( 'Files:', $stream_events[0]['payload']['text'] ); + } + public function test_run_slice_returns_in_progress_with_resume_cursor(): void { $result = Agent_Loop_Helper::get_instance()->run_slice( [ diff --git a/tests/Unit/RestApiTest.php b/tests/Unit/RestApiTest.php index 2f93aef..3a3dc81 100644 --- a/tests/Unit/RestApiTest.php +++ b/tests/Unit/RestApiTest.php @@ -326,14 +326,15 @@ public function test_chat_routes_use_global_manage_options_permission_callback() array_filter( WordPress_Stubs::$rest_routes, static function ( array $route ): bool { - return in_array( $route['route'], [ '/chat/message', '/chat/history' ], true ); + return in_array( $route['route'], [ '/chat/message', '/chat/stream', '/chat/history' ], true ); } ) ); - $this->assertCount( 2, $chat_routes ); + $this->assertCount( 3, $chat_routes ); $this->assertSame( 'clawpress_check_permissions', $chat_routes[0]['args']['permission_callback'] ); $this->assertSame( 'clawpress_check_permissions', $chat_routes[1]['args']['permission_callback'] ); + $this->assertSame( 'clawpress_check_permissions', $chat_routes[2]['args']['permission_callback'] ); } public function test_chat_send_message_returns_message_and_reply(): void { diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f3e8cfd..06af418 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -47,7 +47,12 @@ define( 'CLAWPRESS_URL', 'https://example.test/wp-content/plugins/clawpress/' ); } +if ( ! defined( 'CLAWPRESS_ALLOW_AI_CLIENT_STUBS' ) ) { + define( 'CLAWPRESS_ALLOW_AI_CLIENT_STUBS', true ); +} + require_once __DIR__ . '/Support/WordPressStubs.php'; +require_once __DIR__ . '/Support/AiClientStubs.php'; require_once __DIR__ . '/Support/TestCase.php'; require_once __DIR__ . '/Support/AgentRuntimeWpdb.php'; require_once $plugin_root . '/vendor/autoload.php';