@songsid/agend 2.3.2-beta.3 → 2.3.2-beta.5

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 (223) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +5 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/antigravity-usage-scan.d.ts +58 -0
  7. package/dist/antigravity-usage-scan.js +257 -0
  8. package/dist/antigravity-usage-scan.js.map +1 -0
  9. package/dist/antigravity-usage-worker.d.ts +1 -0
  10. package/dist/antigravity-usage-worker.js +10 -0
  11. package/dist/antigravity-usage-worker.js.map +1 -0
  12. package/dist/auth-api.js +2 -0
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/kiro-identity.d.ts +45 -0
  15. package/dist/backend/kiro-identity.js +74 -0
  16. package/dist/backend/kiro-identity.js.map +1 -1
  17. package/dist/backend/kiro.d.ts +36 -1
  18. package/dist/backend/kiro.js +131 -6
  19. package/dist/backend/kiro.js.map +1 -1
  20. package/dist/backend/types.d.ts +6 -0
  21. package/dist/backend/types.js.map +1 -1
  22. package/dist/backend-signin-guide.d.ts +1 -0
  23. package/dist/backend-signin-guide.js +9 -0
  24. package/dist/backend-signin-guide.js.map +1 -0
  25. package/dist/backend-signin-pending.d.ts +10 -0
  26. package/dist/backend-signin-pending.js +44 -0
  27. package/dist/backend-signin-pending.js.map +1 -0
  28. package/dist/browser-backend-signins.d.ts +56 -0
  29. package/dist/browser-backend-signins.js +189 -0
  30. package/dist/browser-backend-signins.js.map +1 -0
  31. package/dist/cache-ledger.d.ts +38 -2
  32. package/dist/cache-ledger.js +133 -8
  33. package/dist/cache-ledger.js.map +1 -1
  34. package/dist/cache-prices.d.ts +2 -1
  35. package/dist/cache-prices.js +5 -2
  36. package/dist/cache-prices.js.map +1 -1
  37. package/dist/cache-service.d.ts +68 -0
  38. package/dist/cache-service.js +355 -13
  39. package/dist/cache-service.js.map +1 -1
  40. package/dist/channel/adapters/discord.js +7 -4
  41. package/dist/channel/adapters/discord.js.map +1 -1
  42. package/dist/channel/mcp-tools.js +7 -1
  43. package/dist/channel/mcp-tools.js.map +1 -1
  44. package/dist/classic-channel-manager.d.ts +5 -0
  45. package/dist/classic-channel-manager.js +14 -1
  46. package/dist/classic-channel-manager.js.map +1 -1
  47. package/dist/cli.js +21 -2
  48. package/dist/cli.js.map +1 -1
  49. package/dist/config-validator.js +22 -5
  50. package/dist/config-validator.js.map +1 -1
  51. package/dist/config.d.ts +19 -0
  52. package/dist/config.js +31 -1
  53. package/dist/config.js.map +1 -1
  54. package/dist/cpu-quota.d.ts +23 -0
  55. package/dist/cpu-quota.js +86 -0
  56. package/dist/cpu-quota.js.map +1 -0
  57. package/dist/daemon.js +22 -7
  58. package/dist/daemon.js.map +1 -1
  59. package/dist/dashboard-permissions.d.ts +20 -6
  60. package/dist/dashboard-permissions.js +58 -9
  61. package/dist/dashboard-permissions.js.map +1 -1
  62. package/dist/fleet-manager.d.ts +51 -1
  63. package/dist/fleet-manager.js +416 -32
  64. package/dist/fleet-manager.js.map +1 -1
  65. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  66. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  67. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +24 -1
  68. package/dist/general-knowledge/skills/web-appearance/SKILL.md +24 -1
  69. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +6 -3
  70. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +12 -5
  71. package/dist/instance-config-impact.js +10 -5
  72. package/dist/instance-config-impact.js.map +1 -1
  73. package/dist/instance-lifecycle.d.ts +17 -0
  74. package/dist/instance-lifecycle.js +32 -4
  75. package/dist/instance-lifecycle.js.map +1 -1
  76. package/dist/kiro-db-reader.d.ts +5 -0
  77. package/dist/kiro-db-reader.js +18 -0
  78. package/dist/kiro-db-reader.js.map +1 -1
  79. package/dist/kiro-engine-status.d.ts +2 -0
  80. package/dist/kiro-engine-status.js +9 -0
  81. package/dist/kiro-engine-status.js.map +1 -1
  82. package/dist/kiro-migrate.d.ts +49 -0
  83. package/dist/kiro-migrate.js +105 -0
  84. package/dist/kiro-migrate.js.map +1 -0
  85. package/dist/kiro-transcript-lane.d.ts +2 -0
  86. package/dist/kiro-transcript-lane.js.map +1 -1
  87. package/dist/kiro-transcript-worker.js +1 -0
  88. package/dist/kiro-transcript-worker.js.map +1 -1
  89. package/dist/kiro-usage-run.d.ts +7 -0
  90. package/dist/kiro-usage-run.js +39 -0
  91. package/dist/kiro-usage-run.js.map +1 -0
  92. package/dist/kiro-usage-scan.d.ts +49 -0
  93. package/dist/kiro-usage-scan.js +223 -0
  94. package/dist/kiro-usage-scan.js.map +1 -0
  95. package/dist/kiro-usage-worker.d.ts +1 -0
  96. package/dist/kiro-usage-worker.js +10 -0
  97. package/dist/kiro-usage-worker.js.map +1 -0
  98. package/dist/kiro-usage.d.ts +58 -0
  99. package/dist/kiro-usage.js +310 -0
  100. package/dist/kiro-usage.js.map +1 -0
  101. package/dist/locale.js +72 -8
  102. package/dist/locale.js.map +1 -1
  103. package/dist/log-service.js +1 -1
  104. package/dist/log-service.js.map +1 -1
  105. package/dist/login-url.d.ts +4 -0
  106. package/dist/login-url.js +22 -0
  107. package/dist/login-url.js.map +1 -0
  108. package/dist/needs-you-hub.d.ts +2 -1
  109. package/dist/needs-you-hub.js +1 -1
  110. package/dist/needs-you-hub.js.map +1 -1
  111. package/dist/needs-you.d.ts +8 -2
  112. package/dist/needs-you.js +6 -0
  113. package/dist/needs-you.js.map +1 -1
  114. package/dist/notice-delivery.d.ts +15 -0
  115. package/dist/notice-delivery.js +37 -0
  116. package/dist/notice-delivery.js.map +1 -0
  117. package/dist/opencode-usage-scan.d.ts +28 -0
  118. package/dist/opencode-usage-scan.js +92 -0
  119. package/dist/opencode-usage-scan.js.map +1 -0
  120. package/dist/opencode-usage-worker.d.ts +1 -0
  121. package/dist/opencode-usage-worker.js +10 -0
  122. package/dist/opencode-usage-worker.js.map +1 -0
  123. package/dist/outbound-handlers.js +22 -2
  124. package/dist/outbound-handlers.js.map +1 -1
  125. package/dist/outbound-schemas.d.ts +24 -0
  126. package/dist/outbound-schemas.js +21 -0
  127. package/dist/outbound-schemas.js.map +1 -1
  128. package/dist/public-web-gateway.js +1 -1
  129. package/dist/public-web-gateway.js.map +1 -1
  130. package/dist/quickstart-api.js +13 -2
  131. package/dist/quickstart-api.js.map +1 -1
  132. package/dist/quickstart.js +4 -1
  133. package/dist/quickstart.js.map +1 -1
  134. package/dist/reply-triggers.d.ts +2 -0
  135. package/dist/reply-triggers.js +3 -0
  136. package/dist/reply-triggers.js.map +1 -0
  137. package/dist/settings-api.js +27 -4
  138. package/dist/settings-api.js.map +1 -1
  139. package/dist/settings-change.js +17 -14
  140. package/dist/settings-change.js.map +1 -1
  141. package/dist/settings-effect.js +7 -1
  142. package/dist/settings-effect.js.map +1 -1
  143. package/dist/settings-instance-patch.js +4 -1
  144. package/dist/settings-instance-patch.js.map +1 -1
  145. package/dist/setup-form.d.ts +1 -1
  146. package/dist/setup-form.js +7 -1
  147. package/dist/setup-form.js.map +1 -1
  148. package/dist/setup-host.d.ts +9 -0
  149. package/dist/setup-host.js +126 -6
  150. package/dist/setup-host.js.map +1 -1
  151. package/dist/setup-signin-assets.d.ts +2 -0
  152. package/dist/setup-signin-assets.js +6 -0
  153. package/dist/setup-signin-assets.js.map +1 -0
  154. package/dist/setup-wizard.js +4 -0
  155. package/dist/setup-wizard.js.map +1 -1
  156. package/dist/structured-signin-transport.d.ts +8 -0
  157. package/dist/structured-signin-transport.js +79 -0
  158. package/dist/structured-signin-transport.js.map +1 -0
  159. package/dist/structured-signin.d.ts +131 -0
  160. package/dist/structured-signin.js +388 -0
  161. package/dist/structured-signin.js.map +1 -0
  162. package/dist/tool-permissions.js +12 -0
  163. package/dist/tool-permissions.js.map +1 -1
  164. package/dist/topic-commands.js +6 -2
  165. package/dist/topic-commands.js.map +1 -1
  166. package/dist/transcript-sources.d.ts +26 -2
  167. package/dist/transcript-sources.js +87 -40
  168. package/dist/transcript-sources.js.map +1 -1
  169. package/dist/turn-reply-guard.d.ts +1 -1
  170. package/dist/turn-reply-guard.js +1 -1
  171. package/dist/turn-reply-guard.js.map +1 -1
  172. package/dist/types.d.ts +19 -8
  173. package/dist/ui/backend-signin.js +63 -10
  174. package/dist/ui/fleet-office-editor.js +10 -6
  175. package/dist/ui/fleet-office-layout.js +34 -28
  176. package/dist/ui/fleet-office-model.js +6 -3
  177. package/dist/ui/fleet-office-overview.js +2 -2
  178. package/dist/ui/fleet-office-pack.d.ts +3 -1
  179. package/dist/ui/fleet-office-pack.js +58 -7
  180. package/dist/ui/fleet-office-skin.js +13 -4
  181. package/dist/ui/fleet-office-store.js +27 -1
  182. package/dist/ui/fleet-office-view.js +5 -4
  183. package/dist/ui/fleet-office-zones.js +155 -6
  184. package/dist/ui/fleet-office.js +169 -41
  185. package/dist/ui/fleet-usage.js +19 -15
  186. package/dist/ui/panel-log.js +2 -2
  187. package/dist/ui/panel-needs.js +4 -4
  188. package/dist/ui/panel-settings.js +32 -10
  189. package/dist/ui/settings-dialogs.js +8 -1
  190. package/dist/ui/settings-model.js +14 -6
  191. package/dist/ui/settings-registry.js +4 -0
  192. package/dist/ui/settings-strings.js +14 -0
  193. package/dist/ui/shared/app-i18n.js +4 -2
  194. package/dist/ui/shared/app-shell.js +14 -6
  195. package/dist/ui/shared/app-stream.js +2 -1
  196. package/dist/ui/shared/app.css +18 -2
  197. package/dist/ui/shared/side-panel.js +95 -0
  198. package/dist/ui/structured-signin.js +115 -0
  199. package/dist/usage-report.d.ts +20 -0
  200. package/dist/usage-report.js +54 -17
  201. package/dist/usage-report.js.map +1 -1
  202. package/dist/usage-rollup.d.ts +9 -1
  203. package/dist/usage-rollup.js +20 -4
  204. package/dist/usage-rollup.js.map +1 -1
  205. package/dist/web-api.d.ts +4 -0
  206. package/dist/web-api.js +110 -0
  207. package/dist/web-api.js.map +1 -1
  208. package/dist/web-auth.js +1 -1
  209. package/dist/web-auth.js.map +1 -1
  210. package/dist/web-backend-login.d.ts +1 -7
  211. package/dist/web-backend-login.js +2 -24
  212. package/dist/web-backend-login.js.map +1 -1
  213. package/dist/web-office-acts.d.ts +173 -0
  214. package/dist/web-office-acts.js +285 -0
  215. package/dist/web-office-acts.js.map +1 -0
  216. package/dist/web-office-skins.d.ts +1 -1
  217. package/dist/web-office-skins.js +1 -1
  218. package/dist/web-office-skins.js.map +1 -1
  219. package/dist/web-settings-patch.d.ts +2 -1
  220. package/dist/web-settings-patch.js +4 -1
  221. package/dist/web-settings-patch.js.map +1 -1
  222. package/package.json +2 -1
  223. package/templates/windows-shortcuts.ps1 +18 -7
@@ -16,7 +16,8 @@ import { onPrefsChanged, savePref } from "./app-prefs.js";
16
16
  import { applyPageTheme, pageNow, resolvedMode, themesStore } from "./app-themes.js";
17
17
  import { ErrorState, Skeleton } from "./ui-states.js";
18
18
  import { InstanceNav, RosterNav, namesStore, setNamesPrimary } from "./instance-nav.js";
19
- import { UsageRail, usageRailStore } from "./header-tools.js";
19
+ import { usageRailStore } from "./header-tools.js";
20
+ import { RightRail, sidePanelStore } from "./side-panel.js";
20
21
  import { OwnerCssKeepBar } from "./owner-css.js";
21
22
  import { AgentWidget, fleetBinds, sidebarWidget, sidebarWidgetStore } from "./agent-widget.js";
22
23
 
@@ -89,14 +90,17 @@ export function openSearch(preset = {}) {
89
90
  }
90
91
  /**
91
92
  * Ctrl+K (⌘K) anywhere opens search; Ctrl+F (⌘F) in an agent's chat opens it narrowed to that agent — elsewhere the
92
- * browser's own find stays. Not while another dialog is open (search, open already, has its input focused).
93
+ * browser's own find stays. Not while another dialog is open (search, open already, has its input focused); a docked
94
+ * side panel (#2585) is not one.
93
95
  */
94
96
  export function handleSearchKey(e) {
95
97
  if (!searchOffered(true)) return;
96
98
  if (e.isComposing || e.altKey || e.shiftKey || !(isMacLike() ? e.metaKey : e.ctrlKey)) return;
97
99
  const key = (e.key || "").toLowerCase();
98
100
  if (key !== "k" && key !== "f") return;
99
- if (typeof document !== "undefined" && document.querySelector("dialog[open]")) return;
101
+ // #2585: a docked side panel is not modal — the page beside it is in use, so search is too (a result that leaves
102
+ // the page asks first when the panel holds changes: side-panel.js).
103
+ if (typeof document !== "undefined" && document.querySelector("dialog[open]:not(.side-panel)")) return;
100
104
  const route = navStore.get().route;
101
105
  if (key === "f" && !(route && route.panel === "chat" && route.instance)) return;
102
106
  e.preventDefault();
@@ -127,7 +131,7 @@ export const sidebarWidthStore = createStore({ width: null });
127
131
  function mainBudget() {
128
132
  const vw = viewportWidth();
129
133
  if (vw < 1200 || typeof document === "undefined") return vw;
130
- const rail = document.querySelector(".u-rail");
134
+ const rail = document.querySelector(".shell > .u-rail, .shell > .side-panel"); // #2585: a docked panel in its place
131
135
  if (!rail) return vw;
132
136
  const w = Math.round(rail.getBoundingClientRect ? rail.getBoundingClientRect().width : 0) || RAIL_W;
133
137
  return vw - w;
@@ -181,11 +185,15 @@ function SidebarResize() {
181
185
  // once the page has drawn its change.
182
186
  const again = () => afterPaint(applyKeptSidebarWidth);
183
187
  const offRail = usageRailStore.subscribe(again);
188
+ // #2585: a docked panel in the rail's place (it is published on every render of its editor: only its coming and
189
+ // going changes the room).
190
+ let docked = !!sidePanelStore.get().panel;
191
+ const offPanel = sidePanelStore.subscribe(() => { const now = !!sidePanelStore.get().panel; if (now !== docked) { docked = now; again(); } });
184
192
  const offNav = navStore.subscribe(again);
185
193
  const w = win();
186
194
  if (w && typeof w.addEventListener === "function") w.addEventListener("resize", applyKeptSidebarWidth);
187
195
  return () => {
188
- offRail(); offNav();
196
+ offRail(); offPanel(); offNav();
189
197
  if (w && typeof w.removeEventListener === "function") w.removeEventListener("resize", applyKeptSidebarWidth);
190
198
  if (activeDrag) activeDrag.end(false); // unmounted mid-drag: nothing kept, nothing left behind
191
199
  };
@@ -787,7 +795,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
787
795
  ${viewOnly || guest || !shell.banner ? null : html`<${shell.banner} />`}
788
796
  <${Outlet} route=${nav.route} seq=${nav.seq} panels=${panels} />
789
797
  </main>
790
- ${guest ? null : html`<${UsageRail} route=${nav.route} />`}
798
+ ${guest ? null : html`<${RightRail} route=${nav.route} />`}
791
799
  <${BottomTabs} route=${nav.route} viewOnly=${viewOnly} />
792
800
  ${shell.dialog ? html`<${shell.dialog.Component} ...${shell.dialog.props} onClose=${closeDialog} />` : null}
793
801
  ${viewOnly || guest || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
@@ -36,7 +36,8 @@
36
36
  // tasks — the task board changed: the card numbers a write touched (SSE only: the board reads again when it is shown).
37
37
  // offices — the stored offices changed ({ rev }; SSE only: the Office view reads /ui/offices again).
38
38
  // office_skins — a skin pack was uploaded or deleted ({ rev }; SSE only: the Office view reads /ui/office-skins again).
39
- const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "reactions", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks", "offices", "office_skins"];
39
+ // office_acts — the agents' live acts in the Office view changed ({ acts }, #2578; a poll page reads /ui/office-acts).
40
+ const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "reactions", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks", "offices", "office_skins", "office_acts"];
40
41
  const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
41
42
  const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
42
43
  const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
@@ -1348,6 +1348,18 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1348
1348
  @media (prefers-reduced-motion: no-preference) { .u-crow { transition: background-color 0.15s var(--ease); } }
1349
1349
  .u-rail-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--s-3) var(--s-4); display: flex; flex-direction: column; gap: var(--s-3); }
1350
1350
  @media (max-width: 1199px) { .u-rail { display: none; } }
1351
+ /* #2585: the docked side panel (side-panel.js), in the usage rail's place beside the page — a non-modal dialog, so the
1352
+ shared dialog's head, body and actions, laid out as a full-height column. Below the rail's width it is never drawn
1353
+ (the editor is the centred Dialog there). */
1354
+ .dlg.side-panel { position: static; inset: auto; margin: 0; width: 400px; max-width: 400px; height: auto; max-height: none; flex-shrink: 0;
1355
+ display: flex; flex-direction: column; min-height: 0; border-radius: 0; box-shadow: none; border-left: 1px solid var(--border); background: var(--bg); color: var(--text); }
1356
+ .side-panel .dlg-box { flex: 1; min-height: 0; max-height: none; }
1357
+ .side-panel .dlg-head { padding: var(--s-3) var(--s-3) var(--s-3) var(--s-5); border-bottom: 1px solid var(--border); }
1358
+ .side-panel .dlg-body { flex: 1; min-height: 0; padding-top: var(--s-4); }
1359
+ .side-panel .dlg-actions { padding: var(--s-3) var(--s-5); border-top: 1px solid var(--border); }
1360
+ @media (max-width: 1199px) { .dlg.side-panel { display: none; } }
1361
+ /* A toast (Offices saved.) beside a docked panel, not over its Cancel and Save. */
1362
+ @media (min-width: 1200px) { .shell:has(> .side-panel) .toasts { right: calc(400px + var(--s-6)); } }
1351
1363
  @media (max-width: 899px) {
1352
1364
  .hide-narrow { display: none; }
1353
1365
  .v-id { padding-left: var(--s-3); padding-right: var(--s-3); }
@@ -1731,7 +1743,7 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1731
1743
  .oc-office > .oc-bar { margin: 0; padding: var(--s-3) var(--s-5) var(--s-2); }
1732
1744
  .oc-office > .oc-legend { margin: 0 var(--s-5) var(--s-2); }
1733
1745
  .oo { flex: 1 0 auto; display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; width: 100%; }
1734
- .oo > :not(.oo-stage) { margin-left: var(--s-5); margin-right: var(--s-5); }
1746
+ .oo > :not(.oo-stage):not(dialog) { margin-left: var(--s-5); margin-right: var(--s-5); } /* not the editors' dialogs: they centre themselves (#2585) */
1735
1747
  .oo-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
1736
1748
  .oo-pick { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--text-2); }
1737
1749
  .oo-pick .tb-select { max-width: 200px; }
@@ -1839,13 +1851,16 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
1839
1851
  .ol-bar { position: absolute; left: 0; right: 0; bottom: 0; z-index: 8; display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); padding: var(--s-2);
1840
1852
  background: var(--surface); border-top: 1px solid var(--border); }
1841
1853
  .ol-actions { display: inline-flex; gap: var(--s-2); margin-left: auto; }
1854
+ /* #2585: Edit layout's controls docked in the side panel (a wide desk): one column, the waypoint's own row wrapping. */
1855
+ .ol-side { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-3); }
1856
+ .ol-side .ol-corridors { display: flex; flex-wrap: wrap; gap: var(--s-2); }
1842
1857
  .ol-corridors { display: inline-flex; flex-wrap: wrap; gap: 4px; }
1843
1858
  .ol-msg { flex-basis: 100%; margin: 0; }
1844
1859
  @media (max-width: 640px) {
1845
1860
  /* A phone: the bars at the column's narrower padding; the stage at least most of a screen; no overview (as the canvas). */
1846
1861
  .oc-office > .oc-bar { padding: var(--s-2) var(--s-3); }
1847
1862
  .oc-office > .oc-legend { margin: 0 var(--s-3) var(--s-2); }
1848
- .oo > :not(.oo-stage) { margin-left: var(--s-3); margin-right: var(--s-3); }
1863
+ .oo > :not(.oo-stage):not(dialog) { margin-left: var(--s-3); margin-right: var(--s-3); }
1849
1864
  .oo-stage { min-height: max(300px, calc(100dvh - 300px)); }
1850
1865
  .oo-pick .tb-select { max-width: 150px; }
1851
1866
  .oo-mini { display: none; }
@@ -1921,6 +1936,7 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
1921
1936
  .us-bar { display: block; width: 100%; height: 6px; margin: 4px 0; }
1922
1937
  .us-bar-bg { fill: var(--surface-2); } .us-bar-fg { fill: var(--us-0); }
1923
1938
  .us-meta { display: block; font-size: var(--fs-xs); color: var(--text-3); overflow-wrap: anywhere; }
1939
+ .us-est { display: block; font-size: var(--fs-xs); font-weight: var(--fw-regular); color: var(--text-3); }
1924
1940
  .us-flags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
1925
1941
  .us-trend, .us-heat { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
1926
1942
  .us-trend h3, .us-heat h3, .us-rest h3 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
@@ -0,0 +1,95 @@
1
+ // #2585 (the user): settings-like editors docked on the right. On a screen wide enough for the usage rail (RAIL_MEDIA,
2
+ // #1673) an editor opens in the rail's place, beside the page — not modal: the page stays in view and in use (the
3
+ // Office re-fits beside it). While one is open the rail steps aside; it comes back as it was when the panel closes (its
4
+ // shown/hidden choice is never touched). Narrower, the same editor is the shared Dialog (centred; a phone's sheet).
5
+ //
6
+ // <${SidePanel} title=… onClose=… actions=… busy=…>…body…</${SidePanel}>: rendered = open, the owner unmounts it to
7
+ // close (like Dialog). Docked, the owner renders nothing itself: it publishes its title, body and actions to the shell's
8
+ // host (sidePanelStore) on every render, so the body keeps the owner's state and closures; the owner going (a close, a
9
+ // navigation) takes the panel with it. Esc in the panel and its × close it (not while `busy`); focus moves into it when
10
+ // it opens and goes back to what opened it when it closes. Docked, the body renders under the shell, not under its
11
+ // owner: a context provided above the owner does not reach it (pass what it needs as props, or read a store).
12
+ // `dirty`: the editor holds unsaved changes. Docked, the page beside it is in use (its links, search): leaving the page
13
+ // then closes the panel as Cancel does — and with changes, asks first (the app's leave guard; one at a time).
14
+ // `dock` false keeps it from docking (the Office in fullscreen, where the shell is out of view); `fallback`, when given,
15
+ // is what it is below the rail's width (or not docked) instead of the Dialog — Edit layout's bar over the canvas.
16
+ import { html, useEffect, useLayoutEffect, useRef } from "./app-html.js";
17
+ import { createStore, useStore } from "./app-store.js";
18
+ import { t } from "./app-i18n.js";
19
+ import { Icon } from "./ui-icons.js";
20
+ import { Dialog } from "./ui-dialog.js";
21
+ import { confirmDialog } from "./ui-confirm.js";
22
+ import { navigate, setLeaveGuard } from "./app-nav.js";
23
+ import { UsageRail, useRailWidth } from "./header-tools.js";
24
+
25
+ /** { panel: { id, title, label, body, actions, busy, onClose } | null } — the one docked panel. */
26
+ export const sidePanelStore = createStore({ panel: null });
27
+ let seq = 0;
28
+ const clear = (id) => { if (sidePanelStore.get().panel?.id === id) sidePanelStore.set({ panel: null }); };
29
+
30
+ /** An editor: docked on a wide screen, the shared Dialog (`wide`) elsewhere. */
31
+ export function SidePanel({ title, label, onClose, actions, busy = false, dirty = false, dock = true, fallback, children }) {
32
+ const docked = useRailWidth() && dock;
33
+ const id = useRef(0);
34
+ if (!id.current) id.current = ++seq;
35
+ const opener = useRef(undefined);
36
+ if (opener.current === undefined) opener.current = typeof document !== "undefined" ? document.activeElement : null;
37
+ useLayoutEffect(() => {
38
+ if (docked) sidePanelStore.set({ panel: { id: id.current, title, label, body: children, actions, busy, onClose } });
39
+ else clear(id.current);
40
+ });
41
+ useLayoutEffect(() => () => {
42
+ const host = typeof document !== "undefined" ? document.querySelector(".side-panel") : null;
43
+ const inside = !!host && (host.contains(document.activeElement) || document.activeElement === document.body);
44
+ clear(id.current);
45
+ // Focus back to what opened it — when it was in the panel (or nowhere): a person who moved on keeps their place.
46
+ const back = opener.current;
47
+ if ((inside || !host) && back && typeof back.focus === "function" && document.contains?.(back)) back.focus();
48
+ }, []);
49
+ // Leaving the page with changes in the docked panel: asked in the app's dialog; yes = Cancel, then on to there.
50
+ const live = useRef(null);
51
+ live.current = { title, onClose };
52
+ useEffect(() => {
53
+ if (!docked || !dirty) return undefined;
54
+ let asking = false;
55
+ return setLeaveGuard((_to, path) => {
56
+ if (!asking) {
57
+ asking = true;
58
+ confirmDialog({ message: t("app.sidePanelLeave", live.current.title), confirmLabel: t("app.sidePanelDiscard"), danger: true }).then((yes) => {
59
+ asking = false;
60
+ if (!yes) return;
61
+ live.current.onClose();
62
+ navigate(path, { force: true });
63
+ });
64
+ }
65
+ return false;
66
+ });
67
+ }, [docked, dirty]);
68
+ if (docked) return null;
69
+ if (fallback !== undefined) return fallback;
70
+ return html`<${Dialog} title=${title} label=${label} onClose=${onClose} actions=${actions} busy=${busy} wide=${true}>${children}</${Dialog}>`;
71
+ }
72
+
73
+ /** The docked panel itself: a non-modal dialog in the rail's place. */
74
+ function SidePanelHost({ panel }) {
75
+ const box = useRef(null);
76
+ // Into the panel as it opens (its box: no ring; Tab moves on), once per panel.
77
+ useLayoutEffect(() => { if (box.current && typeof box.current.focus === "function") box.current.focus({ preventScroll: true }); }, [panel.id]);
78
+ const close = () => { if (!panel.busy) panel.onClose(); };
79
+ const onKeyDown = (e) => { if (e.key === "Escape" && !e.defaultPrevented) { e.preventDefault(); close(); } };
80
+ return html`<dialog open data-trust="dialog" class="dlg side-panel" aria-label=${panel.label || panel.title} onKeyDown=${onKeyDown}>
81
+ <div class="dlg-box" tabindex="-1" ref=${box}>
82
+ <div class="dlg-head"><h2 class="dlg-title">${panel.title}</h2>
83
+ <button type="button" class="icon-btn dlg-x" aria-label=${t("app.close")} title=${t("app.close")} onClick=${close}><${Icon} name="close" /></button></div>
84
+ <div class="dlg-body">${panel.body}</div>
85
+ ${panel.actions ? html`<div class="dlg-actions">${panel.actions}</div>` : null}
86
+ </div>
87
+ </dialog>`;
88
+ }
89
+
90
+ /** The shell's right-hand column: a docked panel when one is open (wide screens), else the usage rail. */
91
+ export function RightRail({ route }) {
92
+ const { panel } = useStore(sidePanelStore);
93
+ const wide = useRailWidth();
94
+ return panel && wide ? html`<${SidePanelHost} panel=${panel} />` : html`<${UsageRail} route=${route} />`;
95
+ }
@@ -0,0 +1,115 @@
1
+ /* Shared browser controls. No terminal relay, provider output or secrets in diagnostics. */
2
+ const NAMES = { "kiro-cli": "Kiro", "claude-code": "Claude Code", codex: "Codex", antigravity: "Antigravity (console fallback)" };
3
+ const CHOICES = { builder_id: "Builder ID", google: "Google", github: "GitHub", organization: "Your organization", subscription: "Claude subscription", console: "Anthropic Console" };
4
+ const ERRORS = {
5
+ unsupported_prompt: "This CLI screen is not supported. Cancel and use the console fallback.",
6
+ cleanup_failed: "The login process could not be confirmed closed. Close it on the host and restart setup before retrying.",
7
+ signin_cleanup_failed: "The login process could not be confirmed closed. Close it on the host and restart setup before retrying.",
8
+ signin_action_stale: "That prompt changed or was already answered. Refresh its status; do not submit it again.",
9
+ signin_console_only: "This backend uses the explicit console fallback.",
10
+ backend_signin_local_only: "Backend sign-in is available only on this computer. Use the local setup page, or sign in later.",
11
+ signin_session_expired: "This setup session has ended. Reopen setup on the host.",
12
+ expired: "This login attempt expired. Start a new attempt.",
13
+ auth_unconfirmed: "The CLI exited, but credentials could not be verified. Retry or sign in later.",
14
+ provider_rejected: "The provider rejected this sign-in. Retry or sign in later.",
15
+ };
16
+
17
+ export function mountStructuredSignIns(root, { endpoint, fetcher = fetch, changed = () => {} } = {}) {
18
+ const doc = root.ownerDocument;
19
+ let view = null, busy = false, disposed = false, timer = null, generation = 0, message = "", rendered = "";
20
+ const el = (tag, text, parent = root) => { const node = doc.createElement(tag); if (text) node.textContent = text; parent.append(node); return node; };
21
+ const errorText = code => ERRORS[code] || "Could not complete sign-in. Check status, retry, or sign in later.";
22
+ const button = (parent, label, action) => { const b = el("button", label, parent); b.type = "button"; b.disabled = busy; b.onclick = action; return b; };
23
+ const request = async data => {
24
+ const response = await fetcher(endpoint, { method: data ? "POST" : "GET", cache: "no-store", headers: { "Content-Type": "application/json" }, ...(data ? { body: JSON.stringify(data) } : {}) });
25
+ const body = await response.json().catch(() => null);
26
+ if (!response.ok) throw new Error(body?.error || "signin_failed");
27
+ return body;
28
+ };
29
+ const accept = next => { view = next; root.dataset.ready = String(view.ready); changed(view); };
30
+ async function perform(data) {
31
+ if (busy || disposed) return;
32
+ busy = true; generation++; message = ""; render();
33
+ try { const next = await request(data); if (!disposed) accept(next); }
34
+ catch (error) { if (!disposed) message = errorText(error?.message); }
35
+ finally { busy = false; if (!disposed) render(); }
36
+ }
37
+ function render() {
38
+ const signature = JSON.stringify([view, busy, message]);
39
+ if (disposed || rendered === signature) return;
40
+ rendered = signature; root.replaceChildren();
41
+ el("h2", "Sign in your AI backends");
42
+ el("p", "Use the controls here; no terminal typing is needed for Kiro, Claude Code or Codex. You can sign in later from the dashboard.");
43
+ if (message) { const alert = el("p", message); alert.setAttribute("role", "alert"); }
44
+ if (!view) { button(root, "Check sign-ins", () => perform({ operation: "refresh" })); return; }
45
+ for (const backend of view.selected) {
46
+ const card = el("section"); card.dataset.backend = backend;
47
+ el("h3", NAMES[backend] || backend, card);
48
+ const state = view.states[backend], pending = view.pending.includes(backend), skipped = view.skipped.includes(backend);
49
+ el("p", skipped ? "Sign in later · remains in Needs you" : state === "valid" ? "Signed in ✓" : state === "not_installed" ? "Not installed on this host" : state === "console_only" ? "Use the explicit console fallback, or sign in later." : "Sign-in needed", card);
50
+ const login = view.login?.backend === backend ? view.login : null;
51
+ const active = login && login.phase !== "done" && !(login.phase === "failed" && login.reason);
52
+ if (active) {
53
+ el("p", login.phase === "waiting" || login.phase === "starting" ? "Waiting for the CLI…" : "Login: " + login.phase.replaceAll("_", " "), card);
54
+ if (login.phase === "method" && login.canAct) for (const choice of login.choices || []) {
55
+ if (CHOICES[choice]) button(card, CHOICES[choice], () => perform({ operation: "input", id: login.id, revision: login.revision, action: { kind: "method", choice } }));
56
+ }
57
+ if (login.url) {
58
+ // The host already allowlists these; still never turn a non-HTTPS URL into a link.
59
+ let url; try { url = new URL(login.url); } catch {}
60
+ if (url?.protocol === "https:" && !url.username && !url.password) {
61
+ const link = el("a", "Open provider sign-in", card); link.href = url.href; link.target = "_blank"; link.rel = "noopener noreferrer";
62
+ }
63
+ }
64
+ if (login.code) { el("p", "Device code:", card); el("code", login.code, card); }
65
+ if (login.canAct && ["code", "start_url", "region"].includes(login.phase)) {
66
+ const label = el("label", { code: "Paste the returned code", start_url: "Organization Start URL", region: "AWS region" }[login.phase], card);
67
+ const input = el("input", null, label); input.type = login.phase === "code" ? "password" : "text"; input.autocomplete = "off"; input.spellcheck = false;
68
+ input.setAttribute("aria-label", label.textContent); input.disabled = busy;
69
+ button(card, "Submit " + login.phase.replaceAll("_", " "), () => { const value = input.value; input.value = ""; void perform({ operation: "input", id: login.id, revision: login.revision, action: { kind: login.phase, value } }); });
70
+ }
71
+ if (["method", "code", "start_url", "region"].includes(login.phase) && !login.canAct) el("p", "Answer sent. Waiting for the next screen; it will not be sent twice.", card);
72
+ button(card, "Cancel sign-in", () => perform({ operation: "cancel" }));
73
+ } else {
74
+ if (login?.reason && login.reason !== "cancelled") el("p", errorText(login.reason), card);
75
+ const usable = !["antigravity"].includes(backend) && state !== "not_installed";
76
+ if (usable) button(card, state === "valid" ? "Sign in again" : "Sign in", () => {
77
+ const relogin = state === "valid";
78
+ if (relogin && !globalThis.confirm("Sign in again? Kiro will sign out its existing account first.")) return;
79
+ void perform({ operation: "start", backend, relogin });
80
+ });
81
+ }
82
+ if (pending && !skipped) button(card, "Sign in later", () => perform({ operation: "skip", backend }));
83
+ }
84
+ button(root, "Check sign-ins", () => perform({ operation: "refresh" }));
85
+ if (view.ready) el("p", "You can continue setup.");
86
+ }
87
+ async function poll() {
88
+ if (disposed) return;
89
+ const before = generation;
90
+ if (!busy) try { const next = await request(); if (!disposed && before === generation) { accept(next); render(); } }
91
+ catch (error) { if (!disposed && before === generation) { message = errorText(error?.message); render(); } }
92
+ if (!disposed) timer = setTimeout(poll, 1000);
93
+ }
94
+ render(); void poll();
95
+ return {
96
+ addBackends(backends) {
97
+ if (view && Array.isArray(backends) && backends.every(b => NAMES[b]) && backends.some(b => !view.selected.includes(b)))
98
+ return perform({ operation: "select", backends: [...new Set([...view.selected, ...backends])] });
99
+ },
100
+ addBackend(backend) { if (view && NAMES[backend] && !view.selected.includes(backend)) return perform({ operation: "select", backends: [...view.selected, backend] }); },
101
+ dispose() { disposed = true; generation++; clearTimeout(timer); root.replaceChildren(); },
102
+ };
103
+ }
104
+
105
+ const setupRoot = typeof document === "undefined" ? null : document.getElementById("setupSignins");
106
+ if (setupRoot) {
107
+ const cards = mountStructuredSignIns(setupRoot, { endpoint: "setup/signins" });
108
+ const backend = document.getElementById("backend");
109
+ const select = () => cards.addBackend(backend?.value);
110
+ backend?.addEventListener("change", select);
111
+ const observer = new MutationObserver(select); if (backend) observer.observe(backend, { childList: true });
112
+ // Initial GET may finish after the environment list, so also observe our readiness result.
113
+ const ready = new MutationObserver(select); ready.observe(setupRoot, { attributes: true, attributeFilter: ["data-ready"] });
114
+ addEventListener("pagehide", () => { observer.disconnect(); ready.disconnect(); cards.dispose(); }, { once: true });
115
+ }
@@ -7,6 +7,9 @@
7
7
  * Honest by construction: an amount is "API-equivalent at list price (checked <date>) — not money spent"; an unpriced
8
8
  * model's tokens are shown and its amount is not; Codex's cache writes are "not reported" (every CLI seen records 0),
9
9
  * never priced as uncached; an instance whose backend AgEnD cannot measure is listed with its reason, never as $0.
10
+ * Kiro records no token counts: its rows are ESTIMATED from text size (kiro-usage.ts, design §10.2) and say so — on the
11
+ * row (`estimated`), on any total that includes one (`includes_estimates`, with the estimated part of the amount), in
12
+ * the API, on the page and in the CSV. An estimate never mixes silently into a measured number.
10
13
  */
11
14
  import type { Ledger } from "./cache-ledger.js";
12
15
  import { type UsageDays } from "./usage-rollup.js";
@@ -27,6 +30,12 @@ export declare const usageBasis: (checked?: string) => string;
27
30
  * latest whole days and says from which date (`dailyFrom`); the rows and totals always cover the whole range.
28
31
  */
29
32
  export declare const USAGE_DAILY_MAX = 20000;
33
+ /** The backends whose usage is estimated, not measured (#2122 §10.2: Kiro records no token counts). */
34
+ export declare const ESTIMATED_BACKENDS: ReadonlySet<string>;
35
+ /** The backends whose transcripts report reasoning tokens (#2122 §10.3); elsewhere a 0 means not reported. */
36
+ export declare const REPORTS_REASONING: ReadonlySet<string>;
37
+ /** The backends whose transcripts carry a cache-write field that stays 0: "not reported" until a real write is seen. */
38
+ export declare const WRITES_UNREPORTED: ReadonlySet<string>;
30
39
  export interface UsageInstanceInput {
31
40
  name: string;
32
41
  /** Its backend now: what it is collected with from here on (each slice of its history carries its own). */
@@ -63,7 +72,10 @@ export interface UsageInput {
63
72
  export interface UsageTotals {
64
73
  /** Uncached input. */
65
74
  input: number;
75
+ /** Output without its reasoning part — the units of tokscale's daily rows (design §10.3). */
66
76
  output: number;
77
+ /** The part of the output a CLI reports as reasoning (Codex); 0 where none is reported (Claude). */
78
+ reasoning: number;
67
79
  cacheRead: number;
68
80
  cacheWrite5m: number;
69
81
  cacheWrite1h: number;
@@ -73,6 +85,12 @@ export interface UsageTotals {
73
85
  }
74
86
  export interface UsageRow extends UsageTotals {
75
87
  key: string;
88
+ /** Whether any of it comes from a CLI that reports reasoning (REPORTS_REASONING): false → its 0 is "not reported". */
89
+ reasoningReported: boolean;
90
+ /** Some of it is estimated (ESTIMATED_BACKENDS); flagged "estimated" (a row) or "includes_estimates" (the totals). */
91
+ estimated: boolean;
92
+ /** The estimated part of `amount` (null: nothing estimated is priced). */
93
+ amountEstimated: number | null;
76
94
  total: number;
77
95
  /** Delivered messages; null where a message has no such key (by model, by profile). */
78
96
  messages: number | null;
@@ -92,6 +110,8 @@ export interface UsageRow extends UsageTotals {
92
110
  }
93
111
  export interface UsageDaily extends UsageTotals {
94
112
  date: string;
113
+ reasoningReported: boolean;
114
+ estimated: boolean;
95
115
  zone: string;
96
116
  instance: string;
97
117
  backend: string;
@@ -10,6 +10,12 @@ export const usageBasis = (checked = PRICES_CHECKED) => `API-equivalent at list
10
10
  */
11
11
  export const USAGE_DAILY_MAX = 20_000;
12
12
  const M = 1_000_000;
13
+ /** The backends whose usage is estimated, not measured (#2122 §10.2: Kiro records no token counts). */
14
+ export const ESTIMATED_BACKENDS = new Set(["kiro-cli"]);
15
+ /** The backends whose transcripts report reasoning tokens (#2122 §10.3); elsewhere a 0 means not reported. */
16
+ export const REPORTS_REASONING = new Set(["codex", "grok", "muse", "opencode", "antigravity"]);
17
+ /** The backends whose transcripts carry a cache-write field that stays 0: "not reported" until a real write is seen. */
18
+ export const WRITES_UNREPORTED = new Set(["codex", "muse"]);
13
19
  const DATE = /^\d{4}-\d{2}-\d{2}$/;
14
20
  /** A range as asked (query string or tool argument); anything else is the last 7 days. */
15
21
  export function usageRange(kind, from, to) {
@@ -31,19 +37,23 @@ export function rangeDates(range, now, zone) {
31
37
  case "custom": return { from: range.from, to: range.to };
32
38
  }
33
39
  }
34
- const zero = () => ({ input: 0, output: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheWrite: 0, requests: 0 });
35
- const addTotals = (into, [n, uncached, read, w5, w1h, w, out]) => {
40
+ const zero = () => ({ input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheWrite: 0, requests: 0 });
41
+ /** One slice into a row: `reasoning` is the part of the slice's output reported as reasoning (shown beside the rest). */
42
+ const addTotals = (into, [n, uncached, read, w5, w1h, w, out], reasoning) => {
43
+ const r = Math.min(Math.max(0, reasoning), out);
36
44
  into.requests += n;
37
45
  into.input += uncached;
38
46
  into.cacheRead += read;
39
47
  into.cacheWrite5m += w5;
40
48
  into.cacheWrite1h += w1h;
41
49
  into.cacheWrite += w;
42
- into.output += out;
50
+ into.output += out - r;
51
+ into.reasoning += r;
43
52
  };
44
53
  const splitKey = (mk) => { const bar = mk.lastIndexOf("|"); return { model: mk.slice(0, bar), long: mk.slice(bar + 1) === "1" }; };
45
54
  /**
46
- * The list-price amount of one slice, or null when its model is not in the table. Codex writes not reported are 0 in the
55
+ * The list-price amount of one slice, or null when its model is not in the table. Reasoning is a part of the slice's
56
+ * output, so the whole output is priced at the output rate once. Codex writes not reported are 0 in the
47
57
  * ledger (it marks writesSeen on the first one above 0), so they add nothing and are never priced as uncached input.
48
58
  */
49
59
  function amountOf(model, long, [, uncached, read, w5, w1h, w, out]) {
@@ -52,6 +62,7 @@ function amountOf(model, long, [, uncached, read, w5, w1h, w, out]) {
52
62
  return null;
53
63
  return (uncached * r.input + read * r.read + w5 * r.write5m + w1h * r.write1h + w * r.write + out * r.output) / M;
54
64
  }
65
+ const estimatedSlice = (s) => ESTIMATED_BACKENDS.has(s.backend);
55
66
  const unknownSource = (s) => s.backend === "unknown" || s.profile === "unknown";
56
67
  export function buildUsageReport(input) {
57
68
  const { from, to } = rangeDates(input.range, input.now, input.zone);
@@ -70,14 +81,14 @@ export function buildUsageReport(input) {
70
81
  if (!i.ledger || !i.days)
71
82
  continue;
72
83
  coverage.push({ name: i.name, intervals: i.days.coverage.map(([a, b]) => [a, b]) });
73
- // Codex: a write is reported only once a real one has been seen (every CLI so far records 0).
84
+ // Codex and Muse: a write is reported only once a real one has been seen (every CLI so far records 0).
74
85
  const writesSeen = i.ledger.writesSeen === true;
75
86
  const base = { instance: i.name, team: i.team };
76
87
  for (const row of i.days.rows) {
77
88
  if (!inRange(row.date))
78
89
  continue;
79
90
  for (const [mk, tot] of Object.entries(row.m))
80
- slices.push({ ...base, backend: row.be, writesReported: row.be !== "codex" || writesSeen, date: row.date, zone: row.zone, profile: row.pf, ...splitKey(mk), tot });
91
+ slices.push({ ...base, backend: row.be, writesReported: !WRITES_UNREPORTED.has(row.be) || writesSeen, date: row.date, zone: row.zone, profile: row.pf, ...splitKey(mk), tot, reasoning: row.r?.[mk] ?? 0, cost: row.c?.[mk] ?? 0 });
81
92
  }
82
93
  for (const [hk, hour] of Object.entries(i.ledger.hours)) {
83
94
  const h = Number(hk);
@@ -88,7 +99,7 @@ export function buildUsageReport(input) {
88
99
  continue;
89
100
  for (const src of hourSources(hour)) {
90
101
  for (const [mk, tot] of Object.entries(src.m))
91
- slices.push({ ...base, backend: src.backend, writesReported: src.backend !== "codex" || writesSeen, date, zone: input.zone, profile: src.profile, ...splitKey(mk), tot });
102
+ slices.push({ ...base, backend: src.backend, writesReported: !WRITES_UNREPORTED.has(src.backend) || writesSeen, date, zone: input.zone, profile: src.profile, ...splitKey(mk), tot, reasoning: src.r[mk] ?? 0, cost: src.c[mk] ?? 0 });
92
103
  }
93
104
  }
94
105
  }
@@ -102,7 +113,7 @@ export function buildUsageReport(input) {
102
113
  }
103
114
  };
104
115
  const rows = new Map();
105
- const newRow = (key) => ({ key, ...zero(), total: 0, messages: null, amount: null, priced: "none", flags: [], unpriced: [], instances: [], backends: [], models: [], profiles: [], _amount: 0, _pricedAny: false, _unpricedAny: false });
116
+ const newRow = (key) => ({ key, ...zero(), reasoningReported: false, estimated: false, amountEstimated: null, _estAmount: 0, _estPriced: false, total: 0, messages: null, amount: null, priced: "none", flags: [], unpriced: [], instances: [], backends: [], models: [], profiles: [], _amount: 0, _pricedAny: false, _unpricedAny: false });
106
117
  const uniq = (list, v) => { if (!list.includes(v))
107
118
  list.push(v); };
108
119
  // Daily rows: one per day, zone, agent, backend, model, tier and profile (an hour's slices summed into their day).
@@ -110,12 +121,18 @@ export function buildUsageReport(input) {
110
121
  for (const s of slices) {
111
122
  const key = keyOf(s);
112
123
  const row = rows.get(key) ?? (rows.set(key, newRow(key)), rows.get(key));
113
- addTotals(row, s.tot);
124
+ addTotals(row, s.tot, s.reasoning);
125
+ if (REPORTS_REASONING.has(s.backend))
126
+ row.reasoningReported = true;
114
127
  uniq(row.instances, s.instance);
115
128
  uniq(row.backends, s.backend);
116
129
  uniq(row.models, s.model);
117
130
  uniq(row.profiles, s.profile);
118
- const amount = amountOf(s.model, s.long, s.tot);
131
+ // List price first; a model the table lacks takes the cost its CLI reported (Grok), said so (design §10.2).
132
+ const listed = amountOf(s.model, s.long, s.tot), reported = listed === null && s.cost > 0;
133
+ const amount = listed ?? (reported ? s.cost : null);
134
+ if (reported)
135
+ uniq(row.flags, "cost_reported");
119
136
  if (amount === null) {
120
137
  row._unpricedAny = true;
121
138
  uniq(row.unpriced, s.model);
@@ -124,6 +141,14 @@ export function buildUsageReport(input) {
124
141
  row._pricedAny = true;
125
142
  row._amount += amount;
126
143
  }
144
+ if (estimatedSlice(s)) {
145
+ row.estimated = true;
146
+ uniq(row.flags, "estimated");
147
+ if (amount !== null) {
148
+ row._estPriced = true;
149
+ row._estAmount += amount;
150
+ }
151
+ }
127
152
  if (!s.writesReported)
128
153
  uniq(row.flags, "cache_write_not_reported");
129
154
  if (unknownSource(s))
@@ -132,11 +157,13 @@ export function buildUsageReport(input) {
132
157
  const dk = [s.date, s.zone, s.instance, s.backend, s.model, tier, s.profile].join("\u0000");
133
158
  let d = dailyBy.get(dk);
134
159
  if (!d) {
135
- d = { date: s.date, zone: s.zone, instance: s.instance, backend: s.backend, model: s.model, tier, profile: s.profile, ...zero(), amount: amount === null ? null : 0,
136
- flags: [...(s.writesReported ? [] : ["cache_write_not_reported"]), ...(amount === null ? ["not_priced"] : []), ...(unknownSource(s) ? ["source_unknown"] : [])] };
160
+ d = { date: s.date, zone: s.zone, instance: s.instance, backend: s.backend, model: s.model, tier, profile: s.profile, ...zero(), reasoningReported: REPORTS_REASONING.has(s.backend), estimated: estimatedSlice(s), amount: amount === null ? null : 0,
161
+ flags: [...(estimatedSlice(s) ? ["estimated"] : []), ...(s.writesReported ? [] : ["cache_write_not_reported"]), ...(amount === null ? ["not_priced"] : []), ...(unknownSource(s) ? ["source_unknown"] : [])] };
137
162
  dailyBy.set(dk, d);
138
163
  }
139
- addTotals(d, s.tot);
164
+ addTotals(d, s.tot, s.reasoning);
165
+ if (reported && !d.flags.includes("cost_reported"))
166
+ d.flags.push("cost_reported");
140
167
  if (amount !== null && d.amount !== null)
141
168
  d.amount += amount;
142
169
  }
@@ -188,8 +215,9 @@ export function buildUsageReport(input) {
188
215
  row.messages ??= 0;
189
216
  }
190
217
  const finish = (row) => {
191
- const { _amount, _pricedAny, _unpricedAny, ...rest } = row;
192
- rest.total = rest.input + rest.output + rest.cacheRead + rest.cacheWrite5m + rest.cacheWrite1h + rest.cacheWrite;
218
+ const { _amount, _pricedAny, _unpricedAny, _estAmount, _estPriced, ...rest } = row;
219
+ rest.amountEstimated = _estPriced ? _estAmount : null;
220
+ rest.total = rest.input + rest.output + rest.reasoning + rest.cacheRead + rest.cacheWrite5m + rest.cacheWrite1h + rest.cacheWrite;
193
221
  rest.amount = _pricedAny ? _amount : null;
194
222
  rest.priced = _pricedAny ? (_unpricedAny ? "partial" : "all") : "none";
195
223
  if (_unpricedAny)
@@ -200,15 +228,24 @@ export function buildUsageReport(input) {
200
228
  .sort((a, b) => (b.amount ?? -1) - (a.amount ?? -1) || b.total - a.total || a.key.localeCompare(b.key));
201
229
  const totals = finish(newRow("all"));
202
230
  for (const r of out) {
203
- for (const k of ["input", "output", "cacheRead", "cacheWrite5m", "cacheWrite1h", "cacheWrite", "requests"])
231
+ for (const k of ["input", "output", "reasoning", "cacheRead", "cacheWrite5m", "cacheWrite1h", "cacheWrite", "requests"])
204
232
  totals[k] += r[k];
205
233
  totals.total += r.total;
206
234
  if (r.messages !== null)
207
235
  totals.messages = (totals.messages ?? 0) + r.messages;
208
236
  if (r.amount !== null)
209
237
  totals.amount = (totals.amount ?? 0) + r.amount;
238
+ if (r.reasoningReported)
239
+ totals.reasoningReported = true;
240
+ if (r.estimated) {
241
+ totals.estimated = true;
242
+ uniq(totals.flags, "includes_estimates");
243
+ }
244
+ if (r.amountEstimated !== null)
245
+ totals.amountEstimated = (totals.amountEstimated ?? 0) + r.amountEstimated;
210
246
  for (const f of r.flags)
211
- uniq(totals.flags, f);
247
+ if (f !== "estimated")
248
+ uniq(totals.flags, f);
212
249
  for (const m of r.unpriced)
213
250
  uniq(totals.unpriced, m);
214
251
  }