@shivaedev/work-board 0.17.0 → 0.17.1

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 (181) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +4 -4
  3. package/assets/react.css +1 -1
  4. package/assets/react.js +34 -33
  5. package/dist/browser/responses/cardContext.d.ts +8 -1
  6. package/dist/browser/responses/cardContext.d.ts.map +1 -1
  7. package/dist/browser/responses/cardContext.js +14 -6
  8. package/dist/browser/responses/cardContext.js.map +1 -1
  9. package/dist/browser/views/schema.d.ts +11 -0
  10. package/dist/browser/views/schema.d.ts.map +1 -1
  11. package/dist/browser/views/schema.js +1 -0
  12. package/dist/browser/views/schema.js.map +1 -1
  13. package/dist/client/formStatus.d.ts +1 -1
  14. package/dist/client/formStatus.d.ts.map +1 -1
  15. package/dist/client/formStatus.js +2 -2
  16. package/dist/client/formStatus.js.map +1 -1
  17. package/dist/client/react/AnswerContext.d.ts.map +1 -1
  18. package/dist/client/react/AnswerContext.js +5 -2
  19. package/dist/client/react/AnswerContext.js.map +1 -1
  20. package/dist/client/react/AnswerForm.d.ts.map +1 -1
  21. package/dist/client/react/AnswerForm.js +6 -2
  22. package/dist/client/react/AnswerForm.js.map +1 -1
  23. package/dist/client/react/AnswerMarkdown.d.ts +6 -0
  24. package/dist/client/react/AnswerMarkdown.d.ts.map +1 -0
  25. package/dist/client/react/AnswerMarkdown.js +7 -0
  26. package/dist/client/react/AnswerMarkdown.js.map +1 -0
  27. package/dist/client/react/AnswerMore.d.ts.map +1 -1
  28. package/dist/client/react/AnswerMore.js +2 -1
  29. package/dist/client/react/AnswerMore.js.map +1 -1
  30. package/dist/client/react/AnswerPanel.d.ts.map +1 -1
  31. package/dist/client/react/AnswerPanel.js +3 -2
  32. package/dist/client/react/AnswerPanel.js.map +1 -1
  33. package/dist/client/react/AnswerPrompts.d.ts.map +1 -1
  34. package/dist/client/react/AnswerPrompts.js +16 -10
  35. package/dist/client/react/AnswerPrompts.js.map +1 -1
  36. package/dist/client/react/BoardShell.js +1 -1
  37. package/dist/client/react/BoardShell.js.map +1 -1
  38. package/dist/client/react/DocumentMain.d.ts.map +1 -1
  39. package/dist/client/react/DocumentMain.js +1 -1
  40. package/dist/client/react/DocumentMain.js.map +1 -1
  41. package/dist/client/react/GoalPage.js +1 -1
  42. package/dist/client/react/GoalPage.js.map +1 -1
  43. package/dist/client/react/GoalProgressHeader.js +1 -1
  44. package/dist/client/react/GoalProgressHeader.js.map +1 -1
  45. package/dist/client/react/LibraryMain.d.ts.map +1 -1
  46. package/dist/client/react/LibraryMain.js +1 -1
  47. package/dist/client/react/LibraryMain.js.map +1 -1
  48. package/dist/client/react/LibraryPage.js +1 -1
  49. package/dist/client/react/LibraryPage.js.map +1 -1
  50. package/dist/client/react/ProjectionMain.d.ts.map +1 -1
  51. package/dist/client/react/ProjectionMain.js +1 -1
  52. package/dist/client/react/ProjectionMain.js.map +1 -1
  53. package/dist/client/react/PromptOption.d.ts.map +1 -1
  54. package/dist/client/react/PromptOption.js +7 -5
  55. package/dist/client/react/PromptOption.js.map +1 -1
  56. package/dist/client/react/RemainingMain.d.ts.map +1 -1
  57. package/dist/client/react/RemainingMain.js +1 -1
  58. package/dist/client/react/RemainingMain.js.map +1 -1
  59. package/dist/client/react/ResponseHistory.d.ts.map +1 -1
  60. package/dist/client/react/ResponseHistory.js +11 -5
  61. package/dist/client/react/ResponseHistory.js.map +1 -1
  62. package/dist/client/react/SystemMain.d.ts.map +1 -1
  63. package/dist/client/react/SystemMain.js +1 -1
  64. package/dist/client/react/SystemMain.js.map +1 -1
  65. package/dist/client/react/TodayNeeds.d.ts +5 -0
  66. package/dist/client/react/TodayNeeds.d.ts.map +1 -1
  67. package/dist/client/react/TodayNeeds.js +6 -3
  68. package/dist/client/react/TodayNeeds.js.map +1 -1
  69. package/dist/client/react/TodayOptionButton.d.ts +15 -0
  70. package/dist/client/react/TodayOptionButton.d.ts.map +1 -0
  71. package/dist/client/react/TodayOptionButton.js +10 -0
  72. package/dist/client/react/TodayOptionButton.js.map +1 -0
  73. package/dist/client/react/TodayOptions.d.ts.map +1 -1
  74. package/dist/client/react/TodayOptions.js +11 -9
  75. package/dist/client/react/TodayOptions.js.map +1 -1
  76. package/dist/client/react/TodayPage.d.ts.map +1 -1
  77. package/dist/client/react/TodayPage.js +7 -4
  78. package/dist/client/react/TodayPage.js.map +1 -1
  79. package/dist/client/react/activeRead.js +1 -1
  80. package/dist/client/react/activeRead.js.map +1 -1
  81. package/dist/client/react/loadBoardPage.d.ts.map +1 -1
  82. package/dist/client/react/loadBoardPage.js +1 -1
  83. package/dist/client/react/loadBoardPage.js.map +1 -1
  84. package/dist/client/react/loadProjection.d.ts.map +1 -1
  85. package/dist/client/react/loadProjection.js +1 -1
  86. package/dist/client/react/loadProjection.js.map +1 -1
  87. package/dist/client/react/loadRemaining.d.ts +201 -201
  88. package/dist/client/react/loadRemaining.d.ts.map +1 -1
  89. package/dist/client/react/loadRemaining.js +1 -1
  90. package/dist/client/react/loadRemaining.js.map +1 -1
  91. package/dist/client/react/pageQueries.js +1 -1
  92. package/dist/client/react/pageQueries.js.map +1 -1
  93. package/dist/client/react/resolvedDocumentFailure.d.ts.map +1 -0
  94. package/dist/client/react/resolvedDocumentFailure.js.map +1 -0
  95. package/dist/client/react/routeQuery.d.ts.map +1 -0
  96. package/dist/client/react/routeQuery.js.map +1 -0
  97. package/dist/client/react/useResponseForm.d.ts.map +1 -1
  98. package/dist/client/react/useResponseForm.js +2 -2
  99. package/dist/client/react/useResponseForm.js.map +1 -1
  100. package/dist/client/responses/summary.d.ts +1 -0
  101. package/dist/client/responses/summary.d.ts.map +1 -1
  102. package/dist/client/responses/summary.js +12 -1
  103. package/dist/client/responses/summary.js.map +1 -1
  104. package/dist/rpc/contract.d.ts +33 -0
  105. package/dist/rpc/contract.d.ts.map +1 -1
  106. package/dist/server/attention/inbox.d.ts +13 -2
  107. package/dist/server/attention/inbox.d.ts.map +1 -1
  108. package/dist/server/attention/inbox.js +44 -27
  109. package/dist/server/attention/inbox.js.map +1 -1
  110. package/dist/server/attention/managedRequestReceipt.d.ts +10 -0
  111. package/dist/server/attention/managedRequestReceipt.d.ts.map +1 -1
  112. package/dist/server/attention/managedRequestReceipt.js +29 -10
  113. package/dist/server/attention/managedRequestReceipt.js.map +1 -1
  114. package/dist/server/attention/projection.d.ts.map +1 -1
  115. package/dist/server/attention/projection.js +2 -1
  116. package/dist/server/attention/projection.js.map +1 -1
  117. package/dist/server/attention/requestInbox.d.ts +3 -3
  118. package/dist/server/attention/requestInbox.js +2 -2
  119. package/dist/server/attention/requestInbox.js.map +1 -1
  120. package/dist/server/attention/requests.d.ts +1 -0
  121. package/dist/server/attention/requests.d.ts.map +1 -1
  122. package/dist/server/attention/requests.js +7 -3
  123. package/dist/server/attention/requests.js.map +1 -1
  124. package/dist/server/mcp/client.d.ts +874 -863
  125. package/dist/server/mcp/client.d.ts.map +1 -1
  126. package/dist/server/mcp/queries.d.ts +33 -0
  127. package/dist/server/mcp/queries.d.ts.map +1 -1
  128. package/dist/server/packageInfo.d.ts +1 -1
  129. package/dist/server/packageInfo.js +1 -1
  130. package/dist/server/views/projectionQueries.d.ts +11 -0
  131. package/dist/server/views/projectionQueries.d.ts.map +1 -1
  132. package/docs/delivery/README.md +2 -2
  133. package/package.json +4 -4
  134. package/src/browser/responses/cardContext.ts +15 -6
  135. package/src/browser/views/schema.ts +1 -0
  136. package/src/client/formStatus.ts +2 -2
  137. package/src/client/react/AnswerContext.tsx +9 -4
  138. package/src/client/react/AnswerForm.tsx +25 -6
  139. package/src/client/react/AnswerMarkdown.tsx +15 -0
  140. package/src/client/react/AnswerMore.tsx +58 -46
  141. package/src/client/react/AnswerPanel.tsx +14 -8
  142. package/src/client/react/AnswerPrompts.tsx +33 -18
  143. package/src/client/react/BoardShell.tsx +1 -1
  144. package/src/client/react/DocumentMain.tsx +1 -1
  145. package/src/client/react/GoalPage.tsx +1 -1
  146. package/src/client/react/GoalProgressHeader.tsx +1 -1
  147. package/src/client/react/LibraryMain.tsx +1 -1
  148. package/src/client/react/LibraryPage.tsx +1 -1
  149. package/src/client/react/ProjectionMain.tsx +1 -1
  150. package/src/client/react/PromptOption.tsx +39 -24
  151. package/src/client/react/RemainingMain.tsx +1 -1
  152. package/src/client/react/ResponseHistory.tsx +53 -14
  153. package/src/client/react/SystemMain.tsx +1 -1
  154. package/src/client/react/TodayNeeds.tsx +21 -6
  155. package/src/client/react/TodayOptionButton.tsx +37 -0
  156. package/src/client/react/TodayOptions.tsx +36 -28
  157. package/src/client/react/TodayPage.tsx +18 -3
  158. package/src/client/react/activeRead.ts +1 -1
  159. package/src/client/react/entry.css +0 -6
  160. package/src/client/react/loadBoardPage.ts +1 -1
  161. package/src/client/react/loadProjection.ts +1 -1
  162. package/src/client/react/loadRemaining.ts +1 -1
  163. package/src/client/react/pageQueries.ts +1 -1
  164. package/src/client/react/useResponseForm.ts +4 -2
  165. package/src/client/responses/summary.ts +13 -1
  166. package/src/server/attention/inbox.ts +51 -27
  167. package/src/server/attention/managedRequestReceipt.ts +41 -10
  168. package/src/server/attention/projection.ts +5 -2
  169. package/src/server/attention/requestInbox.ts +2 -2
  170. package/src/server/attention/requests.ts +7 -2
  171. package/src/server/packageInfo.ts +1 -1
  172. package/dist/client/page/resolvedDocumentFailure.d.ts.map +0 -1
  173. package/dist/client/page/resolvedDocumentFailure.js.map +0 -1
  174. package/dist/client/page/routeQuery.d.ts.map +0 -1
  175. package/dist/client/page/routeQuery.js.map +0 -1
  176. /package/dist/client/{page → react}/resolvedDocumentFailure.d.ts +0 -0
  177. /package/dist/client/{page → react}/resolvedDocumentFailure.js +0 -0
  178. /package/dist/client/{page → react}/routeQuery.d.ts +0 -0
  179. /package/dist/client/{page → react}/routeQuery.js +0 -0
  180. /package/src/client/{page → react}/resolvedDocumentFailure.ts +0 -0
  181. /package/src/client/{page → react}/routeQuery.ts +0 -0
@@ -4,7 +4,6 @@ import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
4
4
  import { useCallback, useContext, useEffect, useState } from "react";
5
5
  import { useQuery } from "@shivaedev/effect-react/result-state.ts";
6
6
  import type { BrowserClient } from "#client/client.ts";
7
- import { navigationQuery } from "#client/page/routeQuery.ts";
8
7
  import { AgeTicker } from "./Age.tsx";
9
8
  import { FleetMain } from "./FleetMain.tsx";
10
9
  import { FormContext } from "./FormContext.ts";
@@ -15,6 +14,7 @@ import { emptyMainLocation, type MainLocation } from "./mainLocation.ts";
15
14
  import { PhoneNavigation } from "./PhoneNavigation.tsx";
16
15
  import { ReadingLibrary } from "./ReadingLibrary.tsx";
17
16
  import { ReadingReadyContext, readingReady } from "./ReadingReadyContext.ts";
17
+ import { navigationQuery } from "./routeQuery.ts";
18
18
  import { SearchDialog } from "./SearchDialog.tsx";
19
19
  import { SessionContext } from "./SessionContext.ts";
20
20
  import type { ShellApi } from "./ShellApi.ts";
@@ -2,11 +2,11 @@ import { Cause, Option } from "effect";
2
2
  import { useEffect, useState } from "react";
3
3
  import { useQuery } from "@shivaedev/effect-react/result-state.ts";
4
4
  import type { Document } from "#browser/document/schema.ts";
5
- import { documentQuery } from "#client/page/routeQuery.ts";
6
5
  import { DocumentPage } from "./DocumentPage.tsx";
7
6
  import type { MainLocation } from "./mainLocation.ts";
8
7
  import { ProjectionMain } from "./ProjectionMain.tsx";
9
8
  import { RemainingMain } from "./RemainingMain.tsx";
9
+ import { documentQuery } from "./routeQuery.ts";
10
10
  import type { ShellApi } from "./ShellApi.ts";
11
11
  import { shellIds } from "./shellIds.ts";
12
12
  import { useDocumentPage } from "./useDocumentPage.ts";
@@ -1,10 +1,10 @@
1
1
  import { Option } from "effect";
2
2
  import { useQuery } from "@shivaedev/effect-react/result-state.ts";
3
3
  import type { Document } from "#browser/document/schema.ts";
4
- import { routeQueryOptions } from "#client/page/routeQuery.ts";
5
4
  import { GoalCriteria } from "./GoalCriteria.tsx";
6
5
  import { GoalWorkTable } from "./GoalWorkTable.tsx";
7
6
  import { targetsGoal } from "./goalWork.ts";
7
+ import { routeQueryOptions } from "./routeQuery.ts";
8
8
  import type { ShellApi } from "./ShellApi.ts";
9
9
 
10
10
  export function GoalPage({ api, document }: { readonly api: ShellApi; readonly document: Document }) {
@@ -2,8 +2,8 @@ import { Option } from "effect";
2
2
  import { useId } from "react";
3
3
  import { useQuery } from "@shivaedev/effect-react/result-state.ts";
4
4
  import type { Document } from "#browser/document/schema.ts";
5
- import { routeQueryOptions } from "#client/page/routeQuery.ts";
6
5
  import { GoalProgressSummary } from "./GoalProgressSummary.tsx";
6
+ import { routeQueryOptions } from "./routeQuery.ts";
7
7
  import type { ShellApi } from "./ShellApi.ts";
8
8
 
9
9
  export function GoalProgressHeader({ api, document }: { readonly api: ShellApi; readonly document: Document }) {
@@ -1,7 +1,7 @@
1
- import { routeQueryOptions } from "#client/page/routeQuery.ts";
2
1
  import { LibraryPage } from "./LibraryPage.tsx";
3
2
  import type { MainLocation } from "./mainLocation.ts";
4
3
  import { ProjectionFrame } from "./ProjectionFrame.tsx";
4
+ import { routeQueryOptions } from "./routeQuery.ts";
5
5
  import type { ShellApi } from "./ShellApi.ts";
6
6
  export function LibraryMain({
7
7
  api,
@@ -1,8 +1,8 @@
1
1
  import { Option } from "effect";
2
2
  import { useState } from "react";
3
3
  import { useQuery } from "@shivaedev/effect-react/result-state.ts";
4
- import { routeQueryOptions } from "#client/page/routeQuery.ts";
5
4
  import { LibraryContents } from "./LibraryContents.tsx";
5
+ import { routeQueryOptions } from "./routeQuery.ts";
6
6
  import type { ShellApi } from "./ShellApi.ts";
7
7
 
8
8
  export function LibraryPage({ api, url = "/_board/library" }: { readonly api: ShellApi; readonly url?: string }) {
@@ -1,9 +1,9 @@
1
1
  import { documentSourceId } from "#client/document/layout.ts";
2
- import { routeQueryOptions } from "#client/page/routeQuery.ts";
3
2
  import { HomeBoard } from "./HomeBoard.tsx";
4
3
  import type { MainLocation } from "./mainLocation.ts";
5
4
  import { ProjectionFrame } from "./ProjectionFrame.tsx";
6
5
  import type { ProjectionRoute } from "./projectionRoute.ts";
6
+ import { routeQueryOptions } from "./routeQuery.ts";
7
7
  import type { ShellApi } from "./ShellApi.ts";
8
8
  import { TodayPage } from "./TodayPage.tsx";
9
9
  import { WorkPage } from "./WorkPage.tsx";
@@ -1,8 +1,10 @@
1
1
  import { useId } from "react";
2
2
  import type { Document } from "#browser/document/schema.ts";
3
- import { optionLabel } from "#browser/responses/cardContext.ts";
3
+ import { optionText } from "#browser/responses/cardContext.ts";
4
4
  import type { Prompt } from "#browser/responses/Prompt.ts";
5
- import { ResponseMarkdown } from "./ResponseMarkdown.tsx";
5
+ import { Badge } from "#client/react/components/ui/Badge.tsx";
6
+ import { cn } from "#client/react/lib/cn.ts";
7
+
6
8
  export function PromptOption({
7
9
  document,
8
10
  prompt,
@@ -20,37 +22,50 @@ export function PromptOption({
20
22
  }) {
21
23
  const source = document.metadata.options?.[choice];
22
24
  const id = useId();
23
- const control = (
24
- <input
25
- checked={checked}
26
- data-option-source={option.source}
27
- hidden
28
- id={id}
29
- name={`choice-${prompt.id}`}
30
- onChange={(event) => choose(option.id, event.currentTarget.checked)}
31
- onInput={(event) => choose(option.id, event.currentTarget.checked)}
32
- type={prompt.mode === "one" ? "radio" : "checkbox"}
33
- value={option.id}
34
- />
35
- );
36
- const label = source?.label ?? optionLabel(option.id, option.source, document.requests.find((request) => request.open)?.preview?.reason ?? "");
25
+ const authored = optionText(option.id, option.source, document.requests.find((request) => request.open)?.preview?.reason ?? "");
26
+ const label = source?.label ?? authored.label;
27
+ const description = source?.description ?? authored.description;
37
28
  return (
38
29
  <div>
39
- {control}
30
+ <input
31
+ checked={checked}
32
+ data-option-source={option.source}
33
+ hidden
34
+ id={id}
35
+ name={`choice-${prompt.id}`}
36
+ onChange={(event) => choose(option.id, event.currentTarget.checked)}
37
+ onInput={(event) => choose(option.id, event.currentTarget.checked)}
38
+ type={prompt.mode === "one" ? "radio" : "checkbox"}
39
+ value={option.id}
40
+ />
40
41
  <button
41
42
  aria-pressed={checked}
42
- className="question-choice"
43
+ className={cn(
44
+ "question-choice flex w-full items-start gap-3 rounded-lg border bg-background px-4 py-3 text-left transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
45
+ checked && "border-primary bg-accent",
46
+ )}
43
47
  data-choice={option.id}
44
48
  onClick={() => choose(option.id, prompt.mode !== "many" || !checked)}
45
49
  type="button"
46
50
  >
47
- <strong>
48
- {label}
49
- {label === document.metadata.recommended && " · Recommended"}
50
- </strong>
51
- {source !== undefined && <small>{source.description}</small>}
51
+ <span
52
+ aria-hidden="true"
53
+ className={cn(
54
+ "mt-0.5 grid size-4 shrink-0 place-items-center border border-muted-foreground",
55
+ prompt.mode === "many" ? "rounded-sm" : "rounded-full",
56
+ checked && "border-primary",
57
+ )}
58
+ >
59
+ {checked ? <span className={cn("size-2 bg-primary", prompt.mode === "many" ? "rounded-[1px]" : "rounded-full")} /> : null}
60
+ </span>
61
+ <span className="grid gap-1">
62
+ <strong className="flex flex-wrap items-center gap-2 font-medium text-sm">
63
+ {label}
64
+ {label === document.metadata.recommended && <Badge variant="outline">Recommended</Badge>}
65
+ </strong>
66
+ {Boolean(description) && <small className="text-muted-foreground text-sm">{description}</small>}
67
+ </span>
52
68
  </button>
53
- {source === undefined && <ResponseMarkdown file={document.file} source={option.source.replace(/^#{1,6}\s+.+$/mu, "")} />}
54
69
  </div>
55
70
  );
56
71
  }
@@ -1,4 +1,3 @@
1
- import { routeQueryOptions } from "#client/page/routeQuery.ts";
2
1
  import { GoalsPage } from "./GoalsPage.tsx";
3
2
  import { HandoffPage } from "./HandoffPage.tsx";
4
3
  import { LibraryMain } from "./LibraryMain.tsx";
@@ -6,6 +5,7 @@ import type { MainLocation } from "./mainLocation.ts";
6
5
  import { ProjectionFrame } from "./ProjectionFrame.tsx";
7
6
  import { ResultPage } from "./ResultPage.tsx";
8
7
  import type { RemainingRoute } from "./remainingRoute.ts";
8
+ import { routeQueryOptions } from "./routeQuery.ts";
9
9
  import type { ShellApi } from "./ShellApi.ts";
10
10
  import { StartPage } from "./StartPage.tsx";
11
11
  import { SystemMain } from "./SystemMain.tsx";
@@ -1,6 +1,12 @@
1
1
  import type { DocumentRequest } from "#browser/document/schema.ts";
2
- import { ResponseMarkdown } from "./ResponseMarkdown.tsx";
2
+ import type { RecordedResponse } from "#browser/responses/schema.ts";
3
+ import { chosenLabels } from "#client/responses/summary.ts";
4
+ import { Age } from "./Age.tsx";
5
+ import { AnswerMarkdown } from "./AnswerMarkdown.tsx";
3
6
  import { shellIds } from "./shellIds.ts";
7
+
8
+ const KINDS = { "answer": "answered", "clarify": "asked back", "not_now": "said not now" } as const;
9
+
4
10
  export function ResponseHistory({
5
11
  request,
6
12
  id = shellIds.responseHistory,
@@ -11,34 +17,67 @@ export function ResponseHistory({
11
17
  const records = request?.responses ?? [];
12
18
  const preview = request?.preview;
13
19
  return (
14
- <section id={id}>
15
- <h2>Recorded responses</h2>
20
+ <section className="space-y-3 border-t pt-4" id={id}>
21
+ <h2 className="font-medium text-sm">Recorded responses</h2>
16
22
  {records.length === 0 ? (
17
- <p>No response is recorded for this source revision.</p>
23
+ <p className="text-muted-foreground text-sm">No response is recorded yet.</p>
18
24
  ) : (
19
- records.map((record) => <ResponseEntry current={record.response.question === preview?.id} key={record.id} record={record} />)
25
+ records.map((record) => (
26
+ <ResponseEntry current={record.response.question === preview?.id} key={record.id} record={record} request={request} />
27
+ ))
20
28
  )}
21
29
  </section>
22
30
  );
23
31
  }
24
- function ResponseEntry({ record, current }: { readonly record: import("#browser/responses/schema.ts").RecordedResponse; readonly current: boolean }) {
32
+ function ResponseEntry({
33
+ record,
34
+ current,
35
+ request,
36
+ }: {
37
+ readonly record: RecordedResponse;
38
+ readonly current: boolean;
39
+ readonly request: DocumentRequest | undefined;
40
+ }) {
41
+ const answers = record.response.answers;
42
+ const chosen = chosenLabels(request?.prompts ?? [], answers);
43
+ const notes = (answers ?? []).map((answer) => answer.text.trim()).filter(Boolean);
25
44
  return (
26
- <article id={record.id}>
27
- <h3>
28
- <a href={`/responses/${record.id}.md`}>
29
- {record.response.author} · {record.response.type}
45
+ <article className="space-y-2 rounded-lg bg-muted/50 px-4 py-3 text-sm" id={record.id}>
46
+ <h3 className="flex flex-wrap items-baseline gap-x-1.5 font-normal">
47
+ <a className="font-medium underline-offset-4 hover:underline" href={`/responses/${record.id}.md`}>
48
+ {record.response.author}
30
49
  </a>
50
+ <span className="text-muted-foreground">{KINDS[record.response.type]}</span>
51
+ <Age modified={record.response.recordedAt} short />
31
52
  </h3>
32
- <p>
53
+ {answers === undefined ? (
54
+ <AnswerMarkdown file={`responses/${record.id}.md`} source={record.body} />
55
+ ) : (
56
+ <>
57
+ <p>
58
+ <span className="text-muted-foreground">Chose </span>
59
+ <strong className="font-medium">{chosen || "no listed option"}</strong>
60
+ </p>
61
+ {notes.map((note) => (
62
+ <p className="whitespace-pre-wrap" key={note}>
63
+ {note}
64
+ </p>
65
+ ))}
66
+ <details className="text-muted-foreground text-xs">
67
+ <summary className="cursor-pointer select-none">Full response</summary>
68
+ <AnswerMarkdown className="mt-2 text-foreground text-sm" file={`responses/${record.id}.md`} source={record.body} />
69
+ </details>
70
+ </>
71
+ )}
72
+ <p className="text-muted-foreground text-xs" title={record.response.reviewedRevision}>
33
73
  {current ? "This reviewed revision" : "Earlier reviewed context — does not approve the current source"} ·{" "}
34
- <code>{record.response.reviewedRevision}</code>
74
+ <code>{record.response.reviewedRevision.slice(0, 12)}</code>
35
75
  </p>
36
76
  {Boolean(record.response.supersedes) && (
37
- <p>
77
+ <p className="text-muted-foreground text-xs">
38
78
  Explicitly supersedes <a href={`/responses/${record.response.supersedes}.md`}>{record.response.supersedes}</a>.
39
79
  </p>
40
80
  )}
41
- <ResponseMarkdown file={`responses/${record.id}.md`} source={record.body} />
42
81
  </article>
43
82
  );
44
83
  }
@@ -1,7 +1,7 @@
1
1
  import { useContext } from "react";
2
- import { routeQueryOptions } from "#client/page/routeQuery.ts";
3
2
  import type { MainLocation } from "./mainLocation.ts";
4
3
  import { ProjectionFrame } from "./ProjectionFrame.tsx";
4
+ import { routeQueryOptions } from "./routeQuery.ts";
5
5
  import { SessionContext } from "./SessionContext.ts";
6
6
  import type { ShellApi } from "./ShellApi.ts";
7
7
  import { SystemPage } from "./SystemPage.tsx";
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from "react";
1
2
  import type { InboxProjection } from "#browser/views/schema.ts";
2
3
  import { StatusChip } from "#client/react/components/ui/StatusChip.tsx";
3
4
  import { Age } from "./Age.tsx";
@@ -13,7 +14,9 @@ export function TodayNeeds({ api, data }: { readonly api: ShellApi; readonly dat
13
14
  <ul aria-label="Needs you" className="m-0 list-none divide-y rounded-lg border border-border p-0">
14
15
  {data.you.map((item) => (
15
16
  <li className="h-12 max-h-12" data-attention-id={item.id} key={`${item.file}:${item.href}`}>
16
- <TodayRow api={api} item={item} />
17
+ <TodayRow item={item} status="needs-you">
18
+ <TodayOptions api={api} item={item} />
19
+ </TodayRow>
17
20
  </li>
18
21
  ))}
19
22
  </ul>
@@ -22,13 +25,25 @@ export function TodayNeeds({ api, data }: { readonly api: ShellApi; readonly dat
22
25
  );
23
26
  }
24
27
 
25
- function TodayRow({ api, item }: { readonly api: ShellApi; readonly item: Item }) {
28
+ export function TodayLater({ items }: { readonly items: readonly Item[] }) {
26
29
  return (
27
- <div className="today-row flex h-full min-w-0 items-center gap-2 px-2" data-attention-row>
30
+ <ul aria-label="Later" className="m-0 list-none divide-y rounded-lg border border-border p-0">
31
+ {items.map((item) => (
32
+ <li className="h-12 max-h-12" key={item.file}>
33
+ <TodayRow item={item} status="deferred" />
34
+ </li>
35
+ ))}
36
+ </ul>
37
+ );
38
+ }
39
+
40
+ function TodayRow({ children, item, status }: { readonly children?: ReactNode; readonly item: Item; readonly status: "deferred" | "needs-you" }) {
41
+ return (
42
+ <div className="today-row flex h-full min-w-0 items-center gap-2 px-2" data-attention-row={status === "needs-you" ? "" : undefined}>
28
43
  <StatusChip
29
- aria-label="Needs you"
44
+ aria-label={status === "needs-you" ? "Needs you" : "Deferred"}
30
45
  className="shrink-0 max-sm:size-6 max-sm:justify-center max-sm:gap-0 max-sm:px-1 max-sm:text-[0px] max-sm:[&_svg]:shrink-0"
31
- status="needs-you"
46
+ status={status}
32
47
  />
33
48
  <div className="min-w-0 flex-1">
34
49
  <div className="flex min-w-0 items-baseline gap-2">
@@ -48,7 +63,7 @@ function TodayRow({ api, item }: { readonly api: ShellApi; readonly item: Item }
48
63
  <span className="shrink-0 text-[10px] text-muted-foreground tabular-nums">
49
64
  {item.modified === undefined ? "Age ?" : <Age modified={item.modified} short />}
50
65
  </span>
51
- <TodayOptions api={api} item={item} />
66
+ {children}
52
67
  </div>
53
68
  );
54
69
  }
@@ -0,0 +1,37 @@
1
+ import { optionText } from "#browser/responses/cardContext.ts";
2
+ import { Button } from "#client/react/components/ui/Button.tsx";
3
+
4
+ export function TodayOptionButton({
5
+ disabled,
6
+ onChoose,
7
+ option,
8
+ reason,
9
+ recommended,
10
+ source,
11
+ }: {
12
+ readonly disabled: boolean;
13
+ readonly onChoose: () => void;
14
+ readonly option: { readonly id: string; readonly source: string };
15
+ readonly reason: string;
16
+ readonly recommended: string | undefined;
17
+ readonly source: { readonly label: string; readonly description: string } | undefined;
18
+ }) {
19
+ const authored = optionText(option.id, option.source, reason);
20
+ const text = source?.label ?? authored.label;
21
+ const description = source?.description ?? authored.description;
22
+ return (
23
+ <Button
24
+ className="h-auto w-full flex-col items-start gap-0.5 whitespace-normal px-3 py-2 text-left"
25
+ disabled={disabled}
26
+ onClick={onChoose}
27
+ size="sm"
28
+ variant="outline"
29
+ >
30
+ <span>
31
+ {text}
32
+ {recommended === text ? " · Recommended" : null}
33
+ </span>
34
+ {description ? <span className="font-normal text-muted-foreground text-xs">{description}</span> : null}
35
+ </Button>
36
+ );
37
+ }
@@ -4,18 +4,20 @@ import { useContext, useMemo, useState } from "react";
4
4
  import { useAction, useQuery } from "@shivaedev/effect-react/result-state.ts";
5
5
  import type { Document, DocumentRequest } from "#browser/document/schema.ts";
6
6
  import { answerBody } from "#browser/responses/answerBody.ts";
7
- import { optionLabel } from "#browser/responses/cardContext.ts";
7
+ import { promptHeading } from "#browser/responses/cardContext.ts";
8
+ import { reasonMarkdown } from "#browser/responses/reasonMarkdown.ts";
8
9
  import type { InboxProjection } from "#browser/views/schema.ts";
9
- import { documentQuery, navigationQuery, routeQueryOptions } from "#client/page/routeQuery.ts";
10
- import { Button } from "#client/react/components/ui/Button.tsx";
11
10
  import { Popover } from "#client/react/components/ui/Popover.tsx";
12
11
  import { authorLabel } from "#client/responses/authorLabel.ts";
12
+ import { chosenLabels } from "#client/responses/summary.ts";
13
13
  import { saveFailure } from "#client/saveFailure.ts";
14
14
  import { fileUrl } from "#files/url.ts";
15
15
  import { responseUrl } from "#responses/url.ts";
16
16
  import { createResponse } from "./createResponse.ts";
17
17
  import { useFormClient } from "./FormContext.ts";
18
+ import { documentQuery, navigationQuery, routeQueryOptions } from "./routeQuery.ts";
18
19
  import type { ShellApi } from "./ShellApi.ts";
20
+ import { TodayOptionButton } from "./TodayOptionButton.tsx";
19
21
 
20
22
  export function TodayOptions({ api, item }: { readonly api: ShellApi; readonly item: InboxProjection["you"][number] }) {
21
23
  const query = useQuery(documentQuery(api, fileUrl(item.file)), { keepPreviousData: true });
@@ -28,11 +30,11 @@ export function TodayOptions({ api, item }: { readonly api: ShellApi; readonly i
28
30
  && !request.promptUnavailable
29
31
  && request.prompts?.length === 1
30
32
  && request.prompts[0]?.mode === "one" ? (
31
- <Popover label={`Answer ${item.title}`} trigger="Answer" triggerClassName="h-8 w-auto px-1 text-xs">
32
- <a className="mb-3 block text-xs" href={item.href}>
33
- Open full question
34
- </a>
33
+ <Popover className="w-80" label={`Answer ${item.title}`} trigger="Answer" triggerClassName="h-8 w-auto px-1 text-xs">
35
34
  <InlineOptions api={api} document={data} key={request.preview.reviewedRevision} request={request} />
35
+ <a className="mt-3 block border-t px-2 pt-2 text-muted-foreground text-xs hover:text-foreground" href={item.href}>
36
+ Open the full question with its context →
37
+ </a>
36
38
  </Popover>
37
39
  ) : (
38
40
  <a aria-label={`Answer ${item.title}`} className="shrink-0 text-xs" href={item.href}>
@@ -78,7 +80,9 @@ function InlineOptions({ api, document: data, request }: { readonly api: ShellAp
78
80
  const action = useAction(actionAtom);
79
81
  const failure = Option.getOrUndefined(Option.flatMap(action.cause, Cause.findErrorOption));
80
82
  const saved = Option.getOrUndefined(action.data);
81
- let status = saved ? `Saved: responses/${saved.id}.md. This records feedback, not acceptance.` : "";
83
+ let status = saved
84
+ ? `Saved: your answer “${chosenLabels(request.prompts ?? [], saved.answers)}” is recorded. The agent reads it the next time it checks.`
85
+ : "";
82
86
  if (failure) {
83
87
  status = typeof failure === "string" ? failure : saveFailure(failure, "response").text;
84
88
  }
@@ -86,33 +90,37 @@ function InlineOptions({ api, document: data, request }: { readonly api: ShellAp
86
90
  status = "Pending: registering reviewed context, then recording response…";
87
91
  }
88
92
  const prompt = request.prompts?.[0];
93
+ const reason = reasonMarkdown(request.preview?.reason ?? "").split("\n")[0];
89
94
  return (
90
- <fieldset aria-label={`Options for ${data.identity}`} className="m-0 min-w-0 space-y-2 border-0 px-2 pb-2">
91
- <div className="flex flex-wrap gap-2">
92
- {prompt?.options.map((option, index) => {
93
- const text = data.metadata.options?.[index]?.label ?? optionLabel(option.id, option.source, request.preview?.reason ?? "");
94
- return (
95
- <Button
96
- disabled={!(data.writable && author.trim()) || action.pending || Boolean(saved)}
97
- key={option.id}
98
- onClick={() => action.dispatch(option.id)}
99
- size="sm"
100
- variant="outline"
101
- >
102
- {text}
103
- {data.metadata.recommended === text && " · Recommended"}
104
- </Button>
105
- );
106
- })}
95
+ <fieldset aria-label={`Options for ${data.identity}`} className="m-0 min-w-0 space-y-3 border-0 px-2 pb-1">
96
+ {Boolean(reason) && <p className="m-0 font-medium text-sm">{reason}</p>}
97
+ {prompt ? <p className="m-0 text-muted-foreground text-xs">{promptHeading(prompt.source).title ?? "Choose one option."}</p> : null}
98
+ <div className="grid gap-2">
99
+ {prompt?.options.map((option, index) => (
100
+ <TodayOptionButton
101
+ disabled={!(data.writable && author.trim()) || action.pending || Boolean(saved)}
102
+ key={option.id}
103
+ onChoose={() => action.dispatch(option.id)}
104
+ option={option}
105
+ reason={request.preview?.reason ?? ""}
106
+ recommended={data.metadata.recommended}
107
+ source={data.metadata.options?.[index]}
108
+ />
109
+ ))}
107
110
  </div>
108
111
  <details className="text-muted-foreground text-xs">
109
112
  <summary className="cursor-pointer">Answer as {author || "…"}</summary>
110
- <label>
113
+ <label className="mt-2 flex items-center gap-2">
111
114
  Local author label{" "}
112
- <input className="rounded border px-2 py-1" maxLength={200} onChange={(event) => setAuthor(event.currentTarget.value)} value={author} />
115
+ <input
116
+ className="rounded border bg-background px-2 py-1"
117
+ maxLength={200}
118
+ onChange={(event) => setAuthor(event.currentTarget.value)}
119
+ value={author}
120
+ />
113
121
  </label>
114
122
  </details>
115
- {!data.writable && <p className="m-0 text-muted-foreground text-xs">Read-only. Enable responses to record an answer.</p>}
123
+ {!data.writable && <p className="m-0 text-muted-foreground text-xs">This Board is read-only. Start it with --responses to record answers.</p>}
116
124
  {Boolean(status) && (
117
125
  <p className="m-0 text-xs" role="status">
118
126
  {status}
@@ -1,15 +1,15 @@
1
1
  import { Option } from "effect";
2
2
  import { useQuery } from "@shivaedev/effect-react/result-state.ts";
3
3
  import type { InboxProjection } from "#browser/views/schema.ts";
4
- import { navigationQuery, routeQueryOptions } from "#client/page/routeQuery.ts";
5
4
  import { fileUrl } from "#files/url.ts";
6
5
  import { inProgressItems } from "./inProgressItems.ts";
6
+ import { navigationQuery, routeQueryOptions } from "./routeQuery.ts";
7
7
  import type { ShellApi } from "./ShellApi.ts";
8
8
  import { TodayGoals } from "./TodayGoals.tsx";
9
- import { TodayNeeds } from "./TodayNeeds.tsx";
9
+ import { TodayLater, TodayNeeds } from "./TodayNeeds.tsx";
10
10
  import { TodayProgress } from "./TodayProgress.tsx";
11
11
 
12
- const todayIds = { goals: "today-goals", needs: "needs-you", progress: "in-progress" };
12
+ const todayIds = { goals: "today-goals", later: "later", needs: "needs-you", progress: "in-progress" };
13
13
 
14
14
  export function TodayPage({ api, data }: { readonly api: ShellApi; readonly data: InboxProjection }) {
15
15
  const query = useQuery(api.goals.query(undefined, routeQueryOptions), { keepPreviousData: true });
@@ -43,6 +43,7 @@ export function TodayPage({ api, data }: { readonly api: ShellApi; readonly data
43
43
  <h2 className="mt-0 mb-3 font-semibold text-lg">In progress</h2>
44
44
  <TodayProgress data={fleet} failed={Option.isSome(fleetQuery.cause)} />
45
45
  </section>
46
+ {data.complete && data.later.length > 0 && <Later items={data.later} />}
46
47
  </div>
47
48
  <section className="min-w-0" id={todayIds.goals}>
48
49
  <h2 className="mt-0 mb-3 font-semibold text-lg">Goals</h2>
@@ -67,3 +68,17 @@ function NeedsYou({ api, data }: { readonly api: ShellApi; readonly data: InboxP
67
68
  </section>
68
69
  );
69
70
  }
71
+
72
+ function Later({ items }: { readonly items: InboxProjection["later"] }) {
73
+ return (
74
+ <section className="min-w-0" id={todayIds.later}>
75
+ <details>
76
+ <summary className="mb-3 cursor-pointer">
77
+ <h2 className="m-0 inline font-semibold text-lg">Later</h2>
78
+ <span className="ml-2 text-muted-foreground text-sm tabular-nums">{items.length}</span>
79
+ </summary>
80
+ <TodayLater items={items} />
81
+ </details>
82
+ </section>
83
+ );
84
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Cause } from "effect";
2
2
  import * as Atom from "effect/reactivity/Atom";
3
- import { resolvedDocumentFailure } from "#client/page/resolvedDocumentFailure.ts";
4
3
  import { pageQueries } from "./pageQueries.ts";
4
+ import { resolvedDocumentFailure } from "./resolvedDocumentFailure.ts";
5
5
  import type { ShellApi } from "./ShellApi.ts";
6
6
 
7
7
  function health(value: { readonly _tag: string; readonly waiting: boolean; readonly cause?: Cause.Cause<unknown> }) {
@@ -219,12 +219,6 @@
219
219
  }
220
220
  }
221
221
 
222
- .answer-cards > .response-panel {
223
- max-width: 72ch;
224
- padding: 1rem;
225
- margin: 0;
226
- }
227
-
228
222
  @layer components {
229
223
  figure.diagram {
230
224
  @apply mx-0;
@@ -2,13 +2,13 @@ import { isRedirect, type LoaderFnContext, redirect } from "@tanstack/react-rout
2
2
  import { Predicate } from "effect";
3
3
  import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
4
4
  import { loadQuery } from "@shivaedev/effect-react/router.ts";
5
- import { documentQuery, navigationQuery } from "#client/page/routeQuery.ts";
6
5
  import type { ShellOptions } from "./BoardShell.tsx";
7
6
  import { documentRoute } from "./documentRoute.ts";
8
7
  import { loadProjection } from "./loadProjection.ts";
9
8
  import { loadRemaining } from "./loadRemaining.ts";
10
9
  import { projectionRoute } from "./projectionRoute.ts";
11
10
  import { remainingRoute } from "./remainingRoute.ts";
11
+ import { documentQuery, navigationQuery } from "./routeQuery.ts";
12
12
 
13
13
  export type RouterShell = ShellOptions & { readonly registry: AtomRegistry.AtomRegistry };
14
14
 
@@ -1,8 +1,8 @@
1
1
  import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
2
2
  import { loadQuery } from "@shivaedev/effect-react/router.ts";
3
- import { routeQueryOptions } from "#client/page/routeQuery.ts";
4
3
  import { pageQueries } from "./pageQueries.ts";
5
4
  import type { ProjectionRoute } from "./projectionRoute.ts";
5
+ import { routeQueryOptions } from "./routeQuery.ts";
6
6
  import type { ShellApi } from "./ShellApi.ts";
7
7
  export function loadProjection(
8
8
  registry: AtomRegistry.AtomRegistry,
@@ -1,7 +1,7 @@
1
1
  import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
2
2
  import { loadQuery } from "@shivaedev/effect-react/router.ts";
3
- import { routeQueryOptions } from "#client/page/routeQuery.ts";
4
3
  import type { RemainingRoute } from "./remainingRoute.ts";
4
+ import { routeQueryOptions } from "./routeQuery.ts";
5
5
  import type { ShellApi } from "./ShellApi.ts";
6
6
  export function loadRemaining(
7
7
  registry: AtomRegistry.AtomRegistry,
@@ -1,6 +1,6 @@
1
1
  import type * as AsyncResult from "effect/reactivity/AsyncResult";
2
2
  import type * as Atom from "effect/reactivity/Atom";
3
- import { routeQueryOptions } from "#client/page/routeQuery.ts";
3
+ import { routeQueryOptions } from "./routeQuery.ts";
4
4
  import type { ShellApi } from "./ShellApi.ts";
5
5
 
6
6
  export function pageQueries(api: ShellApi, kind: string, url: string): readonly Atom.Atom<AsyncResult.AsyncResult<unknown, unknown>>[] {
@@ -11,7 +11,7 @@ import { authorLabel } from "#client/responses/authorLabel.ts";
11
11
  import { type ResponseInput, responseFormSchema } from "#client/responses/formSchema.ts";
12
12
  import { responsePreview } from "#client/responses/preview.ts";
13
13
  import { retainContext } from "#client/responses/retainContext.ts";
14
- import { responseFeedback, responseSummary } from "#client/responses/summary.ts";
14
+ import { chosenLabels, responseFeedback, responseSummary } from "#client/responses/summary.ts";
15
15
  import { sourceChanged } from "#client/sourceChanged.ts";
16
16
  import { createResponse } from "./createResponse.ts";
17
17
  import { useFormClient } from "./FormContext.ts";
@@ -116,7 +116,9 @@ export function useResponseForm(source: QuestionPreview, prompts: readonly Promp
116
116
  setInteraction("cleared");
117
117
  }
118
118
  const previewed = preview !== undefined && same && Equal.equals(preview, values);
119
- const { outcome, text: status } = formStatus(created, interaction, "response", (saved) => responseSaved(saved.id, created.dirty));
119
+ const { outcome, text: status } = formStatus(created, interaction, "response", (saved) =>
120
+ responseSaved(saved.id, created.dirty, chosenLabels(prompts, saved.answers)),
121
+ );
120
122
  const summary = responseSummary(values, prompts, answers(values), attempted);
121
123
  return {
122
124
  answers: answers(values),