@hienlh/ppm 0.23.22 → 0.23.24

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 (463) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/CLAUDE.md +3 -1
  3. package/README.md +1 -1
  4. package/assets/skills/ppm/SKILL.md +1 -1
  5. package/assets/skills/ppm/references/http-api.md +2 -1
  6. package/dist/web/assets/{accounts-settings-section-CiNmIj02.js → accounts-settings-section-Cg-LncY6.js} +1 -1
  7. package/dist/web/assets/accounts-settings-section-Cg-LncY6.js.br +0 -0
  8. package/dist/web/assets/accounts-settings-section-Cg-LncY6.js.gz +0 -0
  9. package/dist/web/assets/{agent-session-sheet-DRR36PDA.js → agent-session-sheet-DMdV8GOu.js} +2 -2
  10. package/dist/web/assets/agent-session-sheet-DMdV8GOu.js.br +0 -0
  11. package/dist/web/assets/agent-session-sheet-DMdV8GOu.js.gz +0 -0
  12. package/dist/web/assets/{agent-session-window-content-DMhy16BM.js → agent-session-window-content-CYg58J-V.js} +1 -1
  13. package/dist/web/assets/agent-session-window-content-CYg58J-V.js.br +0 -0
  14. package/dist/web/assets/agent-session-window-content-CYg58J-V.js.gz +0 -0
  15. package/dist/web/assets/{ai-resource-editor-BG8boPw7.js → ai-resource-editor-BvEnn0Z_.js} +1 -1
  16. package/dist/web/assets/ai-resource-editor-BvEnn0Z_.js.br +0 -0
  17. package/dist/web/assets/ai-resource-editor-BvEnn0Z_.js.gz +0 -0
  18. package/dist/web/assets/{ai-settings-section-De5dYWlC.js → ai-settings-section-Bo-nf2rY.js} +1 -1
  19. package/dist/web/assets/ai-settings-section-DAqiu_11.js +1 -0
  20. package/dist/web/assets/ai-settings-section-DAqiu_11.js.br +0 -0
  21. package/dist/web/assets/ai-settings-section-DAqiu_11.js.gz +0 -0
  22. package/dist/web/assets/{android-tab-lHDxD93G.js → android-tab-C-wmlFc0.js} +1 -1
  23. package/dist/web/assets/android-tab-C-wmlFc0.js.br +0 -0
  24. package/dist/web/assets/android-tab-C-wmlFc0.js.gz +0 -0
  25. package/dist/web/assets/{appearance-settings-section-DO-ZyoMj.js → appearance-settings-section-CYqX6BGh.js} +1 -1
  26. package/dist/web/assets/appearance-settings-section-CYqX6BGh.js.br +0 -0
  27. package/dist/web/assets/appearance-settings-section-CYqX6BGh.js.gz +0 -0
  28. package/dist/web/assets/{apps-panel-CH-0CHYY.js → apps-panel-D1VTBxgC.js} +1 -1
  29. package/dist/web/assets/apps-panel-D1VTBxgC.js.br +0 -0
  30. package/dist/web/assets/apps-panel-D1VTBxgC.js.gz +0 -0
  31. package/dist/web/assets/{branch-review-tab-CdGtvekz.js → branch-review-tab-BG6vxSsC.js} +1 -1
  32. package/dist/web/assets/branch-review-tab-BG6vxSsC.js.br +0 -0
  33. package/dist/web/assets/branch-review-tab-BG6vxSsC.js.gz +0 -0
  34. package/dist/web/assets/chat-tab-BAwGLGyR.js +19 -0
  35. package/dist/web/assets/chat-tab-BAwGLGyR.js.br +0 -0
  36. package/dist/web/assets/chat-tab-BAwGLGyR.js.gz +0 -0
  37. package/dist/web/assets/{chat-tab-DMqWnFIo.js → chat-tab-BJ0Mi2sd.js} +1 -1
  38. package/dist/web/assets/chat-tab-BJ0Mi2sd.js.br +0 -0
  39. package/dist/web/assets/chat-tab-BJ0Mi2sd.js.gz +0 -0
  40. package/dist/web/assets/{code-editor-BCv8ONNS.js → code-editor-DuwbwTpM.js} +4 -4
  41. package/dist/web/assets/code-editor-DuwbwTpM.js.br +0 -0
  42. package/dist/web/assets/code-editor-DuwbwTpM.js.gz +0 -0
  43. package/dist/web/assets/{codex-reset-credit-button-CGMbIHak.js → codex-reset-credit-button-tIwfnf2G.js} +1 -1
  44. package/dist/web/assets/codex-reset-credit-button-tIwfnf2G.js.br +0 -0
  45. package/dist/web/assets/{codex-reset-credit-button-CGMbIHak.js.gz → codex-reset-credit-button-tIwfnf2G.js.gz} +0 -0
  46. package/dist/web/assets/{conflict-editor-3WmzqBlW.js → conflict-editor-D2n-hQqs.js} +1 -1
  47. package/dist/web/assets/conflict-editor-D2n-hQqs.js.br +0 -0
  48. package/dist/web/assets/conflict-editor-D2n-hQqs.js.gz +0 -0
  49. package/dist/web/assets/{connection-form-tab-BoWucvAe.js → connection-form-tab-DYqPNq_-.js} +1 -1
  50. package/dist/web/assets/connection-form-tab-DYqPNq_-.js.br +0 -0
  51. package/dist/web/assets/connection-form-tab-DYqPNq_-.js.gz +0 -0
  52. package/dist/web/assets/{csv-preview-BCgGsy7F.js → csv-preview-Xh3Z6BS5.js} +1 -1
  53. package/dist/web/assets/csv-preview-Xh3Z6BS5.js.br +0 -0
  54. package/dist/web/assets/csv-preview-Xh3Z6BS5.js.gz +0 -0
  55. package/dist/web/assets/{db-tab-parts-o6QMHBbA.js → db-tab-parts-Dqnb3GCP.js} +1 -1
  56. package/dist/web/assets/db-tab-parts-Dqnb3GCP.js.br +0 -0
  57. package/dist/web/assets/db-tab-parts-Dqnb3GCP.js.gz +0 -0
  58. package/dist/web/assets/{design-settings-section-seo6SdSk.js → design-settings-section-DNNWgxF9.js} +1 -1
  59. package/dist/web/assets/design-settings-section-DNNWgxF9.js.br +0 -0
  60. package/dist/web/assets/design-settings-section-DNNWgxF9.js.gz +0 -0
  61. package/dist/web/assets/{design-tab-B-f_b2LM.js → design-tab-BoAdSpZZ.js} +1 -1
  62. package/dist/web/assets/design-tab-BoAdSpZZ.js.br +0 -0
  63. package/dist/web/assets/design-tab-BoAdSpZZ.js.gz +0 -0
  64. package/dist/web/assets/{designs-sidebar-panel-CVT_TQP7.js → designs-sidebar-panel-BKw7HFAz.js} +1 -1
  65. package/dist/web/assets/designs-sidebar-panel-BKw7HFAz.js.br +0 -0
  66. package/dist/web/assets/designs-sidebar-panel-BKw7HFAz.js.gz +0 -0
  67. package/dist/web/assets/{diff-viewer-y9uBp_8u.js → diff-viewer-CRmfCAZX.js} +1 -1
  68. package/dist/web/assets/diff-viewer-CRmfCAZX.js.br +0 -0
  69. package/dist/web/assets/diff-viewer-CRmfCAZX.js.gz +0 -0
  70. package/dist/web/assets/{docx-preview-Dn_x3Inm.js → docx-preview-zIrtL09u.js} +1 -1
  71. package/dist/web/assets/docx-preview-zIrtL09u.js.br +0 -0
  72. package/dist/web/assets/docx-preview-zIrtL09u.js.gz +0 -0
  73. package/dist/web/assets/{editor-dialog-BwNXSajD.js → editor-dialog-jMYvXVla.js} +1 -1
  74. package/dist/web/assets/editor-dialog-jMYvXVla.js.br +0 -0
  75. package/dist/web/assets/editor-dialog-jMYvXVla.js.gz +0 -0
  76. package/dist/web/assets/{editor-language-service-XA_TnPLN.js → editor-language-service-Yb-61Bro.js} +1 -1
  77. package/dist/web/assets/editor-language-service-Yb-61Bro.js.br +0 -0
  78. package/dist/web/assets/editor-language-service-Yb-61Bro.js.gz +0 -0
  79. package/dist/web/assets/{explorer-body-DukR7zM9.js → explorer-body-C6qKehmd.js} +1 -1
  80. package/dist/web/assets/explorer-body-C6qKehmd.js.br +0 -0
  81. package/dist/web/assets/explorer-body-C6qKehmd.js.gz +0 -0
  82. package/dist/web/assets/{explorer-window-content-BjTZ44S3.js → explorer-window-content-B9P8grrY.js} +1 -1
  83. package/dist/web/assets/explorer-window-content-B9P8grrY.js.br +0 -0
  84. package/dist/web/assets/explorer-window-content-B9P8grrY.js.gz +0 -0
  85. package/dist/web/assets/{extension-manager-section-C4WD1BPQ.js → extension-manager-section-C45v1QtG.js} +1 -1
  86. package/dist/web/assets/extension-manager-section-C45v1QtG.js.br +0 -0
  87. package/dist/web/assets/extension-manager-section-C45v1QtG.js.gz +0 -0
  88. package/dist/web/assets/{extension-webview-CV2ujflc.js → extension-webview-DIxM-xkH.js} +1 -1
  89. package/dist/web/assets/extension-webview-DIxM-xkH.js.br +0 -0
  90. package/dist/web/assets/extension-webview-DIxM-xkH.js.gz +0 -0
  91. package/dist/web/assets/{files-settings-section-hS5ixQ8o.js → files-settings-section-DeVlzKXd.js} +1 -1
  92. package/dist/web/assets/files-settings-section-DeVlzKXd.js.br +0 -0
  93. package/dist/web/assets/files-settings-section-DeVlzKXd.js.gz +0 -0
  94. package/dist/web/assets/format-date-CkyHzZLA.js +1 -0
  95. package/dist/web/assets/{general-settings-section-BnKYY0AA.js → general-settings-section-CLVDIEym.js} +1 -1
  96. package/dist/web/assets/general-settings-section-CLVDIEym.js.br +0 -0
  97. package/dist/web/assets/general-settings-section-CLVDIEym.js.gz +0 -0
  98. package/dist/web/assets/{git-log-panel-JiWfR5xv.js → git-log-panel-D3Q9NRll.js} +1 -1
  99. package/dist/web/assets/git-log-panel-D3Q9NRll.js.br +0 -0
  100. package/dist/web/assets/git-log-panel-D3Q9NRll.js.gz +0 -0
  101. package/dist/web/assets/{git-review-tab-DgvcY__Q.js → git-review-tab-ORgiYbva.js} +1 -1
  102. package/dist/web/assets/git-review-tab-ORgiYbva.js.br +0 -0
  103. package/dist/web/assets/git-review-tab-ORgiYbva.js.gz +0 -0
  104. package/dist/web/assets/{glide-data-grid-r1MKxDjz.js → glide-data-grid-CtWl1BTk.js} +1 -1
  105. package/dist/web/assets/glide-data-grid-CtWl1BTk.js.br +0 -0
  106. package/dist/web/assets/glide-data-grid-CtWl1BTk.js.gz +0 -0
  107. package/dist/web/assets/{grid-save-dialog-Fa0yA-Ep.js → grid-save-dialog-eV25Mqpg.js} +1 -1
  108. package/dist/web/assets/grid-save-dialog-eV25Mqpg.js.br +0 -0
  109. package/dist/web/assets/grid-save-dialog-eV25Mqpg.js.gz +0 -0
  110. package/dist/web/assets/{group-chat-tab-B2Q8yqSX.js → group-chat-tab-CYmbYXN2.js} +1 -1
  111. package/dist/web/assets/group-chat-tab-CYmbYXN2.js.br +0 -0
  112. package/dist/web/assets/group-chat-tab-CYmbYXN2.js.gz +0 -0
  113. package/dist/web/assets/{impexp-tab-B8Ht4l32.js → impexp-tab-D2OKxpFq.js} +1 -1
  114. package/dist/web/assets/impexp-tab-D2OKxpFq.js.br +0 -0
  115. package/dist/web/assets/impexp-tab-D2OKxpFq.js.gz +0 -0
  116. package/dist/web/assets/{index-BtAn_krv.js → index-D8QDpdgs.js} +7 -7
  117. package/dist/web/assets/index-D8QDpdgs.js.br +0 -0
  118. package/dist/web/assets/index-D8QDpdgs.js.gz +0 -0
  119. package/dist/web/assets/{jira-watcher-section--ibKZfBs.js → jira-watcher-section-B3OaMlim.js} +1 -1
  120. package/dist/web/assets/{keybindings-store-Cwxl4tHv.js → keybindings-store-C_aRhBu4.js} +1 -1
  121. package/dist/web/assets/{keyboard-shortcuts-section-aoiLpWwf.js → keyboard-shortcuts-section-DXRyKrSy.js} +1 -1
  122. package/dist/web/assets/keyboard-shortcuts-section-DXRyKrSy.js.br +0 -0
  123. package/dist/web/assets/keyboard-shortcuts-section-DXRyKrSy.js.gz +0 -0
  124. package/dist/web/assets/{language-servers-section-BkGAezo6.js → language-servers-section-BTs9iuZc.js} +2 -2
  125. package/dist/web/assets/language-servers-section-BTs9iuZc.js.br +0 -0
  126. package/dist/web/assets/language-servers-section-BTs9iuZc.js.gz +0 -0
  127. package/dist/web/assets/{logs-app-DYgPtmjm.js → logs-app-BlhVTOYN.js} +1 -1
  128. package/dist/web/assets/logs-app-BlhVTOYN.js.br +0 -0
  129. package/dist/web/assets/logs-app-BlhVTOYN.js.gz +0 -0
  130. package/dist/web/assets/{logs-tab-BbDosK63.js → logs-tab-kqNARbz0.js} +1 -1
  131. package/dist/web/assets/{logs-window-content-CEXAyv-f.js → logs-window-content-UUCJJCqG.js} +1 -1
  132. package/dist/web/assets/{lsp-client-CIcphNmU.js → lsp-client-BTlK_8lb.js} +1 -1
  133. package/dist/web/assets/lsp-client-BTlK_8lb.js.br +0 -0
  134. package/dist/web/assets/lsp-client-BTlK_8lb.js.gz +0 -0
  135. package/dist/web/assets/{markdown-renderer-BNzQeoAO.js → markdown-renderer-ClGxDien.js} +1 -1
  136. package/dist/web/assets/{markdown-renderer-dk1w3goH.js → markdown-renderer-DpsToFFB.js} +1 -1
  137. package/dist/web/assets/markdown-renderer-DpsToFFB.js.br +0 -0
  138. package/dist/web/assets/markdown-renderer-DpsToFFB.js.gz +0 -0
  139. package/dist/web/assets/{mobile-explorer-sheet-D4lPZQZc.js → mobile-explorer-sheet-CybJbScN.js} +1 -1
  140. package/dist/web/assets/mobile-explorer-sheet-CybJbScN.js.br +0 -0
  141. package/dist/web/assets/mobile-explorer-sheet-CybJbScN.js.gz +0 -0
  142. package/dist/web/assets/{new-design-dialog-CIq7xspW.js → new-design-dialog-9WTgcJoj.js} +1 -1
  143. package/dist/web/assets/new-design-dialog-9WTgcJoj.js.br +0 -0
  144. package/dist/web/assets/new-design-dialog-9WTgcJoj.js.gz +0 -0
  145. package/dist/web/assets/{notification-store-Rv36noYX.js → notification-store-CPVq5ec5.js} +1 -1
  146. package/dist/web/assets/{notifications-settings-section-CoWMtXCC.js → notifications-settings-section-xrYII-pL.js} +1 -1
  147. package/dist/web/assets/notifications-settings-section-xrYII-pL.js.br +0 -0
  148. package/dist/web/assets/notifications-settings-section-xrYII-pL.js.gz +0 -0
  149. package/dist/web/assets/{ppmbot-settings-section-Ct0UwA6T.js → ppmbot-settings-section-Bc46iDP9.js} +1 -1
  150. package/dist/web/assets/ppmbot-settings-section-Bc46iDP9.js.br +0 -0
  151. package/dist/web/assets/ppmbot-settings-section-Bc46iDP9.js.gz +0 -0
  152. package/dist/web/assets/{problems-panel-D-HT-0PE.js → problems-panel-D49-Xkzd.js} +1 -1
  153. package/dist/web/assets/problems-panel-D49-Xkzd.js.br +0 -0
  154. package/dist/web/assets/problems-panel-D49-Xkzd.js.gz +0 -0
  155. package/dist/web/assets/{process-table-CXN0s4Pq.js → process-table-NxYTZ0n9.js} +1 -1
  156. package/dist/web/assets/process-table-NxYTZ0n9.js.br +0 -0
  157. package/dist/web/assets/process-table-NxYTZ0n9.js.gz +0 -0
  158. package/dist/web/assets/{proxy-settings-section-e6WWzP8n.js → proxy-settings-section-Exx7cQc4.js} +1 -1
  159. package/dist/web/assets/proxy-settings-section-Exx7cQc4.js.br +0 -0
  160. package/dist/web/assets/proxy-settings-section-Exx7cQc4.js.gz +0 -0
  161. package/dist/web/assets/{query-tab-C7C67t8F.js → query-tab-BWtJPuxw.js} +1 -1
  162. package/dist/web/assets/query-tab-BWtJPuxw.js.br +0 -0
  163. package/dist/web/assets/query-tab-BWtJPuxw.js.gz +0 -0
  164. package/dist/web/assets/{remote-access-settings-section-gGoATCF9.js → remote-access-settings-section-C4K__DNW.js} +1 -1
  165. package/dist/web/assets/remote-access-settings-section-C4K__DNW.js.br +0 -0
  166. package/dist/web/assets/remote-access-settings-section-C4K__DNW.js.gz +0 -0
  167. package/dist/web/assets/{remote-desktop-clipboard-notice-D5KjvMZx.js → remote-desktop-clipboard-notice-DQyysBwL.js} +1 -1
  168. package/dist/web/assets/remote-desktop-clipboard-notice-DQyysBwL.js.br +0 -0
  169. package/dist/web/assets/remote-desktop-clipboard-notice-DQyysBwL.js.gz +0 -0
  170. package/dist/web/assets/{remote-desktop-mobile-sheet-CY_DsGXZ.js → remote-desktop-mobile-sheet-DZtEFmgf.js} +2 -2
  171. package/dist/web/assets/remote-desktop-mobile-sheet-DZtEFmgf.js.br +0 -0
  172. package/dist/web/assets/remote-desktop-mobile-sheet-DZtEFmgf.js.gz +0 -0
  173. package/dist/web/assets/{remote-desktop-mobile-view-DdpI-RF8.js → remote-desktop-mobile-view-VW7HdPUX.js} +1 -1
  174. package/dist/web/assets/remote-desktop-mobile-view-VW7HdPUX.js.br +0 -0
  175. package/dist/web/assets/{remote-desktop-mobile-view-DdpI-RF8.js.gz → remote-desktop-mobile-view-VW7HdPUX.js.gz} +0 -0
  176. package/dist/web/assets/{remote-desktop-readiness-gate-WIyCCAv5.js → remote-desktop-readiness-gate-B3tuP9Kt.js} +1 -1
  177. package/dist/web/assets/remote-desktop-readiness-gate-B3tuP9Kt.js.br +0 -0
  178. package/dist/web/assets/remote-desktop-readiness-gate-B3tuP9Kt.js.gz +0 -0
  179. package/dist/web/assets/{remote-desktop-window-content-BMORZLZt.js → remote-desktop-window-content-n0NoDCaE.js} +1 -1
  180. package/dist/web/assets/remote-desktop-window-content-n0NoDCaE.js.br +0 -0
  181. package/dist/web/assets/remote-desktop-window-content-n0NoDCaE.js.gz +0 -0
  182. package/dist/web/assets/{rename-object-dialog-pIgWe_AL.js → rename-object-dialog-rLTW4A55.js} +1 -1
  183. package/dist/web/assets/rename-object-dialog-rLTW4A55.js.br +0 -0
  184. package/dist/web/assets/rename-object-dialog-rLTW4A55.js.gz +0 -0
  185. package/dist/web/assets/{review-code-oTNcTYlI.js → review-code-DNfsaNO1.js} +1 -1
  186. package/dist/web/assets/review-code-DNfsaNO1.js.br +0 -0
  187. package/dist/web/assets/review-code-DNfsaNO1.js.gz +0 -0
  188. package/dist/web/assets/{save-as-dialog-CnPLGTaL.js → save-as-dialog-BSz8WrNZ.js} +1 -1
  189. package/dist/web/assets/save-as-dialog-BSz8WrNZ.js.br +0 -0
  190. package/dist/web/assets/save-as-dialog-BSz8WrNZ.js.gz +0 -0
  191. package/dist/web/assets/{schedules-settings-section-CREqHG_i.js → schedules-settings-section-DqUZ-H2K.js} +1 -1
  192. package/dist/web/assets/schedules-settings-section-DqUZ-H2K.js.br +0 -0
  193. package/dist/web/assets/schedules-settings-section-DqUZ-H2K.js.gz +0 -0
  194. package/dist/web/assets/{session-review-tab-nFVJYYJH.js → session-review-tab-DKtrFb9y.js} +1 -1
  195. package/dist/web/assets/session-review-tab-DKtrFb9y.js.br +0 -0
  196. package/dist/web/assets/session-review-tab-DKtrFb9y.js.gz +0 -0
  197. package/dist/web/assets/{settings-body-DNzH0wOQ.js → settings-body-C42xA9Dw.js} +1 -1
  198. package/dist/web/assets/settings-body-C42xA9Dw.js.br +0 -0
  199. package/dist/web/assets/settings-body-C42xA9Dw.js.gz +0 -0
  200. package/dist/web/assets/{settings-rows-D02_4sDY.js → settings-rows-C7ZatT9O.js} +1 -1
  201. package/dist/web/assets/settings-rows-C7ZatT9O.js.br +0 -0
  202. package/dist/web/assets/settings-rows-C7ZatT9O.js.gz +0 -0
  203. package/dist/web/assets/{settings-section-content-DWopMfmq.js → settings-section-content-B_iXJpcK.js} +2 -2
  204. package/dist/web/assets/settings-section-content-B_iXJpcK.js.br +0 -0
  205. package/dist/web/assets/settings-section-content-B_iXJpcK.js.gz +0 -0
  206. package/dist/web/assets/settings-section-content-rxfQEQ66.js +1 -0
  207. package/dist/web/assets/{settings-tab-Cg0v5kO7.js → settings-tab-PRw6OpEb.js} +1 -1
  208. package/dist/web/assets/{settings-window-content-CqhwxJJn.js → settings-window-content-WITQsGfW.js} +1 -1
  209. package/dist/web/assets/{slash-command-picker-Bcizxvbh.js → slash-command-picker-DmDs8nOj.js} +1 -1
  210. package/dist/web/assets/slash-command-picker-DmDs8nOj.js.br +0 -0
  211. package/dist/web/assets/slash-command-picker-DmDs8nOj.js.gz +0 -0
  212. package/dist/web/assets/{sql-object-tab-b1Z2zVBm.js → sql-object-tab-D6P4hzkb.js} +1 -1
  213. package/dist/web/assets/sql-object-tab-D6P4hzkb.js.br +0 -0
  214. package/dist/web/assets/sql-object-tab-D6P4hzkb.js.gz +0 -0
  215. package/dist/web/assets/{sqlite-viewer-DhPv28tq.js → sqlite-viewer-mUitnzHB.js} +1 -1
  216. package/dist/web/assets/sqlite-viewer-mUitnzHB.js.br +0 -0
  217. package/dist/web/assets/sqlite-viewer-mUitnzHB.js.gz +0 -0
  218. package/dist/web/assets/{structure-save-dialog-sGUpqs8D.js → structure-save-dialog-CmbNlmBY.js} +1 -1
  219. package/dist/web/assets/structure-save-dialog-CmbNlmBY.js.br +0 -0
  220. package/dist/web/assets/structure-save-dialog-CmbNlmBY.js.gz +0 -0
  221. package/dist/web/assets/{structure-tab-Bo2OmDOc.js → structure-tab-DCPNuFTe.js} +1 -1
  222. package/dist/web/assets/structure-tab-DCPNuFTe.js.br +0 -0
  223. package/dist/web/assets/structure-tab-DCPNuFTe.js.gz +0 -0
  224. package/dist/web/assets/{system-monitor-body-C475Et9u.js → system-monitor-body-BhxyqSqu.js} +2 -2
  225. package/dist/web/assets/system-monitor-body-BhxyqSqu.js.br +0 -0
  226. package/dist/web/assets/system-monitor-body-BhxyqSqu.js.gz +0 -0
  227. package/dist/web/assets/{system-monitor-tab-Cm52yoW8.js → system-monitor-tab-B_miogQI.js} +1 -1
  228. package/dist/web/assets/{system-monitor-window-content-YYsGHxZF.js → system-monitor-window-content-8IogR6tR.js} +1 -1
  229. package/dist/web/assets/{tab-host-window-content-BKI6QoDa.js → tab-host-window-content-BWHQjgWZ.js} +1 -1
  230. package/dist/web/assets/tab-host-window-content-BWHQjgWZ.js.br +0 -0
  231. package/dist/web/assets/tab-host-window-content-BWHQjgWZ.js.gz +0 -0
  232. package/dist/web/assets/{table-tab-D-nKWMJR.js → table-tab-E3YMueu_.js} +1 -1
  233. package/dist/web/assets/table-tab-E3YMueu_.js.br +0 -0
  234. package/dist/web/assets/{table-tab-D-nKWMJR.js.gz → table-tab-E3YMueu_.js.gz} +0 -0
  235. package/dist/web/assets/{telegram-bot-token-field-BeNS2X2k.js → telegram-bot-token-field-CwW24m62.js} +1 -1
  236. package/dist/web/assets/telegram-bot-token-field-CwW24m62.js.br +0 -0
  237. package/dist/web/assets/telegram-bot-token-field-CwW24m62.js.gz +0 -0
  238. package/dist/web/assets/{terminal-tab-BbP753Ab.js → terminal-tab-BAe5cWEL.js} +1 -1
  239. package/dist/web/assets/terminal-tab-BAe5cWEL.js.br +0 -0
  240. package/dist/web/assets/terminal-tab-BAe5cWEL.js.gz +0 -0
  241. package/dist/web/assets/{tool-cards-Dh8Ekn2W.js → tool-cards-DrCiSH2r.js} +2 -2
  242. package/dist/web/assets/tool-cards-DrCiSH2r.js.br +0 -0
  243. package/dist/web/assets/tool-cards-DrCiSH2r.js.gz +0 -0
  244. package/dist/web/assets/{tools-settings-section-DGZRIULE.js → tools-settings-section-S-vBr2af.js} +1 -1
  245. package/dist/web/assets/tools-settings-section-S-vBr2af.js.br +0 -0
  246. package/dist/web/assets/tools-settings-section-S-vBr2af.js.gz +0 -0
  247. package/dist/web/assets/{use-inline-blame-BxwM-Wkj.js → use-inline-blame-BzVaaCCt.js} +1 -1
  248. package/dist/web/assets/use-inline-blame-BzVaaCCt.js.br +0 -0
  249. package/dist/web/assets/use-inline-blame-BzVaaCCt.js.gz +0 -0
  250. package/dist/web/assets/{use-lsp-BqvwRKmM.js → use-lsp-BwDL9Fqk.js} +1 -1
  251. package/dist/web/assets/use-lsp-BwDL9Fqk.js.br +0 -0
  252. package/dist/web/assets/use-lsp-BwDL9Fqk.js.gz +0 -0
  253. package/dist/web/assets/{use-monaco-theme-DU0EpDHH.js → use-monaco-theme-DljSc3Xo.js} +1 -1
  254. package/dist/web/assets/{use-process-kill-CVN_c4KF.js → use-process-kill-YDcDqDtG.js} +1 -1
  255. package/dist/web/assets/use-process-kill-YDcDqDtG.js.br +0 -0
  256. package/dist/web/assets/use-process-kill-YDcDqDtG.js.gz +0 -0
  257. package/dist/web/assets/{use-sql-schema-info-C0wBmR6-.js → use-sql-schema-info-cAI0vn7d.js} +1 -1
  258. package/dist/web/assets/use-sql-schema-info-cAI0vn7d.js.br +0 -0
  259. package/dist/web/assets/use-sql-schema-info-cAI0vn7d.js.gz +0 -0
  260. package/dist/web/assets/{use-websocket-K0OUXbbY.js → use-websocket-Djo_ruX_.js} +1 -1
  261. package/dist/web/assets/{web-preview-tab-DOgLv8Gt.js → web-preview-tab-DXy0hRef.js} +1 -1
  262. package/dist/web/assets/web-preview-tab-DXy0hRef.js.br +0 -0
  263. package/dist/web/assets/web-preview-tab-DXy0hRef.js.gz +0 -0
  264. package/dist/web/index.html +2 -2
  265. package/dist/web/index.html.br +0 -0
  266. package/dist/web/index.html.gz +0 -0
  267. package/dist/web/sw.js +1 -1
  268. package/dist/web/sw.js.br +0 -0
  269. package/dist/web/sw.js.gz +0 -0
  270. package/docs/architecture/ai-chat-and-providers.md +29 -0
  271. package/docs/lessons-learned.md +14 -4
  272. package/package.json +1 -1
  273. package/src/providers/claude-agent-sdk-query-options.ts +13 -2
  274. package/src/providers/claude-agent-sdk.ts +112 -30
  275. package/src/providers/claude-approval-queue.ts +75 -0
  276. package/src/providers/claude-effort-status.ts +211 -0
  277. package/src/server/routes/chat.ts +17 -0
  278. package/src/server/routes/settings.ts +16 -0
  279. package/src/server/ws/chat-effort-status.ts +73 -0
  280. package/src/server/ws/chat.ts +30 -3
  281. package/src/shared/effort-status.ts +128 -0
  282. package/src/types/api.ts +3 -1
  283. package/src/types/chat.ts +8 -0
  284. package/src/web/components/chat/chat-scroll-nav.tsx +12 -8
  285. package/src/web/components/chat/chat-tab.tsx +15 -11
  286. package/src/web/components/chat/message-action-bar.tsx +19 -4
  287. package/src/web/components/chat/message-input.tsx +6 -0
  288. package/src/web/components/chat/message-list.tsx +55 -21
  289. package/src/web/components/chat/message-user-bubble.tsx +2 -2
  290. package/src/web/components/chat/model-thinking-selector-helpers.ts +79 -0
  291. package/src/web/components/chat/model-thinking-selector.tsx +36 -10
  292. package/src/web/components/chat/session-changes-context.tsx +29 -1
  293. package/src/web/components/layout/reparenting-tab.tsx +53 -14
  294. package/src/web/components/settings/ai-settings-section.tsx +32 -0
  295. package/src/web/hooks/use-chat.ts +16 -0
  296. package/src/web/hooks/use-effort-status.ts +46 -0
  297. package/src/web/hooks/use-relative-time-tick.ts +35 -0
  298. package/src/web/hooks/use-session-file-changes.ts +5 -1
  299. package/src/web/hooks/use-tab-overflow.ts +26 -13
  300. package/src/web/lib/format-date.ts +6 -1
  301. package/dist/web/assets/accounts-settings-section-CiNmIj02.js.br +0 -0
  302. package/dist/web/assets/accounts-settings-section-CiNmIj02.js.gz +0 -0
  303. package/dist/web/assets/agent-session-sheet-DRR36PDA.js.br +0 -0
  304. package/dist/web/assets/agent-session-sheet-DRR36PDA.js.gz +0 -0
  305. package/dist/web/assets/agent-session-window-content-DMhy16BM.js.br +0 -0
  306. package/dist/web/assets/agent-session-window-content-DMhy16BM.js.gz +0 -0
  307. package/dist/web/assets/ai-resource-editor-BG8boPw7.js.br +0 -0
  308. package/dist/web/assets/ai-resource-editor-BG8boPw7.js.gz +0 -0
  309. package/dist/web/assets/ai-settings-section-DMGM1hYx.js +0 -1
  310. package/dist/web/assets/ai-settings-section-DMGM1hYx.js.br +0 -0
  311. package/dist/web/assets/ai-settings-section-DMGM1hYx.js.gz +0 -0
  312. package/dist/web/assets/android-tab-lHDxD93G.js.br +0 -0
  313. package/dist/web/assets/android-tab-lHDxD93G.js.gz +0 -0
  314. package/dist/web/assets/appearance-settings-section-DO-ZyoMj.js.br +0 -0
  315. package/dist/web/assets/appearance-settings-section-DO-ZyoMj.js.gz +0 -0
  316. package/dist/web/assets/apps-panel-CH-0CHYY.js.br +0 -0
  317. package/dist/web/assets/apps-panel-CH-0CHYY.js.gz +0 -0
  318. package/dist/web/assets/branch-review-tab-CdGtvekz.js.br +0 -0
  319. package/dist/web/assets/branch-review-tab-CdGtvekz.js.gz +0 -0
  320. package/dist/web/assets/chat-tab-DMqWnFIo.js.br +0 -0
  321. package/dist/web/assets/chat-tab-DMqWnFIo.js.gz +0 -0
  322. package/dist/web/assets/chat-tab-DzWpwPFb.js +0 -19
  323. package/dist/web/assets/chat-tab-DzWpwPFb.js.br +0 -0
  324. package/dist/web/assets/chat-tab-DzWpwPFb.js.gz +0 -0
  325. package/dist/web/assets/code-editor-BCv8ONNS.js.br +0 -0
  326. package/dist/web/assets/code-editor-BCv8ONNS.js.gz +0 -0
  327. package/dist/web/assets/codex-reset-credit-button-CGMbIHak.js.br +0 -0
  328. package/dist/web/assets/conflict-editor-3WmzqBlW.js.br +0 -0
  329. package/dist/web/assets/conflict-editor-3WmzqBlW.js.gz +0 -0
  330. package/dist/web/assets/connection-form-tab-BoWucvAe.js.br +0 -0
  331. package/dist/web/assets/connection-form-tab-BoWucvAe.js.gz +0 -0
  332. package/dist/web/assets/csv-preview-BCgGsy7F.js.br +0 -0
  333. package/dist/web/assets/csv-preview-BCgGsy7F.js.gz +0 -0
  334. package/dist/web/assets/db-tab-parts-o6QMHBbA.js.br +0 -0
  335. package/dist/web/assets/db-tab-parts-o6QMHBbA.js.gz +0 -0
  336. package/dist/web/assets/design-settings-section-seo6SdSk.js.br +0 -0
  337. package/dist/web/assets/design-settings-section-seo6SdSk.js.gz +0 -0
  338. package/dist/web/assets/design-tab-B-f_b2LM.js.br +0 -0
  339. package/dist/web/assets/design-tab-B-f_b2LM.js.gz +0 -0
  340. package/dist/web/assets/designs-sidebar-panel-CVT_TQP7.js.br +0 -0
  341. package/dist/web/assets/designs-sidebar-panel-CVT_TQP7.js.gz +0 -0
  342. package/dist/web/assets/diff-viewer-y9uBp_8u.js.br +0 -0
  343. package/dist/web/assets/diff-viewer-y9uBp_8u.js.gz +0 -0
  344. package/dist/web/assets/docx-preview-Dn_x3Inm.js.br +0 -0
  345. package/dist/web/assets/docx-preview-Dn_x3Inm.js.gz +0 -0
  346. package/dist/web/assets/editor-dialog-BwNXSajD.js.br +0 -0
  347. package/dist/web/assets/editor-dialog-BwNXSajD.js.gz +0 -0
  348. package/dist/web/assets/editor-language-service-XA_TnPLN.js.br +0 -0
  349. package/dist/web/assets/editor-language-service-XA_TnPLN.js.gz +0 -0
  350. package/dist/web/assets/explorer-body-DukR7zM9.js.br +0 -0
  351. package/dist/web/assets/explorer-body-DukR7zM9.js.gz +0 -0
  352. package/dist/web/assets/explorer-window-content-BjTZ44S3.js.br +0 -0
  353. package/dist/web/assets/explorer-window-content-BjTZ44S3.js.gz +0 -0
  354. package/dist/web/assets/extension-manager-section-C4WD1BPQ.js.br +0 -0
  355. package/dist/web/assets/extension-manager-section-C4WD1BPQ.js.gz +0 -0
  356. package/dist/web/assets/extension-webview-CV2ujflc.js.br +0 -0
  357. package/dist/web/assets/extension-webview-CV2ujflc.js.gz +0 -0
  358. package/dist/web/assets/files-settings-section-hS5ixQ8o.js.br +0 -0
  359. package/dist/web/assets/files-settings-section-hS5ixQ8o.js.gz +0 -0
  360. package/dist/web/assets/format-date-CSHaU2wN.js +0 -1
  361. package/dist/web/assets/general-settings-section-BnKYY0AA.js.br +0 -0
  362. package/dist/web/assets/general-settings-section-BnKYY0AA.js.gz +0 -0
  363. package/dist/web/assets/git-log-panel-JiWfR5xv.js.br +0 -0
  364. package/dist/web/assets/git-log-panel-JiWfR5xv.js.gz +0 -0
  365. package/dist/web/assets/git-review-tab-DgvcY__Q.js.br +0 -0
  366. package/dist/web/assets/git-review-tab-DgvcY__Q.js.gz +0 -0
  367. package/dist/web/assets/glide-data-grid-r1MKxDjz.js.br +0 -0
  368. package/dist/web/assets/glide-data-grid-r1MKxDjz.js.gz +0 -0
  369. package/dist/web/assets/grid-save-dialog-Fa0yA-Ep.js.br +0 -0
  370. package/dist/web/assets/grid-save-dialog-Fa0yA-Ep.js.gz +0 -0
  371. package/dist/web/assets/group-chat-tab-B2Q8yqSX.js.br +0 -0
  372. package/dist/web/assets/group-chat-tab-B2Q8yqSX.js.gz +0 -0
  373. package/dist/web/assets/impexp-tab-B8Ht4l32.js.br +0 -0
  374. package/dist/web/assets/impexp-tab-B8Ht4l32.js.gz +0 -0
  375. package/dist/web/assets/index-BtAn_krv.js.br +0 -0
  376. package/dist/web/assets/index-BtAn_krv.js.gz +0 -0
  377. package/dist/web/assets/keyboard-shortcuts-section-aoiLpWwf.js.br +0 -0
  378. package/dist/web/assets/keyboard-shortcuts-section-aoiLpWwf.js.gz +0 -0
  379. package/dist/web/assets/language-servers-section-BkGAezo6.js.br +0 -0
  380. package/dist/web/assets/language-servers-section-BkGAezo6.js.gz +0 -0
  381. package/dist/web/assets/logs-app-DYgPtmjm.js.br +0 -0
  382. package/dist/web/assets/logs-app-DYgPtmjm.js.gz +0 -0
  383. package/dist/web/assets/lsp-client-CIcphNmU.js.br +0 -0
  384. package/dist/web/assets/lsp-client-CIcphNmU.js.gz +0 -0
  385. package/dist/web/assets/markdown-renderer-dk1w3goH.js.br +0 -0
  386. package/dist/web/assets/markdown-renderer-dk1w3goH.js.gz +0 -0
  387. package/dist/web/assets/mobile-explorer-sheet-D4lPZQZc.js.br +0 -0
  388. package/dist/web/assets/mobile-explorer-sheet-D4lPZQZc.js.gz +0 -0
  389. package/dist/web/assets/new-design-dialog-CIq7xspW.js.br +0 -0
  390. package/dist/web/assets/new-design-dialog-CIq7xspW.js.gz +0 -0
  391. package/dist/web/assets/notifications-settings-section-CoWMtXCC.js.br +0 -0
  392. package/dist/web/assets/notifications-settings-section-CoWMtXCC.js.gz +0 -0
  393. package/dist/web/assets/ppmbot-settings-section-Ct0UwA6T.js.br +0 -0
  394. package/dist/web/assets/ppmbot-settings-section-Ct0UwA6T.js.gz +0 -0
  395. package/dist/web/assets/problems-panel-D-HT-0PE.js.br +0 -0
  396. package/dist/web/assets/problems-panel-D-HT-0PE.js.gz +0 -0
  397. package/dist/web/assets/process-table-CXN0s4Pq.js.br +0 -0
  398. package/dist/web/assets/process-table-CXN0s4Pq.js.gz +0 -0
  399. package/dist/web/assets/proxy-settings-section-e6WWzP8n.js.br +0 -0
  400. package/dist/web/assets/proxy-settings-section-e6WWzP8n.js.gz +0 -0
  401. package/dist/web/assets/query-tab-C7C67t8F.js.br +0 -0
  402. package/dist/web/assets/query-tab-C7C67t8F.js.gz +0 -0
  403. package/dist/web/assets/remote-access-settings-section-gGoATCF9.js.br +0 -0
  404. package/dist/web/assets/remote-access-settings-section-gGoATCF9.js.gz +0 -0
  405. package/dist/web/assets/remote-desktop-clipboard-notice-D5KjvMZx.js.br +0 -0
  406. package/dist/web/assets/remote-desktop-clipboard-notice-D5KjvMZx.js.gz +0 -0
  407. package/dist/web/assets/remote-desktop-mobile-sheet-CY_DsGXZ.js.br +0 -0
  408. package/dist/web/assets/remote-desktop-mobile-sheet-CY_DsGXZ.js.gz +0 -0
  409. package/dist/web/assets/remote-desktop-mobile-view-DdpI-RF8.js.br +0 -0
  410. package/dist/web/assets/remote-desktop-readiness-gate-WIyCCAv5.js.br +0 -0
  411. package/dist/web/assets/remote-desktop-readiness-gate-WIyCCAv5.js.gz +0 -0
  412. package/dist/web/assets/remote-desktop-window-content-BMORZLZt.js.br +0 -0
  413. package/dist/web/assets/remote-desktop-window-content-BMORZLZt.js.gz +0 -0
  414. package/dist/web/assets/rename-object-dialog-pIgWe_AL.js.br +0 -0
  415. package/dist/web/assets/rename-object-dialog-pIgWe_AL.js.gz +0 -0
  416. package/dist/web/assets/review-code-oTNcTYlI.js.br +0 -0
  417. package/dist/web/assets/review-code-oTNcTYlI.js.gz +0 -0
  418. package/dist/web/assets/save-as-dialog-CnPLGTaL.js.br +0 -0
  419. package/dist/web/assets/save-as-dialog-CnPLGTaL.js.gz +0 -0
  420. package/dist/web/assets/schedules-settings-section-CREqHG_i.js.br +0 -0
  421. package/dist/web/assets/schedules-settings-section-CREqHG_i.js.gz +0 -0
  422. package/dist/web/assets/session-review-tab-nFVJYYJH.js.br +0 -0
  423. package/dist/web/assets/session-review-tab-nFVJYYJH.js.gz +0 -0
  424. package/dist/web/assets/settings-body-DNzH0wOQ.js.br +0 -0
  425. package/dist/web/assets/settings-body-DNzH0wOQ.js.gz +0 -0
  426. package/dist/web/assets/settings-rows-D02_4sDY.js.br +0 -0
  427. package/dist/web/assets/settings-rows-D02_4sDY.js.gz +0 -0
  428. package/dist/web/assets/settings-section-content-BYW_9mHv.js +0 -1
  429. package/dist/web/assets/settings-section-content-DWopMfmq.js.br +0 -0
  430. package/dist/web/assets/settings-section-content-DWopMfmq.js.gz +0 -0
  431. package/dist/web/assets/slash-command-picker-Bcizxvbh.js.br +0 -0
  432. package/dist/web/assets/slash-command-picker-Bcizxvbh.js.gz +0 -0
  433. package/dist/web/assets/sql-object-tab-b1Z2zVBm.js.br +0 -0
  434. package/dist/web/assets/sql-object-tab-b1Z2zVBm.js.gz +0 -0
  435. package/dist/web/assets/sqlite-viewer-DhPv28tq.js.br +0 -0
  436. package/dist/web/assets/sqlite-viewer-DhPv28tq.js.gz +0 -0
  437. package/dist/web/assets/structure-save-dialog-sGUpqs8D.js.br +0 -0
  438. package/dist/web/assets/structure-save-dialog-sGUpqs8D.js.gz +0 -0
  439. package/dist/web/assets/structure-tab-Bo2OmDOc.js.br +0 -0
  440. package/dist/web/assets/structure-tab-Bo2OmDOc.js.gz +0 -0
  441. package/dist/web/assets/system-monitor-body-C475Et9u.js.br +0 -0
  442. package/dist/web/assets/system-monitor-body-C475Et9u.js.gz +0 -0
  443. package/dist/web/assets/tab-host-window-content-BKI6QoDa.js.br +0 -0
  444. package/dist/web/assets/tab-host-window-content-BKI6QoDa.js.gz +0 -0
  445. package/dist/web/assets/table-tab-D-nKWMJR.js.br +0 -0
  446. package/dist/web/assets/telegram-bot-token-field-BeNS2X2k.js.br +0 -0
  447. package/dist/web/assets/telegram-bot-token-field-BeNS2X2k.js.gz +0 -0
  448. package/dist/web/assets/terminal-tab-BbP753Ab.js.br +0 -0
  449. package/dist/web/assets/terminal-tab-BbP753Ab.js.gz +0 -0
  450. package/dist/web/assets/tool-cards-Dh8Ekn2W.js.br +0 -0
  451. package/dist/web/assets/tool-cards-Dh8Ekn2W.js.gz +0 -0
  452. package/dist/web/assets/tools-settings-section-DGZRIULE.js.br +0 -0
  453. package/dist/web/assets/tools-settings-section-DGZRIULE.js.gz +0 -0
  454. package/dist/web/assets/use-inline-blame-BxwM-Wkj.js.br +0 -0
  455. package/dist/web/assets/use-inline-blame-BxwM-Wkj.js.gz +0 -0
  456. package/dist/web/assets/use-lsp-BqvwRKmM.js.br +0 -0
  457. package/dist/web/assets/use-lsp-BqvwRKmM.js.gz +0 -0
  458. package/dist/web/assets/use-process-kill-CVN_c4KF.js.br +0 -0
  459. package/dist/web/assets/use-process-kill-CVN_c4KF.js.gz +0 -0
  460. package/dist/web/assets/use-sql-schema-info-C0wBmR6-.js.br +0 -0
  461. package/dist/web/assets/use-sql-schema-info-C0wBmR6-.js.gz +0 -0
  462. package/dist/web/assets/web-preview-tab-DOgLv8Gt.js.br +0 -0
  463. package/dist/web/assets/web-preview-tab-DOgLv8Gt.js.gz +0 -0
@@ -1,4 +1,4 @@
1
- import type { ReplyReference } from "../../../shared/chat-reply";
1
+ import { decodeReply, type ReplyReference } from "../../../shared/chat-reply";
2
2
  import { createReplyReference, resolveReplyMessage } from "./message-reply-reference";
3
3
  import { useEffect, useRef, useState, useMemo, useCallback, useLayoutEffect, memo } from "react";
4
4
  import { userMessageOrdinals } from "@/lib/message-ordinals";
@@ -205,8 +205,12 @@ export function MessageList({
205
205
  // Mirror the lib's scroll-element ref into state so effects/nav re-run when the
206
206
  // scroll container mounts late (it appears only after the loading screen).
207
207
  const [scrollEl, setScrollEl] = useState<HTMLDivElement | null>(null);
208
+ // Read by the callbacks every bubble holds: one built on the state above changes when
209
+ // the container mounts, which drew the whole transcript twice on every open.
210
+ const scrollElRef = useRef<HTMLDivElement | null>(null);
208
211
  const setScrollRef = useCallback((el: HTMLDivElement | null) => {
209
212
  scrollRef(el);
213
+ scrollElRef.current = el;
210
214
  setScrollEl(el);
211
215
  }, [scrollRef]);
212
216
 
@@ -238,10 +242,11 @@ export function MessageList({
238
242
  // cleanup clears the pending flash, so a second jump can't leave a stale highlight.
239
243
  const flashCleanupRef = useRef<(() => void) | null>(null);
240
244
  const handleJumpToEdit = useCallback((editRef: string) => {
241
- if (!scrollEl) return;
245
+ const el = scrollElRef.current;
246
+ if (!el) return;
242
247
  flashCleanupRef.current?.();
243
- flashCleanupRef.current = jumpToEdit(scrollEl, editRef);
244
- }, [scrollEl]);
248
+ flashCleanupRef.current = jumpToEdit(el, editRef);
249
+ }, []);
245
250
  useEffect(() => () => flashCleanupRef.current?.(), []);
246
251
 
247
252
  // The Review tab names the turn that wrote each block from what this chat shows.
@@ -338,26 +343,31 @@ export function MessageList({
338
343
  const reply = createReplyReference(message, sessionId, providerId);
339
344
  if (reply) onReply?.(reply);
340
345
  }, [sessionId, providerId, onReply]);
341
- const replyAvailable = useCallback((reply: ReplyReference) => !!resolveReplyMessage(filtered, reply, sessionId, providerId), [filtered, sessionId, providerId]);
342
346
  const replyFlashCleanup = useRef<(() => void) | null>(null);
343
347
  useEffect(() => () => replyFlashCleanup.current?.(), []);
344
348
  /** Scroll to the message at `index` of `filtered` and flash it; false when it is not drawn. */
345
349
  const showMessageAt = useCallback((index: number) => {
346
- const target = scrollEl?.querySelector<HTMLElement>(`[data-msg-index="${index}"]`);
347
- if (!scrollEl || !target) return false;
350
+ const el = scrollElRef.current;
351
+ const target = el?.querySelector<HTMLElement>(`[data-msg-index="${index}"]`);
352
+ if (!el || !target) return false;
348
353
  stopScroll();
349
354
  replyFlashCleanup.current?.();
350
- scrollEl.scrollTo({ top: scrollEl.scrollTop + target.getBoundingClientRect().top - scrollEl.getBoundingClientRect().top - 24, behavior: "smooth" });
355
+ el.scrollTo({ top: el.scrollTop + target.getBoundingClientRect().top - el.getBoundingClientRect().top - 24, behavior: "smooth" });
351
356
  target.classList.add("bg-primary/10", "ring-1", "ring-primary/40");
352
357
  const clear = () => target.classList.remove("bg-primary/10", "ring-1", "ring-primary/40");
353
358
  const timer = setTimeout(clear, 1400);
354
359
  replyFlashCleanup.current = () => { clearTimeout(timer); clear(); };
355
360
  return true;
356
- }, [scrollEl, stopScroll]);
361
+ }, [stopScroll]);
362
+ // Every bubble holds this callback and `filtered` is new on every streamed flush, so
363
+ // depending on it re-rendered the whole transcript ten times a second. Only a click reads it.
364
+ const filteredRef = useRef(filtered);
365
+ useLayoutEffect(() => { filteredRef.current = filtered; }, [filtered]);
357
366
  const handleJumpToReply = useCallback((reply: ReplyReference) => {
358
- const message = resolveReplyMessage(filtered, reply, sessionId, providerId);
359
- if (message) showMessageAt(filtered.indexOf(message));
360
- }, [filtered, sessionId, providerId, showMessageAt]);
367
+ const list = filteredRef.current;
368
+ const message = resolveReplyMessage(list, reply, sessionId, providerId);
369
+ if (message) showMessageAt(list.indexOf(message));
370
+ }, [sessionId, providerId, showMessageAt]);
361
371
 
362
372
  // "Show in chat" from the Review tab: the call's card, or the prompt that asked for it when
363
373
  // the card is not drawn (a sub-agent's call). Waits for the tab to be shown and its history to
@@ -482,14 +492,32 @@ export function MessageList({
482
492
  const el = scrollEl;
483
493
  const sentinel = topSentinelRef.current;
484
494
  if (!el || !sentinel || !hasMore || compactLoadError) return;
495
+ // A transcript parked out of sight (its panel unmounted on a project switch) has no height,
496
+ // so its top always reads as in range; loading then grew the chat by a page on every switch
497
+ // back. What the observer says there is set aside and asked again once there is a height:
498
+ // it reports changes only, so a short transcript whose top stayed in range heard nothing.
499
+ let parked = false;
485
500
  const io = new IntersectionObserver(
486
- (entries) => { if (entries.some((e) => e.isIntersecting)) loadMore(); },
501
+ (entries) => {
502
+ parked = el.clientHeight === 0;
503
+ if (!parked && entries.some((e) => e.isIntersecting)) loadMore();
504
+ },
487
505
  // Start the fetch while the top is still a screenful away, so the next
488
506
  // segment is usually there before the user reaches the end of this one.
489
507
  { root: el, rootMargin: "400px 0px 0px 0px" },
490
508
  );
491
509
  io.observe(sentinel);
492
- return () => io.disconnect();
510
+ const ro = new ResizeObserver(() => {
511
+ if (!parked || el.clientHeight === 0) return;
512
+ parked = false;
513
+ io.unobserve(sentinel);
514
+ io.observe(sentinel);
515
+ });
516
+ ro.observe(el);
517
+ return () => {
518
+ io.disconnect();
519
+ ro.disconnect();
520
+ };
493
521
  }, [scrollEl, hasMore, compactLoadError, loadMore]);
494
522
 
495
523
  if (messagesLoading && (!keepStaleWhileLoading || messages.length === 0)) {
@@ -576,6 +604,10 @@ export function MessageList({
576
604
  }
577
605
  turnCopyText = parts.join("\n\n");
578
606
  }
607
+ // A boolean rather than a lookup the bubble calls while rendering: it has to change
608
+ // when older history brings the quoted message in, and not on every streamed flush.
609
+ const replyTo = msg.role === "user" ? decodeReply(msg.content).replyTo : null;
610
+ const replyAvailable = replyTo ? !!resolveReplyMessage(filtered, replyTo, sessionId, providerId) : undefined;
579
611
  return (
580
612
  <div
581
613
  key={msg.id}
@@ -589,11 +621,10 @@ export function MessageList({
589
621
  <RenderErrorBoundary fallbackContent={msg.content}>
590
622
  <MessageBubble
591
623
  message={msg}
592
- onReply={onReply && sessionId && providerId
593
- ? msg.role === "assistant"
594
- ? turnReplyMessage ? () => handleReply(turnReplyMessage!) : undefined
595
- : handleReply
624
+ onReply={onReply && sessionId && providerId && (msg.role !== "assistant" || turnReplyMessage)
625
+ ? handleReply
596
626
  : undefined}
627
+ replyTarget={turnReplyMessage}
597
628
  onJumpToReply={handleJumpToReply}
598
629
  replyAvailable={replyAvailable}
599
630
  isStreaming={isStreaming && msg.id.startsWith("streaming-")}
@@ -651,10 +682,13 @@ function assistantMessageText(msg: ChatMessage): string {
651
682
  : msg.content;
652
683
  }
653
684
 
654
- const MessageBubble = memo(function MessageBubble({ message, isStreaming, isLastAssistantInTurn, turnCopyText, turnChanges, turn, onJumpToEdit, projectName, onFork, onEdit, isEditing, onDismiss, prevMsgId, sessionId, providerId, versionGroup, onNavigateVersion, versionNavDisabled, bashPartialOutput, onReply, onJumpToReply, replyAvailable }: {
685
+ const MessageBubble = memo(function MessageBubble({ message, isStreaming, isLastAssistantInTurn, turnCopyText, turnChanges, turn, onJumpToEdit, projectName, onFork, onEdit, isEditing, onDismiss, prevMsgId, sessionId, providerId, versionGroup, onNavigateVersion, versionNavDisabled, bashPartialOutput, onReply, replyTarget, onJumpToReply, replyAvailable }: {
655
686
  onReply?: (message: ChatMessage) => void;
687
+ /** Assistant only: the message of this turn that Reply quotes, which may be an earlier one. */
688
+ replyTarget?: ChatMessage;
656
689
  onJumpToReply?: (reply: ReplyReference) => void;
657
- replyAvailable?: (reply: ReplyReference) => boolean;
690
+ /** Whether the message this one quotes is in the list; undefined when it quotes none. */
691
+ replyAvailable?: boolean;
658
692
  message: ChatMessage; isStreaming: boolean; isLastAssistantInTurn?: boolean; turnCopyText?: string; projectName?: string;
659
693
  /** Files this turn changed — drives the action-bar change pill. */
660
694
  turnChanges?: TurnFileChange[];
@@ -734,7 +768,7 @@ const MessageBubble = memo(function MessageBubble({ message, isStreaming, isLast
734
768
  {/* Action bar: only on the last assistant message of the turn, after streaming ends */}
735
769
  {!isStreaming && isLastAssistantInTurn && (
736
770
  <TurnChangeRollup
737
- onReply={onReply ? () => onReply(message) : undefined}
771
+ onReply={onReply && replyTarget ? () => onReply(replyTarget) : undefined}
738
772
  timestamp={message.timestamp}
739
773
  content={turnCopyText ?? assistantMessageText(message)}
740
774
  changes={turnChanges}
@@ -37,7 +37,7 @@ export function UserBubble({ content, messageId, timestamp, projectName, onFork,
37
37
  onEdit?: () => void;
38
38
  onReply?: () => void;
39
39
  onJumpToReply?: (reply: ReplyReference) => void;
40
- replyAvailable?: (reply: ReplyReference) => boolean;
40
+ replyAvailable?: boolean;
41
41
  isEditing?: boolean;
42
42
  sessionId?: string;
43
43
  providerId?: string;
@@ -80,7 +80,7 @@ export function UserBubble({ content, messageId, timestamp, projectName, onFork,
80
80
  : "max-w-[80%] bg-accent-wash border-accent-wash-border text-text",
81
81
  isEditing && "ring-2 ring-primary/60 border-primary/40",
82
82
  )}>
83
- {replyTo && <ReplyCard reply={replyTo} onJump={replyAvailable?.(replyTo) ? () => onJumpToReply?.(replyTo) : undefined} unavailable={!replyAvailable?.(replyTo)} />}
83
+ {replyTo && <ReplyCard reply={replyTo} onJump={replyAvailable ? () => onJumpToReply?.(replyTo) : undefined} unavailable={!replyAvailable} />}
84
84
  {/* System tags as badges */}
85
85
  {tags.length > 0 && <SystemTagBadges tags={tags} />}
86
86
 
@@ -6,6 +6,12 @@
6
6
  // for the SDK value "xhigh"; the literal "extra" would crash the CLI subprocess,
7
7
  // so it must NEVER appear as a value here.
8
8
 
9
+ import {
10
+ effortEnvMeaning, effortInEffect, effortSettingPlace, isEffortLevel,
11
+ type EffortInEffect, type EffortLevel, type EffortStatus,
12
+ } from "../../../shared/effort-status";
13
+ import { modelDisplayName } from "@/lib/model-display-name";
14
+
9
15
  export interface EffortOption {
10
16
  label: string;
11
17
  value: "low" | "medium" | "high" | "xhigh" | "max";
@@ -31,3 +37,76 @@ export function chipLabel(modelDisplay: string, effortValue: string | null | und
31
37
  const eff = effortLabel(effortValue);
32
38
  return eff ? `${modelDisplay} · ${eff}` : modelDisplay;
33
39
  }
40
+
41
+ export const DEFAULT_EFFORT = EFFORT_OPTIONS.find((o) => o.default)!.value;
42
+
43
+ /** The picked effort beside the one the turns run at, and what overrides the pick if anything does. */
44
+ export function shownEffort(
45
+ effort: string | null | undefined,
46
+ status: EffortStatus | null | undefined,
47
+ ): EffortInEffect & { pick: EffortLevel } {
48
+ const pick = isEffortLevel(effort) ? effort : DEFAULT_EFFORT;
49
+ return { pick, ...effortInEffect(pick, status) };
50
+ }
51
+
52
+ /** Notice text; the names a user would type or search for (a variable, a file) are marked as code. */
53
+ export type NoticePart = string | { code: string };
54
+
55
+ export interface EffortNotice {
56
+ title: string;
57
+ detail: NoticePart[];
58
+ }
59
+
60
+ export function noticeText(parts: NoticePart[]): string {
61
+ return parts.map((part) => (typeof part === "string" ? part : part.code)).join("");
62
+ }
63
+
64
+ const USE_PPM_PICK = "Remove it there to use the effort picked in PPM.";
65
+
66
+ /**
67
+ * Why the effort the turns run at is not the one picked, naming the setting and where it is, so
68
+ * the user can change it. Null when the pick is what runs.
69
+ */
70
+ export function effortNotice(
71
+ shown: EffortInEffect & { pick: EffortLevel },
72
+ status: EffortStatus | null | undefined,
73
+ modelDisplay: string,
74
+ ): EffortNotice | null {
75
+ if (!shown.reason || !status) return null;
76
+ const observedModel = status.observed?.model;
77
+ const model = observedModel ? modelDisplayName(observedModel.replace(/\[[^\]]*\]$/, "")) : modelDisplay;
78
+ const level = shown.level ? effortLabel(shown.level) : null;
79
+
80
+ const meaning = effortEnvMeaning(status.env?.value);
81
+ if (shown.reason === "env" && status.env && meaning) {
82
+ const setting: NoticePart[] = [{ code: `CLAUDE_CODE_EFFORT_LEVEL=${status.env.value}` }, " in ", { code: effortSettingPlace(status.env) }];
83
+ if (meaning.kind === "auto") {
84
+ // With a cap in place the CLI sends the model's default, clamped, rather than nothing, and
85
+ // until it answers that default is not known here, so the sentence names only the cap.
86
+ const what = status.cap
87
+ ? `Claude Code sends ${model}'s default effort, capped at ${effortLabel(status.cap.value)}.`
88
+ : level
89
+ ? `Claude Code sends ${model}'s default, ${level}.`
90
+ : `no effort is sent, so ${model}'s default applies.`;
91
+ return { title: "Effort is left to the model", detail: [...setting, ` overrides the effort picked here: ${what} `, USE_PPM_PICK] };
92
+ }
93
+ if (meaning.kind === "budget") {
94
+ // Under a cap the CLI reads the number as High, clamped, instead of as a thinking budget.
95
+ if (level) {
96
+ return { title: `Effort is set to ${level} outside PPM`, detail: [...setting, ` overrides the effort picked here; with a maxEffortLevel set, Claude Code runs it as ${level}. `, USE_PPM_PICK] };
97
+ }
98
+ return { title: "A thinking budget replaces the effort", detail: [...setting, " sets a thinking budget instead of an effort level, so the effort picked here is not sent. ", USE_PPM_PICK] };
99
+ }
100
+ const runs = shown.level === meaning.level ? "" : level ? ` ${model} runs it as ${level}.` : ` ${model} takes no effort level, so none is sent.`;
101
+ return { title: `Effort is set to ${effortLabel(meaning.level)} outside PPM`, detail: [...setting, ` overrides the effort picked here.${runs} `, USE_PPM_PICK] };
102
+ }
103
+ if (shown.reason === "cap" && status.cap) {
104
+ const runs = shown.level === status.cap.value || !level ? "" : ` ${model} runs it as ${level}.`;
105
+ return {
106
+ title: `Effort is capped at ${effortLabel(status.cap.value)}`,
107
+ detail: [{ code: `maxEffortLevel: "${status.cap.value}"` }, " in ", { code: effortSettingPlace(status.cap) }, ` lowers any effort picked above it.${runs}`],
108
+ };
109
+ }
110
+ if (!level) return { title: `${model} takes no effort level`, detail: [`Claude Code sends no effort with ${model}, so the effort picked here has no effect.`] };
111
+ return { title: `Runs at ${level}`, detail: [`${effortLabel(shown.pick)} isn't available for ${model}, so Claude Code runs it at ${level}.`] };
112
+ }
@@ -1,10 +1,11 @@
1
1
  import { useState, useEffect, useRef, type ReactNode } from "react";
2
- import { Check, Sparkles, Brain } from "@/lib/icons";
2
+ import { Check, Sparkles, Brain, TriangleAlert } from "@/lib/icons";
3
3
  import { api, projectUrl } from "@/lib/api-client";
4
4
  import { useIsMobile } from "@/hooks/use-is-mobile";
5
5
  import { BottomSheet } from "@/components/ui/mobile-bottom-sheet";
6
6
  import { modelDisplayName } from "@/lib/model-display-name";
7
- import { EFFORT_OPTIONS, chipLabel } from "./model-thinking-selector-helpers";
7
+ import type { EffortStatus } from "../../../shared/effort-status";
8
+ import { EFFORT_OPTIONS, chipLabel, effortLabel, effortNotice, noticeText, shownEffort, type EffortNotice } from "./model-thinking-selector-helpers";
8
9
 
9
10
  interface ModelOption {
10
11
  value: string;
@@ -18,6 +19,8 @@ const MODEL_CACHE_TTL = 5 * 60 * 1000;
18
19
  interface ModelThinkingSelectorProps {
19
20
  model: string | null;
20
21
  effort: string | null;
22
+ /** What overrides the effort pick outside PPM, so the chip shows the effort the turns run at. */
23
+ effortStatus?: EffortStatus | null;
21
24
  thinking: boolean;
22
25
  onModelChange: (model: string) => void;
23
26
  onEffortChange: (effort: string) => void;
@@ -33,8 +36,6 @@ function shortLabel(label: string): string {
33
36
  return label.replace(/^Claude\s+/i, "");
34
37
  }
35
38
 
36
- const DEFAULT_EFFORT = EFFORT_OPTIONS.find((o) => o.default)!.value;
37
-
38
39
  /**
39
40
  * Unified model + thinking picker — one chip, one popup. Mirrors Anthropic's native
40
41
  * picker: model list, effort levels (Extra→SDK "xhigh"), and a thinking on/off toggle.
@@ -43,6 +44,7 @@ const DEFAULT_EFFORT = EFFORT_OPTIONS.find((o) => o.default)!.value;
43
44
  export function ModelThinkingSelector({
44
45
  model,
45
46
  effort,
47
+ effortStatus,
46
48
  thinking,
47
49
  onModelChange,
48
50
  onEffortChange,
@@ -109,9 +111,11 @@ export function ModelThinkingSelector({
109
111
 
110
112
  const current = models.find((m) => m.value === model);
111
113
  const modelDisplay = current ? shortLabel(current.label) : model ? modelDisplayName(model) : "Model";
112
- const effortValue = effort ?? DEFAULT_EFFORT;
114
+ const shown = shownEffort(effort, effortStatus);
115
+ const notice = effortNotice(shown, effortStatus, modelDisplay);
113
116
  const showModelList = models.length > 0 || Boolean(modelStatus);
114
- const chipText = chipLabel(modelDisplay, effortValue);
117
+ // The effort the turns run at, which an override outside PPM can make differ from the pick.
118
+ const chipText = chipLabel(modelDisplay, shown.level);
115
119
 
116
120
  const pick = (fn: () => void) => {
117
121
  fn();
@@ -119,6 +123,8 @@ export function ModelThinkingSelector({
119
123
  };
120
124
 
121
125
  const content = (
126
+ <>
127
+ {notice && <EffortNoticeBanner notice={notice} />}
122
128
  <div className="flex divide-x divide-border">
123
129
  {/* Left column: model list */}
124
130
  {showModelList && (
@@ -137,12 +143,13 @@ export function ModelThinkingSelector({
137
143
  <div className="flex-1 min-w-0 py-1">
138
144
  <SectionLabel>Effort</SectionLabel>
139
145
  {EFFORT_OPTIONS.map((o) => (
140
- <OptionRow key={o.value} active={o.value === effortValue} onClick={() => pick(() => onEffortChange(o.value))}>
146
+ <OptionRow key={o.value} active={o.value === shown.pick} onClick={() => pick(() => onEffortChange(o.value))}>
141
147
  <span className="flex-1 truncate">
142
148
  {o.label}
143
149
  {o.default && <span className="ml-2 text-xs text-text-subtle">Default</span>}
150
+ {o.value === shown.level && shown.level !== shown.pick && <span className="ml-2 text-xs text-warning">In effect</span>}
144
151
  </span>
145
- {o.value === effortValue && <Check className="size-4 shrink-0 text-primary" />}
152
+ {o.value === shown.pick && <Check className="size-4 shrink-0 text-primary" />}
146
153
  </OptionRow>
147
154
  ))}
148
155
  <Separator />
@@ -161,6 +168,7 @@ export function ModelThinkingSelector({
161
168
  </button>
162
169
  </div>
163
170
  </div>
171
+ </>
164
172
  );
165
173
 
166
174
  return (
@@ -174,11 +182,12 @@ export function ModelThinkingSelector({
174
182
  if (!disabled) setOpen((v) => !v);
175
183
  }}
176
184
  className="inline-flex max-w-full items-center gap-1.5 px-[9px] py-1 rounded-full text-[11.5px] text-primary bg-accent-wash border border-accent-wash-border hover:brightness-110 transition-[filter] disabled:opacity-50 disabled:cursor-default"
177
- aria-label={`Model ${modelDisplay}, effort ${effortValue}${thinking ? ", thinking on" : ""}`}
178
- title={disabled ? "Can't change while running" : chipText}
185
+ aria-label={`Model ${modelDisplay}, effort ${shown.level ? effortLabel(shown.level) : "none"}${notice ? ` (${notice.title})` : ""}${thinking ? ", thinking on" : ""}`}
186
+ title={disabled ? "Can't change while running" : notice ? `${chipText}\n${notice.title}: ${noticeText(notice.detail)}` : chipText}
179
187
  >
180
188
  <Sparkles className="h-3.5 w-3.5 shrink-0" />
181
189
  <span className="min-w-0 max-w-[140px] truncate">{chipText}</span>
190
+ {notice && <TriangleAlert className="h-3 w-3 shrink-0 text-warning" />}
182
191
  {thinking && <Brain className="h-3 w-3 shrink-0" />}
183
192
  </button>
184
193
 
@@ -204,6 +213,23 @@ export function ModelThinkingSelector({
204
213
  );
205
214
  }
206
215
 
216
+ /** Above both columns, so it reads at full width in the phone's sheet too. */
217
+ function EffortNoticeBanner({ notice }: { notice: EffortNotice }) {
218
+ return (
219
+ <div role="note" className="flex gap-2 border-b border-border bg-warning/10 px-3 py-2.5 text-sm leading-relaxed text-text-secondary md:text-xs">
220
+ <TriangleAlert className="mt-0.5 size-4 shrink-0 text-warning" />
221
+ <div className="min-w-0">
222
+ <div className="font-medium text-text-primary">{notice.title}</div>
223
+ <p className="m-0 break-words">
224
+ {notice.detail.map((part, i) => typeof part === "string"
225
+ ? part
226
+ : <code key={i} className="rounded bg-surface-elevated px-1 font-mono text-[0.92em]">{part.code}</code>)}
227
+ </p>
228
+ </div>
229
+ </div>
230
+ );
231
+ }
232
+
207
233
  function SectionLabel({ children }: { children: ReactNode }) {
208
234
  return <div className="px-3 py-1.5 text-xs font-medium text-text-secondary">{children}</div>;
209
235
  }
@@ -3,8 +3,9 @@
3
3
  * that answer them too: each turn's change tray keeps, reverts and reopens the blocks its edits
4
4
  * wrote, and has to see the same list the bar does — one request for the whole chat.
5
5
  */
6
- import { createContext, useContext } from "react";
6
+ import { createContext, useCallback, useContext, useMemo, useRef } from "react";
7
7
  import type { SessionFileChange } from "../../../shared/session-file-changes";
8
+ import { openSessionReview } from "@/lib/session-file-changes-client";
8
9
 
9
10
  export interface SessionChangesValue {
10
11
  projectName: string;
@@ -19,3 +20,30 @@ export interface SessionChangesValue {
19
20
  export const SessionChangesContext = createContext<SessionChangesValue | null>(null);
20
21
 
21
22
  export const useSessionChanges = () => useContext(SessionChangesContext);
23
+
24
+ /**
25
+ * The value the chat provides, and the Review opener its changes bar uses.
26
+ *
27
+ * Every turn's action bar reads this context, so the value has to stay the same object while
28
+ * the agent streams: `paths` is a new array on every flush, which is why it is read only when
29
+ * the Review tab opens.
30
+ */
31
+ export function useSessionChangesValue({ projectName, sessionId, title, providerId, changes }: {
32
+ projectName: string;
33
+ sessionId: string | null;
34
+ title?: string;
35
+ providerId: string;
36
+ changes: { files: SessionFileChange[]; paths: string[]; refresh: () => void };
37
+ }): { value: SessionChangesValue | null; openReview: (path?: string) => void } {
38
+ const pathsRef = useRef(changes.paths);
39
+ pathsRef.current = changes.paths;
40
+ const openReview = useCallback((path?: string) => {
41
+ if (!sessionId) return;
42
+ openSessionReview({ projectName, sessionId, title, providerId, paths: pathsRef.current, selectPath: path });
43
+ }, [projectName, sessionId, title, providerId]);
44
+ const { files, refresh } = changes;
45
+ const value = useMemo((): SessionChangesValue | null => (
46
+ projectName && sessionId ? { projectName, sessionId, files, refresh, openReview } : null
47
+ ), [projectName, sessionId, files, refresh, openReview]);
48
+ return { value, openReview };
49
+ }
@@ -53,6 +53,32 @@ export function ReparentingTab({
53
53
  const panelIdRef = useRef(panelId);
54
54
  panelIdRef.current = panelId;
55
55
 
56
+ // Where each scroller in the tab last stood, as its own scroll events said. A move resets
57
+ // every offset, and on a project switch the old panel unmounts first, taking the wrapper off
58
+ // the page before the move below runs — too late to read them then. A chat came back at its
59
+ // top, and its history loader took that for the reader scrolling up and fetched another page
60
+ // on every switch back.
61
+ // Held weakly: a scroller React removed would keep its whole tree alive, and a chat replaces
62
+ // its transcript's on every session switch.
63
+ const scrolledRef = useRef(new WeakMap<Element, { top: number; left: number }>());
64
+ /** The keys of `scrolledRef`, which a WeakMap cannot list. */
65
+ const scrollersRef = useRef(new Set<WeakRef<Element>>());
66
+ /** Moved while hidden, when there was no layout to put the offsets back into. */
67
+ const restorePendingRef = useRef(false);
68
+ const restoreScroll = () => {
69
+ for (const ref of scrollersRef.current) {
70
+ const el = ref.deref();
71
+ const at = el && scrolledRef.current.get(el);
72
+ if (!el || !at || !wrapper.contains(el)) {
73
+ scrollersRef.current.delete(ref);
74
+ if (el) scrolledRef.current.delete(el);
75
+ continue;
76
+ }
77
+ el.scrollTop = at.top;
78
+ el.scrollLeft = at.left;
79
+ }
80
+ };
81
+
56
82
  // Mount the wrapper and own its removal. The listener is native and capturing because
57
83
  // the tab content is DOM-reparented out of the panel's fiber subtree: the panel never
58
84
  // sees clicks on the content body, so focus is set here, where the panel is known.
@@ -65,17 +91,39 @@ export function ReparentingTab({
65
91
  if (pid === DOCK_PANEL_ID || isWindowPanelId(pid)) return;
66
92
  usePanelStore.getState().setFocusedPanel(pid);
67
93
  };
94
+ const scrolled = scrolledRef.current;
95
+ const scrollers = scrollersRef.current;
96
+ const onScroll = (e: Event) => {
97
+ // Parked out of sight a scroller has no size, and whatever moves it there is not the reader.
98
+ if (wrapper.parentElement === hiddenContainer) return;
99
+ const el = e.target as Element;
100
+ if (el.nodeType !== Node.ELEMENT_NODE) return;
101
+ if (!scrolled.has(el)) {
102
+ for (const ref of scrollers) if (!ref.deref()) scrollers.delete(ref);
103
+ scrollers.add(new WeakRef(el));
104
+ }
105
+ scrolled.set(el, { top: el.scrollTop, left: el.scrollLeft });
106
+ };
68
107
  wrapper.addEventListener("pointerdown", onMouseDown, true);
69
108
  wrapper.addEventListener("focusin", onMouseDown, true);
109
+ wrapper.addEventListener("scroll", onScroll, true);
70
110
  return () => {
71
111
  wrapper.removeEventListener("pointerdown", onMouseDown, true);
72
112
  wrapper.removeEventListener("focusin", onMouseDown, true);
113
+ wrapper.removeEventListener("scroll", onScroll, true);
73
114
  wrapper.remove();
74
115
  };
75
116
  }, [wrapper, hiddenContainer]);
76
117
 
77
118
  useLayoutEffect(() => {
78
119
  wrapper.style.display = isActive ? "" : "none";
120
+ // Only for a tab moved while hidden: a plain tab switch keeps its offsets, and writing them
121
+ // back forces a layout of the whole tab in the middle of the commit.
122
+ if (isActive && restorePendingRef.current) {
123
+ restorePendingRef.current = false;
124
+ restoreScroll();
125
+ }
126
+ // eslint-disable-next-line react-hooks/exhaustive-deps
79
127
  }, [wrapper, isActive]);
80
128
 
81
129
  // Imperatively move the wrapper into the correct panel slot.
@@ -96,21 +144,12 @@ export function ReparentingTab({
96
144
 
97
145
  if (wrapper.parentElement === slot) return;
98
146
 
99
- // Save scroll positions — appendChild resets them during the DOM move
100
- const scrollables: { el: Element; top: number; left: number }[] = [];
101
- wrapper.querySelectorAll("*").forEach((el) => {
102
- if (el.scrollTop || el.scrollLeft) {
103
- scrollables.push({ el, top: el.scrollTop, left: el.scrollLeft });
104
- }
105
- });
106
-
107
147
  slot.appendChild(wrapper);
108
-
109
- // Restore scroll positions synchronously before paint
110
- for (const { el, top, left } of scrollables) {
111
- el.scrollTop = top;
112
- el.scrollLeft = left;
113
- }
148
+ // Before paint, from what the scroll events recorded: walking every element of the tab
149
+ // to read offsets cost a whole transcript's worth of reads per move, and read zeros
150
+ // whenever the wrapper had already left the page.
151
+ restorePendingRef.current = !isActive;
152
+ if (isActive) restoreScroll();
114
153
  });
115
154
 
116
155
  // Portal target for the tab's own dropdowns, tooltips and sheets. It belongs here and
@@ -17,6 +17,9 @@ import { ProviderBadge } from "@/components/chat/provider-selector";
17
17
  import { openSettings } from "./open-settings";
18
18
  import { CodexContextSettings } from "./codex-context-settings";
19
19
  import { McpSignInList } from "@/components/mcp-auth/mcp-sign-in-list";
20
+ import { effortNotice, noticeText, shownEffort } from "@/components/chat/model-thinking-selector-helpers";
21
+ import { modelDisplayName } from "@/lib/model-display-name";
22
+ import type { EffortStatus } from "../../../shared/effort-status";
20
23
  import type { ModelOption } from "../../../types/chat";
21
24
  import type { ProviderProbeStatus } from "../../../providers/provider-probe";
22
25
 
@@ -88,6 +91,21 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
88
91
  .finally(() => setModelsLoading(false));
89
92
  }, [activeTab, activeRegistered]);
90
93
 
94
+ // `CLAUDE_CODE_EFFORT_LEVEL` or a cap in the user's Claude Code settings makes the default
95
+ // below inert for every project, which the select alone would not show.
96
+ const [effortStatus, setEffortStatus] = useState<EffortStatus | null>(null);
97
+ const defaultModel = settings?.providers[activeTab]?.model;
98
+ useEffect(() => {
99
+ setEffortStatus(null);
100
+ if (!activeTab) return;
101
+ let active = true;
102
+ const query = defaultModel ? `?model=${encodeURIComponent(defaultModel)}` : "";
103
+ api.get<{ status: EffortStatus | null }>(`/api/settings/ai/providers/${activeTab}/effort-status${query}`)
104
+ .then(({ status }) => { if (active) setEffortStatus(status); })
105
+ .catch(() => {});
106
+ return () => { active = false; };
107
+ }, [activeTab, defaultModel]);
108
+
91
109
  const retryProbe = async () => {
92
110
  setProbing(true);
93
111
  try {
@@ -108,6 +126,11 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
108
126
 
109
127
  const config = settings?.providers[activeTab];
110
128
  const isSdkProvider = config?.type === "agent-sdk" || (!config?.type && activeTab === "claude");
129
+ const effortOverride = effortNotice(
130
+ shownEffort(config?.effort, effortStatus),
131
+ effortStatus,
132
+ config?.model ? modelDisplayName(config.model) : "the model",
133
+ );
111
134
 
112
135
  const handleSettingsSave = async (patch: Partial<AISettings>) => {
113
136
  if (!settings) return false;
@@ -359,6 +382,15 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
359
382
  ))}
360
383
  </SelectContent>
361
384
  </Select>
385
+ {effortOverride && (
386
+ <div role="note" className="flex items-start gap-2 rounded-md border border-warning/30 bg-warning/5 px-2.5 py-2">
387
+ <AlertTriangle className="size-3.5 text-warning shrink-0 mt-0.5" />
388
+ <div className="min-w-0 flex-1 space-y-1">
389
+ <p className={`${labelSize} text-text-secondary`}>{effortOverride.title}</p>
390
+ <p className="text-[11px] text-muted-foreground break-words">{noticeText(effortOverride.detail)}</p>
391
+ </div>
392
+ </div>
393
+ )}
362
394
  </div>
363
395
 
364
396
  <div className={fieldGap}>
@@ -13,6 +13,7 @@ import type { ChatMessage, ChatEvent } from "../../types/chat";
13
13
  import type { BackgroundAgentStatus } from "../../shared/background-agent-status";
14
14
  import type { PromptCacheState } from "../../shared/prompt-cache-idle";
15
15
  import type { TurnStop } from "../../shared/turn-stop";
16
+ import type { EffortStatus } from "../../shared/effort-status";
16
17
  import { decodeReply, encodeReply, type ReplyReference } from "../../shared/chat-reply";
17
18
  import { prefixTokens } from "../../shared/turn-usage";
18
19
  import { dbExecuteApprovalInput } from "../../shared/db-ai-tools";
@@ -131,6 +132,11 @@ interface UseChatReturn {
131
132
  effort: string | null;
132
133
  /** Switch the per-session effort (low|medium|high|xhigh|max) */
133
134
  setEffort: (effort: string) => void;
135
+ /**
136
+ * What overrides the effort pick outside PPM, as the server last said: undefined until it
137
+ * has, null when nothing can for this session's provider.
138
+ */
139
+ effortStatus: EffortStatus | null | undefined;
134
140
  /** Whether per-session thinking is on */
135
141
  thinking: boolean;
136
142
  /** Toggle per-session thinking on/off */
@@ -251,6 +257,7 @@ export function useChat(
251
257
  const pendingModelRef = useRef<string | null>(null);
252
258
  const [effort, setEffortState] = useState<string | null>(null);
253
259
  const effortRef = useRef<string | null>(null);
260
+ const [effortStatus, setEffortStatus] = useState<EffortStatus | null | undefined>(undefined);
254
261
  // Optimistic until session_state lands; the SDK default is adaptive thinking, so ON
255
262
  // is the honest starting guess.
256
263
  const [thinking, setThinkingState] = useState<boolean>(true);
@@ -1021,6 +1028,11 @@ export function useChat(
1021
1028
  return;
1022
1029
  }
1023
1030
 
1031
+ if ((data as any).type === "effort_status") {
1032
+ setEffortStatus(((data as any).status as EffortStatus | undefined) ?? null);
1033
+ return;
1034
+ }
1035
+
1024
1036
  // Handle compact status events
1025
1037
  if ((data as any).type === "compact_status") {
1026
1038
  const status = (data as any).status;
@@ -1096,6 +1108,8 @@ export function useChat(
1096
1108
  setEffortState(state.effort);
1097
1109
  effortRef.current = state.effort;
1098
1110
  }
1111
+ // Only the greetings that carry the effort carry this; the others leave it as it was.
1112
+ if ("effortStatus" in state) setEffortStatus((state.effortStatus as EffortStatus | null) ?? null);
1099
1113
  // Display only. Writing this back into the ref would promote a server-derived
1100
1114
  // default into an explicit user choice that rides along on every message and
1101
1115
  // pins the session — the path that silently disabled thinking.
@@ -1289,6 +1303,7 @@ export function useChat(
1289
1303
  // Another session's subprocess saw another MCP state; its session_state brings its own.
1290
1304
  setMcpNeedsAuth([]);
1291
1305
  setTurnStop(null);
1306
+ setEffortStatus(undefined);
1292
1307
  // Clear ephemeral pre-compact expansions on session change
1293
1308
  setExpansions(new Map());
1294
1309
  // Drop the previous session's version groups. Keeping them would let the
@@ -1819,6 +1834,7 @@ export function useChat(
1819
1834
  setModel,
1820
1835
  effort,
1821
1836
  setEffort,
1837
+ effortStatus,
1822
1838
  thinking,
1823
1839
  setThinking,
1824
1840
  turnSettings,