@hienlh/ppm 0.8.50 → 0.8.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/web/assets/api-settings-D4bgXrLU.js +1 -0
  3. package/dist/web/assets/chat-tab-wunayDmr.js +7 -0
  4. package/dist/web/assets/{code-editor-DB-y8tPy.js → code-editor-Fw_VrmHT.js} +1 -1
  5. package/dist/web/assets/{database-viewer-D4JQEtMD.js → database-viewer-CZjxdELm.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-ToD0FLsL.js → diff-viewer-B51YfMeK.js} +1 -1
  7. package/dist/web/assets/{git-graph-Dg7SVF-R.js → git-graph-fCVmtbaj.js} +1 -1
  8. package/dist/web/assets/index-CoyMn-Mj.css +2 -0
  9. package/dist/web/assets/index-DMlEKjZt.js +37 -0
  10. package/dist/web/assets/keybindings-store-BzXZa5uC.js +1 -0
  11. package/dist/web/assets/{markdown-renderer-DMHeWMgi.js → markdown-renderer-D_OeJdOH.js} +1 -1
  12. package/dist/web/assets/{postgres-viewer-bUYwSwrp.js → postgres-viewer-BlEIES7N.js} +1 -1
  13. package/dist/web/assets/{settings-store-ChwdK0tt.js → settings-store-DL2KEbtc.js} +2 -2
  14. package/dist/web/assets/settings-tab-DnU5t6Fy.js +1 -0
  15. package/dist/web/assets/{sqlite-viewer-BLUoWIZ5.js → sqlite-viewer-BJ2s8Dng.js} +1 -1
  16. package/dist/web/assets/{terminal-tab-B5sI9TDZ.js → terminal-tab-DAFbT7Sv.js} +2 -2
  17. package/dist/web/assets/{use-monaco-theme-0hXmt0_2.js → use-monaco-theme-DwP4EHdO.js} +1 -1
  18. package/dist/web/index.html +4 -4
  19. package/dist/web/sw.js +1 -1
  20. package/package.json +1 -1
  21. package/src/providers/claude-agent-sdk.ts +21 -8
  22. package/src/server/index.ts +4 -0
  23. package/src/server/routes/proxy.ts +79 -0
  24. package/src/server/routes/settings.ts +53 -1
  25. package/src/services/proxy.service.ts +139 -0
  26. package/src/types/config.ts +1 -0
  27. package/src/web/components/chat/message-list.tsx +2 -125
  28. package/src/web/components/settings/ai-settings-section.tsx +21 -0
  29. package/src/web/components/settings/proxy-settings-section.tsx +217 -0
  30. package/src/web/components/settings/settings-tab.tsx +5 -2
  31. package/src/web/hooks/use-global-keybindings.ts +13 -1
  32. package/src/web/lib/api-settings.ts +19 -0
  33. package/dist/web/assets/api-settings-DfTIjsPW.js +0 -1
  34. package/dist/web/assets/chat-tab-BZSpI1_2.js +0 -7
  35. package/dist/web/assets/index-DdLIa98_.js +0 -28
  36. package/dist/web/assets/index-XRJa3Ncz.css +0 -2
  37. package/dist/web/assets/keybindings-store-BofWHLIC.js +0 -1
  38. package/dist/web/assets/settings-tab-D2bgiL7t.js +0 -1
package/dist/web/sw.js CHANGED
@@ -1 +1 @@
1
- try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"49865f1dc6c4195305930e2e5f60893d","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"eb9818b9094675c0c5d303168f273345","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"9af0be92dcefdc1f1290441cb5ff5d9b","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"a261b429c39dbb75ae97972d7d005e6d","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"79953d804e1bbacecfd79b85fd679016","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"fdcba0d09aac31df7a0bc652f6e739bd","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":null,"url":"assets/utils-DC-bdPS3.js"},{"revision":null,"url":"assets/use-monaco-theme-0hXmt0_2.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/terminal-tab-B5sI9TDZ.js"},{"revision":null,"url":"assets/tag-DJUYe5BQ.js"},{"revision":null,"url":"assets/table-B6neW6Hr.js"},{"revision":null,"url":"assets/tab-store-NOBndc0_.js"},{"revision":null,"url":"assets/sqlite-viewer-BLUoWIZ5.js"},{"revision":null,"url":"assets/settings-tab-D2bgiL7t.js"},{"revision":null,"url":"assets/settings-store-ChwdK0tt.js"},{"revision":null,"url":"assets/react-rgzL83kk.js"},{"revision":null,"url":"assets/react-CYzKIDNi.js"},{"revision":null,"url":"assets/postgres-viewer-bUYwSwrp.js"},{"revision":null,"url":"assets/markdown-renderer-DMHeWMgi.js"},{"revision":null,"url":"assets/keybindings-store-BofWHLIC.js"},{"revision":null,"url":"assets/jsx-runtime-wQxeESYQ.js"},{"revision":null,"url":"assets/input-CE3bFwLk.js"},{"revision":null,"url":"assets/index-XRJa3Ncz.css"},{"revision":null,"url":"assets/index-DdLIa98_.js"},{"revision":null,"url":"assets/git-graph-Dg7SVF-R.js"},{"revision":null,"url":"assets/dist-QgqOdSYG.js"},{"revision":null,"url":"assets/diff-viewer-ToD0FLsL.js"},{"revision":null,"url":"assets/database-viewer-D4JQEtMD.js"},{"revision":null,"url":"assets/columns-2-BZ5wv2wA.js"},{"revision":null,"url":"assets/code-editor-DB-y8tPy.js"},{"revision":null,"url":"assets/chat-tab-BZSpI1_2.js"},{"revision":null,"url":"assets/api-settings-DfTIjsPW.js"},{"revision":null,"url":"assets/api-client-TUmacMRS.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
1
+ try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"9396025b863a420d5e04a9412c23581c","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"eb9818b9094675c0c5d303168f273345","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"9af0be92dcefdc1f1290441cb5ff5d9b","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"a261b429c39dbb75ae97972d7d005e6d","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"79953d804e1bbacecfd79b85fd679016","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"fdcba0d09aac31df7a0bc652f6e739bd","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":null,"url":"assets/utils-DC-bdPS3.js"},{"revision":null,"url":"assets/use-monaco-theme-DwP4EHdO.js"},{"revision":null,"url":"assets/terminal-tab-DAFbT7Sv.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/tag-DJUYe5BQ.js"},{"revision":null,"url":"assets/table-B6neW6Hr.js"},{"revision":null,"url":"assets/tab-store-NOBndc0_.js"},{"revision":null,"url":"assets/sqlite-viewer-BJ2s8Dng.js"},{"revision":null,"url":"assets/settings-tab-DnU5t6Fy.js"},{"revision":null,"url":"assets/settings-store-DL2KEbtc.js"},{"revision":null,"url":"assets/react-rgzL83kk.js"},{"revision":null,"url":"assets/react-CYzKIDNi.js"},{"revision":null,"url":"assets/postgres-viewer-BlEIES7N.js"},{"revision":null,"url":"assets/markdown-renderer-D_OeJdOH.js"},{"revision":null,"url":"assets/keybindings-store-BzXZa5uC.js"},{"revision":null,"url":"assets/jsx-runtime-wQxeESYQ.js"},{"revision":null,"url":"assets/input-CE3bFwLk.js"},{"revision":null,"url":"assets/index-DMlEKjZt.js"},{"revision":null,"url":"assets/index-CoyMn-Mj.css"},{"revision":null,"url":"assets/git-graph-fCVmtbaj.js"},{"revision":null,"url":"assets/dist-QgqOdSYG.js"},{"revision":null,"url":"assets/diff-viewer-B51YfMeK.js"},{"revision":null,"url":"assets/database-viewer-CZjxdELm.js"},{"revision":null,"url":"assets/columns-2-BZ5wv2wA.js"},{"revision":null,"url":"assets/code-editor-Fw_VrmHT.js"},{"revision":null,"url":"assets/chat-tab-wunayDmr.js"},{"revision":null,"url":"assets/api-settings-D4bgXrLU.js"},{"revision":null,"url":"assets/api-client-TUmacMRS.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.8.50",
3
+ "version": "0.8.52",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -68,20 +68,33 @@ export class ClaudeAgentSdkProvider implements AIProvider {
68
68
  // Settings base_url has highest priority
69
69
  const providerConfig = this.getProviderConfig();
70
70
 
71
- // Resolve each auth var: settings > shell env > "" (blocks project .env)
72
- const resolvedApiKey = account
73
- ? (account.accessToken.startsWith("sk-ant-oat") ? "" : account.accessToken)
74
- : (process.env.ANTHROPIC_API_KEY ?? "");
75
- const resolvedOAuth = account
76
- ? (account.accessToken.startsWith("sk-ant-oat") ? account.accessToken : "")
77
- : (process.env.CLAUDE_CODE_OAUTH_TOKEN ?? "");
71
+ // Priority: settings api_key > account token > shell env > "" (blocks project .env)
72
+ const settingsApiKey = providerConfig.api_key?.trim() || "";
73
+
74
+ let resolvedApiKey: string;
75
+ let resolvedOAuth: string;
76
+
77
+ if (settingsApiKey) {
78
+ // Settings api_key overrides everything — treat as direct API key
79
+ resolvedApiKey = settingsApiKey;
80
+ resolvedOAuth = "";
81
+ } else if (account) {
82
+ resolvedApiKey = account.accessToken.startsWith("sk-ant-oat") ? "" : account.accessToken;
83
+ resolvedOAuth = account.accessToken.startsWith("sk-ant-oat") ? account.accessToken : "";
84
+ } else {
85
+ resolvedApiKey = process.env.ANTHROPIC_API_KEY ?? "";
86
+ resolvedOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN ?? "";
87
+ }
88
+
78
89
  const resolvedBaseUrl = providerConfig.base_url
79
90
  || process.env.ANTHROPIC_BASE_URL
80
91
  || "";
81
92
  const resolvedAuthToken = process.env.ANTHROPIC_AUTH_TOKEN ?? "";
82
93
 
83
94
  // Log resolved sources
84
- if (account) {
95
+ if (settingsApiKey) {
96
+ console.log(`[sdk] Auth from settings api_key (length=${settingsApiKey.length})`);
97
+ } else if (account) {
85
98
  console.log(`[sdk] Auth from PPM account (${account.accessToken.startsWith("sk-ant-oat") ? "OAuth" : "API key"})`);
86
99
  } else if (process.env.ANTHROPIC_API_KEY) {
87
100
  console.log(`[sdk] ANTHROPIC_API_KEY from shell env (length=${process.env.ANTHROPIC_API_KEY.length})`);
@@ -13,6 +13,7 @@ import { postgresRoutes } from "./routes/postgres.ts";
13
13
  import { databaseRoutes } from "./routes/database.ts";
14
14
  import { fsBrowseRoutes } from "./routes/fs-browse.ts";
15
15
  import { accountsRoutes } from "./routes/accounts.ts";
16
+ import { proxyRoutes } from "./routes/proxy.ts";
16
17
  import { initAdapters } from "../services/database/init-adapters.ts";
17
18
  import { terminalWebSocket } from "./ws/terminal.ts";
18
19
  import { chatWebSocket } from "./ws/chat.ts";
@@ -101,6 +102,9 @@ if (process.env.NODE_ENV !== "production") {
101
102
  app.get("/api/debug/crash", () => { process.exit(1); });
102
103
  }
103
104
 
105
+ // Proxy routes — before auth middleware (uses own auth key)
106
+ app.route("/proxy", proxyRoutes);
107
+
104
108
  // Auth check endpoint (behind auth middleware)
105
109
  app.use("/api/*", authMiddleware);
106
110
  app.get("/api/auth/check", (c) => c.json(ok(true)));
@@ -0,0 +1,79 @@
1
+ import { Hono } from "hono";
2
+ import { proxyService } from "../../services/proxy.service.ts";
3
+ import { ok, err } from "../../types/api.ts";
4
+
5
+ /**
6
+ * Proxy routes — Anthropic-compatible API proxy.
7
+ * External tools (opencode, cursor, etc.) send requests here
8
+ * and PPM forwards them to Anthropic using account rotation.
9
+ *
10
+ * Mounted at /proxy — so /proxy/v1/messages maps to Anthropic's POST /v1/messages.
11
+ * Uses its own auth (proxy auth key), NOT PPM's auth middleware.
12
+ */
13
+ export const proxyRoutes = new Hono();
14
+
15
+ /** Validate proxy auth key from Authorization header */
16
+ function validateProxyAuth(authHeader: string | undefined): boolean {
17
+ if (!authHeader) return false;
18
+ const key = proxyService.getAuthKey();
19
+ if (!key) return false;
20
+ // Accept both "Bearer <key>" and raw "<key>" (x-api-key style)
21
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
22
+ return token === key;
23
+ }
24
+
25
+ /** CORS preflight for external tools */
26
+ proxyRoutes.options("/*", (c) => {
27
+ return new Response(null, {
28
+ status: 204,
29
+ headers: {
30
+ "Access-Control-Allow-Origin": "*",
31
+ "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
32
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta",
33
+ "Access-Control-Max-Age": "86400",
34
+ },
35
+ });
36
+ });
37
+
38
+ /** POST /proxy/v1/messages — Anthropic Messages API proxy */
39
+ proxyRoutes.post("/v1/messages", async (c) => {
40
+ if (!proxyService.isEnabled()) {
41
+ return c.json({ type: "error", error: { type: "api_error", message: "Proxy is disabled" } }, 503);
42
+ }
43
+
44
+ // Auth check — accept both Authorization and x-api-key headers
45
+ const authHeader = c.req.header("authorization") || c.req.header("x-api-key");
46
+ if (!validateProxyAuth(authHeader)) {
47
+ return c.json({ type: "error", error: { type: "authentication_error", message: "Invalid proxy auth key" } }, 401);
48
+ }
49
+
50
+ const body = await c.req.text();
51
+ const headers: Record<string, string> = {};
52
+ for (const key of ["anthropic-version", "anthropic-beta", "content-type"]) {
53
+ const val = c.req.header(key);
54
+ if (val) headers[key] = val;
55
+ }
56
+
57
+ return proxyService.forward("/v1/messages", "POST", headers, body);
58
+ });
59
+
60
+ /** POST /proxy/v1/messages/count_tokens — token counting proxy */
61
+ proxyRoutes.post("/v1/messages/count_tokens", async (c) => {
62
+ if (!proxyService.isEnabled()) {
63
+ return c.json({ type: "error", error: { type: "api_error", message: "Proxy is disabled" } }, 503);
64
+ }
65
+
66
+ const authHeader = c.req.header("authorization") || c.req.header("x-api-key");
67
+ if (!validateProxyAuth(authHeader)) {
68
+ return c.json({ type: "error", error: { type: "authentication_error", message: "Invalid proxy auth key" } }, 401);
69
+ }
70
+
71
+ const body = await c.req.text();
72
+ const headers: Record<string, string> = {};
73
+ for (const key of ["anthropic-version", "anthropic-beta", "content-type"]) {
74
+ const val = c.req.header(key);
75
+ if (val) headers[key] = val;
76
+ }
77
+
78
+ return proxyService.forward("/v1/messages/count_tokens", "POST", headers, body);
79
+ });
@@ -10,6 +10,7 @@ import {
10
10
  type ThemeConfig,
11
11
  } from "../../types/config.ts";
12
12
  import { ok, err } from "../../types/api.ts";
13
+ import { proxyService } from "../../services/proxy.service.ts";
13
14
 
14
15
  export const settingsRoutes = new Hono();
15
16
 
@@ -17,7 +18,12 @@ export const settingsRoutes = new Hono();
17
18
  function stripSensitiveFields(ai: { providers: Record<string, unknown> }) {
18
19
  const clone = structuredClone(ai);
19
20
  for (const provider of Object.values(clone.providers)) {
20
- delete (provider as Record<string, unknown>).api_key_env;
21
+ const p = provider as Record<string, unknown>;
22
+ delete p.api_key_env;
23
+ // Mask api_key: show only that it's set, not the value
24
+ if (p.api_key && typeof p.api_key === "string" && p.api_key.length > 0) {
25
+ p.api_key = "••••" + (p.api_key as string).slice(-4);
26
+ }
21
27
  }
22
28
  return clone;
23
29
  }
@@ -120,6 +126,10 @@ settingsRoutes.put("/ai", async (c) => {
120
126
  if (body.providers) {
121
127
  updated.providers = { ...currentAi.providers };
122
128
  for (const [name, config] of Object.entries(body.providers)) {
129
+ // Don't overwrite api_key with the masked value from UI
130
+ if (config.api_key && config.api_key.startsWith("••••")) {
131
+ delete config.api_key;
132
+ }
123
133
  updated.providers[name] = {
124
134
  ...currentAi.providers[name],
125
135
  ...config,
@@ -227,3 +237,45 @@ settingsRoutes.post("/telegram/test", async (c) => {
227
237
  return c.json(err((e as Error).message), 500);
228
238
  }
229
239
  });
240
+
241
+ // ── Proxy ────────────────────────────────────────────────────────────
242
+
243
+ /** GET /settings/proxy — proxy status */
244
+ settingsRoutes.get("/proxy", async (c) => {
245
+ const { tunnelService } = await import("../../services/tunnel.service.ts");
246
+ const tunnelUrl = tunnelService.getTunnelUrl();
247
+ const authKey = proxyService.getAuthKey();
248
+ return c.json(ok({
249
+ enabled: proxyService.isEnabled(),
250
+ authKey: authKey ?? null,
251
+ requestCount: proxyService.getRequestCount(),
252
+ tunnelUrl: tunnelUrl ?? null,
253
+ proxyEndpoint: tunnelUrl ? `${tunnelUrl}/proxy/v1/messages` : null,
254
+ }));
255
+ });
256
+
257
+ /** PUT /settings/proxy — update proxy settings */
258
+ settingsRoutes.put("/proxy", async (c) => {
259
+ try {
260
+ const body = await c.req.json<{ enabled?: boolean; authKey?: string; generateKey?: boolean }>();
261
+ if (body.enabled !== undefined) {
262
+ proxyService.setEnabled(body.enabled);
263
+ }
264
+ if (body.generateKey) {
265
+ proxyService.generateAuthKey();
266
+ } else if (body.authKey !== undefined) {
267
+ proxyService.setAuthKey(body.authKey);
268
+ }
269
+ const { tunnelService } = await import("../../services/tunnel.service.ts");
270
+ const tunnelUrl = tunnelService.getTunnelUrl();
271
+ return c.json(ok({
272
+ enabled: proxyService.isEnabled(),
273
+ authKey: proxyService.getAuthKey() ?? null,
274
+ requestCount: proxyService.getRequestCount(),
275
+ tunnelUrl: tunnelUrl ?? null,
276
+ proxyEndpoint: tunnelUrl ? `${tunnelUrl}/proxy/v1/messages` : null,
277
+ }));
278
+ } catch (e) {
279
+ return c.json(err((e as Error).message), 400);
280
+ }
281
+ });
@@ -0,0 +1,139 @@
1
+ import { getConfigValue, setConfigValue } from "./db.service.ts";
2
+ import { accountSelector } from "./account-selector.service.ts";
3
+ import { accountService } from "./account.service.ts";
4
+ import { randomBytes } from "node:crypto";
5
+
6
+ const PROXY_ENABLED_KEY = "proxy_enabled";
7
+ const PROXY_AUTH_KEY = "proxy_auth_key";
8
+
9
+ const ANTHROPIC_API_BASE = "https://api.anthropic.com";
10
+
11
+ class ProxyService {
12
+ private requestCount = 0;
13
+
14
+ isEnabled(): boolean {
15
+ return getConfigValue(PROXY_ENABLED_KEY) === "true";
16
+ }
17
+
18
+ setEnabled(enabled: boolean): void {
19
+ setConfigValue(PROXY_ENABLED_KEY, String(enabled));
20
+ }
21
+
22
+ getAuthKey(): string | null {
23
+ return getConfigValue(PROXY_AUTH_KEY);
24
+ }
25
+
26
+ /** Generate a new random auth key */
27
+ generateAuthKey(): string {
28
+ const key = `ppm-proxy-${randomBytes(16).toString("hex")}`;
29
+ setConfigValue(PROXY_AUTH_KEY, key);
30
+ return key;
31
+ }
32
+
33
+ /** Set a custom auth key */
34
+ setAuthKey(key: string): void {
35
+ setConfigValue(PROXY_AUTH_KEY, key);
36
+ }
37
+
38
+ getRequestCount(): number {
39
+ return this.requestCount;
40
+ }
41
+
42
+ /**
43
+ * Forward a request to Anthropic API using account rotation.
44
+ * Returns a Response object (may be streaming SSE).
45
+ */
46
+ async forward(
47
+ path: string,
48
+ method: string,
49
+ headers: Record<string, string>,
50
+ body: string | null,
51
+ ): Promise<Response> {
52
+ // Pick account via rotation
53
+ const account = accountSelector.next();
54
+ if (!account) {
55
+ return new Response(
56
+ JSON.stringify({ type: "error", error: { type: "authentication_error", message: "No active accounts available" } }),
57
+ { status: 401, headers: { "Content-Type": "application/json" } },
58
+ );
59
+ }
60
+
61
+ // Ensure token is fresh for OAuth accounts
62
+ let token = account.accessToken;
63
+ if (token.startsWith("sk-ant-oat")) {
64
+ const fresh = await accountService.ensureFreshToken(account.id);
65
+ if (fresh) token = fresh.accessToken;
66
+ }
67
+
68
+ // Build upstream headers — forward relevant Anthropic headers
69
+ const upstreamHeaders: Record<string, string> = {
70
+ "Content-Type": "application/json",
71
+ "User-Agent": "ppm-proxy/1.0",
72
+ };
73
+
74
+ // Set auth based on token type
75
+ if (token.startsWith("sk-ant-oat")) {
76
+ upstreamHeaders["Authorization"] = `Bearer ${token}`;
77
+ upstreamHeaders["anthropic-beta"] = headers["anthropic-beta"] || "oauth-2025-04-20";
78
+ } else {
79
+ upstreamHeaders["x-api-key"] = token;
80
+ }
81
+
82
+ // Forward anthropic-version header
83
+ if (headers["anthropic-version"]) {
84
+ upstreamHeaders["anthropic-version"] = headers["anthropic-version"];
85
+ }
86
+ // Forward anthropic-beta if present from client
87
+ if (headers["anthropic-beta"]) {
88
+ upstreamHeaders["anthropic-beta"] = headers["anthropic-beta"];
89
+ }
90
+
91
+ const url = `${ANTHROPIC_API_BASE}${path}`;
92
+ console.log(`[proxy] ${method} ${path} → account ${account.email ?? account.id}`);
93
+
94
+ try {
95
+ const upstream = await fetch(url, {
96
+ method,
97
+ headers: upstreamHeaders,
98
+ body: body || undefined,
99
+ signal: AbortSignal.timeout(300_000), // 5min timeout for long streaming
100
+ });
101
+
102
+ this.requestCount++;
103
+
104
+ // Handle rate limit / auth errors for account rotation
105
+ if (upstream.status === 429) {
106
+ accountSelector.onRateLimit(account.id);
107
+ console.log(`[proxy] 429 from Anthropic — account ${account.email ?? account.id} rate limited`);
108
+ } else if (upstream.status === 401) {
109
+ accountSelector.onAuthError(account.id);
110
+ console.log(`[proxy] 401 from Anthropic — account ${account.email ?? account.id} auth error`);
111
+ } else if (upstream.status >= 200 && upstream.status < 300) {
112
+ accountSelector.onSuccess(account.id);
113
+ }
114
+
115
+ // Stream response back as-is (preserves SSE for streaming)
116
+ const responseHeaders = new Headers();
117
+ // Forward key response headers
118
+ for (const key of ["content-type", "x-request-id", "request-id"]) {
119
+ const val = upstream.headers.get(key);
120
+ if (val) responseHeaders.set(key, val);
121
+ }
122
+ // CORS for external tools
123
+ responseHeaders.set("Access-Control-Allow-Origin", "*");
124
+
125
+ return new Response(upstream.body, {
126
+ status: upstream.status,
127
+ headers: responseHeaders,
128
+ });
129
+ } catch (e) {
130
+ console.error(`[proxy] Error forwarding to Anthropic:`, (e as Error).message);
131
+ return new Response(
132
+ JSON.stringify({ type: "error", error: { type: "api_error", message: (e as Error).message } }),
133
+ { status: 502, headers: { "Content-Type": "application/json" } },
134
+ );
135
+ }
136
+ }
137
+ }
138
+
139
+ export const proxyService = new ProxyService();
@@ -46,6 +46,7 @@ export type PermissionMode = typeof VALID_PERMISSION_MODES[number];
46
46
  export interface AIProviderConfig {
47
47
  type: "agent-sdk" | "mock";
48
48
  api_key_env?: string;
49
+ api_key?: string;
49
50
  base_url?: string;
50
51
  // Agent SDK-specific settings (ignored by mock provider)
51
52
  model?: string;
@@ -72,71 +72,15 @@ export function MessageList({
72
72
  );
73
73
  }
74
74
 
75
- // Track which user message is pinned (scrolled above viewport) + push-out offset
76
- const [pinnedContent, setPinnedContent] = useState<string | null>(null);
77
- const [pushOffset, setPushOffset] = useState(0);
78
- const wrapperRef = useRef<HTMLDivElement>(null);
79
- const pinnedRef = useRef<HTMLDivElement>(null);
80
-
81
75
  const filtered = useMemo(() => messages.filter((msg) => {
82
76
  const hasContent = msg.content && msg.content.trim().length > 0;
83
77
  const hasEvents = msg.events && msg.events.length > 0;
84
78
  return hasContent || hasEvents;
85
79
  }), [messages]);
86
80
 
87
- // Observe user message elements to track which one is pinned + push-out transition
88
- useEffect(() => {
89
- const wrapper = wrapperRef.current;
90
- if (!wrapper) return;
91
- const scrollEl = wrapper.querySelector("[data-stick-to-bottom-scroll]") as HTMLElement
92
- ?? wrapper.firstElementChild as HTMLElement;
93
- if (!scrollEl || scrollEl.scrollHeight <= scrollEl.clientHeight) return;
94
-
95
- const handleScroll = () => {
96
- const userEls = wrapper.querySelectorAll<HTMLElement>("[data-user-content]");
97
- const scrollRect = scrollEl.getBoundingClientRect();
98
- const pinnedH = pinnedRef.current?.offsetHeight ?? 0;
99
-
100
- let lastAbove: string | null = null;
101
- let nextTop = Infinity;
102
-
103
- for (let i = 0; i < userEls.length; i++) {
104
- const rect = userEls[i]!.getBoundingClientRect();
105
- if (rect.top < scrollRect.top + 4) {
106
- lastAbove = userEls[i]!.getAttribute("data-user-content");
107
- // Find the next user message after this one
108
- const nextEl = userEls[i + 1];
109
- nextTop = nextEl ? nextEl.getBoundingClientRect().top - scrollRect.top : Infinity;
110
- }
111
- }
112
-
113
- setPinnedContent(lastAbove);
114
- // Push-out: when next header enters the pinned area, offset upward
115
- if (pinnedH > 0 && nextTop < pinnedH) {
116
- setPushOffset(nextTop - pinnedH);
117
- } else {
118
- setPushOffset(0);
119
- }
120
- };
121
-
122
- scrollEl.addEventListener("scroll", handleScroll, { passive: true });
123
- handleScroll();
124
- return () => scrollEl.removeEventListener("scroll", handleScroll);
125
- }, [filtered]);
126
-
127
81
  return (
128
82
  <div className="relative flex-1 overflow-hidden flex flex-col min-h-0">
129
- {/* Pinned header — overlays scroll content like react-listview-sticky-header */}
130
- {pinnedContent && (
131
- <div
132
- ref={pinnedRef}
133
- className="absolute top-0 left-0 right-0 z-20 bg-background"
134
- style={pushOffset < 0 ? { transform: `translateY(${pushOffset}px)` } : undefined}
135
- >
136
- <PinnedUserMessage content={pinnedContent} projectName={projectName} />
137
- </div>
138
- )}
139
- <StickToBottom ref={wrapperRef} className="flex-1 overflow-y-auto overflow-x-hidden" resize="smooth" initial="instant">
83
+ <StickToBottom className="flex-1 overflow-y-auto overflow-x-hidden" resize="smooth" initial="instant">
140
84
  <StickToBottom.Content className="p-4 space-y-4">
141
85
  {filtered.map((msg) => (
142
86
  <MessageBubble
@@ -162,73 +106,6 @@ export function MessageList({
162
106
  );
163
107
  }
164
108
 
165
- /** Compact pinned bar showing the current user message at the top of chat */
166
- function PinnedUserMessage({ content, projectName }: { content: string; projectName?: string }) {
167
- const { files, text } = useMemo(() => {
168
- const parsed = parseUserAttachments(content);
169
- const { cleanText } = extractSystemTags(parsed.text);
170
- return { files: parsed.files, text: cleanText };
171
- }, [content]);
172
-
173
- const [expanded, setExpanded] = useState(false);
174
- const [isOverflowing, setIsOverflowing] = useState(false);
175
- const contentRef = useRef<HTMLDivElement>(null);
176
-
177
- // Reset expanded state when pinned message changes
178
- useEffect(() => { setExpanded(false); }, [content]);
179
-
180
- useEffect(() => {
181
- const el = contentRef.current;
182
- if (!el) return;
183
- const check = () => setIsOverflowing(el.scrollHeight > el.clientHeight + 2);
184
- check();
185
- const ro = new ResizeObserver(check);
186
- ro.observe(el);
187
- return () => ro.disconnect();
188
- }, [text]);
189
-
190
- if (!text && files.length === 0) return null;
191
-
192
- return (
193
- <div className="shrink-0 px-4 pt-3 pb-2">
194
- <div className="rounded-lg bg-primary/10 px-3 py-2 text-sm text-text-primary space-y-2 border border-primary/15 shadow-sm">
195
- {files.length > 0 && (
196
- <div className="flex flex-wrap gap-1.5">
197
- {files.map((filePath, i) => (
198
- <div key={i} className="flex items-center gap-1 rounded-md border border-border/60 bg-background/40 px-1.5 py-0.5 text-[11px] text-text-secondary">
199
- {isImagePath(filePath) ? <ImageIcon className="size-3 shrink-0" /> : <FileText className="size-3 shrink-0" />}
200
- <span className="truncate max-w-32">{basename(filePath)}</span>
201
- </div>
202
- ))}
203
- </div>
204
- )}
205
- {text && (
206
- <div>
207
- <div
208
- ref={contentRef}
209
- className={cn(
210
- "whitespace-pre-wrap break-words transition-all duration-200",
211
- !expanded && "line-clamp-2",
212
- expanded && "max-h-[40vh] overflow-y-auto",
213
- )}
214
- >
215
- {text}
216
- </div>
217
- {(isOverflowing || expanded) && (
218
- <button
219
- onClick={() => setExpanded(!expanded)}
220
- className="flex items-center gap-1 text-xs text-primary/70 hover:text-primary mt-1 transition-colors"
221
- >
222
- {expanded ? <><ChevronUp className="size-3" />Show less</> : <><ChevronDown className="size-3" />Show more</>}
223
- </button>
224
- )}
225
- </div>
226
- )}
227
- </div>
228
- </div>
229
- );
230
- }
231
-
232
109
  /** Floating button to scroll back to bottom when user has scrolled up */
233
110
  function ScrollToBottomButton() {
234
111
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
@@ -374,7 +251,7 @@ function UserBubble({ content, projectName, onFork }: { content: string; project
374
251
  }, [text]);
375
252
 
376
253
  return (
377
- <div data-user-content={content} className="group/user relative rounded-lg bg-primary/10 px-3 py-2 text-sm text-text-primary border border-primary/15 shadow-sm">
254
+ <div className="group/user relative rounded-lg bg-primary/10 px-3 py-2 text-sm text-text-primary border border-primary/15 shadow-sm">
378
255
  {/* System tags as badges */}
379
256
  {tags.length > 0 && <SystemTagBadges tags={tags} />}
380
257
 
@@ -117,6 +117,27 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
117
117
  />
118
118
  </div>
119
119
 
120
+ <div className={fieldGap}>
121
+ <Label htmlFor="ai-api-key" className={compact ? labelSize : undefined}>API Key / Token</Label>
122
+ <Input
123
+ key={`apikey-${revision}`}
124
+ id="ai-api-key"
125
+ type="password"
126
+ defaultValue={config?.api_key ?? ""}
127
+ placeholder="sk-ant-... (optional, overrides accounts)"
128
+ className={compact ? "h-7 text-[11px] font-mono" : "font-mono"}
129
+ onBlur={(e) => {
130
+ const val = e.target.value.trim();
131
+ // Don't save if it's the masked value
132
+ if (val.startsWith("••••")) return;
133
+ handleSave("api_key", val || undefined);
134
+ }}
135
+ />
136
+ <p className={`${compact ? "text-[9px]" : "text-[11px]"} text-muted-foreground`}>
137
+ Direct API key or OAuth token. Leave empty to use connected accounts.
138
+ </p>
139
+ </div>
140
+
120
141
  <div className={fieldGap}>
121
142
  <Label htmlFor="ai-effort" className={compact ? labelSize : undefined}>Effort</Label>
122
143
  <Select