@hienlh/ppm 0.23.22 → 0.23.23

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 +11 -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 +58 -18
  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
@@ -24,6 +24,8 @@ import { isAsyncAgentLaunchAck, isTerminalAgentStatus } from "../../shared/backg
24
24
  import { cacheReleaseDelayMs, selectWarmIdleEvictions } from "../../services/subprocess-retention.ts";
25
25
  import { needsAuthServerNames } from "../../services/mcp-oauth/mcp-oauth-redirect.ts";
26
26
  import { mcpStatusEvent, registerMcpSignInSync } from "./chat-mcp-sign-in-sync.ts";
27
+ import { refreshEffortStatus, sessionEffortStatus } from "./chat-effort-status.ts";
28
+ import type { EffortStatus } from "../../shared/effort-status.ts";
27
29
  import { claudeTranscriptExists } from "../../services/claude-transcript-exists.ts";
28
30
  import { registerMemoryGauge } from "../../services/memory-diagnostics.ts";
29
31
  import { setTabOpenDelivery, tabOpenBroker } from "../../services/tab-tools-mcp/tab-open-broker.ts";
@@ -114,6 +116,11 @@ function resolveSessionEffort(sessionId: string): string | undefined {
114
116
  return getSessionEffort(sessionId) ?? sessionProviderConfig(sessionId)?.effort;
115
117
  }
116
118
 
119
+ /** What overrides the effort shown in session_state, if anything does. */
120
+ function resolveEffortStatus(sessionId: string, entry: SessionEntry): EffortStatus | null {
121
+ return sessionEffortStatus(providerRegistry.get(entry.providerId), entry, resolveSessionModel(sessionId));
122
+ }
123
+
117
124
  /** Whether thinking is effectively ON: per-session override wins, else provider config, else SDK default. */
118
125
  function resolveSessionThinkingEnabled(sessionId: string): boolean {
119
126
  return isThinkingEnabled(
@@ -196,6 +203,8 @@ interface SessionEntry {
196
203
  compactStatus?: "compacting" | null;
197
204
  /** MCP servers the subprocess reported as `needs-auth` at init — drives the chat's sign-in bar */
198
205
  mcpNeedsAuth?: string[];
206
+ /** What the subprocess said at the last turn's start about the effort it sends — see chat-effort-status.ts */
207
+ effortStatus?: EffortStatus;
199
208
  /** toolUseIds of Bash/Agent calls launched with run_in_background — their spy outlives the tool_result */
200
209
  backgroundToolUseIds?: Set<string>;
201
210
  /** Nested-agent children buffered into turnEvents this turn (see MAX_NESTED_TURN_EVENTS) */
@@ -957,9 +966,20 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
957
966
  // System events → transition connecting → thinking, forward compact events
958
967
  if (evType === "system") {
959
968
  const sub = (ev as any).subtype;
960
- if (sub === "init" && Array.isArray(ev.mcpServers)) {
961
- entry.mcpNeedsAuth = needsAuthServerNames(ev.mcpServers);
962
- broadcast(sessionId, mcpStatusEvent(entry.mcpNeedsAuth));
969
+ if (sub === "init") {
970
+ if (Array.isArray(ev.mcpServers)) {
971
+ entry.mcpNeedsAuth = needsAuthServerNames(ev.mcpServers);
972
+ broadcast(sessionId, mcpStatusEvent(entry.mcpNeedsAuth));
973
+ }
974
+ // Not awaited: the turn goes on while the CLI answers.
975
+ void refreshEffortStatus({
976
+ sessionId,
977
+ session: entry,
978
+ provider: providerRegistry.get(entry.providerId),
979
+ isCurrent: () => activeSessions.get(sessionId) === entry,
980
+ broadcast: (event) => broadcast(sessionId, event),
981
+ log: (line) => logSessionEvent(sessionId, "INFO", line),
982
+ });
963
983
  } else if (sub === "compacting") {
964
984
  entry.compactStatus = "compacting";
965
985
  log.debug(`session=${sessionId} compact_status=compacting (persisted on entry)`);
@@ -1388,6 +1408,7 @@ export const chatWebSocket = {
1388
1408
  mcpNeedsAuth: existing.mcpNeedsAuth ?? [],
1389
1409
  model: resolveSessionModel(sessionId),
1390
1410
  effort: resolveSessionEffort(sessionId),
1411
+ effortStatus: resolveEffortStatus(sessionId, existing),
1391
1412
  thinking: resolveSessionThinkingEnabled(sessionId),
1392
1413
  promptCache: promptCacheSnapshot(sessionId, existing),
1393
1414
  turnStop: existing.phase === "idle" ? lastTurnStop(sessionId) : null,
@@ -1557,6 +1578,7 @@ export const chatWebSocket = {
1557
1578
  mcpNeedsAuth: entry.mcpNeedsAuth ?? [],
1558
1579
  model: resolveSessionModel(sessionId),
1559
1580
  effort: resolveSessionEffort(sessionId),
1581
+ effortStatus: resolveEffortStatus(sessionId, entry),
1560
1582
  thinking: resolveSessionThinkingEnabled(sessionId),
1561
1583
  promptCache: promptCacheSnapshot(sessionId, entry),
1562
1584
  turnStop: entry.phase === "idle" ? lastTurnStop(sessionId) : null,
@@ -1779,6 +1801,8 @@ export const chatWebSocket = {
1779
1801
  }
1780
1802
  entry.model = parsed.model;
1781
1803
  setSessionModel(sessionId, parsed.model);
1804
+ // Its levels and limits were the old model's; the settings files stand in until the next turn asks.
1805
+ entry.effortStatus = undefined;
1782
1806
  const provider = providerRegistry.get(providerId);
1783
1807
  const hasLiveStream = provider?.hasStreamingSession?.(sessionId) ?? false;
1784
1808
  // Only abort when idle between turns — never interrupt an active turn.
@@ -1798,6 +1822,7 @@ export const chatWebSocket = {
1798
1822
  mcpNeedsAuth: entry.mcpNeedsAuth ?? [],
1799
1823
  model: resolveSessionModel(sessionId),
1800
1824
  effort: resolveSessionEffort(sessionId),
1825
+ effortStatus: resolveEffortStatus(sessionId, entry),
1801
1826
  thinking: resolveSessionThinkingEnabled(sessionId),
1802
1827
  }));
1803
1828
  } else if (parsed.type === "set_effort") {
@@ -1825,6 +1850,7 @@ export const chatWebSocket = {
1825
1850
  mcpNeedsAuth: entry.mcpNeedsAuth ?? [],
1826
1851
  model: resolveSessionModel(sessionId),
1827
1852
  effort: resolveSessionEffort(sessionId),
1853
+ effortStatus: resolveEffortStatus(sessionId, entry),
1828
1854
  thinking: resolveSessionThinkingEnabled(sessionId),
1829
1855
  }));
1830
1856
  } else if (parsed.type === "set_thinking") {
@@ -1846,6 +1872,7 @@ export const chatWebSocket = {
1846
1872
  mcpNeedsAuth: entry.mcpNeedsAuth ?? [],
1847
1873
  model: resolveSessionModel(sessionId),
1848
1874
  effort: resolveSessionEffort(sessionId),
1875
+ effortStatus: resolveEffortStatus(sessionId, entry),
1849
1876
  thinking: resolveSessionThinkingEnabled(sessionId),
1850
1877
  }));
1851
1878
  } else if (parsed.type === "cancel") {
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The effort a Claude session actually runs at, which is not always the one picked in PPM.
3
+ *
4
+ * Claude Code settles it in this order (measured against the bundled CLI's `get_settings`):
5
+ * `CLAUDE_CODE_EFFORT_LEVEL` beats the `--effort` PPM passes, whether it is in the environment
6
+ * or in the `env` block of a settings file, and a settings file beats the environment; a
7
+ * `maxEffortLevel` in the settings then clamps what is left; and a model without a level runs
8
+ * Extra and Max as High, or sends no effort at all. With
9
+ * `"env": { "CLAUDE_CODE_EFFORT_LEVEL": "max" }` in `~/.claude/settings.json`, every PPM chat
10
+ * ran at Max while the picker said High.
11
+ */
12
+
13
+ export const EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"] as const;
14
+ export type EffortLevel = (typeof EFFORT_LEVELS)[number];
15
+
16
+ export function isEffortLevel(value: unknown): value is EffortLevel {
17
+ return typeof value === "string" && (EFFORT_LEVELS as readonly string[]).includes(value);
18
+ }
19
+
20
+ export function effortRank(level: EffortLevel): number {
21
+ return EFFORT_LEVELS.indexOf(level);
22
+ }
23
+
24
+ /** A settings file, as the CLI names its sources, or the environment PPM starts the CLI with. */
25
+ export type EffortSettingSource =
26
+ | "userSettings"
27
+ | "projectSettings"
28
+ | "localSettings"
29
+ | "flagSettings"
30
+ | "policySettings"
31
+ | "environment";
32
+
33
+ export interface EffortSetting<T> {
34
+ value: T;
35
+ source: EffortSettingSource;
36
+ /** The file it is written in, the way the user would look for it. */
37
+ file?: string;
38
+ }
39
+
40
+ export interface EffortStatus {
41
+ /** `CLAUDE_CODE_EFFORT_LEVEL` as the CLI reads it. Absent when unset or a value the CLI ignores. */
42
+ env?: EffortSetting<string>;
43
+ /** The lowest `maxEffortLevel` the settings files set for the session's model. */
44
+ cap?: EffortSetting<EffortLevel>;
45
+ /**
46
+ * The running CLI's own answer: the effort it was started with and the one it sends. Only
47
+ * the CLI knows its organization's limits and what its model takes, so this outranks the rest.
48
+ */
49
+ observed?: { requested: EffortLevel | null; applied: EffortLevel | null; model?: string };
50
+ /** Levels the model runs as themselves, [] when it takes none. Absent when the CLI did not say. */
51
+ supported?: EffortLevel[];
52
+ }
53
+
54
+ export type EffortEnvMeaning =
55
+ | { kind: "level"; level: EffortLevel }
56
+ /** `auto` or `unset`: no effort is sent and the model's default applies. */
57
+ | { kind: "auto" }
58
+ /** A number: a thinking budget, sent instead of an effort level. */
59
+ | { kind: "budget" };
60
+
61
+ /** What the CLI makes of `CLAUDE_CODE_EFFORT_LEVEL`, parsed as it parses it; null for a value it ignores. */
62
+ export function effortEnvMeaning(raw: string | null | undefined): EffortEnvMeaning | null {
63
+ if (!raw) return null;
64
+ const value = raw.toLowerCase();
65
+ if (value === "auto" || value === "unset") return { kind: "auto" };
66
+ const level = value === "med" ? "medium" : value;
67
+ if (isEffortLevel(level)) return { kind: "level", level };
68
+ return Number.isNaN(parseInt(value, 10)) ? null : { kind: "budget" };
69
+ }
70
+
71
+ export interface EffortInEffect {
72
+ /** The level each request carries; null when none is sent, or when only the CLI can tell which. */
73
+ level: EffortLevel | null;
74
+ /** What decided it when that is not simply the pick. */
75
+ reason: "env" | "cap" | "model" | null;
76
+ }
77
+
78
+ /** The effort a turn runs at given the level picked in PPM, and what overrides the pick if anything does. */
79
+ export function effortInEffect(pick: EffortLevel, status: EffortStatus | null | undefined): EffortInEffect {
80
+ if (!status) return { level: pick, reason: null };
81
+ const env = effortEnvMeaning(status.env?.value);
82
+ const { observed } = status;
83
+ if (env) {
84
+ // The variable outranks every pick, so the CLI's answer stays true whatever is picked since.
85
+ if (observed) return { level: observed.applied, reason: "env" };
86
+ return { level: envLevel(env, status), reason: "env" };
87
+ }
88
+ // A pick made after the CLI last answered is checked against the limits it reported.
89
+ const level = observed && observed.requested === pick ? observed.applied : withinLimits(pick, status);
90
+ if (level === pick) return { level, reason: null };
91
+ const capped = !!status.cap && effortRank(pick) > effortRank(status.cap.value);
92
+ return { level, reason: capped ? "cap" : "model" };
93
+ }
94
+
95
+ /**
96
+ * What the CLI makes of the variable before it has said so itself. A settings cap changes it:
97
+ * a number is then read as High and `auto` sends the model's own default, each clamped to the cap,
98
+ * where without one neither sends an effort. That default differs by model (measured: Medium on
99
+ * Opus 5.5, High on Opus 4.5, 4.6 and Sonnet 4.6), so under any cap but Low, which leaves nothing
100
+ * lower, only the CLI's answer can say which level `auto` gives.
101
+ */
102
+ function envLevel(env: EffortEnvMeaning, status: EffortStatus): EffortLevel | null {
103
+ if (env.kind === "level") return withinLimits(env.level, status);
104
+ if (!status.cap) return null;
105
+ if (env.kind === "budget") return withinLimits("high", status);
106
+ return status.cap.value === "low" ? withinLimits("low", status) : null;
107
+ }
108
+
109
+ function withinLimits(level: EffortLevel, status: EffortStatus): EffortLevel | null {
110
+ if (status.supported?.length === 0) return null;
111
+ const capped = status.cap && effortRank(level) > effortRank(status.cap.value) ? status.cap.value : level;
112
+ // The CLI's own fallback for a model without Extra or Max.
113
+ const unsupported = status.supported && !status.supported.includes(capped);
114
+ return unsupported && (capped === "xhigh" || capped === "max") ? "high" : capped;
115
+ }
116
+
117
+ /** Where a setting lives, for a sentence that tells the user where to change it. */
118
+ export function effortSettingPlace(setting: EffortSetting<unknown>): string {
119
+ if (setting.file) return setting.file;
120
+ switch (setting.source) {
121
+ case "userSettings": return "~/.claude/settings.json";
122
+ case "projectSettings": return ".claude/settings.json";
123
+ case "localSettings": return ".claude/settings.local.json";
124
+ case "policySettings": return "managed settings";
125
+ case "flagSettings": return "the settings PPM passes to Claude Code";
126
+ case "environment": return "the environment PPM runs in";
127
+ }
128
+ }
package/src/types/api.ts CHANGED
@@ -77,7 +77,9 @@ export type ChatWsServerMessage =
77
77
  | { type: "error"; message: string }
78
78
  | { type: "account_info"; accountId: string; accountLabel: string }
79
79
  | { type: "phase_changed"; phase: SessionPhase; elapsed?: number }
80
- | { type: "session_state"; sessionId: string; phase: SessionPhase; pendingApproval: { requestId: string; tool: string; input: unknown } | null; sessionTitle: string | null; model?: string; effort?: string; thinking?: boolean; turnStop?: import("../shared/turn-stop").TurnStop | null }
80
+ | { type: "session_state"; sessionId: string; phase: SessionPhase; pendingApproval: { requestId: string; tool: string; input: unknown } | null; sessionTitle: string | null; model?: string; effort?: string; thinking?: boolean; effortStatus?: import("../shared/effort-status").EffortStatus | null; turnStop?: import("../shared/turn-stop").TurnStop | null }
81
+ /** What the session's CLI said about its effort when the turn started. */
82
+ | { type: "effort_status"; status: import("../shared/effort-status").EffortStatus }
81
83
  /** The turn that just ended was ended by an error. Sent just before its `done`. */
82
84
  | { type: "turn_stop"; stop: import("../shared/turn-stop").TurnStop }
83
85
  | { type: "turn_events"; events: unknown[]; streamSeq?: number; truncated?: boolean }
package/src/types/chat.ts CHANGED
@@ -106,6 +106,14 @@ export interface AIProvider {
106
106
  getUsage?(sessionId?: string, pickedAccountId?: string): Promise<UsageInfo>;
107
107
  /** True when a live streaming subprocess exists for this session */
108
108
  hasStreamingSession?(sessionId: string): boolean;
109
+ /**
110
+ * The effort the session's live subprocess sends and what overrides the pick there; null
111
+ * when none is running or it cannot say. Only providers whose effort can be overridden
112
+ * outside PPM implement it.
113
+ */
114
+ readEffortStatus?(sessionId: string): Promise<import("../shared/effort-status").EffortStatus | null>;
115
+ /** The same overrides, worked out from settings files for a session nothing has started yet. */
116
+ estimateEffortStatus?(projectPath: string | undefined, model?: string): import("../shared/effort-status").EffortStatus;
109
117
  /** Prompt-cache lifetime for this session, in ms — how long holding its subprocess pays. */
110
118
  promptCacheTtlMs?(sessionId: string): number;
111
119
  }
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
2
2
  import { ChevronUp, ChevronDown, ChevronsDown, ChevronsUpDown } from "@/lib/icons";
3
3
  import { cn } from "@/lib/utils";
4
4
  import { useIsMobile } from "@/hooks/use-is-mobile";
@@ -52,14 +52,18 @@ export function ChatScrollNav({ scrollElement, userIndices, scrollToBottom }: {
52
52
  const rowTop = (el: HTMLElement, row: HTMLElement, elTop: number) =>
53
53
  row.getBoundingClientRect().top - elTop + el.scrollTop;
54
54
 
55
- const userRows = useCallback((el: HTMLElement) =>
56
- Array.from(el.querySelectorAll<HTMLElement>("[data-msg-index]"))
57
- .filter((r) => userIndices.includes(Number(r.dataset.msgIndex))),
58
- [userIndices]);
55
+ const userRows = useCallback((el: HTMLElement) => {
56
+ const users = new Set(userIndices);
57
+ return Array.from(el.querySelectorAll<HTMLElement>("[data-msg-index]"))
58
+ .filter((r) => users.has(Number(r.dataset.msgIndex)));
59
+ }, [userIndices]);
59
60
 
60
- useEffect(() => {
61
+ // Followed only while the cluster is open, since nothing at rest shows it. A streaming answer
62
+ // scrolls the transcript on every flush, and each scroll measured every user row; a layout
63
+ // effect so opening shows the real position, not the last one.
64
+ useLayoutEffect(() => {
61
65
  const el = scrollElement;
62
- if (!el) return;
66
+ if (!el || !open) return;
63
67
  let raf = 0;
64
68
  const recompute = () => {
65
69
  const top = el.scrollTop;
@@ -91,7 +95,7 @@ export function ChatScrollNav({ scrollElement, userIndices, scrollToBottom }: {
91
95
  el.removeEventListener("scroll", schedule);
92
96
  ro.disconnect();
93
97
  };
94
- }, [scrollElement, userRows]);
98
+ }, [scrollElement, userRows, open]);
95
99
 
96
100
  // Auto-collapse. Restarts on every jump so a burst of navigation keeps it open.
97
101
  useEffect(() => {
@@ -9,6 +9,7 @@ import { splitAttachmentMarkers } from "@/lib/attachment-marker-split";
9
9
  import type { ChatAttemptEvent } from "@/lib/chat-attempt-lifecycle";
10
10
  import { useChat } from "@/hooks/use-chat";
11
11
  import { useChatPrewarm } from "@/hooks/use-chat-prewarm";
12
+ import { useEffortStatus } from "@/hooks/use-effort-status";
12
13
  import { useUsage } from "@/hooks/use-usage";
13
14
  import { useDesignSessionRedirect } from "@/hooks/use-design-session-redirect";
14
15
  import { useTabStore } from "@/stores/tab-store";
@@ -26,9 +27,8 @@ import { AgentSessionProvider, normalizeProviderId } from "./agent-session-conte
26
27
  import { McpSignInBar } from "@/components/mcp-auth/mcp-sign-in-bar";
27
28
  import { SessionChangesBar } from "./session-changes-bar";
28
29
  import { CONTINUE_AFTER_STOP, TurnStopBar } from "./turn-stop-bar";
29
- import { SessionChangesContext, type SessionChangesValue } from "./session-changes-context";
30
+ import { SessionChangesContext, useSessionChangesValue } from "./session-changes-context";
30
31
  import { useSessionFileChanges } from "@/hooks/use-session-file-changes";
31
- import { openSessionReview } from "@/lib/session-file-changes-client";
32
32
  import { useTeamActivityFeed } from "@/hooks/use-team-activity-feed";
33
33
  import { MessageInput, type ChatAttachment, type MessagePriority } from "./message-input";
34
34
  import { SlashCommandPicker, type SlashItem } from "./slash-command-picker";
@@ -458,6 +458,7 @@ function ChatTabContent({ metadata, tabId, onNewSession, onFork, historyFilter }
458
458
  setModel,
459
459
  effort,
460
460
  setEffort,
461
+ effortStatus: sessionEffortStatus,
461
462
  thinking,
462
463
  setThinking,
463
464
  turnSettings,
@@ -481,17 +482,19 @@ function ChatTabContent({ metadata, tabId, onNewSession, onFork, historyFilter }
481
482
  refetchMessages();
482
483
  }, [isConnected, reconnect, refetchMessages]);
483
484
 
485
+ const effortStatus = useEffortStatus({
486
+ projectName,
487
+ // Not while the provider is still a guess: the answer would be about another provider.
488
+ providerId: preparation.pending ? undefined : preparation.providerId ?? providerId,
489
+ model,
490
+ sessionStatus: sessionEffortStatus,
491
+ });
492
+
484
493
  const sessionChanges = useSessionFileChanges({ projectName, sessionId, messages, isStreaming });
485
- const openReview = useCallback((path?: string) => {
486
- if (!sessionId) return;
487
- openSessionReview({ projectName, sessionId, title: sessionTitle || undefined, providerId, paths: sessionChanges.paths, selectPath: path });
488
- }, [projectName, sessionId, sessionTitle, providerId, sessionChanges.paths]);
489
494
  // Each turn's change tray answers the same list the bar shows.
490
- const sessionChangesValue = useMemo((): SessionChangesValue | null => (
491
- projectName && sessionId
492
- ? { projectName, sessionId, files: sessionChanges.files, refresh: sessionChanges.refresh, openReview }
493
- : null
494
- ), [projectName, sessionId, sessionChanges.files, sessionChanges.refresh, openReview]);
495
+ const { value: sessionChangesValue, openReview } = useSessionChangesValue({
496
+ projectName, sessionId, title: sessionTitle || undefined, providerId, changes: sessionChanges,
497
+ });
495
498
 
496
499
  // `model`/`effort`/`thinking` are what change when a pick does; the picks sent are read
497
500
  // through `turnSettings`, which leaves out whatever the user has not chosen.
@@ -1492,6 +1495,7 @@ function ChatTabContent({ metadata, tabId, onNewSession, onFork, historyFilter }
1492
1495
  model={model}
1493
1496
  onModelChange={!preparation?.pending && !firstSendPending ? setModel : undefined}
1494
1497
  effort={effort}
1498
+ effortStatus={effortStatus}
1495
1499
  onEffortChange={setEffort}
1496
1500
  thinking={thinking}
1497
1501
  onThinkingChange={setThinking}
@@ -3,16 +3,33 @@ import { Copy, Check } from "@/lib/icons";
3
3
  import { cn } from "@/lib/utils";
4
4
  import { formatRelativeDate } from "@/lib/format-date";
5
5
  import { copyToClipboard } from "@/lib/clipboard";
6
+ import { useRelativeTimeTick } from "@/hooks/use-relative-time-tick";
7
+
8
+ // The fields `toLocaleString()` prints, in one formatter: a long chat draws hundreds of these.
9
+ let fullDateFormat: Intl.DateTimeFormat | undefined;
6
10
 
7
11
  /** Full timestamp for the relative-time tooltip. */
8
12
  function fullDate(iso: string): string {
9
13
  try {
10
- return new Date(iso).toLocaleString();
14
+ fullDateFormat ??= new Intl.DateTimeFormat(undefined, {
15
+ year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric",
16
+ });
17
+ return fullDateFormat.format(new Date(iso));
11
18
  } catch {
12
19
  return "";
13
20
  }
14
21
  }
15
22
 
23
+ /** The relative time, kept current on its own so the message around it is never redrawn for it. */
24
+ function RelativeTime({ timestamp }: { timestamp: string }) {
25
+ useRelativeTimeTick();
26
+ return (
27
+ <time title={fullDate(timestamp)} className="pr-1 tabular-nums">
28
+ {formatRelativeDate(timestamp)}
29
+ </time>
30
+ );
31
+ }
32
+
16
33
  /**
17
34
  * Ghost-pill action button (icon + label) for the message action bar.
18
35
  * 44px min touch target on mobile; tighter on desktop. Always visible.
@@ -84,9 +101,7 @@ export function MessageActionBar({
84
101
  className,
85
102
  )}
86
103
  >
87
- <time title={fullDate(timestamp)} className="pr-1 tabular-nums">
88
- {formatRelativeDate(timestamp)}
89
- </time>
104
+ <RelativeTime timestamp={timestamp} />
90
105
  <CopyButton content={content} />
91
106
  {children}
92
107
  </div>
@@ -27,6 +27,7 @@ import { ReplyCard } from "./reply-card";
27
27
  import type { ReplyReference } from "../../../shared/chat-reply";
28
28
  import { PromptCacheChip } from "./prompt-cache-chip";
29
29
  import type { PromptCacheState } from "../../../shared/prompt-cache-idle";
30
+ import type { EffortStatus } from "../../../shared/effort-status";
30
31
 
31
32
  /**
32
33
  * Base64 payload for an image file, or undefined when it cannot be read.
@@ -144,6 +145,8 @@ interface MessageInputProps {
144
145
  onModelChange?: (model: string) => void;
145
146
  /** Current per-session effort (null = provider default) */
146
147
  effort?: string | null;
148
+ /** What overrides the effort pick outside PPM (CLAUDE_CODE_EFFORT_LEVEL, a settings cap, the model) */
149
+ effortStatus?: EffortStatus | null;
147
150
  /** Effort change handler */
148
151
  onEffortChange?: (effort: string) => void;
149
152
  /** Current per-session thinking on/off */
@@ -191,6 +194,7 @@ export const MessageInput = memo(function MessageInput({
191
194
  model,
192
195
  onModelChange,
193
196
  effort,
197
+ effortStatus,
194
198
  onEffortChange,
195
199
  thinking,
196
200
  onThinkingChange,
@@ -1048,6 +1052,7 @@ export const MessageInput = memo(function MessageInput({
1048
1052
  <ModelThinkingSelector
1049
1053
  model={model ?? null}
1050
1054
  effort={effort ?? null}
1055
+ effortStatus={effortStatus}
1051
1056
  thinking={thinking ?? false}
1052
1057
  onModelChange={onModelChange}
1053
1058
  onEffortChange={onEffortChange ?? (() => {})}
@@ -1152,6 +1157,7 @@ export const MessageInput = memo(function MessageInput({
1152
1157
  <ModelThinkingSelector
1153
1158
  model={model ?? null}
1154
1159
  effort={effort ?? null}
1160
+ effortStatus={effortStatus}
1155
1161
  thinking={thinking ?? false}
1156
1162
  onModelChange={onModelChange}
1157
1163
  onEffortChange={onEffortChange ?? (() => {})}
@@ -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