@shivaedev/work-board 0.17.1 → 0.18.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 (193) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +23 -21
  3. package/assets/react.css +1 -1
  4. package/assets/react.js +37 -37
  5. package/dist/browser/responses/cardContext.d.ts +1 -1
  6. package/dist/browser/responses/cardContext.d.ts.map +1 -1
  7. package/dist/browser/responses/cardContext.js +1 -4
  8. package/dist/browser/responses/cardContext.js.map +1 -1
  9. package/dist/browser/views/now.d.ts +5 -0
  10. package/dist/browser/views/now.d.ts.map +1 -1
  11. package/dist/browser/views/now.js +38 -2
  12. package/dist/browser/views/now.js.map +1 -1
  13. package/dist/client/document/View.js +1 -1
  14. package/dist/client/document/View.js.map +1 -1
  15. package/dist/client/formStatus.d.ts +1 -1
  16. package/dist/client/formStatus.d.ts.map +1 -1
  17. package/dist/client/react/BoardShell.js +1 -1
  18. package/dist/client/react/BoardShell.js.map +1 -1
  19. package/dist/client/react/DocumentMain.js +1 -1
  20. package/dist/client/react/DocumentMain.js.map +1 -1
  21. package/dist/client/react/FleetMain.js +1 -1
  22. package/dist/client/react/FleetMain.js.map +1 -1
  23. package/dist/client/react/FormField.js +1 -1
  24. package/dist/client/react/FormField.js.map +1 -1
  25. package/dist/client/react/GoalPage.js +1 -1
  26. package/dist/client/react/GoalPage.js.map +1 -1
  27. package/dist/client/react/GoalProgressHeader.js +1 -1
  28. package/dist/client/react/GoalProgressHeader.js.map +1 -1
  29. package/dist/client/react/Header.d.ts.map +1 -1
  30. package/dist/client/react/Header.js +3 -2
  31. package/dist/client/react/Header.js.map +1 -1
  32. package/dist/client/react/LibraryContents.js +1 -1
  33. package/dist/client/react/LibraryContents.js.map +1 -1
  34. package/dist/client/react/LibraryPage.js +1 -1
  35. package/dist/client/react/LibraryPage.js.map +1 -1
  36. package/dist/client/react/LiveControls.js +1 -1
  37. package/dist/client/react/LiveControls.js.map +1 -1
  38. package/dist/client/react/PromptOption.js +1 -1
  39. package/dist/client/react/PromptOption.js.map +1 -1
  40. package/dist/client/react/ReadingHistory.js +1 -1
  41. package/dist/client/react/ReadingHistory.js.map +1 -1
  42. package/dist/client/react/SearchPalette.js +1 -1
  43. package/dist/client/react/SearchPalette.js.map +1 -1
  44. package/dist/client/react/Sidebar.d.ts.map +1 -1
  45. package/dist/client/react/Sidebar.js +9 -2
  46. package/dist/client/react/Sidebar.js.map +1 -1
  47. package/dist/client/react/SystemFleetLane.d.ts.map +1 -1
  48. package/dist/client/react/SystemFleetLane.js +2 -1
  49. package/dist/client/react/SystemFleetLane.js.map +1 -1
  50. package/dist/client/react/SystemHealth.d.ts +2 -1
  51. package/dist/client/react/SystemHealth.d.ts.map +1 -1
  52. package/dist/client/react/SystemHealth.js +3 -2
  53. package/dist/client/react/SystemHealth.js.map +1 -1
  54. package/dist/client/react/SystemMain.d.ts.map +1 -1
  55. package/dist/client/react/SystemMain.js +6 -2
  56. package/dist/client/react/SystemMain.js.map +1 -1
  57. package/dist/client/react/SystemPage.d.ts +2 -1
  58. package/dist/client/react/SystemPage.d.ts.map +1 -1
  59. package/dist/client/react/SystemPage.js +2 -2
  60. package/dist/client/react/SystemPage.js.map +1 -1
  61. package/dist/client/react/TodayOptionButton.d.ts +1 -2
  62. package/dist/client/react/TodayOptionButton.d.ts.map +1 -1
  63. package/dist/client/react/TodayOptionButton.js +2 -2
  64. package/dist/client/react/TodayOptionButton.js.map +1 -1
  65. package/dist/client/react/TodayOptions.js +8 -6
  66. package/dist/client/react/TodayOptions.js.map +1 -1
  67. package/dist/client/react/TodayPage.js +14 -2
  68. package/dist/client/react/TodayPage.js.map +1 -1
  69. package/dist/client/react/WorkCard.d.ts.map +1 -1
  70. package/dist/client/react/WorkCard.js +7 -2
  71. package/dist/client/react/WorkCard.js.map +1 -1
  72. package/dist/client/react/WorkLane.d.ts.map +1 -1
  73. package/dist/client/react/WorkLane.js +1 -1
  74. package/dist/client/react/WorkLane.js.map +1 -1
  75. package/dist/client/react/WorkNow.js +1 -1
  76. package/dist/client/react/WorkNow.js.map +1 -1
  77. package/dist/client/react/destinationName.d.ts +2 -0
  78. package/dist/client/react/destinationName.d.ts.map +1 -0
  79. package/dist/client/react/destinationName.js +4 -0
  80. package/dist/client/react/destinationName.js.map +1 -0
  81. package/dist/client/react/loadBoardPage.js +1 -1
  82. package/dist/client/react/loadBoardPage.js.map +1 -1
  83. package/dist/client/react/loadProjection.d.ts +135 -135
  84. package/dist/client/react/loadProjection.js +1 -1
  85. package/dist/client/react/loadProjection.js.map +1 -1
  86. package/dist/client/react/loadRemaining.d.ts +17 -17
  87. package/dist/client/react/loadRemaining.js +1 -1
  88. package/dist/client/react/loadRemaining.js.map +1 -1
  89. package/dist/client/react/useHandoffForm.js +1 -1
  90. package/dist/client/react/useHandoffForm.js.map +1 -1
  91. package/dist/client/react/useProjectionData.d.ts +1 -1
  92. package/dist/client/react/useProjectionData.js +1 -1
  93. package/dist/client/react/useProjectionData.js.map +1 -1
  94. package/dist/client/react/useResponseForm.js +1 -1
  95. package/dist/client/react/useResponseForm.js.map +1 -1
  96. package/dist/client/responses/summary.js +1 -1
  97. package/dist/client/responses/summary.js.map +1 -1
  98. package/dist/diagnostics/source.js +1 -1
  99. package/dist/diagnostics/source.js.map +1 -1
  100. package/dist/rpc/contract.d.ts +1930 -1930
  101. package/dist/rpc/contract.d.ts.map +1 -1
  102. package/dist/server/Responses.js +2 -2
  103. package/dist/server/Responses.js.map +1 -1
  104. package/dist/server/board.js +1 -1
  105. package/dist/server/board.js.map +1 -1
  106. package/dist/server/document/answerBody.d.ts.map +1 -1
  107. package/dist/server/document/answerBody.js +9 -1
  108. package/dist/server/document/answerBody.js.map +1 -1
  109. package/dist/server/document/reviewedWork.d.ts +3 -0
  110. package/dist/server/document/reviewedWork.d.ts.map +1 -0
  111. package/dist/server/document/reviewedWork.js +23 -0
  112. package/dist/server/document/reviewedWork.js.map +1 -0
  113. package/dist/server/files/changes.d.ts.map +1 -1
  114. package/dist/server/files/changes.js +1 -1
  115. package/dist/server/files/changes.js.map +1 -1
  116. package/dist/server/files/watchDirectories.d.ts +1 -1
  117. package/dist/server/files/watchDirectories.d.ts.map +1 -1
  118. package/dist/server/files/watchDirectories.js +71 -12
  119. package/dist/server/files/watchDirectories.js.map +1 -1
  120. package/dist/server/handoffs/Service.js +2 -2
  121. package/dist/server/handoffs/Service.js.map +1 -1
  122. package/dist/server/handoffs/source.js +1 -1
  123. package/dist/server/handoffs/source.js.map +1 -1
  124. package/dist/server/http/boardRoutes.js +1 -1
  125. package/dist/server/http/boardRoutes.js.map +1 -1
  126. package/dist/server/mcp/client.d.ts +896 -896
  127. package/dist/server/mcp/client.d.ts.map +1 -1
  128. package/dist/server/mcp/queries.d.ts +2947 -2947
  129. package/dist/server/packageInfo.d.ts +1 -1
  130. package/dist/server/packageInfo.js +1 -1
  131. package/dist/server/render/Highlighter.js +1 -1
  132. package/dist/server/render/Highlighter.js.map +1 -1
  133. package/dist/server/responses/locate.js +1 -1
  134. package/dist/server/responses/locate.js.map +1 -1
  135. package/dist/server/rpc/handlers.js +2 -2
  136. package/dist/server/rpc/handlers.js.map +1 -1
  137. package/dist/server/serve.js +1 -1
  138. package/dist/server/serve.js.map +1 -1
  139. package/dist/server/workspace.js +1 -1
  140. package/dist/server/workspace.js.map +1 -1
  141. package/package.json +18 -362
  142. package/src/browser/responses/cardContext.ts +1 -4
  143. package/src/browser/views/now.ts +42 -2
  144. package/src/client/document/View.ts +1 -1
  145. package/src/client/formStatus.ts +1 -1
  146. package/src/client/react/BoardShell.tsx +1 -1
  147. package/src/client/react/DocumentMain.tsx +1 -1
  148. package/src/client/react/FleetMain.tsx +1 -1
  149. package/src/client/react/FormField.tsx +1 -1
  150. package/src/client/react/GoalPage.tsx +1 -1
  151. package/src/client/react/GoalProgressHeader.tsx +1 -1
  152. package/src/client/react/Header.tsx +3 -2
  153. package/src/client/react/LibraryContents.tsx +1 -1
  154. package/src/client/react/LibraryPage.tsx +1 -1
  155. package/src/client/react/LiveControls.tsx +1 -1
  156. package/src/client/react/PromptOption.tsx +1 -1
  157. package/src/client/react/ReadingHistory.tsx +1 -1
  158. package/src/client/react/SearchPalette.tsx +1 -1
  159. package/src/client/react/Sidebar.tsx +10 -2
  160. package/src/client/react/SystemFleetLane.tsx +2 -1
  161. package/src/client/react/SystemHealth.tsx +24 -4
  162. package/src/client/react/SystemMain.tsx +6 -2
  163. package/src/client/react/SystemPage.tsx +8 -2
  164. package/src/client/react/TodayOptionButton.tsx +1 -3
  165. package/src/client/react/TodayOptions.tsx +7 -6
  166. package/src/client/react/TodayPage.tsx +18 -4
  167. package/src/client/react/WorkCard.tsx +16 -8
  168. package/src/client/react/WorkLane.tsx +8 -2
  169. package/src/client/react/WorkNow.tsx +1 -1
  170. package/src/client/react/destinationName.ts +3 -0
  171. package/src/client/react/loadBoardPage.ts +1 -1
  172. package/src/client/react/loadProjection.ts +1 -1
  173. package/src/client/react/loadRemaining.ts +1 -1
  174. package/src/client/react/useHandoffForm.ts +1 -1
  175. package/src/client/react/useProjectionData.ts +1 -1
  176. package/src/client/react/useResponseForm.ts +1 -1
  177. package/src/client/responses/summary.ts +1 -1
  178. package/src/diagnostics/source.ts +1 -1
  179. package/src/server/Responses.ts +2 -2
  180. package/src/server/board.ts +1 -1
  181. package/src/server/document/answerBody.ts +10 -1
  182. package/src/server/document/reviewedWork.ts +24 -0
  183. package/src/server/files/changes.ts +1 -0
  184. package/src/server/files/watchDirectories.ts +90 -27
  185. package/src/server/handoffs/Service.ts +2 -2
  186. package/src/server/handoffs/source.ts +1 -1
  187. package/src/server/http/boardRoutes.ts +1 -1
  188. package/src/server/packageInfo.ts +1 -1
  189. package/src/server/render/Highlighter.ts +1 -1
  190. package/src/server/responses/locate.ts +1 -1
  191. package/src/server/rpc/handlers.ts +2 -2
  192. package/src/server/serve.ts +1 -1
  193. package/src/server/workspace.ts +1 -1
@@ -1,5 +1,5 @@
1
1
  import { Option } from "effect";
2
- import type { SaveState } from "@shivaedev/effect-react/save-state.ts";
2
+ import type { SaveState } from "@shivaedev/effect-react/client/saveState.ts";
3
3
  import { type SaveFailure, saveFailure } from "./saveFailure.ts";
4
4
 
5
5
  export type DraftInteraction = "draft" | "cleared" | "submitted";
@@ -2,7 +2,7 @@ import { useRouterState } from "@tanstack/react-router";
2
2
  import { Option } from "effect";
3
3
  import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
4
4
  import { useCallback, useContext, useEffect, useState } from "react";
5
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
5
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
6
6
  import type { BrowserClient } from "#client/client.ts";
7
7
  import { AgeTicker } from "./Age.tsx";
8
8
  import { FleetMain } from "./FleetMain.tsx";
@@ -1,6 +1,6 @@
1
1
  import { Cause, Option } from "effect";
2
2
  import { useEffect, useState } from "react";
3
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
3
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
4
4
  import type { Document } from "#browser/document/schema.ts";
5
5
  import { DocumentPage } from "./DocumentPage.tsx";
6
6
  import type { MainLocation } from "./mainLocation.ts";
@@ -1,6 +1,6 @@
1
1
  import { Option } from "effect";
2
2
  import type { ComponentProps } from "react";
3
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
3
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
4
4
  import { BoardMain } from "./BoardMain.tsx";
5
5
  import { FleetItemsContext } from "./FleetItemsContext.ts";
6
6
  export function FleetMain(props: ComponentProps<typeof BoardMain>) {
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode, useId, useLayoutEffect, useRef } from "react";
2
- import { useField } from "@shivaedev/effect-form/react.ts";
2
+ import { useField } from "@shivaedev/effect-form/client/react.ts";
3
3
  import type { Fields, Form, Name } from "@shivaedev/effect-form/shape.ts";
4
4
 
5
5
  type Control = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
@@ -1,5 +1,5 @@
1
1
  import { Option } from "effect";
2
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
2
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
3
3
  import type { Document } from "#browser/document/schema.ts";
4
4
  import { GoalCriteria } from "./GoalCriteria.tsx";
5
5
  import { GoalWorkTable } from "./GoalWorkTable.tsx";
@@ -1,6 +1,6 @@
1
1
  import { Option } from "effect";
2
2
  import { useId } from "react";
3
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
3
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
4
4
  import type { Document } from "#browser/document/schema.ts";
5
5
  import { GoalProgressSummary } from "./GoalProgressSummary.tsx";
6
6
  import { routeQueryOptions } from "./routeQuery.ts";
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
3
3
  import { Badge } from "#client/react/components/ui/Badge.tsx";
4
4
  import { Button } from "#client/react/components/ui/Button.tsx";
5
5
  import { buttonVariants } from "#client/react/components/ui/buttonVariants.ts";
6
+ import { destinationName } from "#client/react/destinationName.ts";
6
7
  import { cn } from "#client/react/lib/cn.ts";
7
8
  import { KeyboardHints } from "./KeyboardHints.tsx";
8
9
  import type { MainLocation } from "./mainLocation.ts";
@@ -12,7 +13,7 @@ import type { AppearanceControls } from "./useAppearance.ts";
12
13
  import type { SidebarControls } from "./useSidebar.ts";
13
14
  export function InboxCount({ count, id }: { readonly count: number | null; readonly id: string }) {
14
15
  return (
15
- <Badge aria-label={`${count ?? "?"} waiting`} id={id} role="status">
16
+ <Badge aria-label={count === null ? "Needs you unavailable" : `${count} need you`} id={id} role="status">
16
17
  {count ?? "?"}
17
18
  </Badge>
18
19
  );
@@ -57,7 +58,7 @@ export function Header({
57
58
  <Breadcrumbs file={main.title ?? main.file} />
58
59
  <a
59
60
  aria-current={main.view === "today" ? "page" : undefined}
60
- aria-label="Today"
61
+ aria-label={destinationName("Today", count, "need you")}
61
62
  className={cn(buttonVariants({ size: "sm", variant: "ghost" }), "hidden md:inline-flex")}
62
63
  href="/"
63
64
  id={shellIds.inboxTopbar}
@@ -1,6 +1,6 @@
1
1
  import { Option } from "effect";
2
2
  import { useState } from "react";
3
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
3
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
4
4
  import type { Library, LibraryFile } from "#browser/navigation/library.ts";
5
5
  import { Age } from "./Age.tsx";
6
6
  import type { ShellApi } from "./ShellApi.ts";
@@ -1,6 +1,6 @@
1
1
  import { Option } from "effect";
2
2
  import { useState } from "react";
3
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
3
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
4
4
  import { LibraryContents } from "./LibraryContents.tsx";
5
5
  import { routeQueryOptions } from "./routeQuery.ts";
6
6
  import type { ShellApi } from "./ShellApi.ts";
@@ -2,7 +2,7 @@ import { useAtomValue } from "@effect/atom-react";
2
2
  import { Option } from "effect";
3
3
  import * as Atom from "effect/reactivity/Atom";
4
4
  import { useMemo } from "react";
5
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
5
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
6
6
  import type { BrowserClient } from "#client/client.ts";
7
7
  import { activeRead } from "./activeRead.ts";
8
8
  import { liveStatus } from "./liveStatus.ts";
@@ -22,7 +22,7 @@ export function PromptOption({
22
22
  }) {
23
23
  const source = document.metadata.options?.[choice];
24
24
  const id = useId();
25
- const authored = optionText(option.id, option.source, document.requests.find((request) => request.open)?.preview?.reason ?? "");
25
+ const authored = optionText(option.id, option.source);
26
26
  const label = source?.label ?? authored.label;
27
27
  const description = source?.description ?? authored.description;
28
28
  return (
@@ -1,5 +1,5 @@
1
1
  import { Option } from "effect";
2
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
2
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
3
3
  import type { Item } from "#client/react/LibraryContext.ts";
4
4
  import { fileUrl } from "#files/url.ts";
5
5
  import type { ShellApi } from "./ShellApi.ts";
@@ -1,6 +1,6 @@
1
1
  import { Option } from "effect";
2
2
  import { useEffect, useRef, useState } from "react";
3
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
3
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
4
4
  import { SearchResults } from "#client/react/SearchResults.tsx";
5
5
  import type { ShellApi } from "#client/react/ShellApi.ts";
6
6
  import { matchingCommands, type SearchItem } from "#client/react/searchCommands.ts";
@@ -1,10 +1,11 @@
1
1
  import { Option } from "effect";
2
2
  import { PanelsTopLeft } from "lucide-react";
3
3
  import { useContext, useEffect } from "react";
4
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
4
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
5
5
  import type { Navigation } from "#browser/Navigation.ts";
6
6
  import { Badge } from "#client/react/components/ui/Badge.tsx";
7
7
  import { buttonVariants } from "#client/react/components/ui/buttonVariants.ts";
8
+ import { destinationName } from "#client/react/destinationName.ts";
8
9
  import { GoalProgress } from "#client/react/GoalProgress.tsx";
9
10
  import { InboxCount } from "#client/react/Header.tsx";
10
11
  import { type Item, LibraryContext } from "#client/react/LibraryContext.ts";
@@ -38,6 +39,12 @@ export function Sidebar({
38
39
  today: data?.inboxCount,
39
40
  work: data?.counts.now,
40
41
  };
42
+ function name(view: (typeof workspaceDestinations)[number]["view"], label: string) {
43
+ if (view === "today") {
44
+ return destinationName(label, counts.today, "need you");
45
+ }
46
+ return view === "work" ? destinationName(label, counts.work, "open tasks") : label;
47
+ }
41
48
  useEffect(() => {
42
49
  const sidebar = document.getElementById(shellIds.sidebar);
43
50
  if (controls.narrow && controls.open) {
@@ -66,6 +73,7 @@ export function Sidebar({
66
73
  {workspaceDestinations.map(({ label, href, view, id }) => (
67
74
  <a
68
75
  aria-current={(view === "today" ? home : main.view === view) ? "page" : undefined}
76
+ aria-label={name(view, label)}
69
77
  className={cn(
70
78
  buttonVariants({ variant: "ghost" }),
71
79
  "justify-between no-underline",
@@ -77,7 +85,7 @@ export function Sidebar({
77
85
  >
78
86
  <span>{label}</span>
79
87
  {view === "today" && <InboxCount count={counts.today ?? null} id={shellIds.inboxCount} />}
80
- {view === "work" && <Badge>{counts.work ?? "?"}</Badge>}
88
+ {view === "work" && counts.work !== undefined && <Badge aria-hidden="true">{counts.work}</Badge>}
81
89
  </a>
82
90
  ))}
83
91
  </nav>
@@ -1,4 +1,5 @@
1
1
  import type { FleetItemsProjection } from "#browser/views/FleetItemsProjection.ts";
2
+ import { fleetHolder } from "#browser/views/now.ts";
2
3
  import { Badge } from "#client/react/components/ui/Badge.tsx";
3
4
  import { Card } from "#client/react/components/ui/Card.tsx";
4
5
 
@@ -29,7 +30,7 @@ function FleetRow({ item }: { readonly item: Item }) {
29
30
  {title}
30
31
  </a>
31
32
  <Badge variant="outline">{item.stage}</Badge>
32
- {Boolean(item.attentionOwner) && <span className="text-muted-foreground text-xs">Waiting on {item.attentionOwner}</span>}
33
+ <span className="text-muted-foreground text-xs">{fleetHolder(item)}</span>
33
34
  {Boolean(item.prUrl) && (
34
35
  <a aria-label={`Pull request for ${title}`} className="text-sm underline" href={item.prUrl}>
35
36
  PR ↗
@@ -1,6 +1,17 @@
1
1
  import type { FleetHealth } from "#diagnostics/health.ts";
2
2
  import { ObservedTime } from "./diagnosticsValues.tsx";
3
- export function SystemHealth({ value, now }: { readonly value: FleetHealth; readonly now: number }) {
3
+
4
+ const waitingOn = { automation: "Waiting on Fleet agents", orchestrator: "Waiting on the orchestrator" } as const;
5
+
6
+ export function SystemHealth({
7
+ value,
8
+ now,
9
+ needsYou,
10
+ }: {
11
+ readonly value: FleetHealth;
12
+ readonly now: number;
13
+ readonly needsYou?: number | null | undefined;
14
+ }) {
4
15
  let freshness = "Current snapshot";
5
16
  if (now < value.observedAt) {
6
17
  freshness = "Publisher clock is ahead";
@@ -26,11 +37,20 @@ export function SystemHealth({ value, now }: { readonly value: FleetHealth; read
26
37
  <strong>{value.queued}</strong>
27
38
  </dd>
28
39
  </div>
29
- {Object.entries(value.waiting).map(([owner, count]) => (
40
+ <div data-needs-you>
41
+ <dt>Needs you</dt>
42
+ <dd className="flex flex-wrap items-baseline gap-2">
43
+ <strong>{needsYou ?? "Unavailable"}</strong>
44
+ <a className="text-xs" href="/#needs-you">
45
+ Open Today
46
+ </a>
47
+ </dd>
48
+ </div>
49
+ {(["orchestrator", "automation"] as const).map((owner) => (
30
50
  <div key={owner}>
31
- <dt>Waiting on {owner}</dt>
51
+ <dt>{waitingOn[owner]}</dt>
32
52
  <dd>
33
- <strong>{count}</strong>
53
+ <strong>{value.waiting[owner]}</strong>
34
54
  </dd>
35
55
  </div>
36
56
  ))}
@@ -1,7 +1,9 @@
1
+ import { Option } from "effect";
1
2
  import { useContext } from "react";
3
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
2
4
  import type { MainLocation } from "./mainLocation.ts";
3
5
  import { ProjectionFrame } from "./ProjectionFrame.tsx";
4
- import { routeQueryOptions } from "./routeQuery.ts";
6
+ import { navigationQuery, routeQueryOptions } from "./routeQuery.ts";
5
7
  import { SessionContext } from "./SessionContext.ts";
6
8
  import type { ShellApi } from "./ShellApi.ts";
7
9
  import { SystemPage } from "./SystemPage.tsx";
@@ -18,12 +20,14 @@ export function SystemMain({
18
20
  }) {
19
21
  const atom = api.diagnostics.query(undefined, routeQueryOptions);
20
22
  useDiagnosticsPolling(useContext(SessionContext), atom, api.fleetItems.query(undefined, routeQueryOptions));
23
+ const navigation = useQuery(navigationQuery(api), { keepPreviousData: true });
24
+ const needsYou = Option.getOrUndefined(navigation.data)?.inboxCount;
21
25
  return (
22
26
  <ProjectionFrame
23
27
  atom={atom}
24
28
  location={() => ({ file: "System", status: 200, view: "system" })}
25
29
  onPage={onPage}
26
- render={(data) => <SystemPage data={data} />}
30
+ render={(data) => <SystemPage data={data} needsYou={needsYou} />}
27
31
  route="system"
28
32
  url={url}
29
33
  />
@@ -7,7 +7,13 @@ import { FleetItemsContext } from "./FleetItemsContext.ts";
7
7
  import { githubLimited } from "./githubLimited.ts";
8
8
  import { SystemFleetLane } from "./SystemFleetLane.tsx";
9
9
  import { SystemHealth } from "./SystemHealth.tsx";
10
- export function SystemPage({ data: { sources: data, now } }: { readonly data: DiagnosticsProjection }) {
10
+ export function SystemPage({
11
+ data: { sources: data, now },
12
+ needsYou,
13
+ }: {
14
+ readonly data: DiagnosticsProjection;
15
+ readonly needsYou?: number | null | undefined;
16
+ }) {
11
17
  const items = useContext(FleetItemsContext);
12
18
  const fleet = data.fleet.state === "ready" ? { ...data.fleet, observedAt: data.fleet.value.observedAt } : data.fleet;
13
19
  return (
@@ -38,7 +44,7 @@ export function SystemPage({ data: { sources: data, now } }: { readonly data: Di
38
44
  || now < fleet.value.observedAt)
39
45
  }
40
46
  name="Fleet"
41
- render={(value) => <SystemHealth now={now} value={value} />}
47
+ render={(value) => <SystemHealth needsYou={needsYou} now={now} value={value} />}
42
48
  source={fleet}
43
49
  stale={fleet.state === "ready" && now - fleet.value.observedAt > fleet.value.staleAfterMs}
44
50
  />
@@ -5,18 +5,16 @@ export function TodayOptionButton({
5
5
  disabled,
6
6
  onChoose,
7
7
  option,
8
- reason,
9
8
  recommended,
10
9
  source,
11
10
  }: {
12
11
  readonly disabled: boolean;
13
12
  readonly onChoose: () => void;
14
13
  readonly option: { readonly id: string; readonly source: string };
15
- readonly reason: string;
16
14
  readonly recommended: string | undefined;
17
15
  readonly source: { readonly label: string; readonly description: string } | undefined;
18
16
  }) {
19
- const authored = optionText(option.id, option.source, reason);
17
+ const authored = optionText(option.id, option.source);
20
18
  const text = source?.label ?? authored.label;
21
19
  const description = source?.description ?? authored.description;
22
20
  return (
@@ -1,7 +1,7 @@
1
1
  import { RegistryContext } from "@effect/atom-react";
2
2
  import { Cause, Effect, Option } from "effect";
3
- import { useContext, useMemo, useState } from "react";
4
- import { useAction, useQuery } from "@shivaedev/effect-react/result-state.ts";
3
+ import { useContext, useMemo, useRef, useState } from "react";
4
+ import { useAction, useQuery } from "@shivaedev/effect-react/client/resultState.ts";
5
5
  import type { Document, DocumentRequest } from "#browser/document/schema.ts";
6
6
  import { answerBody } from "#browser/responses/answerBody.ts";
7
7
  import { promptHeading } from "#browser/responses/cardContext.ts";
@@ -45,6 +45,8 @@ export function TodayOptions({ api, item }: { readonly api: ShellApi; readonly i
45
45
 
46
46
  function InlineOptions({ api, document: data, request }: { readonly api: ShellApi; readonly document: Document; readonly request: DocumentRequest }) {
47
47
  const registry = useContext(RegistryContext);
48
+ const requestRef = useRef(request);
49
+ requestRef.current = request;
48
50
  const client = useFormClient();
49
51
  const [label] = useState(() => authorLabel(() => localStorage, document.documentElement.dataset.workspace));
50
52
  const [author, setAuthor] = useState(() => label.read("") || "maintainer");
@@ -52,8 +54,8 @@ function InlineOptions({ api, document: data, request }: { readonly api: ShellAp
52
54
  () =>
53
55
  client.runtime.fn(
54
56
  Effect.fn("Board.answerOption")(function* (choice: string) {
55
- const preview = request.preview;
56
- const prompt = request.prompts?.[0];
57
+ const preview = requestRef.current.preview;
58
+ const prompt = requestRef.current.prompts?.[0];
57
59
  if (!(preview && prompt && author.trim())) {
58
60
  return yield* Effect.fail("Add your local author label.");
59
61
  }
@@ -75,7 +77,7 @@ function InlineOptions({ api, document: data, request }: { readonly api: ShellAp
75
77
  return saved;
76
78
  }),
77
79
  ),
78
- [client, api, registry, request, author, data.file, label.save],
80
+ [client, api, registry, author, data.file, label.save],
79
81
  );
80
82
  const action = useAction(actionAtom);
81
83
  const failure = Option.getOrUndefined(Option.flatMap(action.cause, Cause.findErrorOption));
@@ -102,7 +104,6 @@ function InlineOptions({ api, document: data, request }: { readonly api: ShellAp
102
104
  key={option.id}
103
105
  onChoose={() => action.dispatch(option.id)}
104
106
  option={option}
105
- reason={request.preview?.reason ?? ""}
106
107
  recommended={data.metadata.recommended}
107
108
  source={data.metadata.options?.[index]}
108
109
  />
@@ -1,5 +1,5 @@
1
1
  import { Option } from "effect";
2
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
2
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
3
3
  import type { InboxProjection } from "#browser/views/schema.ts";
4
4
  import { fileUrl } from "#files/url.ts";
5
5
  import { inProgressItems } from "./inProgressItems.ts";
@@ -30,11 +30,11 @@ export function TodayPage({ api, data }: { readonly api: ShellApi; readonly data
30
30
  )}
31
31
  </header>
32
32
  <nav aria-label="Today summary" className="today-strip flex items-center gap-1 whitespace-nowrap text-xs tabular-nums sm:gap-2 sm:text-sm">
33
- <a href={`#${todayIds.needs}`}>{data.complete ? data.you.length : "?"} need you</a>
33
+ <a href={`#${todayIds.needs}`}>{data.complete ? `${data.you.length} need you` : "Needs you unavailable"}</a>
34
34
  <span aria-hidden="true">·</span>
35
- <a href={`#${todayIds.progress}`}>{progress?.length ?? "?"} in progress</a>
35
+ <a href={`#${todayIds.progress}`}>{progressSummary(progress?.length, fleet?.state === "unavailable" || Option.isSome(fleetQuery.cause))}</a>
36
36
  <span aria-hidden="true">·</span>
37
- <a href={`#${todayIds.goals}`}>{goals?.goals.length ?? "?"} active goals</a>
37
+ <a href={`#${todayIds.goals}`}>{goalsSummary(goals?.goals.length, Option.isSome(query.cause))}</a>
38
38
  </nav>
39
39
  <div className="today-grid grid min-w-0 gap-6 xl:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
40
40
  <div className="min-w-0 space-y-6">
@@ -82,3 +82,17 @@ function Later({ items }: { readonly items: InboxProjection["later"] }) {
82
82
  </section>
83
83
  );
84
84
  }
85
+
86
+ function progressSummary(count: number | undefined, unavailable: boolean) {
87
+ if (count !== undefined) {
88
+ return `${count} in progress`;
89
+ }
90
+ return unavailable ? "Fleet unavailable" : "Loading Fleet…";
91
+ }
92
+
93
+ function goalsSummary(count: number | undefined, failed: boolean) {
94
+ if (count !== undefined) {
95
+ return `${count} active goals`;
96
+ }
97
+ return failed ? "Goals unavailable" : "Loading goals…";
98
+ }
@@ -1,4 +1,4 @@
1
- import { cardStatus, type FleetWork, holderOf } from "#browser/views/now.ts";
1
+ import { cardStatus, type FleetWork, holderOf, stageTime } from "#browser/views/now.ts";
2
2
  import type { WorkProjection } from "#browser/views/schema.ts";
3
3
  import type { State } from "#browser/views/state.ts";
4
4
  import { Card } from "#client/react/components/ui/Card.tsx";
@@ -17,9 +17,10 @@ export function WorkCard({
17
17
  readonly fleet: FleetWork | undefined;
18
18
  }) {
19
19
  const holder = holderOf(item.metadata, fleet);
20
+ const kind = item.metadata.kind === "task" ? undefined : item.metadata.kind;
20
21
  return (
21
22
  <Card className="work-card" data-item={item.id} data-selected={item.id === state.item ? "true" : undefined}>
22
- <p className="work-kind">{item.metadata.kind ?? ""}</p>
23
+ {kind !== undefined && <p className="work-kind">{kind}</p>}
23
24
  <h3>
24
25
  <WorkItemTitle item={item} state={state} />
25
26
  </h3>
@@ -27,12 +28,19 @@ export function WorkCard({
27
28
  <StatusChip status={cardStatus(item.metadata, fleet, item.waiting)} title={item.waiting} />
28
29
  <PrChip url={fleet?.prUrl} />
29
30
  </div>
30
- {holder && <p className="work-owner">Holder: {holder}</p>}
31
- {fleet?.stageEnteredAt !== undefined && fleet.stageEnteredAt !== null && (
32
- <p className="work-stage">
33
- In {fleet.stage} for <Age modified={fleet.stageEnteredAt} short />
34
- </p>
35
- )}
31
+ {holder && <p className="work-owner">{holder}</p>}
32
+ {fleet?.stageEnteredAt !== undefined && fleet.stageEnteredAt !== null && <StageTime entered={fleet.stageEnteredAt} stage={fleet.stage} />}
36
33
  </Card>
37
34
  );
38
35
  }
36
+
37
+ function StageTime({ stage, entered }: { readonly stage: FleetWork["stage"]; readonly entered: number }) {
38
+ const { ended, label } = stageTime(stage);
39
+ return (
40
+ <p className="work-stage">
41
+ {label} {ended ? "" : "for "}
42
+ <Age modified={entered} short />
43
+ {ended ? " ago" : ""}
44
+ </p>
45
+ );
46
+ }
@@ -23,9 +23,15 @@ export function WorkLane({
23
23
  const [open, setOpen] = useState(!collapsed);
24
24
  return (
25
25
  <section aria-label={lane} className="work-column min-w-0">
26
- <h2 className="m-0 mb-3 flex min-h-8 items-center justify-between gap-3 whitespace-nowrap font-semibold text-sm">
26
+ <h2 className="m-0 mb-3 flex min-h-8 items-center justify-between gap-2 font-semibold text-sm">
27
27
  {collapsed ? (
28
- <Button aria-expanded={open} onClick={() => setOpen(!open)} size="sm" variant="outline">
28
+ <Button
29
+ aria-expanded={open}
30
+ className="h-auto min-h-8 shrink whitespace-normal text-left"
31
+ onClick={() => setOpen(!open)}
32
+ size="sm"
33
+ variant="outline"
34
+ >
29
35
  {lane}
30
36
  </Button>
31
37
  ) : (
@@ -22,7 +22,7 @@ export function WorkNow({ data }: { readonly data: WorkProjection }) {
22
22
  Fleet is unavailable. Lanes use authored status. {fleet?.state === "unavailable" ? fleet.reason : "Fleet data has not loaded."}
23
23
  </p>
24
24
  )}
25
- <div className="work-columns work-now grid auto-cols-[minmax(260px,1fr)] grid-flow-col gap-4 overflow-x-auto pb-2 max-md:auto-cols-auto max-md:grid-flow-row">
25
+ <div className="work-columns work-now grid grid-cols-[repeat(6,minmax(8.5rem,1fr))] gap-3 overflow-x-auto pb-2">
26
26
  {nowLanes.map((lane, index) => {
27
27
  const rows = (groups.get(lane) ?? []).filter((item) => index !== 5 || recent.has(item.id));
28
28
  return (
@@ -0,0 +1,3 @@
1
+ export function destinationName(label: string, count: number | null | undefined, unit: string) {
2
+ return count === null || count === undefined ? label : `${label}, ${count} ${unit}`;
3
+ }
@@ -1,7 +1,7 @@
1
1
  import { isRedirect, type LoaderFnContext, redirect } from "@tanstack/react-router";
2
2
  import { Predicate } from "effect";
3
3
  import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
4
- import { loadQuery } from "@shivaedev/effect-react/router.ts";
4
+ import { loadQuery } from "@shivaedev/effect-react/loadQuery.ts";
5
5
  import type { ShellOptions } from "./BoardShell.tsx";
6
6
  import { documentRoute } from "./documentRoute.ts";
7
7
  import { loadProjection } from "./loadProjection.ts";
@@ -1,5 +1,5 @@
1
1
  import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
2
- import { loadQuery } from "@shivaedev/effect-react/router.ts";
2
+ import { loadQuery } from "@shivaedev/effect-react/loadQuery.ts";
3
3
  import { pageQueries } from "./pageQueries.ts";
4
4
  import type { ProjectionRoute } from "./projectionRoute.ts";
5
5
  import { routeQueryOptions } from "./routeQuery.ts";
@@ -1,5 +1,5 @@
1
1
  import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
2
- import { loadQuery } from "@shivaedev/effect-react/router.ts";
2
+ import { loadQuery } from "@shivaedev/effect-react/loadQuery.ts";
3
3
  import type { RemainingRoute } from "./remainingRoute.ts";
4
4
  import { routeQueryOptions } from "./routeQuery.ts";
5
5
  import type { ShellApi } from "./ShellApi.ts";
@@ -1,7 +1,7 @@
1
1
  import { useAtomRef } from "@effect/atom-react";
2
2
  import { Equal, Option, Schema } from "effect";
3
3
  import { useEffect, useState } from "react";
4
- import { useCreate } from "@shivaedev/effect-react/create.ts";
4
+ import { useCreate } from "@shivaedev/effect-react/client/create.ts";
5
5
  import { HandoffInput, type HandoffPreview } from "#browser/handoffs/schema.ts";
6
6
  import type { HandoffProjection } from "#browser/views/remaining.ts";
7
7
  import { formDraft } from "#client/formDraft.ts";
@@ -2,7 +2,7 @@ import { Cause, Option, Predicate } from "effect";
2
2
  import type * as AsyncResult from "effect/reactivity/AsyncResult";
3
3
  import type * as Atom from "effect/reactivity/Atom";
4
4
  import { useEffect, useState } from "react";
5
- import { useQuery } from "@shivaedev/effect-react/result-state.ts";
5
+ import { useQuery } from "@shivaedev/effect-react/client/resultState.ts";
6
6
  import type { ProjectionRoute } from "./projectionRoute.ts";
7
7
  import type { RemainingRoute } from "./remainingRoute.ts";
8
8
 
@@ -1,7 +1,7 @@
1
1
  import { useAtomRef } from "@effect/atom-react";
2
2
  import { Equal, Option, Schema } from "effect";
3
3
  import { useEffect, useState } from "react";
4
- import { useCreate } from "@shivaedev/effect-react/create.ts";
4
+ import { useCreate } from "@shivaedev/effect-react/client/create.ts";
5
5
  import type { Prompt } from "#browser/responses/Prompt.ts";
6
6
  import type { PromptAnswer, QuestionPreview } from "#browser/responses/schema.ts";
7
7
  import { formDraft } from "#client/formDraft.ts";
@@ -31,7 +31,7 @@ export function chosenLabels(prompts: readonly Prompt[], answers: readonly Promp
31
31
  return prompts
32
32
  .flatMap((prompt) => {
33
33
  const selected = answers?.find((answer) => answer.prompt === prompt.id)?.selected ?? [];
34
- return prompt.options.filter((option) => selected.includes(option.id)).map((option) => optionText(option.id, option.source, "").label);
34
+ return prompt.options.filter((option) => selected.includes(option.id)).map((option) => optionText(option.id, option.source).label);
35
35
  })
36
36
  .join(", ");
37
37
  }
@@ -1,5 +1,5 @@
1
1
  import { Clock, Context, Effect, Schema } from "effect";
2
- import { defineService } from "@shivaedev/effect-service/define-service.ts";
2
+ import { defineService } from "@shivaedev/effect-service/defineService.ts";
3
3
  import * as limits from "#browser/diagnostics/schema.ts";
4
4
  import type { FleetHealth } from "./health.ts";
5
5
 
@@ -1,7 +1,7 @@
1
1
  import { Clock, Effect, FileSystem, Path, Schema, Semaphore } from "effect";
2
- import { defineService } from "@shivaedev/effect-service/define-service.ts";
2
+ import { defineService } from "@shivaedev/effect-service/defineService.ts";
3
3
  import { Conflict, Forbidden, PreconditionFailed, ServiceUnavailable } from "@shivaedev/platform/errors/taxonomy.ts";
4
- import { rpcBoundary } from "@shivaedev/platform/rpc-server/boundary.ts";
4
+ import { rpcBoundary } from "@shivaedev/platform/server/rpc-server/boundary.ts";
5
5
  import type { DraftInput } from "#browser/responses/schema.ts";
6
6
  import { awaitResponse } from "#server/responses/await.ts";
7
7
  import { responseHistory } from "#server/responses/history.ts";
@@ -2,7 +2,7 @@ import { Effect, type FileSystem, Layer, type Path, type PlatformError } from "e
2
2
  import { HttpRouter } from "effect/http";
3
3
  import { RpcSerialization, RpcServer } from "effect/rpc";
4
4
  import type { InternalServerError } from "@shivaedev/platform/errors/taxonomy.ts";
5
- import { localRpcGuard } from "@shivaedev/platform/rpc-server/local.ts";
5
+ import { localRpcGuard } from "@shivaedev/platform/server/rpc-server/local.ts";
6
6
  import type { RenderFailed } from "#render/Failed.ts";
7
7
  import { workRpcs } from "#rpc/contract.ts";
8
8
  import { boardRuntime } from "#server/boardRuntime.ts";
@@ -5,13 +5,14 @@ import type { DocumentRequest } from "#browser/document/schema.ts";
5
5
  import { reasonMarkdown } from "#browser/responses/reasonMarkdown.ts";
6
6
  import type { MetadataDocument } from "#server/metadata/model.ts";
7
7
  import { documentBody } from "./body.ts";
8
+ import { REVIEWED_WORK_HEADING, reviewedWork } from "./reviewedWork.ts";
8
9
 
9
10
  export function answerBody(document: MetadataDocument, requests: readonly DocumentRequest[]): string {
10
11
  const source = documentBody(document);
11
12
  const cards = requests.filter((request) => request.open && request.preview?.source === document.file);
12
13
  const managed = document.parsed.fields.attention?.some((request) => request.managed);
13
14
  const title = managed ? requests[0]?.title : undefined;
14
- if (cards.length === 0 && !title) {
15
+ if (cards.length === 0 && !title && !managed) {
15
16
  return source;
16
17
  }
17
18
  const tree = unified().use(remarkParse).use(remarkDirective).parse(source);
@@ -28,6 +29,10 @@ export function answerBody(document: MetadataDocument, requests: readonly Docume
28
29
  if (index === 0 && node.type === "heading" && node.depth === 1 && title) {
29
30
  return [`# ${title}`];
30
31
  }
32
+ const reviewed = managed && node.type === "code" && underReviewedWork(tree.children[index - 1], source) ? reviewedWork(node.value) : undefined;
33
+ if (reviewed !== undefined) {
34
+ return [reviewed];
35
+ }
31
36
  return [source.slice(node.position?.start.offset, node.position?.end.offset)];
32
37
  })
33
38
  .join("\n\n");
@@ -94,3 +99,7 @@ function inCardSection(nodes: import("mdast").RootContent[], index: number, sour
94
99
  }
95
100
  return false;
96
101
  }
102
+
103
+ function underReviewedWork(node: import("mdast").RootContent | undefined, source: string) {
104
+ return node?.type === "heading" && source.slice(node.position?.start.offset, node.position?.end.offset).trim() === REVIEWED_WORK_HEADING;
105
+ }