@shivaedev/work-board 0.16.0 → 0.17.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 (197) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +4 -4
  3. package/assets/react.js +52 -39
  4. package/dist/browser/views/remaining.d.ts +0 -240
  5. package/dist/browser/views/remaining.d.ts.map +1 -1
  6. package/dist/browser/views/remaining.js +0 -2
  7. package/dist/browser/views/remaining.js.map +1 -1
  8. package/dist/client/react/DocumentAttention.d.ts.map +1 -1
  9. package/dist/client/react/DocumentAttention.js +2 -3
  10. package/dist/client/react/DocumentAttention.js.map +1 -1
  11. package/dist/client/react/ProjectionFrame.js +1 -1
  12. package/dist/client/react/ProjectionFrame.js.map +1 -1
  13. package/dist/client/react/RemainingMain.d.ts.map +1 -1
  14. package/dist/client/react/RemainingMain.js +0 -3
  15. package/dist/client/react/RemainingMain.js.map +1 -1
  16. package/dist/client/react/ResultPage.d.ts.map +1 -1
  17. package/dist/client/react/ResultPage.js +2 -1
  18. package/dist/client/react/ResultPage.js.map +1 -1
  19. package/dist/client/react/ShellApi.d.ts +0 -1
  20. package/dist/client/react/ShellApi.d.ts.map +1 -1
  21. package/dist/client/react/TodayOptions.d.ts.map +1 -1
  22. package/dist/client/react/TodayOptions.js +2 -2
  23. package/dist/client/react/TodayOptions.js.map +1 -1
  24. package/dist/client/react/entry.js +1 -1
  25. package/dist/client/react/entry.js.map +1 -1
  26. package/dist/client/react/loadBoardPage.d.ts.map +1 -1
  27. package/dist/client/react/loadBoardPage.js +5 -13
  28. package/dist/client/react/loadBoardPage.js.map +1 -1
  29. package/dist/client/react/loadRemaining.d.ts +109 -349
  30. package/dist/client/react/loadRemaining.d.ts.map +1 -1
  31. package/dist/client/react/loadRemaining.js +0 -2
  32. package/dist/client/react/loadRemaining.js.map +1 -1
  33. package/dist/client/react/pageQueries.d.ts.map +1 -1
  34. package/dist/client/react/pageQueries.js +0 -3
  35. package/dist/client/react/pageQueries.js.map +1 -1
  36. package/dist/client/react/readerLink.js +1 -1
  37. package/dist/client/react/readerLink.js.map +1 -1
  38. package/dist/client/react/remainingRoute.d.ts +1 -1
  39. package/dist/client/react/remainingRoute.d.ts.map +1 -1
  40. package/dist/client/react/remainingRoute.js +0 -2
  41. package/dist/client/react/remainingRoute.js.map +1 -1
  42. package/dist/responses/url.js +1 -1
  43. package/dist/responses/url.js.map +1 -1
  44. package/dist/rpc/PageUrl.d.ts.map +1 -1
  45. package/dist/rpc/PageUrl.js +0 -1
  46. package/dist/rpc/PageUrl.js.map +1 -1
  47. package/dist/rpc/contract.d.ts +2203 -2926
  48. package/dist/rpc/contract.d.ts.map +1 -1
  49. package/dist/rpc/contract.js +6 -9
  50. package/dist/rpc/contract.js.map +1 -1
  51. package/dist/rpc/responseContract.d.ts +56 -56
  52. package/dist/server/{responses/service.d.ts → Responses.d.ts} +9 -21
  53. package/dist/server/Responses.d.ts.map +1 -0
  54. package/dist/server/{responses/service.js → Responses.js} +22 -22
  55. package/dist/server/Responses.js.map +1 -0
  56. package/dist/server/attention/inbox.d.ts +2 -0
  57. package/dist/server/attention/inbox.d.ts.map +1 -1
  58. package/dist/server/attention/inbox.js +1 -0
  59. package/dist/server/attention/inbox.js.map +1 -1
  60. package/dist/server/attention/projection.js +1 -1
  61. package/dist/server/attention/projection.js.map +1 -1
  62. package/dist/server/attention/requestInbox.d.ts +3 -0
  63. package/dist/server/attention/requestInbox.d.ts.map +1 -1
  64. package/dist/server/attention/requestInbox.js +1 -0
  65. package/dist/server/attention/requestInbox.js.map +1 -1
  66. package/dist/server/boardRuntime.d.ts +2 -2
  67. package/dist/server/boardRuntime.d.ts.map +1 -1
  68. package/dist/server/boardRuntime.js +12 -16
  69. package/dist/server/boardRuntime.js.map +1 -1
  70. package/dist/server/boardServices.d.ts +3 -4
  71. package/dist/server/boardServices.d.ts.map +1 -1
  72. package/dist/server/boardServices.js +5 -4
  73. package/dist/server/boardServices.js.map +1 -1
  74. package/dist/server/boardSocket.d.ts +1 -1
  75. package/dist/server/boardSocket.d.ts.map +1 -1
  76. package/dist/server/document/read.d.ts +4 -10
  77. package/dist/server/document/read.d.ts.map +1 -1
  78. package/dist/server/document/read.js +5 -5
  79. package/dist/server/document/read.js.map +1 -1
  80. package/dist/server/document/requests.d.ts.map +1 -1
  81. package/dist/server/document/requests.js +1 -2
  82. package/dist/server/document/requests.js.map +1 -1
  83. package/dist/server/handoffs/Service.d.ts +1 -2
  84. package/dist/server/handoffs/Service.d.ts.map +1 -1
  85. package/dist/server/handoffs/Service.js +14 -13
  86. package/dist/server/handoffs/Service.js.map +1 -1
  87. package/dist/server/http/assets.d.ts +0 -3
  88. package/dist/server/http/assets.d.ts.map +1 -1
  89. package/dist/server/http/assets.js +1 -7
  90. package/dist/server/http/assets.js.map +1 -1
  91. package/dist/server/http/boardRoutes.d.ts.map +1 -1
  92. package/dist/server/http/boardRoutes.js +3 -3
  93. package/dist/server/http/boardRoutes.js.map +1 -1
  94. package/dist/server/integration/responses.d.ts +207 -4
  95. package/dist/server/integration/responses.d.ts.map +1 -1
  96. package/dist/server/integration/responses.js +4 -2
  97. package/dist/server/integration/responses.js.map +1 -1
  98. package/dist/server/mcp/client.d.ts +2 -242
  99. package/dist/server/mcp/client.d.ts.map +1 -1
  100. package/dist/server/mcp/queries.d.ts +24 -744
  101. package/dist/server/mcp/queries.d.ts.map +1 -1
  102. package/dist/server/mcp/queries.js +3 -3
  103. package/dist/server/mcp/queries.js.map +1 -1
  104. package/dist/server/packageInfo.d.ts +1 -1
  105. package/dist/server/packageInfo.js +1 -1
  106. package/dist/server/responses/readingActions.d.ts +10 -1
  107. package/dist/server/responses/readingActions.d.ts.map +1 -1
  108. package/dist/server/responses/readingActions.js +2 -4
  109. package/dist/server/responses/readingActions.js.map +1 -1
  110. package/dist/server/responses/registerQuestion.d.ts +2 -2
  111. package/dist/server/responses/registerQuestion.d.ts.map +1 -1
  112. package/dist/server/responses/registerQuestion.js +3 -3
  113. package/dist/server/responses/registerQuestion.js.map +1 -1
  114. package/dist/server/responses/retry.d.ts +2 -2
  115. package/dist/server/responses/retry.d.ts.map +1 -1
  116. package/dist/server/responses/retry.js +1 -1
  117. package/dist/server/responses/retry.js.map +1 -1
  118. package/dist/server/rpc/HandlerOptions.d.ts +0 -8
  119. package/dist/server/rpc/HandlerOptions.d.ts.map +1 -1
  120. package/dist/server/rpc/handlers.d.ts +1 -1
  121. package/dist/server/rpc/handlers.d.ts.map +1 -1
  122. package/dist/server/rpc/handlers.js +14 -13
  123. package/dist/server/rpc/handlers.js.map +1 -1
  124. package/dist/server/rpc/hints.d.ts +3 -5
  125. package/dist/server/rpc/hints.d.ts.map +1 -1
  126. package/dist/server/rpc/hints.js +5 -1
  127. package/dist/server/rpc/hints.js.map +1 -1
  128. package/dist/server/views/fleetItems.d.ts +2 -2
  129. package/dist/server/views/fleetItems.d.ts.map +1 -1
  130. package/dist/server/views/fleetItems.js +8 -3
  131. package/dist/server/views/fleetItems.js.map +1 -1
  132. package/dist/server/views/projectionQueries.d.ts +5 -9
  133. package/dist/server/views/projectionQueries.d.ts.map +1 -1
  134. package/dist/server/views/projectionQueries.js +6 -6
  135. package/dist/server/views/projectionQueries.js.map +1 -1
  136. package/dist/server/views/remainingQueries.d.ts +4 -252
  137. package/dist/server/views/remainingQueries.d.ts.map +1 -1
  138. package/dist/server/views/remainingQueries.js +11 -19
  139. package/dist/server/views/remainingQueries.js.map +1 -1
  140. package/dist/server/workspace.d.ts +222 -0
  141. package/dist/server/workspace.d.ts.map +1 -0
  142. package/dist/server/workspace.js +25 -0
  143. package/dist/server/workspace.js.map +1 -0
  144. package/package.json +3 -3
  145. package/src/browser/views/remaining.ts +0 -3
  146. package/src/client/react/DocumentAttention.tsx +2 -3
  147. package/src/client/react/ProjectionFrame.tsx +1 -1
  148. package/src/client/react/RemainingMain.tsx +0 -10
  149. package/src/client/react/ResultPage.tsx +2 -2
  150. package/src/client/react/ShellApi.ts +0 -1
  151. package/src/client/react/TodayOptions.tsx +2 -2
  152. package/src/client/react/entry.tsx +1 -1
  153. package/src/client/react/loadBoardPage.ts +5 -14
  154. package/src/client/react/loadRemaining.ts +0 -2
  155. package/src/client/react/pageQueries.ts +0 -3
  156. package/src/client/react/readerLink.ts +1 -1
  157. package/src/client/react/remainingRoute.ts +0 -2
  158. package/src/responses/url.ts +1 -1
  159. package/src/rpc/PageUrl.ts +0 -1
  160. package/src/rpc/contract.ts +6 -9
  161. package/src/server/{responses/service.ts → Responses.ts} +21 -29
  162. package/src/server/attention/inbox.ts +1 -0
  163. package/src/server/attention/projection.ts +1 -1
  164. package/src/server/attention/requestInbox.ts +1 -0
  165. package/src/server/boardRuntime.ts +20 -25
  166. package/src/server/boardServices.ts +7 -7
  167. package/src/server/document/read.ts +6 -14
  168. package/src/server/document/requests.ts +1 -2
  169. package/src/server/handoffs/Service.ts +16 -19
  170. package/src/server/http/assets.ts +1 -7
  171. package/src/server/http/boardRoutes.ts +3 -3
  172. package/src/server/integration/responses.ts +5 -3
  173. package/src/server/mcp/queries.ts +3 -3
  174. package/src/server/packageInfo.ts +1 -1
  175. package/src/server/responses/readingActions.ts +3 -3
  176. package/src/server/responses/registerQuestion.ts +5 -5
  177. package/src/server/responses/retry.ts +3 -3
  178. package/src/server/rpc/HandlerOptions.ts +0 -8
  179. package/src/server/rpc/handlers.ts +17 -15
  180. package/src/server/rpc/hints.ts +6 -3
  181. package/src/server/views/fleetItems.ts +8 -4
  182. package/src/server/views/projectionQueries.ts +6 -7
  183. package/src/server/views/remainingQueries.ts +11 -31
  184. package/src/server/workspace.ts +41 -0
  185. package/assets/native.js +0 -37
  186. package/dist/client/entry.d.ts +0 -3
  187. package/dist/client/entry.d.ts.map +0 -1
  188. package/dist/client/entry.js +0 -3
  189. package/dist/client/entry.js.map +0 -1
  190. package/dist/client/react/RespondPage.d.ts +0 -5
  191. package/dist/client/react/RespondPage.d.ts.map +0 -1
  192. package/dist/client/react/RespondPage.js +0 -5
  193. package/dist/client/react/RespondPage.js.map +0 -1
  194. package/dist/server/responses/service.d.ts.map +0 -1
  195. package/dist/server/responses/service.js.map +0 -1
  196. package/src/client/entry.ts +0 -3
  197. package/src/client/react/RespondPage.tsx +0 -8
@@ -9,26 +9,27 @@ import { handoffFrom, handoffMarkdown, handoffPrompt, matchesHandoff } from "#se
9
9
  import { handoffSource } from "#server/handoffs/source.ts";
10
10
  import { publish } from "#server/responses/publish.ts";
11
11
  import { revisionOf } from "#server/responses/records.ts";
12
- import { ResponseConfig } from "#server/responses/service.ts";
12
+ import { Workspace } from "#server/workspace.ts";
13
13
 
14
14
  export const HandoffsService = defineService({
15
15
  id: "WorkBoard/HandoffsService",
16
16
  initialize: Effect.gen(function* () {
17
- return { fs: yield* FileSystem.FileSystem, lock: yield* Semaphore.make(1), options: yield* ResponseConfig };
17
+ const workspace = yield* Workspace;
18
+ return { fs: yield* FileSystem.FileSystem, lock: yield* Semaphore.make(1), options: yield* workspace.settings(), workspace };
18
19
  }),
19
- methods: ({ options, fs, lock }) => {
20
- const snapshot = options.index.pipe(
20
+ methods: ({ options, fs, lock, workspace }) => {
21
+ const snapshot = workspace.index().pipe(
21
22
  rpcBoundary(ServiceUnavailable, {
22
23
  message: "The workspace could not be indexed.",
23
24
  unexpected: (message) => new ServiceUnavailable({ message }),
24
25
  }),
25
26
  );
26
- const locate = handoffSource(options.root, options.changes.realRoot, snapshot, fs);
27
+ const locate = handoffSource(options.root, options.realRoot, snapshot, fs);
27
28
  const read = Effect.fn("WorkBoard.read")(function* (item: string) {
28
29
  const data = yield* snapshot;
29
30
  const unknown = [...data.unavailable];
30
31
  const records = data.documents.flatMap((document) => {
31
- const record = handoffFrom(document, options.changes.realRoot);
32
+ const record = handoffFrom(document, options.realRoot);
32
33
  if (
33
34
  (record && data.model.ids.get(record.id)?.length !== 1)
34
35
  || (document.parsed.fields.kind === "handoff" && !record)
@@ -45,7 +46,7 @@ export const HandoffsService = defineService({
45
46
  if (!record || record.file !== `handoffs/${input.id}.md` || !matchesHandoff(record, input) || revisionOf(record.context) !== input.revision) {
46
47
  return yield* Effect.fail(new Conflict({ message: "This handoff identity moved, is duplicated or records different direction." }));
47
48
  }
48
- const path = yield* markdownPath(options.root, options.changes.realRoot, record.file);
49
+ const path = yield* markdownPath(options.root, options.realRoot, record.file);
49
50
  if (!path) {
50
51
  return yield* Effect.fail(new NotFound({ message: "The prepared handoff moved or disappeared." }));
51
52
  }
@@ -55,20 +56,20 @@ export const HandoffsService = defineService({
55
56
  unexpected: (message) => new ServiceUnavailable({ message }),
56
57
  }),
57
58
  );
58
- yield* publish(options.root, options.changes.realRoot, record.id, raw, "handoffs");
59
+ yield* publish(options.root, options.realRoot, record.id, raw, "handoffs");
59
60
  return record;
60
61
  });
61
62
 
62
63
  function prepare(input: HandoffInput) {
63
64
  return lock.withPermit(
64
65
  Effect.gen(function* () {
65
- if (!options.enabled) {
66
+ if (!options.writable) {
66
67
  return yield* Effect.fail(new Forbidden({ message: "Restart with --responses to enable local response/handoff writes." }));
67
68
  }
68
69
  const data = yield* snapshot;
69
70
  const existing = data.model.ids.get(input.id);
70
71
  if (existing?.length) {
71
- const record = existing.length === 1 ? handoffFrom(existing[0], options.changes.realRoot) : undefined;
72
+ const record = existing.length === 1 ? handoffFrom(existing[0], options.realRoot) : undefined;
72
73
  return yield* reconcile(record, input);
73
74
  }
74
75
  const current = yield* locate(input.item);
@@ -91,21 +92,17 @@ export const HandoffsService = defineService({
91
92
  state: "requested",
92
93
  },
93
94
  id: input.id,
94
- prompt: handoffPrompt(join(options.changes.realRoot, file)),
95
+ prompt: handoffPrompt(join(options.realRoot, file)),
95
96
  };
96
- yield* publish(
97
- options.root,
98
- options.changes.realRoot,
99
- record.id,
100
- handoffMarkdown(record.id, record.handoff, record.context),
101
- "handoffs",
102
- ).pipe(Effect.ensuring(options.changes.refresh ?? Effect.void));
97
+ yield* publish(options.root, options.realRoot, record.id, handoffMarkdown(record.id, record.handoff, record.context), "handoffs").pipe(
98
+ Effect.ensuring(options.changes.refresh ?? Effect.void),
99
+ );
103
100
  return record;
104
101
  }),
105
102
  );
106
103
  }
107
104
  return { locate, prepare, read };
108
105
  },
109
- requires: [ResponseConfig, FileSystem.FileSystem, Path.Path],
106
+ requires: [Workspace, FileSystem.FileSystem, Path.Path],
110
107
  });
111
108
  export type HandoffsService = typeof HandoffsService.Identifier;
@@ -35,7 +35,6 @@ export function mermaidFile(root: string) {
35
35
 
36
36
  export const MERMAID_ROUTE: HttpRouter.PathInput = `${MERMAID}*`;
37
37
 
38
- export const NATIVE_ROUTE: HttpRouter.PathInput = "/_board/native.js";
39
38
  function missingBundle(file: string) {
40
39
  return `Work Board browser bundle assets/${file} is missing. Run pnpm --filter @shivaedev/work-board build:browser from the repository root to build it.`;
41
40
  }
@@ -57,16 +56,11 @@ const checkAssets = Effect.fn("WorkBoard.checkAssets")(function* (files: readonl
57
56
  }
58
57
  });
59
58
 
60
- export function checkNativeAsset() {
61
- return checkAssets(["native.js"]);
62
- }
63
59
  export function checkBrowserAssets() {
64
- return checkAssets(["native.js", "react.js", "react.css"]);
60
+ return checkAssets(["react.js", "react.css"]);
65
61
  }
66
- export const nativeAsset = bundledAsset("native.js", "text/javascript");
67
62
 
68
63
  const BUNDLED_ASSETS: ReadonlyArray<readonly [HttpRouter.PathInput, ReturnType<typeof bundledAsset>]> = [
69
- [NATIVE_ROUTE, nativeAsset],
70
64
  ["/_board/react.js", bundledAsset("react.js", "text/javascript")],
71
65
  ["/_board/react.css", bundledAsset("react.css", "text/css")],
72
66
  ];
@@ -2,13 +2,13 @@ import { Effect, type FileSystem, type Path, type Scope } from "effect";
2
2
  import { HttpRouter, type HttpServerRequest, HttpServerResponse } from "effect/http";
3
3
  import { loopbackOnly } from "@shivaedev/platform/rpc-server/local.ts";
4
4
  import type { Diagnostics } from "#diagnostics/source.ts";
5
+ import { responseUrl } from "#responses/url.ts";
5
6
  import type { BoardOptions } from "#server/board.ts";
6
7
  import type { boardRuntime } from "#server/boardRuntime.ts";
7
8
  import type { HandoffsService } from "#server/handoffs/Service.ts";
8
- import { identityUrl } from "#server/metadata/links.ts";
9
9
  import { shell } from "#server/page/shell.ts";
10
+ import { Responses } from "#server/Responses.ts";
10
11
  import type { Highlighter } from "#server/render/Highlighter.ts";
11
- import { Responses } from "#server/responses/service.ts";
12
12
  import { ASSET_HANDLERS, MERMAID_ROUTE, mermaidFile } from "./assets.ts";
13
13
  import { attachment } from "./attachment.ts";
14
14
  import { identity } from "./identity.ts";
@@ -47,7 +47,7 @@ export function boardRoutes(options: BoardOptions, runtime: Effect.Success<Retur
47
47
  const params = new URL(request.url, "http://127.0.0.1").searchParams;
48
48
  const service = yield* Responses;
49
49
  const preview = yield* service.locate(params.get("item") ?? "", params.get("request") ?? "");
50
- return HttpServerResponse.redirect(`${identityUrl(preview.item)}#answer-card-${encodeURIComponent(preview.request)}`);
50
+ return HttpServerResponse.redirect(responseUrl(preview.item, preview.request));
51
51
  }).pipe(Effect.catch(() => Effect.succeed(respond("Response unavailable", "text/html", 409)))),
52
52
  );
53
53
  for (const view of ["system", "start", "handoff", "result", "library", "goals", "work"] as const) {
@@ -5,13 +5,14 @@ import {
5
5
  RecordedResponse as RecordedResponseSchema,
6
6
  } from "#browser/responses/schema.ts";
7
7
  import { currentResponses } from "#responses/current.ts";
8
+ import { Responses } from "#server/Responses.ts";
8
9
  import { makeResponse } from "#server/responses/make.ts";
9
10
  import { publish } from "#server/responses/publish.ts";
10
11
  import { readResponses } from "#server/responses/read.ts";
11
12
  import { questionFrom, questionId, questionMarkdown, responseMarkdown, revisionOf } from "#server/responses/records.ts";
12
- import { type ResponseOptions as InternalResponseOptions, ResponseConfig, Responses } from "#server/responses/service.ts";
13
+ import { type WorkspaceOptions as InternalWorkspaceOptions, Workspace, WorkspaceConfig } from "#server/workspace.ts";
13
14
 
14
- export type ResponseOptions = InternalResponseOptions;
15
+ export type WorkspaceOptions = InternalWorkspaceOptions;
15
16
  export type DraftInput = typeof DraftInputSchema.Type;
16
17
  export type Question = typeof QuestionSchema.Type;
17
18
  export type Reading = typeof ReadingSchema.Type;
@@ -28,9 +29,10 @@ export const responses = {
28
29
  questionMarkdown,
29
30
  Reading: ReadingSchema,
30
31
  RecordedResponse: RecordedResponseSchema,
31
- ResponseConfig,
32
32
  Responses,
33
33
  readResponses,
34
34
  responseMarkdown,
35
35
  revisionOf,
36
+ Workspace,
37
+ WorkspaceConfig,
36
38
  } as const;
@@ -2,6 +2,7 @@ import { Clock, Effect } from "effect";
2
2
  import { cardStatus } from "#browser/views/now.ts";
3
3
  import type { InboxProjection } from "#browser/views/schema.ts";
4
4
  import { fileUrl } from "#files/url.ts";
5
+ import { responseUrl } from "#responses/url.ts";
5
6
  import { identityUrl } from "#server/metadata/links.ts";
6
7
  import type { BoardClient } from "./client.ts";
7
8
 
@@ -11,12 +12,11 @@ export const needsYou = Effect.fn("BoardMcp.needsYou")(function* (client: BoardC
11
12
  const you = yield* Effect.forEach(inbox.you, (row) =>
12
13
  Effect.gen(function* () {
13
14
  const document = yield* client("work-board.document", { url: fileUrl(row.file) });
14
- const request = new URL(row.href, "http://localhost").searchParams.get("request");
15
- const decision = document.requests.find((entry) => entry.id === request);
15
+ const decision = document.requests.find((entry) => responseUrl(document.metadata.id ?? "", entry.id) === row.href);
16
16
  return {
17
17
  ...row,
18
18
  ageMs: row.modified === undefined ? null : Math.max(0, now - row.modified),
19
- options: (request ? decision?.options : document.metadata.options) ?? [],
19
+ options: (decision ? decision.options : document.metadata.options) ?? [],
20
20
  prompts: decision?.prompts ?? [],
21
21
  promptUnavailable: decision?.promptUnavailable,
22
22
  recommended: decision?.recommended ?? document.metadata.recommended,
@@ -1,2 +1,2 @@
1
1
  export const NAME = "work-board";
2
- export const VERSION = "0.16.0";
2
+ export const VERSION = "0.17.0";
@@ -3,7 +3,7 @@ import { BadRequest } from "@shivaedev/platform/errors/taxonomy.ts";
3
3
  import { responseUrl } from "#responses/url.ts";
4
4
  import { authoredQuestion } from "#server/attention/inbox.ts";
5
5
  import type { MetadataDocument } from "#server/metadata/model.ts";
6
- import { Responses } from "./service.ts";
6
+ import type { locate } from "./locate.ts";
7
7
  import { questionTemplate } from "./template.ts";
8
8
 
9
9
  export const readingActions = Effect.fn("WorkBoard.readingActions")(function* (
@@ -11,16 +11,16 @@ export const readingActions = Effect.fn("WorkBoard.readingActions")(function* (
11
11
  source: string,
12
12
  enabled: boolean,
13
13
  identity: string | undefined,
14
+ locateQuestion: ReturnType<typeof locate>,
14
15
  ) {
15
16
  if (!(enabled && identity)) {
16
17
  return undefined;
17
18
  }
18
- const service = yield* Responses;
19
19
  const item = identity;
20
20
  const requests = authoredQuestion(document) ? ["answer"] : (document.parsed.fields.attention ?? []).map((request) => request.id);
21
21
  const actions = new Map<string, string>();
22
22
  for (const request of requests) {
23
- const question = yield* service.locate(item, request).pipe(Effect.option);
23
+ const question = yield* locateQuestion(item, request).pipe(Effect.option);
24
24
  if (question._tag === "Some" && question.value.context === source) {
25
25
  const template = yield* Effect.try({
26
26
  catch: (cause) => new BadRequest({ message: cause instanceof Error ? cause.message : "Review the question template and retry." }),
@@ -2,21 +2,21 @@ import { Clock, Effect } from "effect";
2
2
  import { Conflict, Forbidden, PreconditionFailed, type ServiceUnavailable } from "@shivaedev/platform/errors/taxonomy.ts";
3
3
  import type { Question } from "#browser/responses/schema.ts";
4
4
  import type { Snapshot } from "#server/search/snapshot.ts";
5
+ import type { WorkspaceSettings } from "#server/workspace.ts";
5
6
  import type { locate as locateQuestion } from "./locate.ts";
6
7
  import { publish } from "./publish.ts";
7
8
  import type { readResponses } from "./read.ts";
8
9
  import { questionMarkdown } from "./records.ts";
9
- import type { ResponseOptions } from "./service.ts";
10
10
 
11
11
  const HOURS_48 = 48 * 60 * 60 * 1000;
12
12
  export function registerQuestion(
13
- options: ResponseOptions,
13
+ options: WorkspaceSettings,
14
14
  snapshot: Effect.Effect<Snapshot, ServiceUnavailable>,
15
15
  locate: ReturnType<typeof locateQuestion>,
16
16
  read: ReturnType<typeof readResponses>,
17
17
  ) {
18
18
  return Effect.fn("WorkBoard.registerQuestion")(function* (input: { readonly item: string; readonly request: string; readonly revision: string }) {
19
- if (!options.enabled) {
19
+ if (!options.writable) {
20
20
  return yield* Effect.fail(new Forbidden({ message: "Start with --responses to explicitly enable local question/response writes." }));
21
21
  }
22
22
  const preview = yield* locate(input.item, input.request);
@@ -32,7 +32,7 @@ export function registerQuestion(
32
32
  );
33
33
  }
34
34
  const saved = (yield* read(preview.id)).question;
35
- yield* publish(options.root, options.changes.realRoot, saved.id, questionMarkdown(saved));
35
+ yield* publish(options.root, options.realRoot, saved.id, questionMarkdown(saved));
36
36
  return saved;
37
37
  }
38
38
  const now = yield* Clock.currentTimeMillis;
@@ -49,7 +49,7 @@ export function registerQuestion(
49
49
  source: preview.source,
50
50
  },
51
51
  };
52
- yield* publish(options.root, options.changes.realRoot, question.id, questionMarkdown(question)).pipe(
52
+ yield* publish(options.root, options.realRoot, question.id, questionMarkdown(question)).pipe(
53
53
  Effect.ensuring(options.changes.refresh ?? Effect.void),
54
54
  );
55
55
  return question;
@@ -3,15 +3,15 @@ import { Conflict } from "@shivaedev/platform/errors/taxonomy.ts";
3
3
  import type { DraftInput, RecordedResponse } from "#browser/responses/schema.ts";
4
4
  import type { ResponseRejection } from "#rpc/rejections.ts";
5
5
  import type { Snapshot } from "#server/search/snapshot.ts";
6
+ import type { WorkspaceSettings } from "#server/workspace.ts";
6
7
  import { publish } from "./publish.ts";
7
8
  import { matchesResponse, responseMarkdown } from "./records.ts";
8
- import type { ResponseOptions } from "./service.ts";
9
9
 
10
10
  export const retryResponse = Effect.fn("WorkBoard.retryResponse")(function* (
11
11
  existing: RecordedResponse,
12
12
  input: DraftInput,
13
13
  snapshot: Effect.Effect<Snapshot, ResponseRejection>,
14
- options: ResponseOptions,
14
+ options: WorkspaceSettings,
15
15
  ) {
16
16
  if (!matchesResponse(existing, input)) {
17
17
  return yield* Effect.fail(new Conflict({ message: "The response identity already records different content." }));
@@ -19,5 +19,5 @@ export const retryResponse = Effect.fn("WorkBoard.retryResponse")(function* (
19
19
  if ((yield* snapshot).model.ids.get(input.id)?.[0]?.file !== `responses/${input.id}.md`) {
20
20
  return yield* Effect.fail(new Conflict({ message: "The saved response moved. Read its existing record instead of retrying a write." }));
21
21
  }
22
- yield* publish(options.root, options.changes.realRoot, existing.id, responseMarkdown(existing));
22
+ yield* publish(options.root, options.realRoot, existing.id, responseMarkdown(existing));
23
23
  });
@@ -1,16 +1,8 @@
1
1
  import type { Effect } from "effect";
2
2
  import type { ServiceUnavailable } from "@shivaedev/platform/errors/taxonomy.ts";
3
- import type { Document } from "#browser/document/schema.ts";
4
3
  import type { Server } from "#rpc/Server.ts";
5
- import type { Changes } from "#server/files/changes.ts";
6
- import type { Snapshot } from "#server/search/snapshot.ts";
7
4
 
8
5
  export interface HandlerOptions {
9
- readonly changes: Changes;
10
- readonly document?: (url: string) => Effect.Effect<Document, unknown>;
11
6
  readonly fleetPort?: number | null;
12
- readonly home: string | undefined;
13
- readonly index: Effect.Effect<Snapshot, unknown>;
14
7
  readonly server?: Effect.Effect<typeof Server.Type, ServiceUnavailable>;
15
- readonly writable?: boolean;
16
8
  }
@@ -1,19 +1,19 @@
1
1
  import { Effect, Layer, Ref, Stream } from "effect";
2
- import { InternalServerError, NotFound, ServiceUnavailable } from "@shivaedev/platform/errors/taxonomy.ts";
2
+ import { InternalServerError, ServiceUnavailable } from "@shivaedev/platform/errors/taxonomy.ts";
3
3
  import { rpcBoundary } from "@shivaedev/platform/rpc-server/boundary.ts";
4
4
  import { requestTracingLayer } from "@shivaedev/platform/rpc-server/tracing.ts";
5
- import { Diagnostics } from "#diagnostics/source.ts";
6
5
  import { workRpcs } from "#rpc/contract.ts";
7
6
  import { DocumentRejection, ResponseRejection } from "#rpc/rejections.ts";
8
7
  import { HandoffsService } from "#server/handoffs/Service.ts";
9
8
  import { libraryData } from "#server/navigation/libraryData.ts";
10
9
  import { navigationData } from "#server/navigation/model.ts";
11
- import { Responses } from "#server/responses/service.ts";
10
+ import { Responses } from "#server/Responses.ts";
12
11
  import { searchMatch } from "#server/search/match.ts";
13
12
  import { fleetItems } from "#server/views/fleetItems.ts";
14
13
  import { goals } from "#server/views/goals.ts";
15
14
  import { projectionQueries } from "#server/views/projectionQueries.ts";
16
15
  import { remainingQueries } from "#server/views/remainingQueries.ts";
16
+ import { Workspace } from "#server/workspace.ts";
17
17
  import type { HandlerOptions } from "./HandlerOptions.ts";
18
18
  import { hints } from "./hints.ts";
19
19
 
@@ -21,25 +21,29 @@ const readBoundary = rpcBoundary(InternalServerError, { unexpected: (message) =>
21
21
  const documentBoundary = rpcBoundary(DocumentRejection, { unexpected: (message) => new InternalServerError({ message }) });
22
22
  const responseBoundary = rpcBoundary(ResponseRejection, { unexpected: (message) => new InternalServerError({ message }) });
23
23
 
24
- export const handlers = Effect.fn("WorkBoard.handlers")(function* (options: HandlerOptions) {
25
- const { changes, document, home, index } = options;
26
- const projections = projectionQueries({ document, index });
24
+ export const handlers = Effect.fn("WorkBoard.handlers")(function* (options: HandlerOptions = {}) {
25
+ const workspace = yield* Workspace;
26
+ const { changes, home } = yield* workspace.settings();
27
+ const index = workspace.index();
28
+ const document = workspace.document;
29
+ const projections = yield* projectionQueries;
27
30
  const handoffs = yield* HandoffsService;
28
31
  const responses = yield* Responses;
29
- const diagnostics = yield* Diagnostics;
30
- const remaining = remainingQueries(options, { diagnostics, handoffs, responses });
32
+ const remaining = yield* remainingQueries;
31
33
  return workRpcs
32
34
  .toLayer({
33
35
  "work-board.awaitResponse": (input) => Effect.suspend(() => responses.awaitResponse(input)).pipe(responseBoundary),
34
36
  "work-board.board": ({ url }) => Effect.suspend(() => projections.board(url)).pipe(documentBoundary),
35
37
  "work-board.diagnostics": () => remaining.diagnostics().pipe(readBoundary),
36
- "work-board.document": ({ url }) =>
37
- Effect.suspend(() => (document ? document(url) : Effect.fail(new NotFound({ message: "The document is missing." })))).pipe(documentBoundary),
38
- "work-board.fleetItems": () => fleetItems(options),
38
+ "work-board.document": ({ url }) => Effect.suspend(() => document(url)).pipe(documentBoundary),
39
+ "work-board.fleetItems": () => fleetItems(options.fleetPort).pipe(readBoundary),
39
40
  "work-board.goals": () =>
40
41
  Effect.gen(function* () {
41
42
  const snapshot = yield* index;
42
- return goals(snapshot, yield* fleetItems({ ...options, index: Effect.succeed(snapshot) }));
43
+ return goals(
44
+ snapshot,
45
+ yield* fleetItems(options.fleetPort).pipe(Effect.provideService(Workspace, { ...workspace, index: () => Effect.succeed(snapshot) })),
46
+ );
43
47
  }).pipe(readBoundary),
44
48
  "work-board.handoff": ({ url }) => remaining.handoff(url).pipe(documentBoundary),
45
49
  "work-board.handoffSource": ({ item }) => Effect.suspend(() => handoffs.locate(item)).pipe(responseBoundary),
@@ -57,7 +61,6 @@ export const handlers = Effect.fn("WorkBoard.handlers")(function* (options: Hand
57
61
  "work-board.question": ({ item, request }) => Effect.suspend(() => responses.locate(item, request)).pipe(responseBoundary),
58
62
  "work-board.recordResponse": (input) => Effect.suspend(() => responses.record(input)).pipe(responseBoundary),
59
63
  "work-board.registerQuestion": (input) => Effect.suspend(() => responses.register(input)).pipe(responseBoundary),
60
- "work-board.respond": ({ url }) => remaining.respond(url).pipe(documentBoundary),
61
64
  "work-board.responses": ({ question }) => Effect.suspend(() => responses.read(question)).pipe(responseBoundary),
62
65
  "work-board.result": ({ url }) => remaining.result(url).pipe(documentBoundary),
63
66
  "work-board.search": ({ query }) =>
@@ -66,8 +69,7 @@ export const handlers = Effect.fn("WorkBoard.handlers")(function* (options: Hand
66
69
  ),
67
70
  "work-board.server": () => options.server ?? Effect.fail(new ServiceUnavailable({ message: "The board server address is unavailable." })),
68
71
  "work-board.start": () => remaining.start().pipe(readBoundary),
69
- "work-board.subscribe": () =>
70
- hints(changes, index).pipe(Stream.catchCause((cause) => Stream.fromEffect(Effect.failCause(cause).pipe(readBoundary)))),
72
+ "work-board.subscribe": () => hints().pipe(Stream.catchCause((cause) => Stream.fromEffect(Effect.failCause(cause).pipe(readBoundary)))),
71
73
  "work-board.watcher": () => Effect.map(Ref.get(changes.watching), (watching) => ({ watching })).pipe(readBoundary),
72
74
  "work-board.work": ({ url }) => Effect.suspend(() => projections.work(url)).pipe(documentBoundary),
73
75
  })
@@ -1,12 +1,15 @@
1
1
  import { Effect, PubSub, Stream } from "effect";
2
2
  import type { LiveHint } from "@shivaedev/effect-contract/live.ts";
3
- import type { Change, Changes } from "#server/files/changes.ts";
4
- import type { Snapshot } from "#server/search/snapshot.ts";
3
+ import type { Change } from "#server/files/changes.ts";
4
+ import { Workspace } from "#server/workspace.ts";
5
5
  import { affected } from "./affected.ts";
6
6
 
7
- export function hints(changes: Changes, index: Effect.Effect<Snapshot, unknown>) {
7
+ export function hints() {
8
8
  return Stream.unwrap(
9
9
  Effect.gen(function* () {
10
+ const workspace = yield* Workspace;
11
+ const { changes } = yield* workspace.settings();
12
+ const index = workspace.index();
10
13
  const subscription = yield* PubSub.subscribe(changes.events);
11
14
  let sequence = changes.sequence ? yield* changes.sequence : undefined;
12
15
  let previous = yield* Effect.option(index);
@@ -2,10 +2,10 @@ import { Effect } from "effect";
2
2
  import type { FleetItemsProjection } from "#browser/views/FleetItemsProjection.ts";
3
3
  import { DAEMON_PORT } from "#diagnostics/daemonContract.ts";
4
4
  import { daemonUnavailableReason, readDaemon } from "#server/diagnostics/daemon.ts";
5
- import type { HandlerOptions } from "#server/rpc/HandlerOptions.ts";
6
5
  import { itemsOf } from "#server/views/items.ts";
6
+ import { Workspace } from "#server/workspace.ts";
7
7
 
8
- export function fleetItems({ index, fleetPort = DAEMON_PORT }: Pick<HandlerOptions, "index" | "fleetPort">): Effect.Effect<FleetItemsProjection> {
8
+ export function fleetItems(fleetPort: number | null = DAEMON_PORT): Effect.Effect<FleetItemsProjection, never, Workspace> {
9
9
  if (fleetPort === null) {
10
10
  return Effect.succeed({ reason: "Fleet daemon is not configured for this Board", state: "unavailable" });
11
11
  }
@@ -15,7 +15,11 @@ export function fleetItems({ index, fleetPort = DAEMON_PORT }: Pick<HandlerOptio
15
15
  return { reason: daemonUnavailableReason(result.failure, fleetPort), state: "unavailable" as const };
16
16
  }
17
17
  const data = result.success;
18
- const snapshot = yield* index;
18
+ const workspace = yield* Workspace;
19
+ const snapshot = yield* workspace.index().pipe(Effect.catch(() => Effect.succeed(undefined)));
20
+ if (!snapshot) {
21
+ return { reason: "Board documents could not be read for Fleet items", state: "unavailable" as const };
22
+ }
19
23
  const documents = new Map(
20
24
  itemsOf(snapshot).map(({ id, document, title }) => [id, { file: document.file, metadata: document.parsed.fields, title }]),
21
25
  );
@@ -24,5 +28,5 @@ export function fleetItems({ index, fleetPort = DAEMON_PORT }: Pick<HandlerOptio
24
28
  items: data.items.map((item) => ({ ...item, document: documents.get(item.workId) ?? null })),
25
29
  state: "ready" as const,
26
30
  };
27
- }).pipe(Effect.catchCause(() => Effect.succeed({ reason: "Board documents could not be read for Fleet items", state: "unavailable" as const })));
31
+ });
28
32
  }
@@ -1,21 +1,20 @@
1
1
  import { Effect } from "effect";
2
- import { NotFound } from "@shivaedev/platform/errors/taxonomy.ts";
3
2
  import type { BoardProjection, WorkProjection } from "#browser/views/schema.ts";
4
3
  import { stateOf } from "#browser/views/state.ts";
5
4
  import { inboxItems } from "#server/attention/inbox.ts";
6
5
  import { attentionProjection } from "#server/attention/projection.ts";
7
6
  import { identityUrl } from "#server/metadata/links.ts";
8
7
  import { boardOf, countsOf } from "#server/render/board.ts";
9
- import type { HandlerOptions } from "#server/rpc/HandlerOptions.ts";
10
8
  import type { Snapshot } from "#server/search/snapshot.ts";
9
+ import { Workspace } from "#server/workspace.ts";
11
10
  import { fleetApproval } from "./fleetApproval.ts";
12
11
  import type { Item } from "./items.ts";
13
12
  import { projectItems } from "./projection.ts";
14
13
 
15
- export function projectionQueries({ index, document }: { readonly index: HandlerOptions["index"]; readonly document: HandlerOptions["document"] }) {
16
- function read(url: string) {
17
- return document ? document(url) : Effect.fail(new NotFound({ message: "The document is missing." }));
18
- }
14
+ export const projectionQueries = Effect.gen(function* () {
15
+ const workspace = yield* Workspace;
16
+ const index = workspace.index();
17
+ const read = workspace.document;
19
18
  return {
20
19
  board: (url: string) => Effect.map(read(url), (data): BoardProjection => ({ counts: countsOf(boardOf(data.source)), document: data })),
21
20
  inbox: () => Effect.map(index, attentionProjection),
@@ -54,7 +53,7 @@ export function projectionQueries({ index, document }: { readonly index: Handler
54
53
  } satisfies WorkProjection;
55
54
  }),
56
55
  };
57
- }
56
+ });
58
57
  function selectionProblem(snapshot: Snapshot, item: string) {
59
58
  return (snapshot.model.ids.get(item)?.length ?? 0) > 1 ? "has a duplicate ID" : "is missing or its identity changed";
60
59
  }
@@ -1,24 +1,18 @@
1
1
  import { Clock, Effect } from "effect";
2
- import { Conflict, NotFound } from "@shivaedev/platform/errors/taxonomy.ts";
3
- import type { Diagnostics } from "#diagnostics/source.ts";
4
- import type { HandoffsService } from "#server/handoffs/Service.ts";
2
+ import { Conflict } from "@shivaedev/platform/errors/taxonomy.ts";
3
+ import { Diagnostics } from "#diagnostics/source.ts";
4
+ import { HandoffsService } from "#server/handoffs/Service.ts";
5
5
  import { identityUrl } from "#server/metadata/links.ts";
6
6
  import { metadataParse } from "#server/metadata/parse.ts";
7
7
  import { startTemplates } from "#server/page/startTemplates.ts";
8
- import type { Responses } from "#server/responses/service.ts";
9
- import type { HandlerOptions } from "#server/rpc/HandlerOptions.ts";
8
+ import { Workspace } from "#server/workspace.ts";
10
9
  import { resultProjection } from "./resultProjection.ts";
11
- export function remainingQueries(
12
- { changes, document, index, writable }: HandlerOptions,
13
- services: {
14
- readonly diagnostics: typeof Diagnostics.Service;
15
- readonly responses: typeof Responses.Service;
16
- readonly handoffs: typeof HandoffsService.Service;
17
- },
18
- ) {
19
- function read(url: string) {
20
- return document ? document(url) : Effect.fail(new NotFound({ message: "The document is missing." }));
21
- }
10
+ export const remainingQueries = Effect.gen(function* () {
11
+ const workspace = yield* Workspace;
12
+ const { changes, writable } = yield* workspace.settings();
13
+ const index = workspace.index();
14
+ const read = workspace.document;
15
+ const services = { diagnostics: yield* Diagnostics, handoffs: yield* HandoffsService };
22
16
  return {
23
17
  diagnostics: () =>
24
18
  Effect.gen(function* () {
@@ -42,20 +36,6 @@ export function remainingQueries(
42
36
  writable: writable === true,
43
37
  };
44
38
  }),
45
- respond: (url: string) =>
46
- Effect.gen(function* () {
47
- const params = new URL(url, "http://127.0.0.1").searchParams;
48
- const service = services.responses;
49
- const preview = yield* service.locate(params.get("item") ?? "", params.get("request") ?? "");
50
- yield* service.read(preview.id).pipe(Effect.catch((error) => (error._tag === "NotFound" ? Effect.void : Effect.fail(error))));
51
- const responses = yield* service.history(preview);
52
- const data = yield* read(identityUrl(preview.item));
53
- const request = data.requests.find((entry) => entry.id === preview.request);
54
- if (!request) {
55
- return yield* Effect.fail(new Conflict({ message: "The response request is unavailable." }));
56
- }
57
- return { document: data, request: { ...request, preview, responses } };
58
- }),
59
39
  result: (url: string) =>
60
40
  Effect.gen(function* () {
61
41
  const item = new URL(url, "http://127.0.0.1").searchParams.get("item") ?? "";
@@ -71,4 +51,4 @@ export function remainingQueries(
71
51
  }),
72
52
  start: () => Effect.map(changes.files, (files) => ({ empty: files.length === 0, templates: startTemplates })),
73
53
  };
74
- }
54
+ });
@@ -0,0 +1,41 @@
1
+ import { Context, Effect, FileSystem, Path, type PlatformError } from "effect";
2
+ import { defineService } from "@shivaedev/effect-service/define-service.ts";
3
+ import type { Document } from "#browser/document/schema.ts";
4
+ import { readDocument } from "#server/document/read.ts";
5
+ import type { Changes } from "#server/files/changes.ts";
6
+ import type { Snapshot } from "#server/search/snapshot.ts";
7
+
8
+ export interface WorkspaceOptions {
9
+ readonly changes: Changes;
10
+ readonly home?: string | undefined;
11
+ readonly index: Effect.Effect<Snapshot, PlatformError.PlatformError>;
12
+ readonly root: string;
13
+ readonly writable: boolean;
14
+ }
15
+
16
+ export interface WorkspaceSettings extends Omit<WorkspaceOptions, "index"> {
17
+ readonly realRoot: string;
18
+ }
19
+
20
+ export class WorkspaceConfig extends Context.Service<WorkspaceConfig, WorkspaceOptions>()("WorkBoard/WorkspaceConfig") {}
21
+
22
+ export const Workspace = defineService({
23
+ id: "WorkBoard/Workspace",
24
+ initialize: Effect.gen(function* () {
25
+ return yield* WorkspaceConfig;
26
+ }),
27
+ methods: ({ index, ...options }) => {
28
+ const settings = { ...options, realRoot: options.changes.realRoot };
29
+ const read = readDocument(settings);
30
+ function document(url: string): Effect.Effect<Document, Effect.Error<ReturnType<typeof read>>, Effect.Services<ReturnType<typeof read>>> {
31
+ return Effect.flatMap(index, (snapshot) => read(url, snapshot));
32
+ }
33
+ return {
34
+ document,
35
+ index: () => index,
36
+ settings: () => Effect.succeed(settings),
37
+ };
38
+ },
39
+ requires: [WorkspaceConfig, FileSystem.FileSystem, Path.Path],
40
+ });
41
+ export type Workspace = typeof Workspace.Identifier;