forge-openclaw-plugin 0.2.101 → 0.2.103

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 (227) hide show
  1. package/dist/assets/activity-page-Dv6X5ZCV.js +1 -0
  2. package/dist/assets/ai-surface-workspace-CutiG6uS.js +1 -0
  3. package/dist/assets/atlas-panel-jgMRyaHn.js +1 -0
  4. package/dist/assets/{board-BkDRaMp6.js → board-dIX6etHh.js} +1 -1
  5. package/dist/assets/calendar-page-q7Nm5E2U.js +1 -0
  6. package/dist/assets/calendar-rules-DPFsfiRl.js +1 -0
  7. package/dist/assets/calendar-week-toolbar-nVhgB-0s.js +1 -0
  8. package/dist/assets/{charts-P7EVhIog.js → charts-DFnEuIMB.js} +8 -8
  9. package/dist/assets/companion-sync-lab-page-CJ8UTij8.js +1 -0
  10. package/dist/assets/daily-metrics-dashboard-CiJkkkd1.js +1 -0
  11. package/dist/assets/entity-note-count-link-RzBB6ujx.js +1 -0
  12. package/dist/assets/entity-notes-surface-Djz50HvD.js +1 -0
  13. package/dist/assets/execution-board-CtPhI-58.js +1 -0
  14. package/dist/assets/faceted-token-search-DEC6ANa9.js +1 -0
  15. package/dist/assets/flagship-signal-deck-CtHC3mql.js +1 -0
  16. package/dist/assets/floating-action-menu-CKQRhff9.js +1 -0
  17. package/dist/assets/{forms-BFlTgZ3W.js → forms-hB0SqEh-.js} +1 -1
  18. package/dist/assets/goal-detail-page-CF5fNQeR.js +1 -0
  19. package/dist/assets/goals-page-fsY8NzGB.js +1 -0
  20. package/dist/assets/{graph-D6JLqDbD.js → graph-DDUZNRsO.js} +14 -14
  21. package/dist/assets/habits-page-COoGz4kj.js +1 -0
  22. package/dist/assets/index-BAXYM89v.css +1 -0
  23. package/dist/assets/index-EqQsXoat.js +19 -0
  24. package/dist/assets/insight-flow-dialog-DtX_5W7g.js +1 -0
  25. package/dist/assets/insights-page-cBd74ObU.js +8 -0
  26. package/dist/assets/kanban-page-CYav9THw.js +1 -0
  27. package/dist/assets/knowledge-graph-page-D2A-W1jE.js +1 -0
  28. package/dist/assets/{life-force-page-Dy0JTS2G.js → life-force-page-CACGlNhq.js} +1 -1
  29. package/dist/assets/life-force-workspace-BJ4N1I78.js +1 -0
  30. package/dist/assets/{maps-ClgJoCjz.js → maps-C-yOWiDN.js} +1 -1
  31. package/dist/assets/metric-tile-CMadwnGz.js +1 -0
  32. package/dist/assets/{motion-BeD44FeG.js → motion-Lt5B1XuE.js} +1 -1
  33. package/dist/assets/movement-page-D5VqFd2q.js +1 -0
  34. package/dist/assets/note-markdown-BzK2Qlgr.js +3 -0
  35. package/dist/assets/note-tags-input-CZzqJMLc.js +1 -0
  36. package/dist/assets/notes-page-D3Hsh90C.js +1 -0
  37. package/dist/assets/{open-in-graph-button-IXe9SGth.js → open-in-graph-button-BFPKfyK3.js} +1 -1
  38. package/dist/assets/orbit-map-CnyfSmOG.js +1 -0
  39. package/dist/assets/overview-page-CXdWrOV1.js +1 -0
  40. package/dist/assets/page-hero-ffKzgyW3.js +1 -0
  41. package/dist/assets/pill-cluster-C2D0h3lx.js +1 -0
  42. package/dist/assets/{preference-entity-handoff-button-5PzUn42S.js → preference-entity-handoff-button-Dj3V6VxL.js} +1 -1
  43. package/dist/assets/preferences-page-Jo8Gw386.js +1 -0
  44. package/dist/assets/project-collections-qSqp90HN.js +1 -0
  45. package/dist/assets/{project-detail-page-BXK5-4xW.js → project-detail-page-BifhiLQX.js} +1 -1
  46. package/dist/assets/project-management-hierarchy-page-DZ_9klIc.js +1 -0
  47. package/dist/assets/project-management-section-nav-BImLCVvf.js +1 -0
  48. package/dist/assets/projects-page-ptcx6H38.js +1 -0
  49. package/dist/assets/psyche-behaviors-page-BDwXyDta.js +5 -0
  50. package/dist/assets/psyche-flashcards-page-DL1BP1jX.js +1 -0
  51. package/dist/assets/psyche-goal-map-page-ovcpisx1.js +1 -0
  52. package/dist/assets/psyche-graph-EP5GL612.js +1 -0
  53. package/dist/assets/{psyche-metrics-page-CuR9oqEy.js → psyche-metrics-page-Bcu813Rg.js} +1 -1
  54. package/dist/assets/psyche-mode-guide-page-C0g27Xpt.js +1 -0
  55. package/dist/assets/psyche-modes-page-BjKjX5MR.js +1 -0
  56. package/dist/assets/psyche-page-F4tF2W70.js +1 -0
  57. package/dist/assets/psyche-patterns-page-B-14hukK.js +5 -0
  58. package/dist/assets/psyche-questionnaire-builder-page-BdXmoHvK.js +1 -0
  59. package/dist/assets/psyche-questionnaire-detail-page-Cu5uwlJu.js +1 -0
  60. package/dist/assets/psyche-questionnaire-run-detail-page-C3R4PClg.js +1 -0
  61. package/dist/assets/psyche-questionnaire-run-page-Div3iDdt.js +1 -0
  62. package/dist/assets/psyche-questionnaires-page-DpqAPQCp.js +1 -0
  63. package/dist/assets/psyche-report-detail-page-BvWVDKP3.js +3 -0
  64. package/dist/assets/psyche-reports-page-BrbWUlAq.js +1 -0
  65. package/dist/assets/{psyche-schemas-HFmg37Wj.js → psyche-schemas-Dxj554nU.js} +1 -1
  66. package/dist/assets/psyche-schemas-beliefs-page-DYKvAtSD.js +9 -0
  67. package/dist/assets/psyche-screen-time-page-CAOKCyQw.js +1 -0
  68. package/dist/assets/psyche-self-observation-page-F0MVA0UH.js +1 -0
  69. package/dist/assets/psyche-values-page-DyvX-d0o.js +5 -0
  70. package/dist/assets/report-chain-fields-CeC1cJFS.js +1 -0
  71. package/dist/assets/rewards-page-C2loyODo.js +1 -0
  72. package/dist/assets/scheduling-rules-editor-D40AC2jR.js +1 -0
  73. package/dist/assets/schema-badge-FY7818qB.js +1 -0
  74. package/dist/assets/schema-visuals-CvC9a3i6.js +1 -0
  75. package/dist/assets/select-menu-DJsCG6rM.js +1 -0
  76. package/dist/assets/settings-agents-page-BFkJ5AAD.js +6 -0
  77. package/dist/assets/settings-bin-page-D3-Ab-iA.js +1 -0
  78. package/dist/assets/settings-calendar-page-ly_mSTAD.js +5 -0
  79. package/dist/assets/settings-data-page-D7QOqNf-.js +1 -0
  80. package/dist/assets/settings-logs-page-Ca87HEUx.js +1 -0
  81. package/dist/assets/settings-mobile-page-D0jejZot.js +1 -0
  82. package/dist/assets/settings-models-page-DPDsZjSc.js +1 -0
  83. package/dist/assets/settings-page-1cArlSPM.js +1 -0
  84. package/dist/assets/settings-rewards-page-Db4BV1DC.js +1 -0
  85. package/dist/assets/{settings-section-nav-DSOuht_F.js → settings-section-nav-DP9o4peU.js} +1 -1
  86. package/dist/assets/settings-users-page-BHFyDSsd.js +1 -0
  87. package/dist/assets/settings-wiki-page-BNaiupBm.js +1 -0
  88. package/dist/assets/sleep-page-BmOPF0yD.js +1 -0
  89. package/dist/assets/sports-page-BldIiclr.js +1 -0
  90. package/dist/assets/{state-B-4sS1xO.js → state-vCcAT5Hq.js} +1 -1
  91. package/dist/assets/strategies-page-CaTN99qj.js +1 -0
  92. package/dist/assets/strategy-detail-page-DsgFTd-U.js +1 -0
  93. package/dist/assets/strategy-dialog-7X7peRzu.js +1 -0
  94. package/dist/assets/surface-CJI17F3n.js +1 -0
  95. package/dist/assets/{table-WfAPUppN.js → table-BNqMG3_S.js} +1 -1
  96. package/dist/assets/task-detail-page-M1sIIPA8.js +1 -0
  97. package/dist/assets/timebox-planning-dialog-ByojN0AN.js +1 -0
  98. package/dist/assets/today-page-BjDijtn8.js +1 -0
  99. package/dist/assets/training-load-page-BPRmaWmF.js +1 -0
  100. package/dist/assets/{ui-C13Nbgas.js → ui-C1iwpj2-.js} +4 -4
  101. package/dist/assets/use-psyche-focus-target-Ct-acS9G.js +1 -0
  102. package/dist/assets/vendor-Dnkkx2co.js +1067 -0
  103. package/dist/assets/{vitals-page-qre17Nw8.js → vitals-page-CE9zdGLF.js} +1 -1
  104. package/dist/assets/weekly-review-page-CzhTv90n.js +1 -0
  105. package/dist/assets/weight-loss-page-Bp_cIk78.js +5 -0
  106. package/dist/assets/wiki-article-markdown-Dok2uy2p.js +4 -0
  107. package/dist/assets/wiki-editor-page-BuW32Y3f.js +26 -0
  108. package/dist/assets/wiki-ingest-history-page-DZKSBNHV.js +1 -0
  109. package/dist/assets/wiki-ingest-modal-DyqBEZcC.js +1 -0
  110. package/dist/assets/wiki-page-DqxlbLKG.js +1 -0
  111. package/dist/assets/workbench-flow-page-C4nc9jUg.js +5 -0
  112. package/dist/assets/workbench-page-BnyR7SL0.js +1 -0
  113. package/dist/assets/workout-detail-page-DT-c5cHL.js +2 -0
  114. package/dist/index.html +9 -9
  115. package/dist/openclaw/local-runtime.js +41 -14
  116. package/dist/server/server/migrations/067_weight_loss_daily_active_overrides.sql +13 -0
  117. package/dist/server/server/src/app.js +125 -33
  118. package/dist/server/server/src/health-weight-loss.js +554 -77
  119. package/dist/server/server/src/health.js +12 -4
  120. package/dist/server/server/src/movement.js +84 -1
  121. package/dist/server/server/src/openapi.js +123 -18
  122. package/dist/server/server/src/repositories/model-settings.js +21 -12
  123. package/dist/server/server/src/repositories/settings.js +19 -5
  124. package/dist/server/src/components/ui/info-tooltip.js +6 -6
  125. package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +13 -5
  126. package/dist/server/src/lib/api.js +24 -5
  127. package/dist/server/src/lib/theme-system.js +8 -0
  128. package/openclaw.plugin.json +1 -1
  129. package/package.json +3 -3
  130. package/server/migrations/067_weight_loss_daily_active_overrides.sql +13 -0
  131. package/skills/forge-openclaw/SKILL.md +13 -0
  132. package/skills/forge-openclaw/entity_conversation_playbooks.md +7 -1
  133. package/dist/assets/activity-page-CpjuNSHw.js +0 -1
  134. package/dist/assets/ai-surface-workspace-DEAFZruS.js +0 -1
  135. package/dist/assets/atlas-panel-CdVNPotj.js +0 -1
  136. package/dist/assets/calendar-page-DNNt6lfz.js +0 -1
  137. package/dist/assets/calendar-rules-DNJFNsxi.js +0 -1
  138. package/dist/assets/calendar-week-toolbar-BbPwYeN0.js +0 -1
  139. package/dist/assets/companion-sync-lab-page-KxEDigM6.js +0 -1
  140. package/dist/assets/daily-metrics-dashboard-B3cqJgDt.js +0 -1
  141. package/dist/assets/entity-note-count-link-DrhjJZ4i.js +0 -1
  142. package/dist/assets/entity-notes-surface-CkcRsKJQ.js +0 -1
  143. package/dist/assets/execution-board-D07gOocB.js +0 -1
  144. package/dist/assets/faceted-token-search-BxRRcM3q.js +0 -1
  145. package/dist/assets/flagship-signal-deck-cmy82b8_.js +0 -1
  146. package/dist/assets/floating-action-menu-Fs_ZiUMo.js +0 -1
  147. package/dist/assets/goal-detail-page-CqLiNz4f.js +0 -1
  148. package/dist/assets/goals-page-BTk7mg_T.js +0 -1
  149. package/dist/assets/habits-page-BJxagdzx.js +0 -1
  150. package/dist/assets/index-CF4J4R9L.js +0 -19
  151. package/dist/assets/index-CZbuZQjw.css +0 -1
  152. package/dist/assets/insight-flow-dialog-8f3D0GuC.js +0 -1
  153. package/dist/assets/insights-page-D6rOa7uk.js +0 -8
  154. package/dist/assets/kanban-page-XQ7Se6dH.js +0 -1
  155. package/dist/assets/knowledge-graph-page-BtAg8iv3.js +0 -1
  156. package/dist/assets/life-force-workspace-OfyB9HJM.js +0 -1
  157. package/dist/assets/metric-tile-DKpo-8xw.js +0 -1
  158. package/dist/assets/movement-page-Bg_T_Stx.js +0 -1
  159. package/dist/assets/note-markdown-N-uxD3Xt.js +0 -3
  160. package/dist/assets/note-tags-input-Cdu7wiw6.js +0 -1
  161. package/dist/assets/notes-page-CKXnF_KU.js +0 -1
  162. package/dist/assets/orbit-map-Dzi6KliQ.js +0 -1
  163. package/dist/assets/overview-page-1miYqaVS.js +0 -1
  164. package/dist/assets/page-hero-DRy5b2MU.js +0 -1
  165. package/dist/assets/pill-cluster-C9QczVJ2.js +0 -1
  166. package/dist/assets/preferences-page-DtNaF5Q3.js +0 -1
  167. package/dist/assets/project-collections-xPz2mlRr.js +0 -1
  168. package/dist/assets/project-management-hierarchy-page-DtRpMABw.js +0 -1
  169. package/dist/assets/project-management-section-nav-P3ixzPa-.js +0 -1
  170. package/dist/assets/projects-page-C5ViRuf4.js +0 -1
  171. package/dist/assets/psyche-behaviors-page-Dco46sC4.js +0 -5
  172. package/dist/assets/psyche-flashcards-page-ZcoEB8gV.js +0 -1
  173. package/dist/assets/psyche-goal-map-page-CLBAQOI0.js +0 -1
  174. package/dist/assets/psyche-graph-k4tX2tdp.js +0 -1
  175. package/dist/assets/psyche-mode-guide-page-hIVXcCnE.js +0 -1
  176. package/dist/assets/psyche-modes-page-0lYtBlhO.js +0 -1
  177. package/dist/assets/psyche-page-VZ9k9ISp.js +0 -1
  178. package/dist/assets/psyche-patterns-page-gx5nmdGq.js +0 -5
  179. package/dist/assets/psyche-questionnaire-builder-page-Bn0TOISd.js +0 -1
  180. package/dist/assets/psyche-questionnaire-detail-page-CmVzSd_s.js +0 -1
  181. package/dist/assets/psyche-questionnaire-run-detail-page-BsMbmXCG.js +0 -1
  182. package/dist/assets/psyche-questionnaire-run-page-CgkRL2vi.js +0 -1
  183. package/dist/assets/psyche-questionnaires-page-D7V8uLXM.js +0 -1
  184. package/dist/assets/psyche-report-detail-page-OlFq57eL.js +0 -3
  185. package/dist/assets/psyche-reports-page-dVUZjna1.js +0 -1
  186. package/dist/assets/psyche-schemas-beliefs-page-BCgc8FUd.js +0 -9
  187. package/dist/assets/psyche-screen-time-page-B_6BT_WN.js +0 -1
  188. package/dist/assets/psyche-self-observation-page-CEG5mluK.js +0 -1
  189. package/dist/assets/psyche-values-page-DRbRfEd6.js +0 -5
  190. package/dist/assets/report-chain-fields-CALCV3V5.js +0 -1
  191. package/dist/assets/rewards-page-DmC4R_Ps.js +0 -1
  192. package/dist/assets/scheduling-rules-editor-D02s70hr.js +0 -1
  193. package/dist/assets/schema-badge-BZO-qNhO.js +0 -1
  194. package/dist/assets/schema-visuals-D6nxjbYC.js +0 -1
  195. package/dist/assets/select-menu-fYyreSdQ.js +0 -1
  196. package/dist/assets/settings-agents-page-C_v_hMJF.js +0 -6
  197. package/dist/assets/settings-bin-page-DY5bg81n.js +0 -1
  198. package/dist/assets/settings-calendar-page-D1CzE6cg.js +0 -5
  199. package/dist/assets/settings-data-page-CHRQFU9H.js +0 -1
  200. package/dist/assets/settings-logs-page-B04pUwEv.js +0 -1
  201. package/dist/assets/settings-mobile-page-D9kTlYDS.js +0 -1
  202. package/dist/assets/settings-models-page-D26270R2.js +0 -1
  203. package/dist/assets/settings-page-DYDTFlnv.js +0 -1
  204. package/dist/assets/settings-rewards-page-cl4vqqO_.js +0 -1
  205. package/dist/assets/settings-users-page-BU79JB_T.js +0 -1
  206. package/dist/assets/settings-wiki-page-DwAUlyA3.js +0 -1
  207. package/dist/assets/sleep-page-D8NbdhyS.js +0 -1
  208. package/dist/assets/sports-page-CV4Cnzwn.js +0 -1
  209. package/dist/assets/strategies-page-C4qvXnql.js +0 -1
  210. package/dist/assets/strategy-detail-page-DJLo5rfy.js +0 -1
  211. package/dist/assets/strategy-dialog-D3AuUlVz.js +0 -1
  212. package/dist/assets/task-detail-page-z-9u9rF0.js +0 -1
  213. package/dist/assets/timebox-planning-dialog-DB6FLqmI.js +0 -1
  214. package/dist/assets/today-page-BKlu6gx5.js +0 -1
  215. package/dist/assets/training-load-page-CyJQqo_3.js +0 -1
  216. package/dist/assets/use-psyche-focus-target-C1C_XjYG.js +0 -1
  217. package/dist/assets/vendor-DHkYh85p.js +0 -1052
  218. package/dist/assets/weekly-review-page-Cz4vkRcx.js +0 -1
  219. package/dist/assets/weight-loss-page-BQrnOI0y.js +0 -1
  220. package/dist/assets/wiki-article-markdown-DdiR2TJE.js +0 -4
  221. package/dist/assets/wiki-editor-page-DqwoqVFb.js +0 -26
  222. package/dist/assets/wiki-ingest-history-page--evBLbOw.js +0 -1
  223. package/dist/assets/wiki-ingest-modal--ohzFnj2.js +0 -1
  224. package/dist/assets/wiki-page-B_VJFBPA.js +0 -1
  225. package/dist/assets/workbench-flow-page-Du62mtJU.js +0 -5
  226. package/dist/assets/workbench-page-4MKr3iRm.js +0 -1
  227. package/dist/assets/workout-detail-page-DfUbYYw1.js +0 -2
@@ -2777,6 +2777,7 @@ function expectedWorkoutEvidenceCounts(derived) {
2777
2777
  expectedTimeSeriesSamples,
2778
2778
  expectedHeartRateSamples,
2779
2779
  expectedRoutePoints,
2780
+ rawEvidenceVersion,
2780
2781
  hasCurrentRawEvidenceVersion: rawEvidenceVersion === CURRENT_WORKOUT_RAW_EVIDENCE_VERSION,
2781
2782
  hasEvidenceMetadata: syncTimeSeriesCount !== null ||
2782
2783
  captureHeartRateCount !== null ||
@@ -2819,7 +2820,9 @@ function mobileHealthWorkoutImportState(userId) {
2819
2820
  ORDER BY w.started_at DESC`)
2820
2821
  .all(userId);
2821
2822
  const alreadyUploadedWorkoutExternalUids = [];
2823
+ const incompleteWorkoutExternalUids = [];
2822
2824
  let incompleteWorkoutCount = 0;
2825
+ let staleEvidenceVersionWorkoutCount = 0;
2823
2826
  let timeSeriesSampleCount = 0;
2824
2827
  let heartRateSampleCount = 0;
2825
2828
  let routePointCount = 0;
@@ -2829,27 +2832,32 @@ function mobileHealthWorkoutImportState(userId) {
2829
2832
  const actualTimeSeriesCount = Math.max(0, row.time_series_count ?? 0);
2830
2833
  const actualHeartRateCount = Math.max(0, row.heart_rate_count ?? 0);
2831
2834
  const actualRoutePointCount = Math.max(0, row.route_point_count ?? 0);
2832
- const evidenceComplete = evidenceCounts.hasEvidenceMetadata
2833
- ? evidenceCounts.hasCurrentRawEvidenceVersion &&
2834
- actualTimeSeriesCount >= evidenceCounts.expectedTimeSeriesSamples &&
2835
+ const evidenceCountsComplete = evidenceCounts.hasEvidenceMetadata
2836
+ ? actualTimeSeriesCount >= evidenceCounts.expectedTimeSeriesSamples &&
2835
2837
  actualHeartRateCount >= evidenceCounts.expectedHeartRateSamples &&
2836
2838
  actualRoutePointCount >= evidenceCounts.expectedRoutePoints
2837
2839
  : false;
2838
- if (evidenceComplete) {
2840
+ if (evidenceCountsComplete) {
2839
2841
  alreadyUploadedWorkoutExternalUids.push(row.external_uid.toLowerCase());
2840
2842
  timeSeriesSampleCount += actualTimeSeriesCount;
2841
2843
  heartRateSampleCount += actualHeartRateCount;
2842
2844
  routePointCount += actualRoutePointCount;
2845
+ if (!evidenceCounts.hasCurrentRawEvidenceVersion) {
2846
+ staleEvidenceVersionWorkoutCount += 1;
2847
+ }
2843
2848
  }
2844
2849
  else {
2845
2850
  incompleteWorkoutCount += 1;
2851
+ incompleteWorkoutExternalUids.push(row.external_uid.toLowerCase());
2846
2852
  }
2847
2853
  }
2848
2854
  return {
2849
2855
  alreadyUploadedWorkoutExternalUids,
2856
+ incompleteWorkoutExternalUids,
2850
2857
  alreadyUploadedWorkoutCount: alreadyUploadedWorkoutExternalUids.length,
2851
2858
  existingWorkoutCount: rows.length,
2852
2859
  incompleteWorkoutCount,
2860
+ staleEvidenceVersionWorkoutCount,
2853
2861
  timeSeriesSampleCount,
2854
2862
  heartRateSampleCount,
2855
2863
  routePointCount,
@@ -905,6 +905,69 @@ function decodeMovementTimelineCursor(rawValue) {
905
905
  return null;
906
906
  }
907
907
  }
908
+ function metricNumber(metrics, key) {
909
+ const metric = metrics[key];
910
+ if (!metric || typeof metric !== "object") {
911
+ return null;
912
+ }
913
+ const record = metric;
914
+ for (const field of ["latest", "average", "total", "maximum"]) {
915
+ const value = record[field];
916
+ if (typeof value === "number" && Number.isFinite(value)) {
917
+ return value;
918
+ }
919
+ }
920
+ return null;
921
+ }
922
+ function latestKnownBodyMassKg(userId) {
923
+ const nutritionRow = getDatabase()
924
+ .prepare(`SELECT weight_kg
925
+ FROM nutrition_body_checkins
926
+ WHERE user_id = ?
927
+ AND weight_kg IS NOT NULL
928
+ ORDER BY checked_at DESC
929
+ LIMIT 1`)
930
+ .get(userId);
931
+ if (nutritionRow?.weight_kg && nutritionRow.weight_kg > 0) {
932
+ return nutritionRow.weight_kg;
933
+ }
934
+ const summaryRows = getDatabase()
935
+ .prepare(`SELECT metrics_json
936
+ FROM health_daily_summaries
937
+ WHERE user_id = ?
938
+ AND summary_type = 'vitals'
939
+ ORDER BY date_key DESC, updated_at DESC
940
+ LIMIT 14`)
941
+ .all(userId);
942
+ for (const row of summaryRows) {
943
+ const metrics = safeJsonParse(row.metrics_json, {});
944
+ const bodyMass = metricNumber(metrics, "bodyMass");
945
+ if (bodyMass != null && bodyMass > 0) {
946
+ return bodyMass;
947
+ }
948
+ }
949
+ return 80;
950
+ }
951
+ function estimateMovementActiveCalories(input) {
952
+ const met = input.expectedMet;
953
+ if (met == null || met <= 1) {
954
+ return null;
955
+ }
956
+ const seconds = input.movingSeconds > 0
957
+ ? input.movingSeconds
958
+ : durationSeconds(input.startedAt, input.endedAt);
959
+ if (seconds <= 0) {
960
+ return null;
961
+ }
962
+ const bodyMassKg = latestKnownBodyMassKg(input.userId);
963
+ const activeMet = Math.max(0, met - 1);
964
+ const minutes = seconds / 60;
965
+ return {
966
+ caloriesKcal: round((activeMet * 3.5 * bodyMassKg * minutes) / 200, 0),
967
+ bodyMassKg,
968
+ model: "active_met_minus_one_v1"
969
+ };
970
+ }
908
971
  function haversineDistanceMeters(left, right) {
909
972
  const toRadians = (degrees) => (degrees * Math.PI) / 180;
910
973
  const earthRadius = 6_371_000;
@@ -1912,6 +1975,26 @@ function upsertMovementTrip(pairing, settings, input) {
1912
1975
  });
1913
1976
  const effectiveExpectedMet = parsed.expectedMet ??
1914
1977
  inferExpectedMet(parsed.activityType, derivedMetrics.averageSpeedMps ?? parsed.averageSpeedMps);
1978
+ const estimatedCalories = parsed.caloriesKcal == null
1979
+ ? estimateMovementActiveCalories({
1980
+ userId: pairing.user_id,
1981
+ expectedMet: effectiveExpectedMet,
1982
+ movingSeconds: derivedMetrics.movingSeconds,
1983
+ startedAt: derivedMetrics.startedAt,
1984
+ endedAt: derivedMetrics.endedAt
1985
+ })
1986
+ : null;
1987
+ const effectiveCaloriesKcal = parsed.caloriesKcal ?? estimatedCalories?.caloriesKcal ?? null;
1988
+ const metadata = {
1989
+ ...parsed.metadata,
1990
+ ...(estimatedCalories
1991
+ ? {
1992
+ calorieEstimateModel: estimatedCalories.model,
1993
+ calorieEstimateBodyMassKg: estimatedCalories.bodyMassKg,
1994
+ calorieEstimateSource: "forge_server_ingestion"
1995
+ }
1996
+ : {})
1997
+ };
1915
1998
  const id = existing?.id ?? `mtr_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
1916
1999
  getDatabase()
1917
2000
  .prepare(`INSERT INTO movement_trips (
@@ -1946,7 +2029,7 @@ function upsertMovementTrip(pairing, settings, input) {
1946
2029
  linked_people_json = excluded.linked_people_json,
1947
2030
  metadata_json = excluded.metadata_json,
1948
2031
  updated_at = excluded.updated_at`)
1949
- .run(id, parsed.externalUid, pairing.id, pairing.user_id, startPlace?.id ?? null, endPlace?.id ?? null, parsed.label, parsed.status, parsed.travelMode, parsed.activityType, derivedMetrics.startedAt, derivedMetrics.endedAt, derivedMetrics.distanceMeters, derivedMetrics.movingSeconds, derivedMetrics.idleSeconds, derivedMetrics.averageSpeedMps, derivedMetrics.maxSpeedMps, parsed.caloriesKcal, effectiveExpectedMet, JSON.stringify({}), JSON.stringify(uniqStrings(parsed.tags)), JSON.stringify(parsed.linkedEntities), JSON.stringify(parsed.linkedPeople), JSON.stringify(parsed.metadata), existing?.published_note_id ?? null, existing?.created_at ?? now, now);
2032
+ .run(id, parsed.externalUid, pairing.id, pairing.user_id, startPlace?.id ?? null, endPlace?.id ?? null, parsed.label, parsed.status, parsed.travelMode, parsed.activityType, derivedMetrics.startedAt, derivedMetrics.endedAt, derivedMetrics.distanceMeters, derivedMetrics.movingSeconds, derivedMetrics.idleSeconds, derivedMetrics.averageSpeedMps, derivedMetrics.maxSpeedMps, effectiveCaloriesKcal, effectiveExpectedMet, JSON.stringify({}), JSON.stringify(uniqStrings(parsed.tags)), JSON.stringify(parsed.linkedEntities), JSON.stringify(parsed.linkedPeople), JSON.stringify(metadata), existing?.published_note_id ?? null, existing?.created_at ?? now, now);
1950
2033
  reconcileMovementOverlapValidation(pairing.user_id);
1951
2034
  const fresh = getDatabase()
1952
2035
  .prepare(`SELECT * FROM movement_trips WHERE user_id = ? AND external_uid = ?`)
@@ -30,16 +30,14 @@ const HTTP_METHODS = new Set([
30
30
  ]);
31
31
  const mobileHealthSyncProgressSchema = {
32
32
  type: "object",
33
- required: [
34
- "chunkCount",
35
- "receivedBytes",
36
- "receivedCounts",
37
- "byteTotals"
38
- ],
33
+ required: ["chunkCount", "receivedBytes", "receivedCounts", "byteTotals"],
39
34
  properties: {
40
35
  chunkCount: { type: "number" },
41
36
  receivedBytes: { type: "number" },
42
- receivedCounts: { type: "object", additionalProperties: { type: "number" } },
37
+ receivedCounts: {
38
+ type: "object",
39
+ additionalProperties: { type: "number" }
40
+ },
43
41
  byteTotals: { type: "object", additionalProperties: { type: "number" } }
44
42
  }
45
43
  };
@@ -3054,8 +3052,12 @@ export function buildOpenApiDocument() {
3054
3052
  newestUnlock: nullable({
3055
3053
  $ref: "#/components/schemas/GamificationCatalogEntry"
3056
3054
  }),
3057
- nextTargets: arrayOf({ $ref: "#/components/schemas/GamificationCatalogEntry" }),
3058
- recentlyUnlocked: arrayOf({ $ref: "#/components/schemas/GamificationCatalogEntry" })
3055
+ nextTargets: arrayOf({
3056
+ $ref: "#/components/schemas/GamificationCatalogEntry"
3057
+ }),
3058
+ recentlyUnlocked: arrayOf({
3059
+ $ref: "#/components/schemas/GamificationCatalogEntry"
3060
+ })
3059
3061
  }
3060
3062
  };
3061
3063
  const xpMetricsPayload = {
@@ -3100,7 +3102,9 @@ export function buildOpenApiDocument() {
3100
3102
  newestUnlock: nullable({
3101
3103
  $ref: "#/components/schemas/GamificationCatalogEntry"
3102
3104
  }),
3103
- nextTargets: arrayOf({ $ref: "#/components/schemas/GamificationCatalogEntry" }),
3105
+ nextTargets: arrayOf({
3106
+ $ref: "#/components/schemas/GamificationCatalogEntry"
3107
+ }),
3104
3108
  equipment: { $ref: "#/components/schemas/GamificationEquipment" },
3105
3109
  mascot: { $ref: "#/components/schemas/GamificationMascotState" },
3106
3110
  celebrations: arrayOf({
@@ -3652,6 +3656,7 @@ export function buildOpenApiDocument() {
3652
3656
  "movement",
3653
3657
  "lifeForce",
3654
3658
  "workbench",
3659
+ "weightLoss",
3655
3660
  "psyche"
3656
3661
  ],
3657
3662
  properties: {
@@ -3674,6 +3679,7 @@ export function buildOpenApiDocument() {
3674
3679
  movement: { type: "string" },
3675
3680
  lifeForce: { type: "string" },
3676
3681
  workbench: { type: "string" },
3682
+ weightLoss: { type: "string" },
3677
3683
  psyche: { type: "string" }
3678
3684
  }
3679
3685
  },
@@ -3967,6 +3973,22 @@ export function buildOpenApiDocument() {
3967
3973
  "weeklyReview",
3968
3974
  "sleepOverview",
3969
3975
  "sportsOverview",
3976
+ "trainingLoad",
3977
+ "weightLoss",
3978
+ "weightLossTarget",
3979
+ "weightLossDailyActiveCalories",
3980
+ "weightLossFoodsSearch",
3981
+ "weightLossFoodsBarcode",
3982
+ "weightLossFoodLogs",
3983
+ "weightLossFoodLogDetail",
3984
+ "weightLossParse",
3985
+ "weightLossBodyCheckins",
3986
+ "weightLossAppearanceCheckins",
3987
+ "weightLossSubjectiveCheckins",
3988
+ "weightLossGutCheckins",
3989
+ "weightLossPatterns",
3990
+ "weightLossExperiments",
3991
+ "weightLossExperimentDetail",
3970
3992
  "lifeForce",
3971
3993
  "lifeForceProfile",
3972
3994
  "lifeForceWeekdayTemplate",
@@ -4015,6 +4037,22 @@ export function buildOpenApiDocument() {
4015
4037
  weeklyReview: { type: "string" },
4016
4038
  sleepOverview: { type: "string" },
4017
4039
  sportsOverview: { type: "string" },
4040
+ trainingLoad: { type: "string" },
4041
+ weightLoss: { type: "string" },
4042
+ weightLossTarget: { type: "string" },
4043
+ weightLossDailyActiveCalories: { type: "string" },
4044
+ weightLossFoodsSearch: { type: "string" },
4045
+ weightLossFoodsBarcode: { type: "string" },
4046
+ weightLossFoodLogs: { type: "string" },
4047
+ weightLossFoodLogDetail: { type: "string" },
4048
+ weightLossParse: { type: "string" },
4049
+ weightLossBodyCheckins: { type: "string" },
4050
+ weightLossAppearanceCheckins: { type: "string" },
4051
+ weightLossSubjectiveCheckins: { type: "string" },
4052
+ weightLossGutCheckins: { type: "string" },
4053
+ weightLossPatterns: { type: "string" },
4054
+ weightLossExperiments: { type: "string" },
4055
+ weightLossExperimentDetail: { type: "string" },
4018
4056
  lifeForce: { type: "string" },
4019
4057
  lifeForceProfile: { type: "string" },
4020
4058
  lifeForceWeekdayTemplate: { type: "string" },
@@ -4786,9 +4824,16 @@ export function buildOpenApiDocument() {
4786
4824
  sync: {
4787
4825
  type: "object",
4788
4826
  additionalProperties: false,
4789
- required: ["fullSyncCompletedAt", "lastDailySyncAt", "lastSyncedDateKey"],
4827
+ required: [
4828
+ "fullSyncCompletedAt",
4829
+ "lastDailySyncAt",
4830
+ "lastSyncedDateKey"
4831
+ ],
4790
4832
  properties: {
4791
- fullSyncCompletedAt: nullable({ type: "string", format: "date-time" }),
4833
+ fullSyncCompletedAt: nullable({
4834
+ type: "string",
4835
+ format: "date-time"
4836
+ }),
4792
4837
  lastDailySyncAt: nullable({ type: "string", format: "date-time" }),
4793
4838
  lastSyncedDateKey: nullable({ type: "string" })
4794
4839
  }
@@ -4925,10 +4970,20 @@ export function buildOpenApiDocument() {
4925
4970
  sync: {
4926
4971
  type: "object",
4927
4972
  additionalProperties: false,
4928
- required: ["fullSyncCompletedAt", "lastDailySyncAt", "lastSyncedDateKey"],
4973
+ required: [
4974
+ "fullSyncCompletedAt",
4975
+ "lastDailySyncAt",
4976
+ "lastSyncedDateKey"
4977
+ ],
4929
4978
  properties: {
4930
- fullSyncCompletedAt: nullable({ type: "string", format: "date-time" }),
4931
- lastDailySyncAt: nullable({ type: "string", format: "date-time" }),
4979
+ fullSyncCompletedAt: nullable({
4980
+ type: "string",
4981
+ format: "date-time"
4982
+ }),
4983
+ lastDailySyncAt: nullable({
4984
+ type: "string",
4985
+ format: "date-time"
4986
+ }),
4932
4987
  lastSyncedDateKey: nullable({ type: "string" })
4933
4988
  }
4934
4989
  }
@@ -5218,7 +5273,10 @@ export function buildOpenApiDocument() {
5218
5273
  }
5219
5274
  },
5220
5275
  trainingIntelligence: { type: "object", additionalProperties: true },
5221
- activityBreakdown: arrayOf({ type: "object", additionalProperties: true }),
5276
+ activityBreakdown: arrayOf({
5277
+ type: "object",
5278
+ additionalProperties: true
5279
+ }),
5222
5280
  vitalsTrend: arrayOf({ type: "object", additionalProperties: true }),
5223
5281
  sessionSignals: arrayOf({ type: "object", additionalProperties: true }),
5224
5282
  targetModel: { type: "object", additionalProperties: true }
@@ -5345,7 +5403,10 @@ export function buildOpenApiDocument() {
5345
5403
  todayLedger: { type: "object", additionalProperties: true },
5346
5404
  recentMeals: arrayOf(nutritionFoodLog),
5347
5405
  bodyCheckins: arrayOf({ type: "object", additionalProperties: true }),
5348
- appearanceCheckins: arrayOf({ type: "object", additionalProperties: true }),
5406
+ appearanceCheckins: arrayOf({
5407
+ type: "object",
5408
+ additionalProperties: true
5409
+ }),
5349
5410
  energyModel: { type: "object", additionalProperties: true },
5350
5411
  weightTrend: { type: "object", additionalProperties: true },
5351
5412
  foodQuality: { type: "object", additionalProperties: true },
@@ -5820,6 +5881,47 @@ export function buildOpenApiDocument() {
5820
5881
  }
5821
5882
  }
5822
5883
  },
5884
+ "/api/v1/health/weight-loss/daily-active-calories": {
5885
+ patch: {
5886
+ tags: ["Health"],
5887
+ summary: "Set or clear the user-edited active calorie allowance for one weight-loss day",
5888
+ requestBody: {
5889
+ content: {
5890
+ "application/json": {
5891
+ schema: {
5892
+ type: "object",
5893
+ additionalProperties: false,
5894
+ properties: {
5895
+ userId: { type: "string" },
5896
+ dayKey: {
5897
+ type: "string",
5898
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$"
5899
+ },
5900
+ activeCaloriesKcal: {
5901
+ type: ["number", "null"],
5902
+ minimum: 0
5903
+ },
5904
+ notes: { type: "string" }
5905
+ }
5906
+ }
5907
+ }
5908
+ }
5909
+ },
5910
+ responses: {
5911
+ "200": jsonResponse({
5912
+ type: "object",
5913
+ required: ["dayKey"],
5914
+ properties: {
5915
+ dayKey: { type: "string" },
5916
+ override: {
5917
+ type: ["object", "null"],
5918
+ additionalProperties: true
5919
+ }
5920
+ }
5921
+ }, "Updated daily active calorie override")
5922
+ }
5923
+ }
5924
+ },
5823
5925
  "/api/v1/health/weight-loss/foods/search": {
5824
5926
  post: {
5825
5927
  tags: ["Health"],
@@ -8865,7 +8967,10 @@ export function buildOpenApiDocument() {
8865
8967
  "connectionHelp"
8866
8968
  ],
8867
8969
  properties: {
8868
- provider: { type: "string", enum: CALENDAR_PROVIDER_VALUES },
8970
+ provider: {
8971
+ type: "string",
8972
+ enum: CALENDAR_PROVIDER_VALUES
8973
+ },
8869
8974
  label: { type: "string" },
8870
8975
  supportsDedicatedForgeCalendar: { type: "boolean" },
8871
8976
  connectionHelp: { type: "string" }
@@ -124,15 +124,16 @@ export function upsertAiModelConnection(input, secrets, options = {}) {
124
124
  WHERE id = ?`)
125
125
  .get(parsed.id.trim())
126
126
  : undefined;
127
- const id = existing?.id ??
128
- `mdl_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
127
+ const id = existing?.id ?? `mdl_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
129
128
  const now = new Date().toISOString();
130
129
  const provider = parsed.provider;
131
130
  const authMode = parsed.authMode ?? defaultAuthMode(provider);
132
131
  const baseUrl = parsed.baseUrl?.trim() ||
133
132
  existing?.base_url ||
134
133
  defaultBaseUrlForProvider(provider);
135
- let secretId = existing?.secret_id ?? null;
134
+ let secretId = existing?.secret_id && readEncryptedSecret(existing.secret_id)
135
+ ? existing.secret_id
136
+ : null;
136
137
  let accountLabel = existing?.account_label ?? null;
137
138
  if (parsed.provider === "mock") {
138
139
  secretId = null;
@@ -152,6 +153,11 @@ export function upsertAiModelConnection(input, secrets, options = {}) {
152
153
  accountLabel = options.oauthCredential.accountId;
153
154
  storeEncryptedSecret(secretId, secrets.sealJson(options.oauthCredential), `${parsed.label} AI OAuth connection`);
154
155
  }
156
+ if (parsed.provider !== "mock" && !secretId) {
157
+ throw new Error(authMode === "oauth"
158
+ ? "Reconnect OAuth before saving this model connection. The existing credential is missing from Forge's encrypted secret store."
159
+ : "Enter the API key before saving this model connection. The existing credential is missing from Forge's encrypted secret store.");
160
+ }
155
161
  getDatabase()
156
162
  .prepare(`INSERT INTO ai_model_connections (
157
163
  id, label, provider, auth_mode, base_url, model, account_label, secret_id, enabled, metadata_json, created_at, updated_at
@@ -203,7 +209,12 @@ export function syncForgeManagedWikiProfile(secrets) {
203
209
  .get();
204
210
  const connectionId = settings?.forge_wiki_connection_id?.trim() ?? "";
205
211
  const fallbackModel = settings?.forge_wiki_model?.trim() || "gpt-5.4-mini";
206
- const connection = connectionId ? getAiModelConnectionById(connectionId) : null;
212
+ const requestedConnection = connectionId
213
+ ? getAiModelConnectionById(connectionId)
214
+ : null;
215
+ const connection = requestedConnection?.provider === "openai-codex"
216
+ ? requestedConnection
217
+ : null;
207
218
  const row = connectionId
208
219
  ? getDatabase()
209
220
  .prepare(`SELECT secret_id
@@ -214,18 +225,16 @@ export function syncForgeManagedWikiProfile(secrets) {
214
225
  upsertWikiLlmProfile({
215
226
  id: FORGE_MANAGED_WIKI_PROFILE_ID,
216
227
  label: "Forge wiki ingest",
217
- provider: connection?.provider === "openai-compatible"
218
- ? "openai-compatible"
219
- : connection?.provider === "openai-codex"
220
- ? "openai-codex"
221
- : "openai-responses",
222
- baseUrl: connection?.baseUrl ?? DEFAULT_OPENAI_BASE_URL,
228
+ provider: "openai-codex",
229
+ baseUrl: connection?.baseUrl ?? DEFAULT_OPENAI_CODEX_BASE_URL,
223
230
  model: connection?.model ?? fallbackModel,
224
- secretId: row?.secret_id ?? null,
231
+ secretId: connection ? (row?.secret_id ?? null) : null,
225
232
  enabled: true,
226
233
  metadata: {
227
234
  managedBySettings: true,
228
- connectionId: connection?.id ?? null
235
+ connectionId: connection?.id ?? null,
236
+ authMode: "oauth",
237
+ billing: "chatgpt-codex-oauth"
229
238
  }
230
239
  }, secrets);
231
240
  }
@@ -6,7 +6,7 @@ import { logForgeDebug } from "../debug.js";
6
6
  import { recordActivityEvent } from "./activity-events.js";
7
7
  import { recordEventLog } from "./event-log.js";
8
8
  import { resolveGoogleCalendarOauthPublicConfig } from "../services/google-calendar-oauth-config.js";
9
- import { buildConnectionAgentIdentity, FORGE_DEFAULT_AGENT_ID, listAiModelConnections, syncForgeManagedWikiProfile } from "./model-settings.js";
9
+ import { buildConnectionAgentIdentity, FORGE_DEFAULT_AGENT_ID, getAiModelConnectionById, listAiModelConnections, syncForgeManagedWikiProfile } from "./model-settings.js";
10
10
  import { listUsersByIds } from "./users.js";
11
11
  import { agentBootstrapPolicySchema, agentScopePolicySchema, createAgentTokenSchema, legacyAgentBootstrapPolicy, defaultAgentScopePolicy, agentIdentitySchema, customThemeSchema, settingsPayloadSchema, updateSettingsSchema } from "../types.js";
12
12
  const settingsFileSchema = settingsPayloadSchema.deepPartial();
@@ -591,7 +591,10 @@ function buildSettingsPayloadFromDatabase() {
591
591
  const basicChatConnectionId = normalizeModelConnectionId(row.forge_basic_chat_connection_id);
592
592
  const wikiConnectionId = normalizeModelConnectionId(row.forge_wiki_connection_id);
593
593
  const basicChatConnection = connections.find((entry) => entry.id === basicChatConnectionId) ?? null;
594
- const wikiConnection = connections.find((entry) => entry.id === wikiConnectionId) ?? null;
594
+ const wikiConnectionCandidate = connections.find((entry) => entry.id === wikiConnectionId) ?? null;
595
+ const wikiConnection = wikiConnectionCandidate?.provider === "openai-codex"
596
+ ? wikiConnectionCandidate
597
+ : null;
595
598
  const customTheme = parseCustomThemeJson(row.custom_theme_json);
596
599
  return settingsPayloadSchema.parse({
597
600
  profile: {
@@ -832,9 +835,20 @@ function updateSettingsInternal(input, options = {}) {
832
835
  current.modelSettings.forgeAgent.basicChat.model
833
836
  },
834
837
  wiki: {
835
- connectionId: parsed.modelSettings?.forgeAgent?.wiki?.connectionId !== undefined
836
- ? normalizeModelConnectionId(parsed.modelSettings.forgeAgent.wiki.connectionId)
837
- : (current.modelSettings.forgeAgent.wiki.connectionId ?? ""),
838
+ connectionId: (() => {
839
+ const normalized = parsed.modelSettings?.forgeAgent?.wiki?.connectionId !==
840
+ undefined
841
+ ? normalizeModelConnectionId(parsed.modelSettings.forgeAgent.wiki.connectionId)
842
+ : (current.modelSettings.forgeAgent.wiki.connectionId ?? "");
843
+ if (!normalized) {
844
+ return "";
845
+ }
846
+ const connection = getAiModelConnectionById(normalized);
847
+ if (connection?.provider !== "openai-codex") {
848
+ throw new Error("KarpaWiki ingest must use an OpenAI Codex OAuth model connection, not an OpenAI Platform API connection.");
849
+ }
850
+ return normalized;
851
+ })(),
838
852
  model: parsed.modelSettings?.forgeAgent?.wiki?.model?.trim() ||
839
853
  current.modelSettings.forgeAgent.wiki.model
840
854
  }
@@ -8,9 +8,9 @@ const TOOLTIP_MAX_WIDTH_PX = 320;
8
8
  const TOOLTIP_MIN_HEIGHT_BELOW_PX = 180;
9
9
  const TOOLTIP_ESTIMATED_HEIGHT_PX = 220;
10
10
  export function FieldHint({ children, className }) {
11
- return (_jsx("div", { className: cn("text-sm leading-6 text-white/50", className), children: children }));
11
+ return (_jsx("div", { className: cn("text-sm leading-6 text-[var(--ui-ink-soft)]", className), children: children }));
12
12
  }
13
- export function InfoTooltip({ content, title, label = "Explain this field", className, panelClassName }) {
13
+ export function InfoTooltip({ content, title, label = "Explain this field", className, panelClassName, maxWidthPx = TOOLTIP_MAX_WIDTH_PX }) {
14
14
  const [open, setOpen] = useState(false);
15
15
  const [panelStyle, setPanelStyle] = useState({});
16
16
  const containerRef = useRef(null);
@@ -18,7 +18,7 @@ export function InfoTooltip({ content, title, label = "Explain this field", clas
18
18
  const tooltipId = useId();
19
19
  const tooltipPanel = typeof document === "undefined"
20
20
  ? null
21
- : createPortal(_jsxs("span", { id: tooltipId, role: "tooltip", "aria-hidden": !open, "data-state": open ? "open" : "closed", style: panelStyle, className: cn("pointer-events-none fixed z-[9999] grid overflow-y-auto rounded-[8px] border border-white/10 bg-[#0c111e] px-3 py-2.5 text-left font-sans text-sm normal-case leading-6 tracking-normal text-white/74 shadow-[0_18px_48px_rgba(3,8,18,0.42)] transition", open ? "translate-y-0 opacity-100" : "translate-y-1 opacity-0", panelClassName), children: [title ? (_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-[0.14em] text-white/58", children: title })) : null, _jsx("span", { className: "block min-w-0 whitespace-normal break-words", children: content })] }), document.body);
21
+ : createPortal(_jsxs("span", { id: tooltipId, role: "tooltip", "aria-hidden": !open, "data-state": open ? "open" : "closed", style: panelStyle, className: cn("pointer-events-none fixed z-[9999] grid overflow-y-auto rounded-[8px] border border-[var(--ui-border-subtle)] bg-[var(--surface-glass)] px-3 py-2.5 text-left font-sans text-sm normal-case leading-6 tracking-normal text-[var(--ui-ink-medium)] shadow-[var(--ui-shadow-floating)] backdrop-blur-xl transition", open ? "translate-y-0 opacity-100" : "translate-y-1 opacity-0", panelClassName), children: [title ? (_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-[0.14em] text-[var(--ui-ink-soft)]", children: title })) : null, _jsx("span", { className: "block min-w-0 whitespace-normal break-words", children: content })] }), document.body);
22
22
  useLayoutEffect(() => {
23
23
  if (!open) {
24
24
  return;
@@ -31,7 +31,7 @@ export function InfoTooltip({ content, title, label = "Explain this field", clas
31
31
  const rect = trigger.getBoundingClientRect();
32
32
  const width = window.innerWidth < 480
33
33
  ? window.innerWidth - TOOLTIP_GUTTER_PX * 2
34
- : Math.min(TOOLTIP_MAX_WIDTH_PX, window.innerWidth - TOOLTIP_GUTTER_PX * 2);
34
+ : Math.min(maxWidthPx, window.innerWidth - TOOLTIP_GUTTER_PX * 2);
35
35
  const centeredLeft = rect.left + rect.width / 2 - width / 2;
36
36
  const maxLeft = window.innerWidth - width - TOOLTIP_GUTTER_PX;
37
37
  const left = Math.max(TOOLTIP_GUTTER_PX, Math.min(centeredLeft, maxLeft));
@@ -53,7 +53,7 @@ export function InfoTooltip({ content, title, label = "Explain this field", clas
53
53
  window.removeEventListener("resize", positionTooltip);
54
54
  window.removeEventListener("scroll", positionTooltip, true);
55
55
  };
56
- }, [open]);
56
+ }, [maxWidthPx, open]);
57
57
  useEffect(() => {
58
58
  if (!open) {
59
59
  return;
@@ -66,7 +66,7 @@ export function InfoTooltip({ content, title, label = "Explain this field", clas
66
66
  document.addEventListener("pointerdown", handlePointerDown);
67
67
  return () => document.removeEventListener("pointerdown", handlePointerDown);
68
68
  }, [open]);
69
- return (_jsxs("span", { ref: containerRef, className: cn("relative inline-flex items-center", className), onMouseEnter: () => setOpen(true), onMouseLeave: () => setOpen(false), children: [_jsx("button", { ref: triggerRef, type: "button", "aria-label": label, "aria-describedby": open ? tooltipId : undefined, "aria-expanded": open, className: "inline-flex size-5 items-center justify-center rounded-full text-white/42 transition hover:bg-white/[0.06] hover:text-white/78 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[rgba(192,193,255,0.35)]", onFocus: () => setOpen(true), onBlur: () => setOpen(false), onClick: () => setOpen((current) => !current), onKeyDown: (event) => {
69
+ return (_jsxs("span", { ref: containerRef, className: cn("relative inline-flex items-center", className), onMouseEnter: () => setOpen(true), onMouseLeave: () => setOpen(false), children: [_jsx("button", { ref: triggerRef, type: "button", "aria-label": label, "aria-describedby": open ? tooltipId : undefined, "aria-expanded": open, className: "inline-flex size-5 items-center justify-center rounded-full text-[var(--ui-ink-faint)] transition hover:bg-[var(--ui-surface-hover)] hover:text-[var(--ui-ink-medium)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--primary)]/35", onFocus: () => setOpen(true), onBlur: () => setOpen(false), onClick: () => setOpen((current) => !current), onKeyDown: (event) => {
70
70
  if (event.key === "Escape") {
71
71
  setOpen(false);
72
72
  }
@@ -3,22 +3,30 @@ import { Handle, Position } from "@xyflow/react";
3
3
  import { useState } from "react";
4
4
  import { InfoTooltip } from "../../../components/ui/info-tooltip.js";
5
5
  import { cn } from "../../../lib/utils.js";
6
+ const nodeEyebrowClass = "flex items-center gap-1 text-[10px] uppercase tracking-[0.18em] text-[var(--ui-ink-faint)]";
7
+ const nodePillClass = "rounded-full border border-[var(--ui-border-subtle)] bg-[var(--ui-surface-2)] px-2.5 py-1 text-[10px] uppercase tracking-[0.16em] text-[var(--ui-ink-soft)]";
8
+ const nodeSoftPanelClass = "rounded-[18px] border border-[var(--ui-border-subtle)] bg-[var(--ui-surface-2)] px-3 py-2 text-[11px] text-[var(--ui-ink-soft)]";
9
+ const nodeCodePanelClass = "rounded-[18px] border border-[var(--ui-border-subtle)] bg-[var(--ui-code-bg)] p-3 text-[var(--ui-code-text)]";
6
10
  function describePort(port) {
7
- return [port.kind, port.modelName, port.itemKind ? `item:${port.itemKind}` : null]
11
+ return [
12
+ port.kind,
13
+ port.modelName,
14
+ port.itemKind ? `item:${port.itemKind}` : null
15
+ ]
8
16
  .filter(Boolean)
9
17
  .join(" · ");
10
18
  }
11
19
  function PortList({ title, ports, align }) {
12
- return (_jsxs("div", { className: "grid gap-1.5", children: [_jsxs("div", { className: cn("flex items-center gap-1 text-[10px] uppercase tracking-[0.18em] text-white/34", align === "left" ? "text-left" : "text-right"), children: [_jsx("span", { children: title }), _jsx(InfoTooltip, { content: align === "left"
20
+ return (_jsxs("div", { className: "grid gap-1.5", children: [_jsxs("div", { className: cn(nodeEyebrowClass, align === "left" ? "text-left" : "text-right"), children: [_jsx("span", { children: title }), _jsx(InfoTooltip, { content: align === "left"
13
21
  ? "Inputs are values this box expects from upstream nodes."
14
- : "Outputs are values this box publishes for downstream nodes.", label: align === "left" ? "Explain box inputs" : "Explain box outputs" })] }), ports.length === 0 ? (_jsx("div", { className: "rounded-full border border-dashed border-white/10 px-3 py-1.5 text-[11px] text-white/28", children: "None" })) : null, ports.map((port) => (_jsxs("div", { className: cn("relative rounded-[16px] bg-white/[0.05] px-3 py-2 text-[11px] text-white/62", align === "left" ? "pl-5 text-left" : "pr-5 text-right"), children: [_jsx(Handle, { type: align === "left" ? "target" : "source", position: align === "left" ? Position.Left : Position.Right, id: port.key, className: "!size-2.5 !border !border-white/80 !bg-[#b8c5ff]", style: {
22
+ : "Outputs are values this box publishes for downstream nodes.", label: align === "left" ? "Explain box inputs" : "Explain box outputs" })] }), ports.length === 0 ? (_jsx("div", { className: "rounded-full border border-dashed border-[var(--ui-border-subtle)] px-3 py-1.5 text-[11px] text-[var(--ui-ink-faint)]", children: "None" })) : null, ports.map((port) => (_jsxs("div", { className: cn("relative rounded-[16px] border border-[var(--ui-border-subtle)] bg-[var(--ui-surface-2)] px-3 py-2 text-[11px] text-[var(--ui-ink-soft)]", align === "left" ? "pl-5 text-left" : "pr-5 text-right"), children: [_jsx(Handle, { type: align === "left" ? "target" : "source", position: align === "left" ? Position.Left : Position.Right, id: port.key, className: "!size-2.5 !border !border-[var(--ui-surface-1)] !bg-[var(--primary)]", style: {
15
23
  [align]: 6
16
- } }), _jsx("div", { children: port.label }), _jsx("div", { className: "mt-1 text-[10px] text-white/38", children: describePort(port) })] }, port.key)))] }));
24
+ } }), _jsx("div", { children: port.label }), _jsx("div", { className: "mt-1 text-[10px] text-[var(--ui-ink-faint)]", children: describePort(port) })] }, port.key)))] }));
17
25
  }
18
26
  export function createGenericWorkbenchNodeView(definition) {
19
27
  return function GenericWorkbenchNodeView(_props) {
20
28
  const [schemaOpen, setSchemaOpen] = useState(false);
21
- return (_jsxs("div", { className: "min-w-[280px] rounded-[24px] border border-white/10 bg-[linear-gradient(180deg,rgba(20,28,45,0.98),rgba(11,16,29,0.98))] p-3 shadow-[0_26px_80px_rgba(0,0,0,0.4)]", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate text-sm font-semibold text-white", children: definition.title }), _jsx("div", { className: "mt-1 line-clamp-2 text-[12px] leading-5 text-white/48", children: definition.description })] }), _jsx("div", { className: "rounded-full bg-white/[0.06] px-2.5 py-1 text-[10px] uppercase tracking-[0.16em] text-white/56", children: "box" })] }), definition.params.length > 0 ? (_jsxs("div", { className: "mt-3 rounded-[18px] bg-white/[0.04] px-3 py-2 text-[11px] text-white/52", children: [definition.params.length, " param", definition.params.length === 1 ? "" : "s", " configurable in the flow editor"] })) : null, definition.tools.length > 0 ? (_jsxs("div", { className: "mt-2 rounded-[18px] bg-white/[0.04] px-3 py-2 text-[11px] text-white/52", children: [definition.tools.length, " tool", definition.tools.length === 1 ? "" : "s", " available"] })) : null, _jsx("div", { className: "mt-2", children: _jsx("button", { type: "button", className: "rounded-full bg-white/[0.05] px-3 py-1.5 text-[11px] text-white/56 transition hover:bg-white/[0.08] hover:text-white", onClick: () => setSchemaOpen((current) => !current), children: schemaOpen ? "Hide schema" : "Preview schema" }) }), _jsxs("div", { className: "mt-3 grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-3", children: [_jsx(PortList, { title: "Inputs", ports: definition.inputs, align: "left" }), _jsx(PortList, { title: "Outputs", ports: definition.output, align: "right" })] }), schemaOpen ? (_jsxs("div", { className: "mt-3 rounded-[18px] border border-white/8 bg-black/20 p-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-[10px] uppercase tracking-[0.18em] text-white/38", children: [_jsx("span", { children: "Box contract" }), _jsx(InfoTooltip, { content: "This preview summarizes what the box consumes, publishes, and what tools it can expose to AI nodes.", label: "Explain box contract preview" })] }), _jsx("pre", { className: "mt-2 overflow-auto whitespace-pre-wrap text-[11px] leading-5 text-white/64", children: JSON.stringify({
29
+ return (_jsxs("div", { className: "min-w-[280px] rounded-[24px] border border-[var(--ui-border-subtle)] bg-[image:var(--ui-surface-modal)] p-3 shadow-[var(--ui-shadow-floating)]", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate text-sm font-semibold text-[var(--ui-ink-strong)]", children: definition.title }), _jsx("div", { className: "mt-1 line-clamp-2 text-[12px] leading-5 text-[var(--ui-ink-soft)]", children: definition.description })] }), _jsx("div", { className: nodePillClass, children: "box" })] }), definition.params.length > 0 ? (_jsxs("div", { className: `mt-3 ${nodeSoftPanelClass}`, children: [definition.params.length, " param", definition.params.length === 1 ? "" : "s", " configurable in the flow editor"] })) : null, definition.tools.length > 0 ? (_jsxs("div", { className: `mt-2 ${nodeSoftPanelClass}`, children: [definition.tools.length, " tool", definition.tools.length === 1 ? "" : "s", " available"] })) : null, _jsx("div", { className: "mt-2", children: _jsx("button", { type: "button", className: "rounded-full border border-[var(--ui-border-subtle)] bg-[var(--ui-surface-2)] px-3 py-1.5 text-[11px] text-[var(--ui-ink-soft)] transition hover:bg-[var(--ui-surface-hover)] hover:text-[var(--ui-ink-strong)]", onClick: () => setSchemaOpen((current) => !current), children: schemaOpen ? "Hide schema" : "Preview schema" }) }), _jsxs("div", { className: "mt-3 grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-3", children: [_jsx(PortList, { title: "Inputs", ports: definition.inputs, align: "left" }), _jsx(PortList, { title: "Outputs", ports: definition.output, align: "right" })] }), schemaOpen ? (_jsxs("div", { className: `mt-3 ${nodeCodePanelClass}`, children: [_jsxs("div", { className: "flex items-center gap-2 text-[10px] uppercase tracking-[0.18em] text-[var(--ui-ink-faint)]", children: [_jsx("span", { children: "Box contract" }), _jsx(InfoTooltip, { content: "This preview summarizes what the box consumes, publishes, and what tools it can expose to AI nodes.", label: "Explain box contract preview" })] }), _jsx("pre", { className: "mt-2 overflow-auto whitespace-pre-wrap text-[11px] leading-5", children: JSON.stringify({
22
30
  inputs: definition.inputs.map(({ key, kind, required, description, modelName, itemKind, shape, exampleValue }) => ({
23
31
  key,
24
32
  kind,