@hienlh/ppm 0.22.1 → 0.22.2

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 (124) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/assets/skills/ppm/SKILL.md +1 -1
  3. package/assets/skills/ppm/references/cli-reference.md +4 -4
  4. package/assets/skills/ppm/references/http-api.md +1 -1
  5. package/dist/web/assets/{accounts-settings-section-D3BylchP.js → accounts-settings-section-CLEAA_Jc.js} +1 -1
  6. package/dist/web/assets/{ai-resource-editor-BLPcnhTM.js → ai-resource-editor-C4ecid2g.js} +1 -1
  7. package/dist/web/assets/ai-settings-section-BOwGdiyH.js +1 -0
  8. package/dist/web/assets/ai-settings-section-Cqz13gRt.js +1 -0
  9. package/dist/web/assets/{api-client-C1ZwRL4Y.js → api-client-Jn3njtNw.js} +1 -1
  10. package/dist/web/assets/api-client-rSFnnCj4.js +1 -0
  11. package/dist/web/assets/{api-settings-_7Q-ULxp.js → api-settings-BxijvPBR.js} +1 -1
  12. package/dist/web/assets/{api-themes-DRAERo4Q.js → api-themes-CSUarkzD.js} +1 -1
  13. package/dist/web/assets/{appearance-settings-section-CiMdk3sb.js → appearance-settings-section-DbA8L-Vz.js} +1 -1
  14. package/dist/web/assets/{audio-preview-DsYvgKqj.js → audio-preview-CxJd8wRV.js} +1 -1
  15. package/dist/web/assets/{branch-review-tab-B1pFjlKM.js → branch-review-tab-CMTa2uSR.js} +1 -1
  16. package/dist/web/assets/chat-tab-BpdgqjSL.js +15 -0
  17. package/dist/web/assets/{code-editor-BJhOEsnZ.js → code-editor-B7y7-5KE.js} +3 -3
  18. package/dist/web/assets/{codex-daily-guard-CaCQJ4r_.js → codex-daily-guard-ChDPiXDx.js} +1 -1
  19. package/dist/web/assets/{conflict-editor-B7u_p49h.js → conflict-editor-Axs9Ad7A.js} +1 -1
  20. package/dist/web/assets/{csv-preview-CttjJU1e.js → csv-preview-DTUAW2HW.js} +1 -1
  21. package/dist/web/assets/{database-viewer-Ca4HxIfx.js → database-viewer-qXDiWeXj.js} +1 -1
  22. package/dist/web/assets/{diff-viewer-9-XjLIwa.js → diff-viewer-BtLiSpqe.js} +1 -1
  23. package/dist/web/assets/{docx-preview-CXuk-emm.js → docx-preview-Dxvy6h13.js} +1 -1
  24. package/dist/web/assets/{editor-language-service-D0DuEOs_.js → editor-language-service-D75GuLXP.js} +1 -1
  25. package/dist/web/assets/{explorer-body-VdCBqcZu.js → explorer-body-xg1-MhlW.js} +1 -1
  26. package/dist/web/assets/{explorer-window-content-BIsbl7Th.js → explorer-window-content-BeQocW3i.js} +1 -1
  27. package/dist/web/assets/{extension-manager-section-HkY7geS5.js → extension-manager-section-Bm2v3P-n.js} +1 -1
  28. package/dist/web/assets/{extension-webview-DZG8YHX9.js → extension-webview-CzxrAHyL.js} +1 -1
  29. package/dist/web/assets/{file-icons-BXdvggK2.js → file-icons-wS-OmEVu.js} +1 -1
  30. package/dist/web/assets/{file-store-ojgMXgAw.js → file-store-Ds3qJ4hC.js} +1 -1
  31. package/dist/web/assets/{files-settings-section-qnRtkX05.js → files-settings-section-Bd-_GeZU.js} +1 -1
  32. package/dist/web/assets/{format-file-meta-C77h04cZ.js → format-file-meta-PPi3ptkm.js} +1 -1
  33. package/dist/web/assets/{general-settings-section-BAFSZmCE.js → general-settings-section-CfNPHQUk.js} +1 -1
  34. package/dist/web/assets/{git-log-panel-2sGpfEla.js → git-log-panel-CtGxPzH6.js} +1 -1
  35. package/dist/web/assets/{git-repo-store-88Z8v-pF.js → git-repo-store-BCEPfUGT.js} +1 -1
  36. package/dist/web/assets/{glide-data-grid-f0hyITiF.js → glide-data-grid-iSo2vP0q.js} +1 -1
  37. package/dist/web/assets/{group-chat-tab-Ck6sk7yY.js → group-chat-tab-D1lx0zz-.js} +1 -1
  38. package/dist/web/assets/{image-preview-CzCc4_Do.js → image-preview-DeGD0HJu.js} +1 -1
  39. package/dist/web/assets/{index-CEZVxM4G.js → index-CN0G8uYY.js} +5 -5
  40. package/dist/web/assets/{jira-watcher-section-CeLmhvu1.js → jira-watcher-section-kyifZPTv.js} +1 -1
  41. package/dist/web/assets/keybindings-store-L5vV8DEJ.js +1 -0
  42. package/dist/web/assets/{keyboard-shortcuts-section-PVz6kuh2.js → keyboard-shortcuts-section-D2uJq4x9.js} +1 -1
  43. package/dist/web/assets/{markdown-renderer-C140Ijk0.js → markdown-renderer-BX1Q00sC.js} +1 -1
  44. package/dist/web/assets/markdown-renderer-CLUux0vA.js +1 -0
  45. package/dist/web/assets/{media-url-yfqHpPz0.js → media-url-DKK0YHxL.js} +1 -1
  46. package/dist/web/assets/{mobile-explorer-sheet-CUG_owdf.js → mobile-explorer-sheet-BHQDzpwK.js} +1 -1
  47. package/dist/web/assets/notification-store-BHgSy-ci.js +1 -0
  48. package/dist/web/assets/panel-store-D20Zuf3i.js +1 -0
  49. package/dist/web/assets/{pdf-preview-CmBI6E7d.js → pdf-preview-DAcfhfGm.js} +1 -1
  50. package/dist/web/assets/{postgres-viewer-8ZAqEALv.js → postgres-viewer-DPRsmgP5.js} +1 -1
  51. package/dist/web/assets/{ppmbot-settings-section-Cji-Mil-.js → ppmbot-settings-section-B5N1jHgm.js} +1 -1
  52. package/dist/web/assets/{problems-panel-a6_hrWvl.js → problems-panel-D_wZ4_Qg.js} +1 -1
  53. package/dist/web/assets/{process-table-CiIqlOIK.js → process-table-DahflyQn.js} +1 -1
  54. package/dist/web/assets/{project-framework-store-B70Q3dNM.js → project-framework-store-ViZgce6y.js} +1 -1
  55. package/dist/web/assets/{project-store-B87hLRSk.js → project-store-hNvwfqX3.js} +1 -1
  56. package/dist/web/assets/{proxy-settings-section-BjI4IFgp.js → proxy-settings-section-CLg_nlxn.js} +1 -1
  57. package/dist/web/assets/{query-audit-section-By6qyqdB.js → query-audit-section-BrfSo0s_.js} +1 -1
  58. package/dist/web/assets/{remote-desktop-clipboard-notice-eXYvwgx7.js → remote-desktop-clipboard-notice-Fi6cieGM.js} +1 -1
  59. package/dist/web/assets/{remote-desktop-mobile-sheet-Doh-fci-.js → remote-desktop-mobile-sheet-lLZ-oK73.js} +2 -2
  60. package/dist/web/assets/{remote-desktop-mobile-view-Dt8Akc3p.js → remote-desktop-mobile-view-j3v-4rYa.js} +1 -1
  61. package/dist/web/assets/{remote-desktop-readiness-gate-t83fqbID.js → remote-desktop-readiness-gate-CeLvcX_w.js} +1 -1
  62. package/dist/web/assets/{remote-desktop-window-content-CguhZMmx.js → remote-desktop-window-content-lwbkLsTC.js} +1 -1
  63. package/dist/web/assets/{run-in-terminal-CdefEe_R.js → run-in-terminal-D7W4IKb9.js} +1 -1
  64. package/dist/web/assets/{schedules-settings-section-BVkpW5ms.js → schedules-settings-section-C8OS0o80.js} +1 -1
  65. package/dist/web/assets/{send-to-chat-F207yH3u.js → send-to-chat-DNKhmHbi.js} +1 -1
  66. package/dist/web/assets/{settings-body-CoIxYAh6.js → settings-body-DgN22Z0W.js} +2 -2
  67. package/dist/web/assets/{settings-store-krC6bc79.js → settings-store-CsVSVgLp.js} +2 -2
  68. package/dist/web/assets/{settings-tab-7DKVUTbM.js → settings-tab-ChOs94iB.js} +1 -1
  69. package/dist/web/assets/{settings-window-content-3_39TeX-.js → settings-window-content-BnKMmg2a.js} +1 -1
  70. package/dist/web/assets/{sql-query-editor-CQB7Yp7U.js → sql-query-editor-BJZIIQsm.js} +1 -1
  71. package/dist/web/assets/{sqlite-viewer-Bd7rjr1D.js → sqlite-viewer--cMeCZum.js} +1 -1
  72. package/dist/web/assets/{system-monitor-body-DsRXeMyU.js → system-monitor-body-CXiIrn0N.js} +2 -2
  73. package/dist/web/assets/system-monitor-tab-BJKrTSl0.js +1 -0
  74. package/dist/web/assets/{system-monitor-window-content-ClPo4-El.js → system-monitor-window-content-Dy9fZJE3.js} +1 -1
  75. package/dist/web/assets/{tab-host-window-content-d2TAD4PU.js → tab-host-window-content-BezqrAwC.js} +1 -1
  76. package/dist/web/assets/{tab-store-OJa1GPCq.js → tab-store-krAuHQ6_.js} +1 -1
  77. package/dist/web/assets/{team-member-sheet-7d2Qp-ie.js → team-member-sheet-CVM2QC3H.js} +2 -2
  78. package/dist/web/assets/{team-member-window-content-Bctg2gmg.js → team-member-window-content-D4RYKplm.js} +1 -1
  79. package/dist/web/assets/{terminal-tab-0HQic25V.js → terminal-tab-jikhQLct.js} +1 -1
  80. package/dist/web/assets/{tool-cards-CmLLRACd.js → tool-cards-D5jCQvqn.js} +2 -2
  81. package/dist/web/assets/{use-blob-url-CPtCcY3i.js → use-blob-url-DMGlPCQD.js} +1 -1
  82. package/dist/web/assets/{use-inline-blame-sDMjcz1-.js → use-inline-blame-BF8Y1KfV.js} +1 -1
  83. package/dist/web/assets/{use-lsp-CBgID-EU.js → use-lsp-Bx4oVtlk.js} +1 -1
  84. package/dist/web/assets/{use-monaco-theme-gvhJ_jZh.js → use-monaco-theme-DHczXLMb.js} +1 -1
  85. package/dist/web/assets/{use-websocket-C9wlRliB.js → use-websocket-BWZkbleJ.js} +1 -1
  86. package/dist/web/assets/{video-preview-CSDK5j6H.js → video-preview-D191TvX9.js} +1 -1
  87. package/dist/web/assets/{window-pip-registry-yD6esSju.js → window-pip-registry-D1oXq-Dq.js} +1 -1
  88. package/dist/web/assets/ws-auth-BG-cdrQ8.js +1 -0
  89. package/dist/web/index.html +13 -13
  90. package/dist/web/sw.js +1 -1
  91. package/docs/architecture/workspace-and-ui.md +13 -0
  92. package/package.json +1 -1
  93. package/src/cli/commands/init.ts +1 -0
  94. package/src/providers/codex-app-server/codex-history.ts +26 -7
  95. package/src/providers/codex-app-server/codex-provider.ts +6 -4
  96. package/src/server/routes/chat.ts +1 -1
  97. package/src/server/routes/settings.ts +11 -5
  98. package/src/services/config.service.ts +2 -0
  99. package/src/types/chat.ts +1 -1
  100. package/src/types/config.ts +12 -1
  101. package/src/web/components/chat/chat-tab.tsx +162 -38
  102. package/src/web/components/chat/message-input.tsx +24 -0
  103. package/src/web/components/chat/model-thinking-selector.tsx +25 -5
  104. package/src/web/components/chat/new-chat-provider-gate.tsx +68 -0
  105. package/src/web/components/layout/editor-panel.tsx +1 -1
  106. package/src/web/components/layout/reparenting-tab.tsx +4 -2
  107. package/src/web/components/layout/tab-bar.tsx +1 -1
  108. package/src/web/components/settings/ai-settings-section.tsx +36 -0
  109. package/src/web/hooks/use-draft.ts +25 -4
  110. package/src/web/hooks/use-usage.ts +6 -4
  111. package/src/web/lib/api-client.ts +7 -1
  112. package/src/web/lib/api-settings.ts +1 -0
  113. package/src/web/lib/new-chat-provider.ts +8 -0
  114. package/src/web/stores/panel-store.ts +46 -3
  115. package/dist/web/assets/ai-settings-section-BtR7rF-T.js +0 -1
  116. package/dist/web/assets/ai-settings-section-r9JctHNM.js +0 -1
  117. package/dist/web/assets/api-client-pQL53cDQ.js +0 -1
  118. package/dist/web/assets/chat-tab-DZagX06m.js +0 -15
  119. package/dist/web/assets/keybindings-store-C_3ChOEf.js +0 -1
  120. package/dist/web/assets/markdown-renderer-Dz7-DYUg.js +0 -1
  121. package/dist/web/assets/notification-store-6vib1ISy.js +0 -1
  122. package/dist/web/assets/panel-store-CboGJAwh.js +0 -1
  123. package/dist/web/assets/system-monitor-tab-DdSZgux3.js +0 -1
  124. package/dist/web/assets/ws-auth-CEYVQ8NH.js +0 -1
@@ -9,7 +9,7 @@
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com" />
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
11
  <link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600;700&family=Geist:wght@400;500;600;700&display=swap" rel="stylesheet" />
12
- <script type="module" crossorigin src="/assets/index-CEZVxM4G.js"></script>
12
+ <script type="module" crossorigin src="/assets/index-CN0G8uYY.js"></script>
13
13
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-FhOqtrmT.js">
14
14
  <link rel="modulepreload" crossorigin href="/assets/vendor-mermaid-BitiXJlL.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/vendor-markdown-CuWCHIVb.js">
@@ -17,14 +17,14 @@
17
17
  <link rel="modulepreload" crossorigin href="/assets/utils-Bmf_8h7Y.js">
18
18
  <link rel="modulepreload" crossorigin href="/assets/button-B9CTrsEf.js">
19
19
  <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-DoiEKeJ_.js">
20
- <link rel="modulepreload" crossorigin href="/assets/api-client-C1ZwRL4Y.js">
20
+ <link rel="modulepreload" crossorigin href="/assets/api-client-Jn3njtNw.js">
21
21
  <link rel="modulepreload" crossorigin href="/assets/react-Bghu0NUO.js">
22
- <link rel="modulepreload" crossorigin href="/assets/settings-store-krC6bc79.js">
23
- <link rel="modulepreload" crossorigin href="/assets/file-store-ojgMXgAw.js">
22
+ <link rel="modulepreload" crossorigin href="/assets/settings-store-CsVSVgLp.js">
23
+ <link rel="modulepreload" crossorigin href="/assets/file-store-Ds3qJ4hC.js">
24
24
  <link rel="modulepreload" crossorigin href="/assets/window-store-UC1jJeLM.js">
25
- <link rel="modulepreload" crossorigin href="/assets/panel-store-CboGJAwh.js">
26
- <link rel="modulepreload" crossorigin href="/assets/window-pip-registry-yD6esSju.js">
27
- <link rel="modulepreload" crossorigin href="/assets/format-file-meta-C77h04cZ.js">
25
+ <link rel="modulepreload" crossorigin href="/assets/panel-store-D20Zuf3i.js">
26
+ <link rel="modulepreload" crossorigin href="/assets/window-pip-registry-D1oXq-Dq.js">
27
+ <link rel="modulepreload" crossorigin href="/assets/format-file-meta-PPi3ptkm.js">
28
28
  <link rel="modulepreload" crossorigin href="/assets/portal-container-context-BarKmT4S.js">
29
29
  <link rel="modulepreload" crossorigin href="/assets/icons-DQjqk0lJ.js">
30
30
  <link rel="modulepreload" crossorigin href="/assets/context-menu-C6pIsf83.js">
@@ -34,13 +34,13 @@
34
34
  <link rel="modulepreload" crossorigin href="/assets/mobile-bottom-sheet-C3TniEP-.js">
35
35
  <link rel="modulepreload" crossorigin href="/assets/scroll-area-BzeiOd-B.js">
36
36
  <link rel="modulepreload" crossorigin href="/assets/use-is-mobile-TkcwP1Gx.js">
37
- <link rel="modulepreload" crossorigin href="/assets/project-framework-store-B70Q3dNM.js">
38
- <link rel="modulepreload" crossorigin href="/assets/file-icons-BXdvggK2.js">
39
- <link rel="modulepreload" crossorigin href="/assets/git-repo-store-88Z8v-pF.js">
40
- <link rel="modulepreload" crossorigin href="/assets/project-store-B87hLRSk.js">
41
- <link rel="modulepreload" crossorigin href="/assets/ws-auth-CEYVQ8NH.js">
37
+ <link rel="modulepreload" crossorigin href="/assets/project-framework-store-ViZgce6y.js">
38
+ <link rel="modulepreload" crossorigin href="/assets/file-icons-wS-OmEVu.js">
39
+ <link rel="modulepreload" crossorigin href="/assets/git-repo-store-BCEPfUGT.js">
40
+ <link rel="modulepreload" crossorigin href="/assets/project-store-hNvwfqX3.js">
41
+ <link rel="modulepreload" crossorigin href="/assets/ws-auth-BG-cdrQ8.js">
42
42
  <link rel="modulepreload" crossorigin href="/assets/streaming-store-BYukdB56.js">
43
- <link rel="modulepreload" crossorigin href="/assets/tab-store-OJa1GPCq.js">
43
+ <link rel="modulepreload" crossorigin href="/assets/tab-store-krAuHQ6_.js">
44
44
  <link rel="modulepreload" crossorigin href="/assets/apply-theme-CbR31K6I.js">
45
45
  <link rel="modulepreload" crossorigin href="/assets/resolve-theme-B_ukrg_8.js">
46
46
  <link rel="stylesheet" crossorigin href="/assets/index-BH99TIE0.css">
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))}var B=`-precache-`,V=async(e,t=B)=>{let n=(await self.caches.keys()).filter(n=>n.includes(t)&&n.includes(self.registration.scope)&&n!==e);return await Promise.all(n.map(e=>self.caches.delete(e))),n};function H(){self.addEventListener(`activate`,(e=>{let t=a.getPrecacheName();e.waitUntil(V(t).then(e=>{}))}))}function U(e){O().precache(e)}function W(e,t){U(e),z(t)}var G=class extends w{async _handle(e,n){let r=await n.cacheMatch(e),i;if(!r)try{r=await n.fetchAndCachePut(e)}catch(e){e instanceof Error&&(i=e)}if(!r)throw new t(`no-response`,{url:e.url,error:i});return r}};W([{"revision":"e7df0dd1bfe9b16560e99adb2afd00f8","url":"index.html"},{"revision":"26dccd02a2ef7522892015154f5e3680","url":"manifest.webmanifest"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":null,"url":"assets/index-CEZVxM4G.js"},{"revision":null,"url":"assets/index-BH99TIE0.css"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"26dccd02a2ef7522892015154f5e3680","url":"manifest.webmanifest"}]),H();var K=e=>(e.headers.get(`content-type`)??``).startsWith(`text/html`);F(({url:e})=>e.pathname.startsWith(`/assets/`),new G({cacheName:`ppm-assets`,plugins:[{cacheWillUpdate:async({response:e})=>K(e)?null:e,cachedResponseWillBeUsed:async({cachedResponse:e})=>e&&K(e)?null:e}]})),self.skipWaiting(),self.addEventListener(`activate`,()=>{self.clients.claim()});
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))}var B=`-precache-`,V=async(e,t=B)=>{let n=(await self.caches.keys()).filter(n=>n.includes(t)&&n.includes(self.registration.scope)&&n!==e);return await Promise.all(n.map(e=>self.caches.delete(e))),n};function H(){self.addEventListener(`activate`,(e=>{let t=a.getPrecacheName();e.waitUntil(V(t).then(e=>{}))}))}function U(e){O().precache(e)}function W(e,t){U(e),z(t)}var G=class extends w{async _handle(e,n){let r=await n.cacheMatch(e),i;if(!r)try{r=await n.fetchAndCachePut(e)}catch(e){e instanceof Error&&(i=e)}if(!r)throw new t(`no-response`,{url:e.url,error:i});return r}};W([{"revision":"206312c0035b4551edbf7f8f5cf4d80f","url":"index.html"},{"revision":"26dccd02a2ef7522892015154f5e3680","url":"manifest.webmanifest"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":null,"url":"assets/index-CN0G8uYY.js"},{"revision":null,"url":"assets/index-BH99TIE0.css"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"26dccd02a2ef7522892015154f5e3680","url":"manifest.webmanifest"}]),H();var K=e=>(e.headers.get(`content-type`)??``).startsWith(`text/html`);F(({url:e})=>e.pathname.startsWith(`/assets/`),new G({cacheName:`ppm-assets`,plugins:[{cacheWillUpdate:async({response:e})=>K(e)?null:e,cachedResponseWillBeUsed:async({cachedResponse:e})=>e&&K(e)?null:e}]})),self.skipWaiting(),self.addEventListener(`activate`,()=>{self.clients.claim()});
@@ -2,6 +2,19 @@
2
2
 
3
3
  > Part of the [PPM system architecture](../system-architecture.md).
4
4
 
5
+ ## New Chat Provider
6
+
7
+ Settings → AI offers `Always use default provider` and `Follow last focused chat`.
8
+ `ai.default_provider` is the fixed choice or the fallback when the current project has
9
+ no focused chat. Fresh installations use follow-focus; existing configurations without
10
+ `ai.new_chat_provider_mode` retain default mode.
11
+
12
+ Focus memory is per project and browser session, across panels. Editors, terminals and
13
+ panel chrome do not replace it. New tabs snapshot the source at creation and resolve
14
+ settings before mounting chat or claiming an account. An unavailable provider prompts
15
+ for an available choice. Existing tabs, resumed sessions and explicit fork/clear
16
+ providers are preserved; changing the setting only affects new tabs.
17
+
5
18
  ## File Service & Filtering (Lazy-Load Tree, Palette Index)
6
19
 
7
20
  **Component:** FileFilterService + API endpoints `/files/list`, `/files/index`, settings endpoints
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.22.1",
3
+ "version": "0.22.2",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -155,6 +155,7 @@ export async function initProject(options: InitOptions = {}) {
155
155
  configService.set("auth", { enabled: authEnabled, token: authToken });
156
156
  configService.set("ai", {
157
157
  default_provider: "claude",
158
+ new_chat_provider_mode: configService.get("ai").new_chat_provider_mode,
158
159
  providers: {
159
160
  claude: {
160
161
  type: "agent-sdk",
@@ -1,8 +1,9 @@
1
1
  import type { ChatMessage, ChatEvent, SessionInfo } from "../provider.interface.ts";
2
2
  import { stripSharedContext } from "../../shared/provider-context.ts";
3
- import { readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { readdirSync, readFileSync, statSync, realpathSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
  import { homedir } from "node:os";
6
+ import { getPpmDir } from "../../services/ppm-dir.ts";
6
7
  import { redactTruncate } from "./codex-redact.ts";
7
8
  import { parseApplyPatch, changeToToolUse } from "./codex-patch.ts";
8
9
  import { mapRolloutItem } from "./codex-rollout-items.ts";
@@ -449,14 +450,27 @@ export function getRolloutMessages(sessionsDir: string, threadId: string, reques
449
450
 
450
451
  /** True when a path points at a codex rollout in the ambient or a PPM account home. */
451
452
  export function isCodexRolloutPath(p: string): boolean {
452
- const n = normPath(p);
453
- const ambient = normPath(join(homedir(), ".codex", "sessions"));
454
- const managed = normPath(join(homedir(), ".ppm", "codex-accounts"));
455
- return n.endsWith(".jsonl") && (n.includes(ambient) || (n.includes(managed) && n.includes("/sessions/")));
453
+ return withinCodexSessions(p, false);
454
+ }
455
+
456
+ function withinCodexSessions(p: string, canonical: boolean): boolean {
457
+ const normalize = (value: string) => process.platform === "win32" ? normPath(value).replace(/\\/g, "/") : normPath(value);
458
+ const n = normalize(p);
459
+ if (!n.endsWith(".jsonl")) return false;
460
+ const rootPath = (value: string) => {
461
+ if (canonical) { try { return normalize(realpathSync(value)); } catch { return null; } }
462
+ return normalize(value);
463
+ };
464
+ const ambientRoots = [join(homedir(), ".codex", "sessions")];
465
+ if (process.env.CODEX_HOME) ambientRoots.push(join(process.env.CODEX_HOME, "sessions"));
466
+ if (ambientRoots.some((value) => { const root = rootPath(value); return root && n.startsWith(root + "/"); })) return true;
467
+ const managed = rootPath(join(getPpmDir(), "codex-accounts"));
468
+ return !!managed && n.startsWith(managed + "/") && /^[^/]+\/sessions\/.+/.test(n.slice(managed.length + 1));
456
469
  }
457
470
 
458
471
  /**
459
- * One pre-compact segment for the "load more" feature. Jails to ~/.codex/sessions,
472
+ * One pre-compact segment for the "load more" feature. Jails to ambient,
473
+ * CODEX_HOME, or PPM managed-account session directories via real paths;
460
474
  * fail-closed on cwd.
461
475
  *
462
476
  * One segment per request rather than everything before the first boundary, which is
@@ -468,7 +482,12 @@ export function isCodexRolloutPath(p: string): boolean {
468
482
  */
469
483
  export function getCodexPreCompactMessages(file: string, requestedCwd?: string, beforeId?: string): ChatMessage[] {
470
484
  if (!isCodexRolloutPath(file)) throw new Error("Access denied: not a codex rollout");
471
- const resolved = resolve(file);
485
+ let resolved: string;
486
+ try { resolved = realpathSync(resolve(file)); } catch { throw new Error("File not found"); }
487
+ if (!withinCodexSessions(resolved, true)) throw new Error("Access denied: transcript outside Codex sessions");
488
+ const info = statSync(resolved);
489
+ if (!info.isFile()) throw new Error("Not a regular file");
490
+ if (info.size > 256 * 1024 * 1024) throw new Error("Transcript too large");
472
491
  let text: string;
473
492
  try { text = readFileSync(resolved, "utf-8"); } catch { throw new Error("File not found"); }
474
493
  // Read once and reused below. `readSessionMeta` opens and reads the whole rollout again, and
@@ -1147,12 +1147,14 @@ export class CodexAppServerProvider implements AIProvider {
1147
1147
  }
1148
1148
 
1149
1149
  /** Read the session's account without advancing the account-selection strategy. */
1150
- async getUsage(sessionId?: string): Promise<UsageInfo> {
1150
+ async getUsage(sessionId?: string, pickedAccountId?: string): Promise<UsageInfo> {
1151
1151
  // Idempotent, and needed because a caller can reach the provider before the
1152
1152
  // server has started background polling — an unregistered source would make
1153
1153
  // the shared layer answer {} instead of reading codex.
1154
1154
  registerUsageSource(codexUsageSource);
1155
- const accountId = sessionId ? getSessionCodexAccount(sessionId) : null;
1155
+ // An unopened chat has a claimed account but no session binding yet.
1156
+ // Read that exact account without advancing the selection strategy.
1157
+ const accountId = sessionId ? getSessionCodexAccount(sessionId) : pickedAccountId ?? null;
1156
1158
  const bound = accountId ? getCodexAccount(accountId) : null;
1157
1159
  if (bound) {
1158
1160
  // Through the shared layer, so this answers from the cache or the stored
@@ -1166,12 +1168,12 @@ export class CodexAppServerProvider implements AIProvider {
1166
1168
  };
1167
1169
  }
1168
1170
 
1169
- // Nothing bound yet — a session binds on its first send. Name the account
1171
+ // No binding or explicit tab claim — a session binds on its first send. Name the account
1170
1172
  // that will serve it, but do NOT read its quota: that spawns an app-server
1171
1173
  // against a login which is not yet this session's, and which a round-robin
1172
1174
  // or lowest-usage pick may never hand it. So the toolbar can say WHO will
1173
1175
  // answer without PPM touching that account on a session's behalf before
1174
- // the session owns it. The numbers arrive with the first turn.
1176
+ // the session owns it. Quota is read once a tab claims it or a session binds it.
1175
1177
  //
1176
1178
  // Skipped when the session names an account that no longer exists: naming
1177
1179
  // a different one there would be a lie about a binding that already failed.
@@ -135,7 +135,7 @@ chatRoutes.get("/usage", async (c) => {
135
135
  const { invalidateUsage } = await import("../../services/provider-usage/usage-registry.ts");
136
136
  invalidateUsage(providerId);
137
137
  }
138
- try { return c.json(ok(await provider.getUsage(c.req.query("session")))); } catch { return c.json(ok({})); }
138
+ try { return c.json(ok(await provider.getUsage(c.req.query("session"), c.req.query("accountId")))); } catch { return c.json(ok({})); }
139
139
  }
140
140
  return c.json(ok({}));
141
141
  }
@@ -5,9 +5,9 @@ import {
5
5
  validateAIProviderConfig,
6
6
  validateCodexContextConfig,
7
7
  validateDefaultProvider,
8
- VALID_PROVIDERS,
9
8
  DEFAULT_CONFIG,
10
9
  type AIProviderConfig,
10
+ type NewChatProviderMode,
11
11
  type TelegramConfig,
12
12
  type PPMBotConfig,
13
13
  type ThemeConfig,
@@ -184,12 +184,20 @@ settingsRoutes.put("/ai", async (c) => {
184
184
  try {
185
185
  const body = await c.req.json<{
186
186
  default_provider?: string;
187
+ new_chat_provider_mode?: NewChatProviderMode;
187
188
  share_provider_context?: boolean;
188
189
  providers?: Record<string, Partial<AIProviderConfig>>;
189
190
  }>();
190
191
 
191
192
  const currentAi = configService.get("ai");
192
193
 
194
+ if ("new_chat_provider_mode" in body && body.new_chat_provider_mode !== "default" && body.new_chat_provider_mode !== "follow-focus") {
195
+ return c.json(err("new_chat_provider_mode must be one of: default, follow-focus"), 400);
196
+ }
197
+ if ("default_provider" in body && (typeof body.default_provider !== "string" || !body.default_provider)) {
198
+ return c.json(err("default_provider must be a non-empty string"), 400);
199
+ }
200
+
193
201
  if ("share_provider_context" in body && typeof body.share_provider_context !== "boolean") {
194
202
  return c.json(err("share_provider_context must be a boolean"), 400);
195
203
  }
@@ -207,6 +215,7 @@ settingsRoutes.put("/ai", async (c) => {
207
215
  // Merge: body overrides current values (shallow merge per provider)
208
216
  const updated = {
209
217
  ...currentAi,
218
+ new_chat_provider_mode: body.new_chat_provider_mode ?? currentAi.new_chat_provider_mode ?? "default",
210
219
  share_provider_context: body.share_provider_context ?? currentAi.share_provider_context ?? true,
211
220
  ...(body.default_provider && { default_provider: body.default_provider }),
212
221
  };
@@ -231,11 +240,8 @@ settingsRoutes.put("/ai", async (c) => {
231
240
  }
232
241
  }
233
242
 
234
- // Validate default_provider is in allowed list and references existing provider
243
+ // Configured providers include runtime integrations such as Codex.
235
244
  if (body.default_provider) {
236
- if (!VALID_PROVIDERS.includes(body.default_provider as any)) {
237
- return c.json(err(`default_provider must be one of: ${VALID_PROVIDERS.join(", ")}`), 400);
238
- }
239
245
  const dpErr = validateDefaultProvider(updated.default_provider, updated.providers);
240
246
  if (dpErr) return c.json(err(dpErr), 400);
241
247
  }
@@ -167,6 +167,8 @@ class ConfigService {
167
167
  dbProjects: { path: string; name: string; color: string | null }[],
168
168
  ): PpmConfig {
169
169
  const config = structuredClone(DEFAULT_CONFIG);
170
+ // Existing installs without an AI row must also retain the old tab behavior.
171
+ config.ai.new_chat_provider_mode = "default";
170
172
  for (const [key, jsonValue] of Object.entries(dbRows)) {
171
173
  if (key in config && key !== "projects") {
172
174
  try {
package/src/types/chat.ts CHANGED
@@ -70,7 +70,7 @@ export interface AIProvider {
70
70
  /** Drop runtime skill discovery results after a user requests a refresh. */
71
71
  invalidateSkillsCache?(): void;
72
72
  /** Provider-specific usage/quota (rate limits). Used by GET /chat/usage. */
73
- getUsage?(sessionId?: string): Promise<UsageInfo>;
73
+ getUsage?(sessionId?: string, pickedAccountId?: string): Promise<UsageInfo>;
74
74
  /** True when a live streaming subprocess exists for this session */
75
75
  hasStreamingSession?(sessionId: string): boolean;
76
76
  /** Prompt-cache lifetime for this session, in ms — how long holding its subprocess pays. */
@@ -74,8 +74,12 @@ export interface ProjectConfig {
74
74
  image?: string;
75
75
  }
76
76
 
77
+ export type NewChatProviderMode = "default" | "follow-focus";
78
+
77
79
  export interface AIConfig {
78
80
  default_provider: string;
81
+ /** Missing in legacy settings: use the configured default provider. */
82
+ new_chat_provider_mode?: NewChatProviderMode;
79
83
  /** Share project rules and memory between providers. Unset defaults to true. */
80
84
  share_provider_context?: boolean;
81
85
  providers: Record<string, AIProviderConfig>;
@@ -140,6 +144,7 @@ export const DEFAULT_CONFIG: PpmConfig = {
140
144
  projects: [],
141
145
  ai: {
142
146
  default_provider: "claude",
147
+ new_chat_provider_mode: "follow-focus",
143
148
  share_provider_context: true,
144
149
  providers: {
145
150
  claude: {
@@ -280,7 +285,7 @@ export function validateCodexContextConfig(config: Partial<AIProviderConfig>): s
280
285
 
281
286
  /** Validate default_provider references an existing provider key */
282
287
  export function validateDefaultProvider(defaultProvider: string, providers: Record<string, unknown>): string | null {
283
- if (!providers[defaultProvider]) {
288
+ if (!Object.hasOwn(providers, defaultProvider)) {
284
289
  return `default_provider "${defaultProvider}" not found in providers`;
285
290
  }
286
291
  return null;
@@ -293,6 +298,12 @@ export function validateDefaultProvider(defaultProvider: string, providers: Reco
293
298
  export function sanitizeConfig(config: PpmConfig): boolean {
294
299
  let dirty = false;
295
300
 
301
+ // Preserve legacy behavior on upgrade; only fresh configs opt into following focus.
302
+ if (config.ai.new_chat_provider_mode !== "default" && config.ai.new_chat_provider_mode !== "follow-focus") {
303
+ config.ai.new_chat_provider_mode = "default";
304
+ dirty = true;
305
+ }
306
+
296
307
  if (typeof config.ai.share_provider_context !== "boolean") {
297
308
  config.ai.share_provider_context = true;
298
309
  dirty = true;