@shivaedev/work-board 0.7.1 → 0.8.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 (198) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +93 -2
  3. package/assets/agent.js +37 -0
  4. package/assets/native.js +32 -25
  5. package/dist/board.d.ts +1 -0
  6. package/dist/board.d.ts.map +1 -1
  7. package/dist/board.js +15 -2
  8. package/dist/board.js.map +1 -1
  9. package/dist/browser/client.d.ts +19 -79
  10. package/dist/browser/client.d.ts.map +1 -1
  11. package/dist/browser/client.js +12 -1
  12. package/dist/browser/client.js.map +1 -1
  13. package/dist/browser/entry.d.ts +3 -0
  14. package/dist/browser/entry.d.ts.map +1 -0
  15. package/dist/browser/entry.js +7 -0
  16. package/dist/browser/entry.js.map +1 -0
  17. package/dist/browser/responses/draftSession.d.ts +8 -0
  18. package/dist/browser/responses/draftSession.d.ts.map +1 -0
  19. package/dist/browser/responses/draftSession.js +92 -0
  20. package/dist/browser/responses/draftSession.js.map +1 -0
  21. package/dist/browser/responses/drafts.d.ts +16 -0
  22. package/dist/browser/responses/drafts.d.ts.map +1 -0
  23. package/dist/browser/responses/drafts.js +49 -0
  24. package/dist/browser/responses/drafts.js.map +1 -0
  25. package/dist/browser/responses/preview.d.ts +3 -0
  26. package/dist/browser/responses/preview.d.ts.map +1 -0
  27. package/dist/browser/responses/preview.js +4 -0
  28. package/dist/browser/responses/preview.js.map +1 -0
  29. package/dist/browser/responses/schema.d.ts +127 -0
  30. package/dist/browser/responses/schema.d.ts.map +1 -0
  31. package/dist/browser/responses/schema.js +47 -0
  32. package/dist/browser/responses/schema.js.map +1 -0
  33. package/dist/browser/responses/setup.d.ts +2 -0
  34. package/dist/browser/responses/setup.d.ts.map +1 -0
  35. package/dist/browser/responses/setup.js +99 -0
  36. package/dist/browser/responses/setup.js.map +1 -0
  37. package/dist/browser/responses/submit.d.ts +4 -0
  38. package/dist/browser/responses/submit.d.ts.map +1 -0
  39. package/dist/browser/responses/submit.js +36 -0
  40. package/dist/browser/responses/submit.js.map +1 -0
  41. package/dist/browser/session.d.ts +1 -79
  42. package/dist/browser/session.d.ts.map +1 -1
  43. package/dist/cli.d.ts.map +1 -1
  44. package/dist/cli.js +24 -15
  45. package/dist/cli.js.map +1 -1
  46. package/dist/files/changes.d.ts +1 -0
  47. package/dist/files/changes.d.ts.map +1 -1
  48. package/dist/files/changes.js +1 -1
  49. package/dist/files/changes.js.map +1 -1
  50. package/dist/http/respond.d.ts.map +1 -1
  51. package/dist/http/respond.js +2 -0
  52. package/dist/http/respond.js.map +1 -1
  53. package/dist/metadata/backlinks.d.ts.map +1 -1
  54. package/dist/metadata/backlinks.js +2 -0
  55. package/dist/metadata/backlinks.js.map +1 -1
  56. package/dist/metadata/fields.d.ts +17 -1
  57. package/dist/metadata/fields.d.ts.map +1 -1
  58. package/dist/metadata/problems.d.ts.map +1 -1
  59. package/dist/metadata/problems.js +2 -0
  60. package/dist/metadata/problems.js.map +1 -1
  61. package/dist/metadata/schema.d.ts +17 -1
  62. package/dist/metadata/schema.d.ts.map +1 -1
  63. package/dist/metadata/schema.js +6 -2
  64. package/dist/metadata/schema.js.map +1 -1
  65. package/dist/page/attentionDetails.d.ts.map +1 -1
  66. package/dist/page/attentionDetails.js +2 -1
  67. package/dist/page/attentionDetails.js.map +1 -1
  68. package/dist/page/metadataDetails.d.ts.map +1 -1
  69. package/dist/page/metadataDetails.js +6 -0
  70. package/dist/page/metadataDetails.js.map +1 -1
  71. package/dist/page/navigationScript.js +1 -1
  72. package/dist/page/shell.d.ts +1 -1
  73. package/dist/page/shell.d.ts.map +1 -1
  74. package/dist/page/shell.js.map +1 -1
  75. package/dist/page/style.d.ts.map +1 -1
  76. package/dist/page/style.js +2 -1
  77. package/dist/page/style.js.map +1 -1
  78. package/dist/page/swap.d.ts +1 -1
  79. package/dist/page/swap.d.ts.map +1 -1
  80. package/dist/page/swap.js +3 -0
  81. package/dist/page/swap.js.map +1 -1
  82. package/dist/path/Identity.d.ts +3 -0
  83. package/dist/path/Identity.d.ts.map +1 -0
  84. package/dist/path/Identity.js +3 -0
  85. package/dist/path/Identity.js.map +1 -0
  86. package/dist/responses/agent-cli.d.ts +2 -0
  87. package/dist/responses/agent-cli.d.ts.map +1 -0
  88. package/dist/responses/agent-cli.js +31 -0
  89. package/dist/responses/agent-cli.js.map +1 -0
  90. package/dist/responses/agent.d.ts +68 -0
  91. package/dist/responses/agent.d.ts.map +1 -0
  92. package/dist/responses/agent.js +49 -0
  93. package/dist/responses/agent.js.map +1 -0
  94. package/dist/responses/await.d.ts +33 -0
  95. package/dist/responses/await.d.ts.map +1 -0
  96. package/dist/responses/await.js +21 -0
  97. package/dist/responses/await.js.map +1 -0
  98. package/dist/responses/flags.d.ts +6 -0
  99. package/dist/responses/flags.d.ts.map +1 -0
  100. package/dist/responses/flags.js +23 -0
  101. package/dist/responses/flags.js.map +1 -0
  102. package/dist/responses/locate.d.ts +13 -0
  103. package/dist/responses/locate.d.ts.map +1 -0
  104. package/dist/responses/locate.js +50 -0
  105. package/dist/responses/locate.js.map +1 -0
  106. package/dist/responses/outcome.d.ts +65 -0
  107. package/dist/responses/outcome.d.ts.map +1 -0
  108. package/dist/responses/outcome.js +14 -0
  109. package/dist/responses/outcome.js.map +1 -0
  110. package/dist/responses/page.d.ts +6 -0
  111. package/dist/responses/page.d.ts.map +1 -0
  112. package/dist/responses/page.js +25 -0
  113. package/dist/responses/page.js.map +1 -0
  114. package/dist/responses/publish.d.ts +4 -0
  115. package/dist/responses/publish.d.ts.map +1 -0
  116. package/dist/responses/publish.js +56 -0
  117. package/dist/responses/publish.js.map +1 -0
  118. package/dist/responses/publishIn.d.ts +3 -0
  119. package/dist/responses/publishIn.d.ts.map +1 -0
  120. package/dist/responses/publishIn.js +69 -0
  121. package/dist/responses/publishIn.js.map +1 -0
  122. package/dist/responses/read.d.ts +30 -0
  123. package/dist/responses/read.d.ts.map +1 -0
  124. package/dist/responses/read.js +43 -0
  125. package/dist/responses/read.js.map +1 -0
  126. package/dist/responses/records.d.ts +12 -0
  127. package/dist/responses/records.d.ts.map +1 -0
  128. package/dist/responses/records.js +47 -0
  129. package/dist/responses/records.js.map +1 -0
  130. package/dist/responses/retry.d.ts +22 -0
  131. package/dist/responses/retry.d.ts.map +1 -0
  132. package/dist/responses/retry.js +14 -0
  133. package/dist/responses/retry.js.map +1 -0
  134. package/dist/responses/service.d.ts +106 -0
  135. package/dist/responses/service.d.ts.map +1 -0
  136. package/dist/responses/service.js +90 -0
  137. package/dist/responses/service.js.map +1 -0
  138. package/dist/responses/waitFor.d.ts +31 -0
  139. package/dist/responses/waitFor.d.ts.map +1 -0
  140. package/dist/responses/waitFor.js +47 -0
  141. package/dist/responses/waitFor.js.map +1 -0
  142. package/dist/rpc/contract.d.ts +101 -2
  143. package/dist/rpc/contract.d.ts.map +1 -1
  144. package/dist/rpc/contract.js +3 -2
  145. package/dist/rpc/contract.js.map +1 -1
  146. package/dist/rpc/responseContract.d.ts +212 -0
  147. package/dist/rpc/responseContract.d.ts.map +1 -0
  148. package/dist/rpc/responseContract.js +37 -0
  149. package/dist/rpc/responseContract.js.map +1 -0
  150. package/dist/rpc/server.d.ts +3 -1
  151. package/dist/rpc/server.d.ts.map +1 -1
  152. package/dist/rpc/server.js +6 -1
  153. package/dist/rpc/server.js.map +1 -1
  154. package/dist/serve-cli.d.ts +3 -0
  155. package/dist/serve-cli.d.ts.map +1 -0
  156. package/dist/serve-cli.js +20 -0
  157. package/dist/serve-cli.js.map +1 -0
  158. package/package.json +2 -2
  159. package/src/board.ts +16 -2
  160. package/src/browser/client.ts +30 -4
  161. package/src/browser/entry.ts +9 -0
  162. package/src/browser/responses/draftSession.ts +88 -0
  163. package/src/browser/responses/drafts.ts +58 -0
  164. package/src/browser/responses/preview.ts +5 -0
  165. package/src/browser/responses/schema.ts +52 -0
  166. package/src/browser/responses/setup.ts +102 -0
  167. package/src/browser/responses/submit.ts +41 -0
  168. package/src/cli.ts +14 -22
  169. package/src/files/changes.ts +2 -1
  170. package/src/http/respond.ts +2 -0
  171. package/src/metadata/backlinks.ts +2 -0
  172. package/src/metadata/problems.ts +2 -0
  173. package/src/metadata/schema.ts +6 -2
  174. package/src/page/attentionDetails.ts +2 -1
  175. package/src/page/metadataDetails.ts +6 -0
  176. package/src/page/navigationScript.ts +1 -1
  177. package/src/page/shell.ts +1 -1
  178. package/src/page/style.ts +2 -1
  179. package/src/page/swap.ts +3 -0
  180. package/src/path/Identity.ts +3 -0
  181. package/src/responses/agent-cli.ts +35 -0
  182. package/src/responses/agent.ts +59 -0
  183. package/src/responses/await.ts +21 -0
  184. package/src/responses/flags.ts +22 -0
  185. package/src/responses/locate.ts +55 -0
  186. package/src/responses/outcome.ts +15 -0
  187. package/src/responses/page.ts +41 -0
  188. package/src/responses/publish.ts +53 -0
  189. package/src/responses/publishIn.ts +64 -0
  190. package/src/responses/read.ts +45 -0
  191. package/src/responses/records.ts +53 -0
  192. package/src/responses/retry.ts +23 -0
  193. package/src/responses/service.ts +106 -0
  194. package/src/responses/waitFor.ts +58 -0
  195. package/src/rpc/contract.ts +3 -2
  196. package/src/rpc/responseContract.ts +46 -0
  197. package/src/rpc/server.ts +8 -1
  198. package/src/serve-cli.ts +37 -0
@@ -8,8 +8,9 @@ import { navHtml } from "#page/nav.ts";
8
8
  import { searchMatch } from "#search/match.ts";
9
9
  import { ReadFailed, workRpcs } from "./contract.js";
10
10
  import { hints } from "./hints.js";
11
- export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home, index, page }) {
11
+ export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home, index, page, responses }) {
12
12
  const handlers = workRpcs.toLayer({
13
+ "work-board.awaitResponse": responses.awaitResponse,
13
14
  "work-board.history": ({ action, baseline }) => Effect.gen(function* () {
14
15
  const now = yield* Clock.currentTimeMillis;
15
16
  const snapshot = action === "check" ? undefined : yield* index;
@@ -22,6 +23,10 @@ export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home,
22
23
  }
23
24
  return Effect.succeed({ html: new TextDecoder().decode(response.body.body), status: response.status });
24
25
  }), Effect.catch((error) => Effect.fail(error instanceof ReadFailed ? error : new ReadFailed({ operation: "page", status: 500 })))),
26
+ "work-board.question": ({ item, request }) => responses.locate(item, request),
27
+ "work-board.recordResponse": responses.record,
28
+ "work-board.registerQuestion": responses.register,
29
+ "work-board.responses": ({ question }) => responses.read(question),
25
30
  "work-board.search": ({ query }) => Effect.map(index, (snapshot) => ({ ...searchMatch(snapshot.entries, query), unavailable: snapshot.unavailable })).pipe(Effect.catch(() => Effect.fail(new ReadFailed({ operation: "search", status: 500 })))),
26
31
  "work-board.subscribe": () => hints(changes, index),
27
32
  "work-board.watcher": () => Effect.map(Ref.get(changes.watching), (watching) => ({ watching })),
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/rpc/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/rpc/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export declare function runServe(): void;
3
+ //# sourceMappingURL=serve-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve-cli.d.ts","sourceRoot":"","sources":["../src/serve-cli.ts"],"names":[],"mappings":";AAkCA,wBAAgB,QAAQ,SAEvB"}
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
+ import { Effect, Layer, Option, Path } from "effect";
4
+ import { Argument, Command, Flag } from "effect/unstable/cli";
5
+ import { HOST, serveBoard } from "./serve.js";
6
+ const VERSION = "0.8.0";
7
+ const serve = Effect.fn("WorkBoard.serve")(function* (input) {
8
+ const path = yield* Path.Path;
9
+ return yield* Layer.launch(serveBoard({ home: Option.getOrUndefined(input.home), port: input.port, responses: input.responses, root: path.resolve(input.dir) }));
10
+ });
11
+ const workBoard = Command.make("work-board", {
12
+ dir: Argument.directory("dir", { mustExist: true }).pipe(Argument.withDescription("The folder of markdown files to serve")),
13
+ home: Flag.string("home").pipe(Flag.optional, Flag.withDescription("The file shown as a board at /, relative to the folder")),
14
+ port: Flag.integer("port").pipe(Flag.withDefault(4747), Flag.withDescription(`The port to listen on at ${HOST}`)),
15
+ responses: Flag.boolean("responses").pipe(Flag.withDefault(false), Flag.withDescription("Explicitly enable local durable question and response writes (Linux)")),
16
+ }, serve).pipe(Command.withDescription("Serve a folder of markdown files as a live page that updates in place when a file changes."));
17
+ export function runServe() {
18
+ Command.run(workBoard, { version: VERSION }).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
19
+ }
20
+ //# sourceMappingURL=serve-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve-cli.js","sourceRoot":"","sources":["../src/serve-cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,OAAO,GAAG,OAAO,CAAC;AAExB,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,CAAC,QAAQ,CAAC,EAAE,KAKrD;IACA,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IAC9B,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,CACzB,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CACpI,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAC7B,YAAY,EACZ;IACC,GAAG,EAAE,QAAQ,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,uCAAuC,CAAC,CAAC;IAC3H,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,CAAC,wDAAwD,CAAC,CAAC;IAC7H,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,4BAA4B,IAAI,EAAE,CAAC,CAAC;IACjH,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,IAAI,CACxC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EACvB,IAAI,CAAC,eAAe,CAAC,sEAAsE,CAAC,CAC5F;CACD,EACD,KAAK,CACL,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,4FAA4F,CAAC,CAAC,CAAC;AAE9H,MAAM,UAAU,QAAQ;IACvB,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;AAC5G,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/work-board",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Serve a folder of markdown files as a live local page that updates in place when a file changes",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/work-board#readme",
6
6
  "bugs": {
@@ -90,7 +90,7 @@
90
90
  },
91
91
  "scripts": {
92
92
  "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc6 --project tsconfig.build.json && pnpm build:browser",
93
- "build:browser": "esbuild src/browser/session.ts --bundle --platform=browser --format=esm --conditions=source --minify --outfile=assets/native.js",
93
+ "build:browser": "esbuild src/browser/entry.ts --bundle --platform=browser --format=esm --conditions=source --minify --outfile=assets/native.js && esbuild src/responses/agent-cli.ts --bundle --platform=node --format=esm --conditions=source --minify --outfile=assets/agent.js",
94
94
  "ready": "pnpm typecheck && pnpm test && pnpm build",
95
95
  "test": "pnpm test:prepare && vitest run",
96
96
  "test:prepare": "pnpm build:browser",
package/src/board.ts CHANGED
@@ -17,11 +17,14 @@ import { start } from "#http/start.ts";
17
17
  import { work } from "#http/work.ts";
18
18
  import type { RenderFailed } from "#render/failed.ts";
19
19
  import { Highlighter } from "#render/highlighter.ts";
20
+ import { responsePage } from "#responses/page.ts";
21
+ import { responseService } from "#responses/service.ts";
20
22
  import { server } from "#rpc/server.ts";
21
23
  import { searchSnapshot } from "#search/snapshot.ts";
22
24
 
23
25
  export interface BoardOptions {
24
26
  readonly home?: string | undefined;
27
+ readonly responses?: boolean;
25
28
  readonly root: string;
26
29
  }
27
30
 
@@ -59,6 +62,14 @@ const routes = (options: BoardOptions) =>
59
62
  }
60
63
  yield* serve(MERMAID_ROUTE, mermaidFile(mermaid));
61
64
  yield* serve(NATIVE_ROUTE, nativeAsset);
65
+ const responses = yield* responseService({
66
+ changes,
67
+ enabled: options.responses === true,
68
+ index: Effect.provideContext(index, context),
69
+ root: options.root,
70
+ });
71
+ const replyPage = responsePage(responses, changes, home, options.responses === true);
72
+ yield* serve("/_board/respond", replyPage);
62
73
  const pages = page({ home, root: options.root }, changes, index);
63
74
  const items = identity(index, pages, changes, home);
64
75
  yield* serve("/_board/item/*", items);
@@ -67,6 +78,9 @@ const routes = (options: BoardOptions) =>
67
78
  const pathname = new URL(url, "http://127.0.0.1").pathname;
68
79
  let selected: ReturnType<typeof pages>;
69
80
  switch (pathname) {
81
+ case "/_board/respond":
82
+ selected = replyPage(request);
83
+ break;
70
84
  case "/_board/start":
71
85
  selected = startPage();
72
86
  break;
@@ -84,11 +98,11 @@ const routes = (options: BoardOptions) =>
84
98
  }
85
99
  return Effect.provideContext(selected, context);
86
100
  }
87
- const rpc = yield* server({ changes, home, index: Effect.provideContext(index, context), page: readPage });
101
+ const rpc = yield* server({ changes, home, index: Effect.provideContext(index, context), page: readPage, responses });
88
102
  yield* serve(
89
103
  "/_board/rpc",
90
104
  (request) =>
91
- sameOrigin(request)
105
+ sameOrigin(request) && (options.responses !== true || request.headers["content-type"]?.split(";")[0] === "application/ndjson")
92
106
  ? rpc.pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, request))
93
107
  : Effect.succeed(respond("Local same-origin requests only", "text/plain", 403)),
94
108
  "POST",
@@ -4,18 +4,34 @@ import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4
4
  import type * as Atom from "effect/unstable/reactivity/Atom";
5
5
  import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry";
6
6
  import * as AtomRpc from "effect/unstable/reactivity/AtomRpc";
7
+ import type * as Reactivity from "effect/unstable/reactivity/Reactivity";
7
8
  import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
8
- import { bind } from "@shivaedev/effect-contract/bind.ts";
9
+ import type { RpcClientError } from "effect/unstable/rpc/RpcClientError";
10
+ import { type Bound, bind } from "@shivaedev/effect-contract/bind.ts";
11
+ import type { Contract } from "@shivaedev/effect-contract/contract.ts";
9
12
  import { live } from "@shivaedev/effect-contract/live.ts";
10
13
  import { type ResumeOptions, resumeSignal } from "@shivaedev/effect-contract/resume.ts";
11
- import { workContract, workRpcs } from "#rpc/contract.ts";
14
+ import { type ReadFailed, workContract, workRpcs } from "#rpc/contract.ts";
12
15
  import { workspace } from "#rpc/keys.ts";
16
+ import { responseContract } from "#rpc/responseContract.ts";
13
17
 
14
18
  export interface Reader {
15
19
  readonly reader: unique symbol;
16
20
  }
17
21
 
18
- export function browserClient(url: string, options: ResumeOptions = {}) {
22
+ type BoundContract<TContract> =
23
+ TContract extends Contract<infer TName, infer TQueries, infer TCommands, infer TRpcs> ? Bound<TName, TQueries, TCommands, TRpcs, Reader> : never;
24
+ export interface BrowserClient {
25
+ readonly api: BoundContract<typeof workContract>;
26
+ readonly mutate: <A, E>(effect: Effect.Effect<A, E, Reader | Reactivity.Reactivity>, signal?: AbortSignal) => Promise<A>;
27
+ readonly read: <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>, signal?: AbortSignal) => Promise<A>;
28
+ readonly registry: AtomRegistry.AtomRegistry;
29
+ readonly responses: BoundContract<typeof responseContract>;
30
+ readonly run: <A, E>(effect: Effect.Effect<A, E, Reader>, signal?: AbortSignal) => Promise<A>;
31
+ readonly updates: ReturnType<typeof live<Reader, ReadFailed | RpcClientError, never>>;
32
+ }
33
+
34
+ export function browserClient(url: string, options: ResumeOptions = {}): BrowserClient {
19
35
  class Client extends AtomRpc.Service<Reader>()("WorkBoard/Reader", {
20
36
  group: workRpcs,
21
37
  protocol: RpcClient.layerProtocolHttp({ url: `${url}/_board/rpc` }).pipe(Layer.provide([FetchHttpClient.layer, RpcSerialization.layerNdjson])),
@@ -55,5 +71,15 @@ export function browserClient(url: string, options: ResumeOptions = {}) {
55
71
  throw error;
56
72
  }
57
73
  }
58
- return { api, read, registry, run, updates };
74
+ async function mutate<A, E>(effect: Effect.Effect<A, E, Reader | Reactivity.Reactivity>, signal?: AbortSignal) {
75
+ const result = await Effect.runPromise(Effect.result(AtomRegistry.getResult(registry, Client.runtime.atom(effect), { suspendOnWaiting: true })), {
76
+ signal,
77
+ });
78
+ if (result._tag === "Failure") {
79
+ throw result.failure;
80
+ }
81
+ return result.success;
82
+ }
83
+ const responses = bind(responseContract, Client);
84
+ return { api, mutate, read, registry, responses, run, updates };
59
85
  }
@@ -0,0 +1,9 @@
1
+ import { setupResponses } from "#browser/responses/setup.ts";
2
+
3
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
4
+ setupResponses();
5
+ }
6
+
7
+ import { session as ownedSession } from "./session.ts";
8
+
9
+ export const session = ownedSession;
@@ -0,0 +1,88 @@
1
+ import { Clock, Effect } from "effect";
2
+ import { DRAFT_AGE, type Draft, encodeDrafts, readDrafts } from "./drafts.ts";
3
+
4
+ const states = new Map<string, Draft>();
5
+ export function draftSession(key: string, revision: string, storage: Storage | undefined, storageKey: string, report: (message: string) => void) {
6
+ function local() {
7
+ if (!storage) {
8
+ throw new Error("Draft storage unavailable");
9
+ }
10
+ return storage;
11
+ }
12
+ let blocked = false;
13
+ let drafts: Record<string, Draft> = {};
14
+ try {
15
+ const stored = readDrafts(local().getItem(storageKey), Effect.runSync(Clock.currentTimeMillis));
16
+ drafts = stored.drafts;
17
+ if (stored.expired) {
18
+ report("Expired drafts were cleared after 30 days. Saved responses are unaffected.");
19
+ }
20
+ local().setItem(storageKey, encodeDrafts(drafts));
21
+ } catch {
22
+ blocked = true;
23
+ report("Draft history is unavailable or malformed. Keep a copy; edits remain in this window.");
24
+ }
25
+ const memory = states.get(key);
26
+ if (memory && Effect.runSync(Clock.currentTimeMillis) - memory.updatedAt >= DRAFT_AGE) {
27
+ states.delete(key);
28
+ report("A window draft expired after 30 days. Saved responses are unaffected.");
29
+ }
30
+ let current = states.get(key)
31
+ ?? drafts[key] ?? {
32
+ author: "",
33
+ body: "",
34
+ id: `response.${crypto.randomUUID()}`,
35
+ revision,
36
+ type: "answer",
37
+ updatedAt: Effect.runSync(Clock.currentTimeMillis),
38
+ };
39
+ if (current.revision !== revision) {
40
+ report("Source changed since this draft. Your text is retained; review the new source and preview to bind it to this revision.");
41
+ }
42
+ function update(patch: Partial<Draft>) {
43
+ current = { ...(states.get(key) ?? current), ...patch, updatedAt: Effect.runSync(Clock.currentTimeMillis) };
44
+ states.set(key, current);
45
+ try {
46
+ if (blocked) {
47
+ throw new Error("Unknown draft history");
48
+ }
49
+ const latest = readDrafts(local().getItem(storageKey), Effect.runSync(Clock.currentTimeMillis)).drafts;
50
+ local().setItem(storageKey, encodeDrafts({ ...latest, [key]: current }));
51
+ } catch {
52
+ report("Draft could not be persisted (storage full or unavailable). Keep a copy; your text remains in this window.");
53
+ }
54
+ return current;
55
+ }
56
+ function clear() {
57
+ try {
58
+ local().removeItem(storageKey);
59
+ blocked = false;
60
+ states.clear();
61
+ report("Workspace drafts cleared. Saved responses are unchanged; this visible text remains until you navigate away.");
62
+ } catch {
63
+ report("Draft storage could not be cleared.");
64
+ }
65
+ }
66
+ function saved(pending: Draft) {
67
+ if (states.get(key) !== pending) {
68
+ const live = states.get(key);
69
+ if (live?.id === pending.id) {
70
+ update({ id: `response.${crypto.randomUUID()}` });
71
+ }
72
+ return false;
73
+ }
74
+ states.delete(key);
75
+ try {
76
+ const latest = readDrafts(local().getItem(storageKey), Effect.runSync(Clock.currentTimeMillis)).drafts;
77
+ if (latest[key]?.id === pending.id) {
78
+ delete latest[key];
79
+ }
80
+ local().setItem(storageKey, encodeDrafts(latest));
81
+ } catch {
82
+ report("Saved response confirmed, but its browser draft could not be cleared.");
83
+ }
84
+ current = { ...current, body: "", id: `response.${crypto.randomUUID()}`, updatedAt: Effect.runSync(Clock.currentTimeMillis) };
85
+ return true;
86
+ }
87
+ return { clear, current: () => current, saved, update };
88
+ }
@@ -0,0 +1,58 @@
1
+ export const DRAFT_BYTES = 2 * 1024 * 1024;
2
+ export const DRAFT_AGE = 30 * 24 * 60 * 60 * 1000;
3
+ export interface Draft {
4
+ readonly author: string;
5
+ readonly body: string;
6
+ readonly id: string;
7
+ readonly revision: string;
8
+ readonly type: string;
9
+ readonly updatedAt: number;
10
+ }
11
+ export function readDrafts(raw: string | null, now: number): { drafts: Record<string, Draft>; expired: boolean } {
12
+ if (raw === null) {
13
+ return { drafts: {}, expired: false };
14
+ }
15
+ if (new TextEncoder().encode(raw).length > DRAFT_BYTES) {
16
+ throw new Error("Draft storage exceeds the workspace limit.");
17
+ }
18
+ const value: unknown = JSON.parse(raw);
19
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
20
+ throw new Error("Draft storage is malformed.");
21
+ }
22
+ const drafts: Record<string, Draft> = {};
23
+ let expired = false;
24
+ for (const [key, entry] of Object.entries(value)) {
25
+ if (
26
+ typeof entry !== "object"
27
+ || entry === null
28
+ || !("author" in entry)
29
+ || !("body" in entry)
30
+ || !("id" in entry)
31
+ || !("revision" in entry)
32
+ || !("type" in entry)
33
+ || !("updatedAt" in entry)
34
+ || typeof entry.author !== "string"
35
+ || typeof entry.body !== "string"
36
+ || typeof entry.id !== "string"
37
+ || typeof entry.revision !== "string"
38
+ || (entry.type !== "answer" && entry.type !== "clarify" && entry.type !== "not_now")
39
+ || typeof entry.updatedAt !== "number"
40
+ || !Number.isFinite(entry.updatedAt)
41
+ ) {
42
+ throw new Error("Draft storage is malformed.");
43
+ }
44
+ if (entry.updatedAt > now || now - entry.updatedAt >= DRAFT_AGE) {
45
+ expired = true;
46
+ continue;
47
+ }
48
+ drafts[key] = { author: entry.author, body: entry.body, id: entry.id, revision: entry.revision, type: entry.type, updatedAt: entry.updatedAt };
49
+ }
50
+ return { drafts, expired };
51
+ }
52
+ export function encodeDrafts(drafts: Readonly<Record<string, Draft>>): string {
53
+ const value = JSON.stringify(drafts);
54
+ if (new TextEncoder().encode(value).length > DRAFT_BYTES) {
55
+ throw new Error("Draft storage is full. Copy this draft before clearing workspace drafts.");
56
+ }
57
+ return value;
58
+ }
@@ -0,0 +1,5 @@
1
+ import type { Draft } from "./drafts.ts";
2
+
3
+ export function responsePreview(draft: Draft, question: string): string {
4
+ return `New file: responses/${draft.id}.md\nRegistration (if needed): responses/${question}.md\nRecorded time is assigned by the server when saving.\n\n---\n${JSON.stringify({ id: draft.id, kind: "response", response: { author: draft.author, question, reviewedRevision: draft.revision, type: draft.type } }, null, 2)}\n---\n${draft.body}`;
5
+ }
@@ -0,0 +1,52 @@
1
+ import { Schema } from "effect";
2
+ import { Identity } from "#path/Identity.ts";
3
+
4
+ export const Revision = Schema.String.check(Schema.isPattern(/^[a-f\d]{64}$/u));
5
+ export const ResponseKind = Schema.Literals(["answer", "clarify", "not_now"]);
6
+ export const QuestionRecord = Schema.Struct({
7
+ deadline: Schema.Number.check(Schema.isFinite(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(8_640_000_000_000_000)),
8
+ item: Identity,
9
+ reason: Schema.String,
10
+ registeredAt: Schema.Number.check(Schema.isFinite(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(8_640_000_000_000_000)),
11
+ request: Identity,
12
+ reviewedRevision: Revision,
13
+ source: Schema.String,
14
+ });
15
+ export const ResponseRecord = Schema.Struct({
16
+ author: Schema.String.check(Schema.isPattern(/\S/u), Schema.isMaxLength(200)),
17
+ question: Identity,
18
+ recordedAt: Schema.Number.check(Schema.isFinite(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(8_640_000_000_000_000)),
19
+ reviewedRevision: Revision,
20
+ type: ResponseKind,
21
+ });
22
+ export const QuestionPreview = Schema.Struct({
23
+ context: Schema.String,
24
+ id: Identity,
25
+ item: Identity,
26
+ reason: Schema.String,
27
+ request: Identity,
28
+ reviewedRevision: Revision,
29
+ source: Schema.String,
30
+ });
31
+ export type QuestionPreview = typeof QuestionPreview.Type;
32
+ export const Question = Schema.Struct({ context: Schema.String, id: Identity, question: QuestionRecord });
33
+ export const RecordedResponse = Schema.Struct({ body: Schema.String, id: Identity, response: ResponseRecord });
34
+ export const Reading = Schema.Struct({ question: Question, responses: Schema.Array(RecordedResponse) });
35
+ export const WaitReading = Schema.Struct({ expired: Schema.Boolean, question: Question, response: Schema.optional(RecordedResponse) });
36
+ export type WaitReading = typeof WaitReading.Type;
37
+ export class ResponseFailed extends Schema.TaggedError<ResponseFailed>()("ResponseFailed", {
38
+ cause: Schema.optional(Schema.Defect()),
39
+ code: Schema.Literals(["Disabled", "Missing", "Stale", "Conflict", "Unavailable", "Unsupported", "Uncertain", "Unanswered"]),
40
+ message: Schema.String,
41
+ }) {}
42
+ export const DraftInput = Schema.Struct({
43
+ author: ResponseRecord.fields.author,
44
+ body: Schema.String.check(Schema.isPattern(/\S/u), Schema.isMaxLength(32_768)),
45
+ id: Identity,
46
+ question: Identity,
47
+ type: ResponseKind,
48
+ });
49
+ export type Question = typeof Question.Type;
50
+ export type RecordedResponse = typeof RecordedResponse.Type;
51
+ export type Reading = typeof Reading.Type;
52
+ export type DraftInput = typeof DraftInput.Type;
@@ -0,0 +1,102 @@
1
+ import { draftSession } from "./draftSession.ts";
2
+ import { responsePreview } from "./preview.ts";
3
+ import { submitResponse } from "./submit.ts";
4
+
5
+ function attach() {
6
+ const doc = document.getElementById("doc");
7
+ if (doc?.dataset.view !== "response") {
8
+ return;
9
+ }
10
+ const form = doc.querySelector<HTMLFormElement>("#response-form");
11
+ if (!form || form.dataset.bound === "true") {
12
+ return;
13
+ }
14
+ form.dataset.bound = "true";
15
+ const status = form.querySelector<HTMLParagraphElement>("#response-status");
16
+ const author = form.elements.namedItem("author");
17
+ const body = form.elements.namedItem("body");
18
+ const type = form.elements.namedItem("type");
19
+ const submit = form.querySelector<HTMLButtonElement>("#response-submit");
20
+ const preview = form.querySelector<HTMLPreElement>("#response-preview-content");
21
+ const clear = form.querySelector<HTMLButtonElement>("#response-clear");
22
+ const previewButton = form.querySelector<HTMLButtonElement>("#response-preview");
23
+ if (
24
+ !(
25
+ status
26
+ && author instanceof HTMLInputElement
27
+ && body instanceof HTMLTextAreaElement
28
+ && type instanceof HTMLSelectElement
29
+ && submit
30
+ && preview
31
+ && clear
32
+ && previewButton
33
+ )
34
+ ) {
35
+ return;
36
+ }
37
+ if (status.textContent?.includes("JavaScript is required")) {
38
+ status.textContent = "Draft ready. Preview the Markdown before recording it.";
39
+ }
40
+ const item = form.dataset.item ?? "";
41
+ const request = form.dataset.request ?? "";
42
+ const revision = form.dataset.revision ?? "";
43
+ const key = `${item}/${request}`;
44
+ const storage = `work-board:drafts:${document.documentElement.dataset.workspace}`;
45
+ let storageApi: Storage | undefined;
46
+ try {
47
+ storageApi = window.localStorage;
48
+ } catch {
49
+ storageApi = undefined;
50
+ }
51
+ const drafts = draftSession(key, revision, storageApi, storage, (message) => {
52
+ status.textContent = message;
53
+ });
54
+ let draft = drafts.current();
55
+ author.value = draft.author;
56
+ body.value = draft.body;
57
+ type.value = draft.type;
58
+ const fields = { author, body, type };
59
+ function save() {
60
+ draft = drafts.update({ author: fields.author.value, body: fields.body.value, revision: draft.revision, type: fields.type.value });
61
+ }
62
+ form.addEventListener("input", () => {
63
+ save();
64
+ preview.hidden = true;
65
+ submit.disabled = true;
66
+ });
67
+ previewButton.disabled = false;
68
+ clear.disabled = false;
69
+ previewButton.addEventListener("click", () => {
70
+ draft = drafts.update({
71
+ author: author.value,
72
+ body: body.value,
73
+ id: draft.revision === revision ? drafts.current().id : `response.${crypto.randomUUID()}`,
74
+ revision,
75
+ type: type.value,
76
+ });
77
+ preview.textContent = responsePreview(draft, form.dataset.question ?? "");
78
+ preview.hidden = false;
79
+ submit.disabled = form.dataset.writable !== "true" || !form.checkValidity();
80
+ });
81
+ clear.addEventListener("click", drafts.clear);
82
+ form.addEventListener("submit", async (event) => {
83
+ event.preventDefault();
84
+ if (submit.disabled || draft.revision !== revision || !form.reportValidity()) {
85
+ return;
86
+ }
87
+ submit.disabled = true;
88
+ previewButton.disabled = true;
89
+ if (await submitResponse(form, status, drafts, draft)) {
90
+ draft = drafts.current();
91
+ if (form.isConnected) {
92
+ body.value = "";
93
+ }
94
+ }
95
+ previewButton.disabled = false;
96
+ });
97
+ }
98
+ export function setupResponses() {
99
+ document.addEventListener("board-page", attach);
100
+ window.addEventListener("pageshow", attach);
101
+ attach();
102
+ }
@@ -0,0 +1,41 @@
1
+ import { Schema } from "effect";
2
+ import { session } from "#browser/session.ts";
3
+ import type { draftSession } from "./draftSession.ts";
4
+ import type { Draft } from "./drafts.ts";
5
+ import { ResponseFailed, ResponseKind } from "./schema.ts";
6
+
7
+ export async function submitResponse(form: HTMLFormElement, status: HTMLParagraphElement, drafts: ReturnType<typeof draftSession>, pending: Draft) {
8
+ status.dataset.outcome = "pending";
9
+ status.textContent = "Pending: registering reviewed context, then recording response…";
10
+ const native = session(window);
11
+ try {
12
+ const question = await native.mutate(
13
+ native.responses.registerQuestion.run({ item: form.dataset.item ?? "", request: form.dataset.request ?? "", revision: pending.revision }),
14
+ );
15
+ await native.mutate(
16
+ native.responses.recordResponse.run({
17
+ author: pending.author,
18
+ body: pending.body,
19
+ id: pending.id,
20
+ question: question.id,
21
+ type: Schema.decodeUnknownSync(ResponseKind)(pending.type),
22
+ }),
23
+ );
24
+ if (form.isConnected) {
25
+ status.dataset.outcome = "saved";
26
+ status.textContent = `Saved: responses/${pending.id}.md. This records feedback, not acceptance.`;
27
+ }
28
+ const cleared = drafts.saved(pending);
29
+ if (!cleared && form.isConnected) {
30
+ status.textContent += " Your current draft remains unsaved.";
31
+ }
32
+ return cleared;
33
+ } catch (error) {
34
+ if (form.isConnected) {
35
+ const outcome = error instanceof ResponseFailed && error.code !== "Uncertain" ? "rejected" : "uncertain";
36
+ status.dataset.outcome = outcome;
37
+ status.textContent = `${outcome === "rejected" ? "Rejected" : "Uncertain"}: ${String(error)}. Draft retained. Preview and retry with the same identity; saved content will be reconciled.`;
38
+ }
39
+ return false;
40
+ }
41
+ }
package/src/cli.ts CHANGED
@@ -1,24 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
- import { Effect, Layer, Option, Path } from "effect";
4
- import { Argument, Command, Flag } from "effect/unstable/cli";
5
- import { HOST, serveBoard } from "./serve.ts";
2
+ const action = process.argv[2];
3
+ if (action === "wait" || action === "response" || action === "question") {
4
+ try {
5
+ const entry: typeof import("#responses/agent-cli.ts") = await import(new URL("../assets/agent.js", import.meta.url).href);
6
+ await entry.runAgent(process.argv.slice(2));
7
+ } catch (error) {
8
+ process.stderr.write(`${String(error)}\n`);
9
+ process.exitCode ??= 1;
10
+ }
11
+ } else {
12
+ const entry = await import("./serve-cli.ts");
13
+ entry.runServe();
14
+ }
6
15
 
7
- const VERSION = "0.7.1";
8
-
9
- const serve = Effect.fn("WorkBoard.serve")(function* (input: { readonly dir: string; readonly port: number; readonly home: Option.Option<string> }) {
10
- const path = yield* Path.Path;
11
- return yield* Layer.launch(serveBoard({ home: Option.getOrUndefined(input.home), port: input.port, root: path.resolve(input.dir) }));
12
- });
13
-
14
- const workBoard = Command.make(
15
- "work-board",
16
- {
17
- dir: Argument.directory("dir", { mustExist: true }).pipe(Argument.withDescription("The folder of markdown files to serve")),
18
- home: Flag.string("home").pipe(Flag.optional, Flag.withDescription("The file shown as a board at /, relative to the folder")),
19
- port: Flag.integer("port").pipe(Flag.withDefault(4747), Flag.withDescription(`The port to listen on at ${HOST}`)),
20
- },
21
- serve,
22
- ).pipe(Command.withDescription("Serve a folder of markdown files as a live page that updates in place when a file changes."));
23
-
24
- Command.run(workBoard, { version: VERSION }).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
16
+ export {};
@@ -10,6 +10,7 @@ export interface Changes {
10
10
  readonly events: PubSub.PubSub<Change>;
11
11
  readonly files: Effect.Effect<readonly MarkdownFile[], PlatformError.PlatformError>;
12
12
  readonly realRoot: string;
13
+ readonly refresh?: Effect.Effect<void>;
13
14
  readonly revision: Effect.Effect<number>;
14
15
  readonly sequence?: Effect.Effect<number>;
15
16
  readonly watching: Ref.Ref<boolean>;
@@ -65,6 +66,6 @@ export const watchChanges = Effect.fn("WorkBoard.watchChanges")(function* (root:
65
66
  yield* Ref.set(cached, Option.some({ files: listed, generation: now }));
66
67
  return listed;
67
68
  });
68
- const changes: Changes = { events, files, realRoot, revision: Ref.get(generation), sequence: Ref.get(sequence), watching };
69
+ const changes: Changes = { events, files, realRoot, refresh: stale, revision: Ref.get(generation), sequence: Ref.get(sequence), watching };
69
70
  return changes;
70
71
  });
@@ -6,6 +6,8 @@ const POLICY = [
6
6
  "style-src 'self' 'unsafe-inline'",
7
7
  "img-src 'self' data: https:",
8
8
  "object-src 'none'",
9
+ "form-action 'none'",
10
+ "frame-src 'none'",
9
11
  "base-uri 'none'",
10
12
  "frame-ancestors 'none'",
11
13
  ].join("; ");
@@ -10,6 +10,8 @@ export interface Backlink {
10
10
  function references(document: MetadataDocument, model: MetadataModel) {
11
11
  const { fields, lines } = document.parsed;
12
12
  return [
13
+ ...(fields.question ? [{ kind: "reviewed question", line: lines.question, target: referenceFile(fields.question.item, model) }] : []),
14
+ ...(fields.response ? [{ kind: "recorded response", line: lines.response, target: referenceFile(fields.response.question, model) }] : []),
13
15
  ...(fields.attention?.flatMap((request, index) =>
14
16
  request.unblocks.map((target, targetIndex) => ({
15
17
  kind: "attention target",