@shivaedev/work-board 0.14.2 → 0.16.0

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 (279) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -1
  3. package/assets/agent.js +1 -1
  4. package/assets/native.js +25 -25
  5. package/assets/react.css +1 -1
  6. package/assets/react.js +37 -36
  7. package/dist/browser/diagnostics/schema.d.ts +15 -12
  8. package/dist/browser/diagnostics/schema.d.ts.map +1 -1
  9. package/dist/browser/diagnostics/schema.js +12 -2
  10. package/dist/browser/diagnostics/schema.js.map +1 -1
  11. package/dist/browser/views/remaining.d.ts +20 -12
  12. package/dist/browser/views/remaining.d.ts.map +1 -1
  13. package/dist/client/client.d.ts +1 -0
  14. package/dist/client/client.d.ts.map +1 -1
  15. package/dist/client/client.js +1 -1
  16. package/dist/client/client.js.map +1 -1
  17. package/dist/client/document/Mermaid.d.ts.map +1 -1
  18. package/dist/client/document/Mermaid.js +4 -10
  19. package/dist/client/document/Mermaid.js.map +1 -1
  20. package/dist/client/document/View.js +1 -1
  21. package/dist/client/document/View.js.map +1 -1
  22. package/dist/client/formStatus.d.ts +12 -0
  23. package/dist/client/formStatus.d.ts.map +1 -0
  24. package/dist/client/formStatus.js +27 -0
  25. package/dist/client/formStatus.js.map +1 -0
  26. package/dist/client/react/BoardShell.d.ts.map +1 -1
  27. package/dist/client/react/BoardShell.js +36 -37
  28. package/dist/client/react/BoardShell.js.map +1 -1
  29. package/dist/client/react/DocumentContext.d.ts.map +1 -1
  30. package/dist/client/react/DocumentContext.js +12 -7
  31. package/dist/client/react/DocumentContext.js.map +1 -1
  32. package/dist/client/react/FormContext.d.ts +5 -0
  33. package/dist/client/react/FormContext.d.ts.map +1 -0
  34. package/dist/client/react/FormContext.js +10 -0
  35. package/dist/client/react/FormContext.js.map +1 -0
  36. package/dist/client/react/LiveControls.d.ts +4 -3
  37. package/dist/client/react/LiveControls.d.ts.map +1 -1
  38. package/dist/client/react/LiveControls.js +5 -24
  39. package/dist/client/react/LiveControls.js.map +1 -1
  40. package/dist/client/react/MainLocationContext.d.ts +3 -0
  41. package/dist/client/react/MainLocationContext.d.ts.map +1 -0
  42. package/dist/client/react/MainLocationContext.js +7 -0
  43. package/dist/client/react/MainLocationContext.js.map +1 -0
  44. package/dist/client/react/PhoneNavigation.d.ts +3 -1
  45. package/dist/client/react/PhoneNavigation.d.ts.map +1 -1
  46. package/dist/client/react/PhoneNavigation.js +8 -13
  47. package/dist/client/react/PhoneNavigation.js.map +1 -1
  48. package/dist/client/react/ProjectionFrame.d.ts +1 -2
  49. package/dist/client/react/ProjectionFrame.d.ts.map +1 -1
  50. package/dist/client/react/ProjectionFrame.js +10 -4
  51. package/dist/client/react/ProjectionFrame.js.map +1 -1
  52. package/dist/client/react/ReadingLibrary.d.ts.map +1 -1
  53. package/dist/client/react/ReadingLibrary.js +9 -14
  54. package/dist/client/react/ReadingLibrary.js.map +1 -1
  55. package/dist/client/react/ReadingReadyContext.d.ts +6 -0
  56. package/dist/client/react/ReadingReadyContext.d.ts.map +1 -0
  57. package/dist/client/react/ReadingReadyContext.js +7 -0
  58. package/dist/client/react/ReadingReadyContext.js.map +1 -0
  59. package/dist/client/react/SessionContext.d.ts +3 -0
  60. package/dist/client/react/SessionContext.d.ts.map +1 -0
  61. package/dist/client/react/SessionContext.js +3 -0
  62. package/dist/client/react/SessionContext.js.map +1 -0
  63. package/dist/client/react/SystemMain.d.ts.map +1 -1
  64. package/dist/client/react/SystemMain.js +3 -1
  65. package/dist/client/react/SystemMain.js.map +1 -1
  66. package/dist/client/react/SystemPage.d.ts.map +1 -1
  67. package/dist/client/react/SystemPage.js +2 -1
  68. package/dist/client/react/SystemPage.js.map +1 -1
  69. package/dist/client/react/ThemeContext.d.ts +2 -0
  70. package/dist/client/react/ThemeContext.d.ts.map +1 -0
  71. package/dist/client/react/ThemeContext.js +3 -0
  72. package/dist/client/react/ThemeContext.js.map +1 -0
  73. package/dist/client/react/TodayOptions.d.ts.map +1 -1
  74. package/dist/client/react/TodayOptions.js +23 -14
  75. package/dist/client/react/TodayOptions.js.map +1 -1
  76. package/dist/client/react/VisualViewer.d.ts.map +1 -1
  77. package/dist/client/react/VisualViewer.js +6 -5
  78. package/dist/client/react/VisualViewer.js.map +1 -1
  79. package/dist/client/react/boardRouter.d.ts +3 -2
  80. package/dist/client/react/boardRouter.d.ts.map +1 -1
  81. package/dist/client/react/boardRouter.js +2 -1
  82. package/dist/client/react/boardRouter.js.map +1 -1
  83. package/dist/client/react/createResponse.d.ts +32 -4
  84. package/dist/client/react/createResponse.d.ts.map +1 -1
  85. package/dist/client/react/createResponse.js +7 -28
  86. package/dist/client/react/createResponse.js.map +1 -1
  87. package/dist/client/react/diagnosticsValues.d.ts.map +1 -1
  88. package/dist/client/react/diagnosticsValues.js +5 -7
  89. package/dist/client/react/diagnosticsValues.js.map +1 -1
  90. package/dist/client/react/entry.d.ts.map +1 -1
  91. package/dist/client/react/entry.js +2 -34
  92. package/dist/client/react/entry.js.map +1 -1
  93. package/dist/client/react/githubLimited.d.ts +3 -0
  94. package/dist/client/react/githubLimited.d.ts.map +1 -0
  95. package/dist/client/react/githubLimited.js +4 -0
  96. package/dist/client/react/githubLimited.js.map +1 -0
  97. package/dist/client/react/loadProjection.d.ts +135 -135
  98. package/dist/client/react/loadRemaining.d.ts +20 -12
  99. package/dist/client/react/loadRemaining.d.ts.map +1 -1
  100. package/dist/client/react/mainLocation.d.ts +4 -2
  101. package/dist/client/react/mainLocation.d.ts.map +1 -1
  102. package/dist/client/react/mainLocation.js +1 -8
  103. package/dist/client/react/mainLocation.js.map +1 -1
  104. package/dist/client/react/mountBoard.d.ts +3 -0
  105. package/dist/client/react/mountBoard.d.ts.map +1 -0
  106. package/dist/client/react/mountBoard.js +45 -0
  107. package/dist/client/react/mountBoard.js.map +1 -0
  108. package/dist/client/react/navigationLocation.d.ts +3 -1
  109. package/dist/client/react/navigationLocation.d.ts.map +1 -1
  110. package/dist/client/react/navigationLocation.js +11 -1
  111. package/dist/client/react/navigationLocation.js.map +1 -1
  112. package/dist/client/react/retryPage.d.ts +2 -1
  113. package/dist/client/react/retryPage.d.ts.map +1 -1
  114. package/dist/client/react/retryPage.js +3 -4
  115. package/dist/client/react/retryPage.js.map +1 -1
  116. package/dist/client/react/scheduleReading.d.ts.map +1 -1
  117. package/dist/client/react/scheduleReading.js +1 -10
  118. package/dist/client/react/scheduleReading.js.map +1 -1
  119. package/dist/client/react/useAppearance.d.ts +2 -1
  120. package/dist/client/react/useAppearance.d.ts.map +1 -1
  121. package/dist/client/react/useAppearance.js +1 -2
  122. package/dist/client/react/useAppearance.js.map +1 -1
  123. package/dist/client/react/useBoardNavigation.d.ts +1 -1
  124. package/dist/client/react/useBoardNavigation.d.ts.map +1 -1
  125. package/dist/client/react/useBoardNavigation.js +28 -22
  126. package/dist/client/react/useBoardNavigation.js.map +1 -1
  127. package/dist/client/react/useDiagnosticsPolling.d.ts +4 -1
  128. package/dist/client/react/useDiagnosticsPolling.d.ts.map +1 -1
  129. package/dist/client/react/useDiagnosticsPolling.js +9 -7
  130. package/dist/client/react/useDiagnosticsPolling.js.map +1 -1
  131. package/dist/client/react/useDocumentPage.d.ts +1 -1
  132. package/dist/client/react/useDocumentPage.d.ts.map +1 -1
  133. package/dist/client/react/useDocumentPage.js +9 -6
  134. package/dist/client/react/useDocumentPage.js.map +1 -1
  135. package/dist/client/react/useHandoffForm.d.ts +56 -30
  136. package/dist/client/react/useHandoffForm.d.ts.map +1 -1
  137. package/dist/client/react/useHandoffForm.js +19 -30
  138. package/dist/client/react/useHandoffForm.js.map +1 -1
  139. package/dist/client/react/useResponseForm.d.ts +72 -43
  140. package/dist/client/react/useResponseForm.d.ts.map +1 -1
  141. package/dist/client/react/useResponseForm.js +20 -17
  142. package/dist/client/react/useResponseForm.js.map +1 -1
  143. package/dist/client/react/useSidebar.d.ts.map +1 -1
  144. package/dist/client/react/useSidebar.js +5 -3
  145. package/dist/client/react/useSidebar.js.map +1 -1
  146. package/dist/client/react/visualSources.d.ts.map +1 -1
  147. package/dist/client/react/visualSources.js +10 -8
  148. package/dist/client/react/visualSources.js.map +1 -1
  149. package/dist/client/saveFailure.d.ts +8 -0
  150. package/dist/client/saveFailure.d.ts.map +1 -0
  151. package/dist/client/saveFailure.js +8 -0
  152. package/dist/client/saveFailure.js.map +1 -0
  153. package/dist/diagnostics/daemonContract.d.ts +65 -38
  154. package/dist/diagnostics/daemonContract.d.ts.map +1 -1
  155. package/dist/diagnostics/daemonContract.js +2 -1
  156. package/dist/diagnostics/daemonContract.js.map +1 -1
  157. package/dist/diagnostics/health.d.ts +10 -0
  158. package/dist/diagnostics/health.d.ts.map +1 -1
  159. package/dist/diagnostics/health.js +2 -0
  160. package/dist/diagnostics/health.js.map +1 -1
  161. package/dist/diagnostics/source.d.ts +55 -36
  162. package/dist/diagnostics/source.d.ts.map +1 -1
  163. package/dist/diagnostics/source.js +1 -2
  164. package/dist/diagnostics/source.js.map +1 -1
  165. package/dist/render/{failed.d.ts → Failed.d.ts} +1 -1
  166. package/dist/render/Failed.d.ts.map +1 -0
  167. package/dist/render/{failed.js → Failed.js} +1 -1
  168. package/dist/render/Failed.js.map +1 -0
  169. package/dist/render/code.d.ts +2 -2
  170. package/dist/render/code.d.ts.map +1 -1
  171. package/dist/render/code.js +15 -13
  172. package/dist/render/code.js.map +1 -1
  173. package/dist/render/footnoteIds.d.ts +3 -0
  174. package/dist/render/footnoteIds.d.ts.map +1 -0
  175. package/dist/render/{footnotes.js → footnoteIds.js} +11 -9
  176. package/dist/render/footnoteIds.js.map +1 -0
  177. package/dist/rpc/contract.d.ts +2346 -2322
  178. package/dist/rpc/contract.d.ts.map +1 -1
  179. package/dist/rpc/handoffContract.d.ts +33 -33
  180. package/dist/rpc/responseContract.d.ts +56 -56
  181. package/dist/server/board.d.ts +1 -1
  182. package/dist/server/diagnostics/daemon.d.ts +41 -25
  183. package/dist/server/diagnostics/daemon.d.ts.map +1 -1
  184. package/dist/server/diagnostics/fleetHealth.d.ts +10 -0
  185. package/dist/server/diagnostics/fleetHealth.d.ts.map +1 -1
  186. package/dist/server/diagnostics/githubLimits.d.ts +12 -13
  187. package/dist/server/diagnostics/githubLimits.d.ts.map +1 -1
  188. package/dist/server/diagnostics/githubLimits.js +6 -6
  189. package/dist/server/diagnostics/githubLimits.js.map +1 -1
  190. package/dist/server/integration/diagnostics.d.ts +74 -38
  191. package/dist/server/integration/diagnostics.d.ts.map +1 -1
  192. package/dist/server/mcp/client.d.ts +1155 -1147
  193. package/dist/server/mcp/client.d.ts.map +1 -1
  194. package/dist/server/mcp/queries.d.ts +60 -36
  195. package/dist/server/mcp/queries.d.ts.map +1 -1
  196. package/dist/server/metadata/fields.js +1 -1
  197. package/dist/server/metadata/fields.js.map +1 -1
  198. package/dist/server/packageInfo.d.ts +1 -1
  199. package/dist/server/packageInfo.js +1 -1
  200. package/dist/server/render/Highlighter.d.ts +1 -1
  201. package/dist/server/render/Highlighter.js +1 -1
  202. package/dist/server/render/markdown.d.ts +1 -1
  203. package/dist/server/render/markdown.js +2 -2
  204. package/dist/server/render/markdown.js.map +1 -1
  205. package/dist/server/serve.d.ts +1 -1
  206. package/dist/server/views/remainingQueries.d.ts +20 -12
  207. package/dist/server/views/remainingQueries.d.ts.map +1 -1
  208. package/package.json +9 -39
  209. package/src/browser/diagnostics/schema.ts +12 -2
  210. package/src/client/client.ts +2 -1
  211. package/src/client/document/Mermaid.ts +4 -10
  212. package/src/client/document/View.ts +1 -1
  213. package/src/client/formStatus.ts +35 -0
  214. package/src/client/react/BoardShell.tsx +49 -43
  215. package/src/client/react/DocumentContext.tsx +12 -7
  216. package/src/client/react/FormContext.ts +13 -0
  217. package/src/client/react/LiveControls.tsx +5 -38
  218. package/src/client/react/MainLocationContext.ts +8 -0
  219. package/src/client/react/PhoneNavigation.tsx +20 -15
  220. package/src/client/react/ProjectionFrame.tsx +13 -5
  221. package/src/client/react/ReadingLibrary.tsx +9 -14
  222. package/src/client/react/ReadingReadyContext.ts +11 -0
  223. package/src/client/react/SessionContext.ts +4 -0
  224. package/src/client/react/SystemMain.tsx +3 -1
  225. package/src/client/react/SystemPage.tsx +2 -1
  226. package/src/client/react/ThemeContext.ts +3 -0
  227. package/src/client/react/TodayOptions.tsx +40 -33
  228. package/src/client/react/VisualViewer.tsx +6 -6
  229. package/src/client/react/boardRouter.ts +3 -2
  230. package/src/client/react/createResponse.ts +10 -35
  231. package/src/client/react/diagnosticsValues.tsx +20 -12
  232. package/src/client/react/entry.tsx +2 -37
  233. package/src/client/react/githubLimited.ts +5 -0
  234. package/src/client/react/mainLocation.ts +5 -8
  235. package/src/client/react/mountBoard.tsx +49 -0
  236. package/src/client/react/navigationLocation.ts +14 -2
  237. package/src/client/react/retryPage.ts +4 -4
  238. package/src/client/react/scheduleReading.ts +1 -10
  239. package/src/client/react/useAppearance.ts +2 -3
  240. package/src/client/react/useBoardNavigation.ts +30 -23
  241. package/src/client/react/useDiagnosticsPolling.ts +14 -7
  242. package/src/client/react/useDocumentPage.ts +12 -6
  243. package/src/client/react/useHandoffForm.ts +25 -33
  244. package/src/client/react/useResponseForm.ts +20 -18
  245. package/src/client/react/useSidebar.ts +5 -3
  246. package/src/client/react/visualSources.ts +10 -8
  247. package/src/client/saveFailure.ts +14 -0
  248. package/src/diagnostics/daemonContract.ts +2 -1
  249. package/src/diagnostics/health.ts +2 -0
  250. package/src/diagnostics/source.ts +1 -2
  251. package/src/render/code.ts +3 -4
  252. package/src/render/{footnotes.ts → footnoteIds.ts} +12 -10
  253. package/src/server/board.ts +1 -1
  254. package/src/server/diagnostics/githubLimits.ts +9 -11
  255. package/src/server/metadata/fields.ts +1 -1
  256. package/src/server/packageInfo.ts +1 -1
  257. package/src/server/render/Highlighter.ts +1 -1
  258. package/src/server/render/markdown.ts +2 -2
  259. package/dist/client/handoffs/failureStatus.d.ts +0 -2
  260. package/dist/client/handoffs/failureStatus.d.ts.map +0 -1
  261. package/dist/client/handoffs/failureStatus.js +0 -7
  262. package/dist/client/handoffs/failureStatus.js.map +0 -1
  263. package/dist/client/responses/failure.d.ts +0 -5
  264. package/dist/client/responses/failure.d.ts.map +0 -1
  265. package/dist/client/responses/failure.js +0 -13
  266. package/dist/client/responses/failure.js.map +0 -1
  267. package/dist/render/failed.d.ts.map +0 -1
  268. package/dist/render/failed.js.map +0 -1
  269. package/dist/render/footnotes.d.ts +0 -3
  270. package/dist/render/footnotes.d.ts.map +0 -1
  271. package/dist/render/footnotes.js.map +0 -1
  272. package/dist/server/diagnostics/readCommand.d.ts +0 -4
  273. package/dist/server/diagnostics/readCommand.d.ts.map +0 -1
  274. package/dist/server/diagnostics/readCommand.js +0 -20
  275. package/dist/server/diagnostics/readCommand.js.map +0 -1
  276. package/src/client/handoffs/failureStatus.ts +0 -6
  277. package/src/client/responses/failure.ts +0 -13
  278. package/src/server/diagnostics/readCommand.ts +0 -24
  279. /package/src/render/{failed.ts → Failed.ts} +0 -0
@@ -1,11 +1,14 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
  import { KeyboardHints } from "./KeyboardHints.tsx";
3
+ import { useMainLocation } from "./MainLocationContext.ts";
3
4
  import { shellIds } from "./shellIds.ts";
4
5
  import { VisualControls } from "./VisualControls.tsx";
5
6
  import { graphicWidth, visualFilename } from "./visualDimensions.ts";
6
7
  import type { VisualSelection } from "./visualSources.ts";
7
8
 
8
9
  export function VisualViewer({ selection, close }: { readonly selection: VisualSelection; readonly close: () => void }) {
10
+ const page = useMainLocation();
11
+ const openedPageRef = useRef(page);
9
12
  const [selected, setSelected] = useState(selection.selected);
10
13
  const [scale, setScale] = useState(100);
11
14
  const [baseWidth, setBaseWidth] = useState(0);
@@ -27,13 +30,11 @@ export function VisualViewer({ selection, close }: { readonly selection: VisualS
27
30
  return () => URL.revokeObjectURL(url);
28
31
  }, [selection.drawing]);
29
32
  useEffect(() => {
30
- function changed() {
33
+ if (page !== openedPageRef.current) {
31
34
  setStale(true);
32
35
  setStatus("Source updated. This preview is the drawing/image opened earlier; close and reopen to inspect or save the latest source.");
33
36
  }
34
- document.addEventListener("board-page", changed);
35
- return () => document.removeEventListener("board-page", changed);
36
- }, []);
37
+ }, [page]);
37
38
  function handleFitVisual() {
38
39
  setBaseWidth(0);
39
40
  setScale(100);
@@ -114,8 +115,7 @@ export function VisualViewer({ selection, close }: { readonly selection: VisualS
114
115
  </button>
115
116
  </div>
116
117
  <p id={shellIds.visualContext}>
117
- {image ? `${image.alt || "Local image"} · ${selected + 1} of ${selection.images.length} · ${image.local}` : "Mermaid diagram"} ·{" "}
118
- {document.getElementById("doc")?.dataset.file}
118
+ {image ? `${image.alt || "Local image"} · ${selected + 1} of ${selection.images.length} · ${image.local}` : "Mermaid diagram"} · {page.file}
119
119
  {image ? "" : " · Current drawing"}
120
120
  </p>
121
121
  <VisualControls
@@ -1,8 +1,8 @@
1
- import { createRootRouteWithContext, createRoute, createRouter } from "@tanstack/react-router";
1
+ import { createRootRouteWithContext, createRoute, createRouter, type RouterHistory } from "@tanstack/react-router";
2
2
  import { createElement } from "react";
3
3
  import { BoardShell } from "./BoardShell.tsx";
4
4
  import { loadBoardPage, type RouterShell } from "./loadBoardPage.ts";
5
- export function boardRouter(options?: RouterShell) {
5
+ export function boardRouter(options?: RouterShell, history?: RouterHistory) {
6
6
  const root = createRootRouteWithContext<{ readonly shell?: RouterShell }>()({
7
7
  component: () => {
8
8
  const { shell } = root.useRouteContext();
@@ -24,6 +24,7 @@ export function boardRouter(options?: RouterShell) {
24
24
  path: "$",
25
25
  });
26
26
  return createRouter({
27
+ ...(history ? { history } : {}),
27
28
  context: options ? { shell: options } : {},
28
29
  defaultPendingMs: Number.POSITIVE_INFINITY,
29
30
  defaultPreloadStaleTime: 0,
@@ -1,40 +1,15 @@
1
- import { Effect, Equal } from "effect";
1
+ import { Effect } from "effect";
2
2
  import type { QuestionPreview } from "#browser/responses/schema.ts";
3
- import { session } from "#client/entry.ts";
4
- import { responseFailure } from "#client/responses/failure.ts";
5
- import type { ResponseInput, ResponseSubmission } from "#client/responses/formSchema.ts";
3
+ import type { ResponseSubmission } from "#client/responses/formSchema.ts";
4
+ import type { FormClient } from "./FormContext.ts";
6
5
 
7
- export function createResponse(
6
+ export const createResponse = Effect.fn("Board.createResponse")(function* (
7
+ client: FormClient,
8
8
  source: QuestionPreview,
9
9
  submission: ResponseSubmission,
10
- current: () => ResponseInput | undefined,
11
- setOutcome: (outcome: string) => void,
12
- setStatus: (status: string) => void,
13
10
  ) {
14
- return Effect.tryPromise({
15
- catch: (error) => error,
16
- try: async () => {
17
- const native = session(window);
18
- setOutcome("pending");
19
- setStatus("Pending: registering reviewed context, then recording response…");
20
- try {
21
- const question = await native.mutate(
22
- native.responses.registerQuestion.run({ item: source.item, request: source.request, revision: submission.revision }),
23
- );
24
- const { answerContext: _answerContext, recovery: _recovery, ...input } = submission;
25
- await native.mutate(native.responses.recordResponse.run({ ...input, question: question.id }));
26
- const newer = !Equal.equals(current(), submission);
27
- setOutcome("saved");
28
- setStatus(
29
- `Saved: responses/${submission.id}.md. This records feedback, not acceptance.${newer ? " Your current draft remains unsaved." : ""}`,
30
- );
31
- return submission;
32
- } catch (error) {
33
- const failure = responseFailure(error);
34
- setOutcome(failure.outcome);
35
- setStatus(failure.text);
36
- throw error;
37
- }
38
- },
39
- });
40
- }
11
+ const question = yield* client.responses.registerQuestion.run({ item: source.item, request: source.request, revision: submission.revision });
12
+ const { answerContext: _answerContext, recovery: _recovery, ...input } = submission;
13
+ yield* client.responses.recordResponse.run({ ...input, question: question.id });
14
+ return submission;
15
+ });
@@ -1,4 +1,4 @@
1
- import type { CodexLimits, GithubLimits, UsageWindow } from "#browser/diagnostics/schema.ts";
1
+ import type { CodexLimits, GithubLimits, GithubWindow, UsageWindow } from "#browser/diagnostics/schema.ts";
2
2
  import { resetLabel } from "#diagnostics/resetLabel.ts";
3
3
  export function ObservedTime({ at }: { readonly at: number | null }) {
4
4
  return at === null ? (
@@ -71,33 +71,41 @@ export function DiagnosticsWindows({ value, now }: { readonly value: CodexLimits
71
71
  </ul>
72
72
  );
73
73
  }
74
+ const GITHUB_RESOURCES: Readonly<Record<string, string>> = { core: "REST", graphql: "GraphQL", search: "Search" };
75
+ const GITHUB_PAUSES = { "rate-limit": "Rate-limited", reserve: "GitHub paused below the 20% reserve" } as const;
74
76
  export function DiagnosticsGithub({ value, now }: { readonly value: GithubLimits; readonly now: number }) {
75
- const windows = (
76
- [
77
- ["REST", value.core],
78
- ["GraphQL", value.graphql],
79
- ] as const
80
- ).map(([name, window]) => <GithubWindow key={name} name={name} now={now} window={window} />);
81
77
  return (
82
78
  <>
83
- <dl className="grid gap-4 text-sm [&_dt]:text-muted-foreground">{windows}</dl>
84
- <p className="text-muted-foreground text-xs">Cached for one minute across all tabs.</p>
79
+ <dl className="grid gap-4 text-sm [&_dt]:text-muted-foreground">
80
+ {value.map((window) => (
81
+ <GithubResource key={window.resource} now={now} window={window} />
82
+ ))}
83
+ </dl>
84
+ <p className="text-muted-foreground text-xs">Recorded from the rate-limit headers of Fleet's own GitHub responses.</p>
85
85
  </>
86
86
  );
87
87
  }
88
- function GithubWindow({ name, window, now }: { readonly name: string; readonly window: GithubLimits["core"]; readonly now: number }) {
88
+ function GithubResource({ window, now }: { readonly window: GithubWindow; readonly now: number }) {
89
89
  return (
90
- <div>
91
- <dt>{name}</dt>
90
+ <div data-resource={window.resource}>
91
+ <dt>{GITHUB_RESOURCES[window.resource] ?? window.resource}</dt>
92
92
  <dd>
93
93
  <strong>
94
94
  {window.remaining} / {window.limit}
95
95
  </strong>{" "}
96
96
  remaining
97
97
  </dd>
98
+ {window.limitedUntil !== null && window.limitedUntil > now ? (
99
+ <dd>
100
+ <strong>{GITHUB_PAUSES[window.limitedBy ?? "rate-limit"]}</strong> until <Reset at={window.limitedUntil} now={now} />
101
+ </dd>
102
+ ) : null}
98
103
  <dd>
99
104
  Resets <Reset at={window.reset * 1000} now={now} />
100
105
  </dd>
106
+ <dd>
107
+ Observed <time dateTime={new Date(window.observedAt).toISOString()}>{resetLabel(window.observedAt, now)}</time>
108
+ </dd>
101
109
  </div>
102
110
  );
103
111
  }
@@ -1,43 +1,8 @@
1
- import { RegistryContext } from "@effect/atom-react";
2
- import { RouterProvider } from "@tanstack/react-router";
3
- import { createRoot } from "react-dom/client";
4
1
  import { session } from "#client/entry.ts";
5
- import { boardRouter } from "./boardRouter.ts";
2
+ import { mountBoard } from "./mountBoard.tsx";
6
3
  import "#client/react/entry.css";
7
4
 
8
5
  const container = document.querySelector("[data-board-react-root]");
9
6
  if (container) {
10
- const root = createRoot(container);
11
- let native = session(window);
12
- let started = false;
13
- function ready() {
14
- if (started) {
15
- return;
16
- }
17
- started = true;
18
- document.documentElement.dataset.boardReady = "true";
19
- document.documentElement.dataset.js = "true";
20
- }
21
- const reading = history.state?.workBoard;
22
- const router = boardRouter({ api: native.api, client: native, reading, ready, registry: native.registry });
23
- if (reading) {
24
- history.replaceState({ ...history.state, workBoard: reading }, "");
25
- }
26
- function render() {
27
- root.render(
28
- <RegistryContext.Provider value={native.registry}>
29
- <RouterProvider router={router} />
30
- </RegistryContext.Provider>,
31
- );
32
- }
33
- render();
34
- window.addEventListener("pageshow", (event) => {
35
- if (!event.persisted) {
36
- return;
37
- }
38
- native = session(window);
39
- router.update({ context: { shell: { api: native.api, client: native, reading, ready, registry: native.registry } } });
40
- void router.invalidate();
41
- render();
42
- });
7
+ mountBoard(container, () => session(window));
43
8
  }
@@ -0,0 +1,5 @@
1
+ import type { GithubWindow } from "#browser/diagnostics/schema.ts";
2
+
3
+ export function githubLimited(window: GithubWindow, now: number) {
4
+ return window.remaining === 0 || (window.limitedUntil !== null && window.limitedUntil > now);
5
+ }
@@ -1,13 +1,10 @@
1
1
  export interface MainLocation {
2
2
  readonly file: string;
3
+ readonly projection: string;
4
+ readonly route: string;
5
+ readonly status: number;
3
6
  readonly title?: string | undefined;
4
7
  readonly view: string;
5
8
  }
6
- export function displayedLocation(main: HTMLElement | null): MainLocation {
7
- const file = main?.dataset.file ?? "";
8
- const title = main?.querySelector("h1")?.textContent?.trim();
9
- return { file, title, view: main?.dataset.view ?? "" };
10
- }
11
- export function sameMainLocation(previous: MainLocation, next: MainLocation) {
12
- return previous.file === next.file && previous.view === next.view && previous.title === next.title;
13
- }
9
+
10
+ export const emptyMainLocation: MainLocation = { file: "", projection: "document", route: "", status: 200, view: "" };
@@ -0,0 +1,49 @@
1
+ import { RegistryContext } from "@effect/atom-react";
2
+ import { RouterProvider } from "@tanstack/react-router";
3
+ import { createRoot } from "react-dom/client";
4
+ import type { BrowserClient } from "#client/client.ts";
5
+ import { boardRouter } from "./boardRouter.ts";
6
+
7
+ export function mountBoard(container: Element, readSession: () => BrowserClient) {
8
+ const root = createRoot(container);
9
+ let native = readSession();
10
+ let generation = 0;
11
+ let started = false;
12
+ function ready() {
13
+ if (started) {
14
+ return;
15
+ }
16
+ started = true;
17
+ document.documentElement.dataset.boardReady = "true";
18
+ document.documentElement.dataset.js = "true";
19
+ }
20
+ const reading = history.state?.workBoard;
21
+ let router = boardRouter({ api: native.api, client: native, reading, ready, registry: native.registry });
22
+ const navigation = router.history;
23
+ if (reading) {
24
+ history.replaceState({ ...history.state, workBoard: reading }, "");
25
+ }
26
+ function render() {
27
+ root.render(
28
+ <RegistryContext.Provider key={generation} value={native.registry}>
29
+ <RouterProvider router={router} />
30
+ </RegistryContext.Provider>,
31
+ );
32
+ }
33
+ render();
34
+ function restore(event: PageTransitionEvent) {
35
+ if (!event.persisted) {
36
+ return;
37
+ }
38
+ native = readSession();
39
+ generation += 1;
40
+ router = boardRouter({ api: native.api, client: native, reading, ready, registry: native.registry }, navigation);
41
+ render();
42
+ }
43
+ window.addEventListener("pageshow", restore);
44
+ return () => {
45
+ window.removeEventListener("pageshow", restore);
46
+ root.unmount();
47
+ navigation.destroy();
48
+ };
49
+ }
@@ -1,7 +1,8 @@
1
+ import type { AnyRouter } from "@tanstack/react-router";
1
2
  import { readingEntry } from "#client/react/storedReadings.ts";
2
3
 
3
- export function navigationFailure(status: string | undefined) {
4
- const code = Number(status ?? 200);
4
+ export function navigationFailure(status: number) {
5
+ const code = status;
5
6
  if (code < 400) {
6
7
  return "";
7
8
  }
@@ -25,3 +26,14 @@ export function sameReadingLocation(
25
26
  ) {
26
27
  return previous.route === next.route && previous.id === next.id && previous.hash === next.hash;
27
28
  }
29
+
30
+ export function recordEntry(router: AnyRouter, route: string, id: string, native: boolean) {
31
+ if (!native) {
32
+ return;
33
+ }
34
+ router.history.flush?.();
35
+ if (window.location.pathname + window.location.search !== route || window.history.state?.workBoard?.id === id) {
36
+ return;
37
+ }
38
+ window.history.replaceState({ ...window.history.state, workBoard: { id } }, "");
39
+ }
@@ -1,11 +1,11 @@
1
1
  import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
2
+ import type { MainLocation } from "./mainLocation.ts";
2
3
  import { pageQueries } from "./pageQueries.ts";
3
4
  import type { ShellApi } from "./ShellApi.ts";
4
5
 
5
- export function retryPage(api: ShellApi, registry: AtomRegistry.AtomRegistry, url: string) {
6
- const doc = document.getElementById("doc");
7
- const queries = pageQueries(api, doc?.dataset.projection ?? "document", url);
8
- if (Number(doc?.dataset.status ?? 200) < 400 && !queries.some((query) => registry.get(query)._tag === "Failure")) {
6
+ export function retryPage(api: ShellApi, registry: AtomRegistry.AtomRegistry, page: MainLocation) {
7
+ const queries = pageQueries(api, page.projection, page.route);
8
+ if (page.status < 400 && !queries.some((query) => registry.get(query)._tag === "Failure")) {
9
9
  return false;
10
10
  }
11
11
  for (const query of queries) {
@@ -9,18 +9,10 @@ export function scheduleReading(
9
9
  setStatus: (status: string) => void,
10
10
  focus: boolean,
11
11
  ) {
12
- let restored = false;
13
12
  function restore() {
14
- if (restored || !active()) {
15
- document.removeEventListener("board-reading-ready", restore);
13
+ if (!active()) {
16
14
  return;
17
15
  }
18
- const context = doc.querySelector<HTMLElement>(".item-context");
19
- if (context && context.dataset.readingReady !== "true") {
20
- return;
21
- }
22
- restored = true;
23
- document.removeEventListener("board-reading-ready", restore);
24
16
  if (focus && !document.activeElement?.closest("[data-response-form], #response-form, #handoff-form")) {
25
17
  doc.focus({ preventScroll: true });
26
18
  }
@@ -41,6 +33,5 @@ export function scheduleReading(
41
33
  }
42
34
  });
43
35
  }
44
- document.addEventListener("board-reading-ready", restore);
45
36
  requestAnimationFrame(restore);
46
37
  }
@@ -37,7 +37,6 @@ export function useAppearance() {
37
37
  if (doc) {
38
38
  doc.dataset.scheme = scheme;
39
39
  }
40
- document.dispatchEvent(new Event("board-theme"));
41
40
  }, [value.theme, value.density, scheme]);
42
41
  function change(next: Partial<Pick<typeof value, "closed" | "density" | "theme">>) {
43
42
  const appearance = { closed: valueRef.current.closed, density: valueRef.current.density, theme: valueRef.current.theme, ...next };
@@ -50,6 +49,6 @@ export function useAppearance() {
50
49
  valueRef.current = { ...appearance, notice };
51
50
  setValue(valueRef.current);
52
51
  }
53
- return { ...value, change };
52
+ return { ...value, change, scheme };
54
53
  }
55
- export type AppearanceControls = ReturnType<typeof useAppearance>;
54
+ export type AppearanceControls = Omit<ReturnType<typeof useAppearance>, "scheme">;
@@ -1,15 +1,27 @@
1
1
  import { useRouter } from "@tanstack/react-router";
2
- import { useEffect, useState } from "react";
2
+ import { useContext, useEffect, useRef, useState } from "react";
3
3
  import type { RouterShell } from "#client/react/loadBoardPage.ts";
4
- import { currentEntry, navigationFailure, readingKey, sameReadingLocation } from "#client/react/navigationLocation.ts";
4
+ import { useMainLocation } from "#client/react/MainLocationContext.ts";
5
+ import { currentEntry, navigationFailure, readingKey, recordEntry, sameReadingLocation } from "#client/react/navigationLocation.ts";
6
+ import { ReadingReadyContext, readingReady } from "#client/react/ReadingReadyContext.ts";
5
7
  import { readerLink } from "#client/react/readerLink.ts";
6
8
  import { captureReading } from "#client/react/readingState.ts";
7
9
  import { retryPage } from "#client/react/retryPage.ts";
8
10
  import { scheduleReading } from "#client/react/scheduleReading.ts";
9
11
  import { readingEntry, storedReadings } from "#client/react/storedReadings.ts";
10
12
 
11
- export function useBoardNavigation(shell: RouterShell) {
13
+ export function useBoardNavigation(shell: RouterShell, onCurrentDestination?: () => void) {
12
14
  const router = useRouter();
15
+ const main = useMainLocation();
16
+ const { element } = useContext(ReadingReadyContext);
17
+ const readyRef = useRef(element);
18
+ const mainRef = useRef(main);
19
+ const pageRef = useRef<(() => void) | undefined>(undefined);
20
+ useEffect(() => {
21
+ mainRef.current = main;
22
+ readyRef.current = element;
23
+ pageRef.current?.();
24
+ }, [main, element]);
13
25
  const [status, setStatus] = useState("");
14
26
  useEffect(() => {
15
27
  const readings = storedReadings();
@@ -21,7 +33,7 @@ export function useBoardNavigation(shell: RouterShell) {
21
33
  let failure = "";
22
34
  let interrupted = false;
23
35
  function save() {
24
- if (!displayed || document.getElementById("doc")?.dataset.route !== route) {
36
+ if (!displayed || mainRef.current.route !== route || router.state.location.pathname + router.state.location.searchStr !== route) {
25
37
  interrupted ||= displayed !== "";
26
38
  return;
27
39
  }
@@ -34,11 +46,11 @@ export function useBoardNavigation(shell: RouterShell) {
34
46
  const location = router.state.resolvedLocation ?? router.state.location;
35
47
  const nextRoute = location.pathname + location.searchStr;
36
48
  const doc = document.getElementById("doc");
37
- if (doc?.dataset.route !== nextRoute) {
49
+ if (!doc || mainRef.current.route !== nextRoute || !readingReady(doc, readyRef.current)) {
38
50
  return;
39
51
  }
40
52
  const previousFailure = failure;
41
- failure = navigationFailure(doc.dataset.status);
53
+ failure = navigationFailure(mainRef.current.status);
42
54
  if (failure) {
43
55
  setStatus(failure);
44
56
  return;
@@ -57,13 +69,19 @@ export function useBoardNavigation(shell: RouterShell) {
57
69
  route = nextRoute;
58
70
  hash = location.hash;
59
71
  recordEntry(router, nextRoute, id, shell.client !== undefined);
60
- scheduleReading(doc, reading, hash, () => displayed === id && doc.dataset.route === nextRoute, setStatus, focus);
72
+ scheduleReading(doc, reading, hash, () => displayed === id && mainRef.current.route === nextRoute && doc.isConnected, setStatus, focus);
61
73
  }
62
74
  async function open(target: URL) {
63
75
  save();
64
- document.dispatchEvent(new Event("board-navigate"));
65
76
  const url = target.pathname + target.search + target.hash;
66
- if (url === router.state.location.href && retryPage(shell.api, shell.registry, target.pathname + target.search)) {
77
+ if (url === router.state.location.href) {
78
+ onCurrentDestination?.();
79
+ }
80
+ if (
81
+ url === router.state.location.href
82
+ && mainRef.current.route === target.pathname + target.search
83
+ && retryPage(shell.api, shell.registry, mainRef.current)
84
+ ) {
67
85
  await router.invalidate();
68
86
  } else if (url !== router.state.location.href) {
69
87
  await router.navigate({ to: url });
@@ -107,7 +125,7 @@ export function useBoardNavigation(shell: RouterShell) {
107
125
  const resolved = router.subscribe("onResolved", page);
108
126
  window.history.scrollRestoration = "manual";
109
127
  document.addEventListener("click", click);
110
- document.addEventListener("board-page", page);
128
+ pageRef.current = page;
111
129
  window.addEventListener("pagehide", save);
112
130
  page();
113
131
  return () => {
@@ -115,20 +133,9 @@ export function useBoardNavigation(shell: RouterShell) {
115
133
  resolved();
116
134
  document.removeEventListener("submit", submit);
117
135
  document.removeEventListener("click", click);
118
- document.removeEventListener("board-page", page);
136
+ pageRef.current = undefined;
119
137
  window.removeEventListener("pagehide", save);
120
138
  };
121
- }, [router, shell.api, shell.registry, shell.reading, shell.client]);
139
+ }, [router, shell.api, shell.registry, shell.reading, shell.client, onCurrentDestination]);
122
140
  return status;
123
141
  }
124
-
125
- function recordEntry(router: ReturnType<typeof useRouter>, route: string, id: string, native: boolean) {
126
- if (!native) {
127
- return;
128
- }
129
- router.history.flush?.();
130
- if (window.location.pathname + window.location.search !== route || window.history.state?.workBoard?.id === id) {
131
- return;
132
- }
133
- window.history.replaceState({ ...window.history.state, workBoard: { id } }, "");
134
- }
@@ -1,14 +1,17 @@
1
1
  import { RegistryContext } from "@effect/atom-react";
2
2
  import type * as Atom from "effect/reactivity/Atom";
3
3
  import { useContext, useEffect } from "react";
4
- import { session } from "#client/entry.ts";
4
+ import type { BrowserClient } from "#client/client.ts";
5
5
 
6
- export function useDiagnosticsPolling<A, TFleet>(atom: Atom.Atom<A>, fleet?: Atom.Atom<TFleet>) {
6
+ export function useDiagnosticsPolling<A, TFleet>(
7
+ client: (Pick<BrowserClient, "registry"> & { readonly updates: Pick<BrowserClient["updates"], "paused"> }) | undefined,
8
+ atom: Atom.Atom<A>,
9
+ fleet?: Atom.Atom<TFleet>,
10
+ ) {
7
11
  const registry = useContext(RegistryContext);
8
12
  useEffect(() => {
9
13
  function refresh() {
10
- const native = session(window);
11
- if (!(document.hidden || native.registry.get(native.updates.paused))) {
14
+ if (!(document.hidden || client?.registry.get(client.updates.paused))) {
12
15
  registry.refresh(atom);
13
16
  if (fleet) {
14
17
  registry.refresh(fleet);
@@ -16,12 +19,16 @@ export function useDiagnosticsPolling<A, TFleet>(atom: Atom.Atom<A>, fleet?: Ato
16
19
  }
17
20
  }
18
21
  const timer = setInterval(refresh, 15_000);
19
- document.addEventListener("board-updates-resumed", refresh);
22
+ const resumed = client?.registry.subscribe(client.updates.paused, (paused) => {
23
+ if (!paused) {
24
+ refresh();
25
+ }
26
+ });
20
27
  document.addEventListener("visibilitychange", refresh);
21
28
  return () => {
22
29
  clearInterval(timer);
23
- document.removeEventListener("board-updates-resumed", refresh);
30
+ resumed?.();
24
31
  document.removeEventListener("visibilitychange", refresh);
25
32
  };
26
- }, [atom, fleet, registry]);
33
+ }, [atom, fleet, registry, client]);
27
34
  }
@@ -3,7 +3,7 @@ import { flushSync } from "react-dom";
3
3
  import type { Document } from "#browser/document/schema.ts";
4
4
  import { documentSourceId } from "#client/document/layout.ts";
5
5
  import { fileUrl } from "#files/url.ts";
6
- import { displayedLocation, type MainLocation } from "./mainLocation.ts";
6
+ import type { MainLocation } from "./mainLocation.ts";
7
7
  export function useDocumentPage({
8
8
  data,
9
9
  pending,
@@ -25,9 +25,7 @@ export function useDocumentPage({
25
25
  }
26
26
  const main = element;
27
27
  main.dataset.scheme = document.documentElement.dataset.scheme ?? "";
28
- if (main.dataset.route !== url) {
29
- delete main.dataset.route;
30
- }
28
+ delete main.dataset.route;
31
29
  let active = true;
32
30
  let scheduled = false;
33
31
  function announce() {
@@ -72,10 +70,18 @@ function publishPage(main: HTMLElement, data: Document | undefined, status: stri
72
70
  } else {
73
71
  delete main.dataset.identity;
74
72
  }
75
- flushSync(() => onPage(displayedLocation(main)));
76
73
  main.dataset.route = url;
77
74
  main.dataset.status = status;
78
- document.dispatchEvent(new Event("board-page"));
75
+ flushSync(() =>
76
+ onPage({
77
+ file: data?.file ?? "",
78
+ projection: "document",
79
+ route: url,
80
+ status: Number(status),
81
+ title: main.querySelector("h1")?.textContent?.trim(),
82
+ view: "",
83
+ }),
84
+ );
79
85
  }
80
86
  function affectsDocument(record: MutationRecord) {
81
87
  const element = record.target instanceof Element ? record.target : record.target.parentElement;