@shivaedev/work-board 0.7.2 → 0.9.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.
- package/CHANGELOG.md +44 -0
- package/README.md +110 -2
- package/assets/agent.js +37 -0
- package/assets/native.js +54 -25
- package/dist/board.d.ts +1 -0
- package/dist/board.d.ts.map +1 -1
- package/dist/board.js +15 -2
- package/dist/board.js.map +1 -1
- package/dist/browser/client.d.ts +19 -79
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/client.js +12 -1
- package/dist/browser/client.js.map +1 -1
- package/dist/browser/entry.d.ts +3 -0
- package/dist/browser/entry.d.ts.map +1 -0
- package/dist/browser/entry.js +7 -0
- package/dist/browser/entry.js.map +1 -0
- package/dist/browser/responses/Prompt.d.ts +10 -0
- package/dist/browser/responses/Prompt.d.ts.map +1 -0
- package/dist/browser/responses/Prompt.js +2 -0
- package/dist/browser/responses/Prompt.js.map +1 -0
- package/dist/browser/responses/answerBody.d.ts +4 -0
- package/dist/browser/responses/answerBody.d.ts.map +1 -0
- package/dist/browser/responses/answerBody.js +9 -0
- package/dist/browser/responses/answerBody.js.map +1 -0
- package/dist/browser/responses/draftSession.d.ts +8 -0
- package/dist/browser/responses/draftSession.d.ts.map +1 -0
- package/dist/browser/responses/draftSession.js +105 -0
- package/dist/browser/responses/draftSession.js.map +1 -0
- package/dist/browser/responses/drafts.d.ts +21 -0
- package/dist/browser/responses/drafts.d.ts.map +1 -0
- package/dist/browser/responses/drafts.js +68 -0
- package/dist/browser/responses/drafts.js.map +1 -0
- package/dist/browser/responses/preview.d.ts +4 -0
- package/dist/browser/responses/preview.d.ts.map +1 -0
- package/dist/browser/responses/preview.js +5 -0
- package/dist/browser/responses/preview.js.map +1 -0
- package/dist/browser/responses/questionnaireControls.d.ts +10 -0
- package/dist/browser/responses/questionnaireControls.d.ts.map +1 -0
- package/dist/browser/responses/questionnaireControls.js +68 -0
- package/dist/browser/responses/questionnaireControls.js.map +1 -0
- package/dist/browser/responses/schema.d.ts +168 -0
- package/dist/browser/responses/schema.d.ts.map +1 -0
- package/dist/browser/responses/schema.js +57 -0
- package/dist/browser/responses/schema.js.map +1 -0
- package/dist/browser/responses/setup.d.ts +2 -0
- package/dist/browser/responses/setup.d.ts.map +1 -0
- package/dist/browser/responses/setup.js +129 -0
- package/dist/browser/responses/setup.js.map +1 -0
- package/dist/browser/responses/submissionFields.d.ts +15 -0
- package/dist/browser/responses/submissionFields.d.ts.map +1 -0
- package/dist/browser/responses/submissionFields.js +14 -0
- package/dist/browser/responses/submissionFields.js.map +1 -0
- package/dist/browser/responses/submit.d.ts +4 -0
- package/dist/browser/responses/submit.d.ts.map +1 -0
- package/dist/browser/responses/submit.js +30 -0
- package/dist/browser/responses/submit.js.map +1 -0
- package/dist/browser/session.d.ts +1 -79
- package/dist/browser/session.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +24 -15
- package/dist/cli.js.map +1 -1
- package/dist/files/changes.d.ts +1 -0
- package/dist/files/changes.d.ts.map +1 -1
- package/dist/files/changes.js +1 -1
- package/dist/files/changes.js.map +1 -1
- package/dist/http/respond.d.ts.map +1 -1
- package/dist/http/respond.js +2 -0
- package/dist/http/respond.js.map +1 -1
- package/dist/metadata/backlinks.d.ts.map +1 -1
- package/dist/metadata/backlinks.js +5 -0
- package/dist/metadata/backlinks.js.map +1 -1
- package/dist/metadata/fields.d.ts +23 -1
- package/dist/metadata/fields.d.ts.map +1 -1
- package/dist/metadata/problems.d.ts.map +1 -1
- package/dist/metadata/problems.js +3 -0
- package/dist/metadata/problems.js.map +1 -1
- package/dist/metadata/schema.d.ts +23 -1
- package/dist/metadata/schema.d.ts.map +1 -1
- package/dist/metadata/schema.js +6 -2
- package/dist/metadata/schema.js.map +1 -1
- package/dist/page/attentionDetails.d.ts.map +1 -1
- package/dist/page/attentionDetails.js +2 -1
- package/dist/page/attentionDetails.js.map +1 -1
- package/dist/page/metadataDetails.d.ts.map +1 -1
- package/dist/page/metadataDetails.js +6 -0
- package/dist/page/metadataDetails.js.map +1 -1
- package/dist/page/navigationScript.js +1 -1
- package/dist/page/shell.d.ts +1 -1
- package/dist/page/shell.d.ts.map +1 -1
- package/dist/page/shell.js.map +1 -1
- package/dist/page/style.d.ts.map +1 -1
- package/dist/page/style.js +3 -1
- package/dist/page/style.js.map +1 -1
- package/dist/page/swap.d.ts +1 -1
- package/dist/page/swap.d.ts.map +1 -1
- package/dist/page/swap.js +3 -0
- package/dist/page/swap.js.map +1 -1
- package/dist/path/Identity.d.ts +3 -0
- package/dist/path/Identity.d.ts.map +1 -0
- package/dist/path/Identity.js +3 -0
- package/dist/path/Identity.js.map +1 -0
- package/dist/render/markdown.d.ts +1 -0
- package/dist/render/markdown.d.ts.map +1 -1
- package/dist/render/markdown.js +4 -2
- package/dist/render/markdown.js.map +1 -1
- package/dist/render/questionnaireDocuments.d.ts +3 -0
- package/dist/render/questionnaireDocuments.d.ts.map +1 -0
- package/dist/render/questionnaireDocuments.js +11 -0
- package/dist/render/questionnaireDocuments.js.map +1 -0
- package/dist/render/visualDocuments.d.ts.map +1 -1
- package/dist/render/visualDocuments.js +3 -0
- package/dist/render/visualDocuments.js.map +1 -1
- package/dist/responses/agent-cli.d.ts +2 -0
- package/dist/responses/agent-cli.d.ts.map +1 -0
- package/dist/responses/agent-cli.js +31 -0
- package/dist/responses/agent-cli.js.map +1 -0
- package/dist/responses/agent.d.ts +80 -0
- package/dist/responses/agent.d.ts.map +1 -0
- package/dist/responses/agent.js +49 -0
- package/dist/responses/agent.js.map +1 -0
- package/dist/responses/await.d.ts +39 -0
- package/dist/responses/await.d.ts.map +1 -0
- package/dist/responses/await.js +21 -0
- package/dist/responses/await.js.map +1 -0
- package/dist/responses/composerHtml.d.ts +27 -0
- package/dist/responses/composerHtml.d.ts.map +1 -0
- package/dist/responses/composerHtml.js +14 -0
- package/dist/responses/composerHtml.js.map +1 -0
- package/dist/responses/flags.d.ts +6 -0
- package/dist/responses/flags.d.ts.map +1 -0
- package/dist/responses/flags.js +23 -0
- package/dist/responses/flags.js.map +1 -0
- package/dist/responses/history.d.ts +20 -0
- package/dist/responses/history.d.ts.map +1 -0
- package/dist/responses/history.js +16 -0
- package/dist/responses/history.js.map +1 -0
- package/dist/responses/locate.d.ts +13 -0
- package/dist/responses/locate.d.ts.map +1 -0
- package/dist/responses/locate.js +55 -0
- package/dist/responses/locate.js.map +1 -0
- package/dist/responses/make.d.ts +3 -0
- package/dist/responses/make.d.ts.map +1 -0
- package/dist/responses/make.js +16 -0
- package/dist/responses/make.js.map +1 -0
- package/dist/responses/outcome.d.ts +71 -0
- package/dist/responses/outcome.d.ts.map +1 -0
- package/dist/responses/outcome.js +14 -0
- package/dist/responses/outcome.js.map +1 -0
- package/dist/responses/page.d.ts +6 -0
- package/dist/responses/page.d.ts.map +1 -0
- package/dist/responses/page.js +22 -0
- package/dist/responses/page.js.map +1 -0
- package/dist/responses/publish.d.ts +4 -0
- package/dist/responses/publish.d.ts.map +1 -0
- package/dist/responses/publish.js +56 -0
- package/dist/responses/publish.js.map +1 -0
- package/dist/responses/publishIn.d.ts +3 -0
- package/dist/responses/publishIn.d.ts.map +1 -0
- package/dist/responses/publishIn.js +69 -0
- package/dist/responses/publishIn.js.map +1 -0
- package/dist/responses/read.d.ts +36 -0
- package/dist/responses/read.d.ts.map +1 -0
- package/dist/responses/read.js +43 -0
- package/dist/responses/read.js.map +1 -0
- package/dist/responses/records.d.ts +12 -0
- package/dist/responses/records.d.ts.map +1 -0
- package/dist/responses/records.js +49 -0
- package/dist/responses/records.js.map +1 -0
- package/dist/responses/retry.d.ts +34 -0
- package/dist/responses/retry.d.ts.map +1 -0
- package/dist/responses/retry.js +14 -0
- package/dist/responses/retry.js.map +1 -0
- package/dist/responses/service.d.ts +142 -0
- package/dist/responses/service.d.ts.map +1 -0
- package/dist/responses/service.js +95 -0
- package/dist/responses/service.js.map +1 -0
- package/dist/responses/template.d.ts +7 -0
- package/dist/responses/template.d.ts.map +1 -0
- package/dist/responses/template.js +58 -0
- package/dist/responses/template.js.map +1 -0
- package/dist/responses/templateHtml.d.ts +7 -0
- package/dist/responses/templateHtml.d.ts.map +1 -0
- package/dist/responses/templateHtml.js +18 -0
- package/dist/responses/templateHtml.js.map +1 -0
- package/dist/responses/templateLinks.d.ts +3 -0
- package/dist/responses/templateLinks.d.ts.map +1 -0
- package/dist/responses/templateLinks.js +45 -0
- package/dist/responses/templateLinks.js.map +1 -0
- package/dist/responses/templateNodes.d.ts +17 -0
- package/dist/responses/templateNodes.d.ts.map +1 -0
- package/dist/responses/templateNodes.js +47 -0
- package/dist/responses/templateNodes.js.map +1 -0
- package/dist/responses/validateSupersedes.d.ts +5 -0
- package/dist/responses/validateSupersedes.d.ts.map +1 -0
- package/dist/responses/validateSupersedes.js +21 -0
- package/dist/responses/validateSupersedes.js.map +1 -0
- package/dist/responses/validatedInput.d.ts +3 -0
- package/dist/responses/validatedInput.d.ts.map +1 -0
- package/dist/responses/validatedInput.js +43 -0
- package/dist/responses/validatedInput.js.map +1 -0
- package/dist/responses/waitFor.d.ts +37 -0
- package/dist/responses/waitFor.d.ts.map +1 -0
- package/dist/responses/waitFor.js +47 -0
- package/dist/responses/waitFor.js.map +1 -0
- package/dist/rpc/contract.d.ts +125 -2
- package/dist/rpc/contract.d.ts.map +1 -1
- package/dist/rpc/contract.js +3 -2
- package/dist/rpc/contract.js.map +1 -1
- package/dist/rpc/responseContract.d.ts +260 -0
- package/dist/rpc/responseContract.d.ts.map +1 -0
- package/dist/rpc/responseContract.js +37 -0
- package/dist/rpc/responseContract.js.map +1 -0
- package/dist/rpc/server.d.ts +3 -1
- package/dist/rpc/server.d.ts.map +1 -1
- package/dist/rpc/server.js +6 -1
- package/dist/rpc/server.js.map +1 -1
- package/dist/serve-cli.d.ts +3 -0
- package/dist/serve-cli.d.ts.map +1 -0
- package/dist/serve-cli.js +20 -0
- package/dist/serve-cli.js.map +1 -0
- package/package.json +3 -3
- package/src/board.ts +16 -2
- package/src/browser/client.ts +30 -4
- package/src/browser/entry.ts +9 -0
- package/src/browser/responses/Prompt.ts +6 -0
- package/src/browser/responses/answerBody.ts +11 -0
- package/src/browser/responses/draftSession.ts +101 -0
- package/src/browser/responses/drafts.ts +82 -0
- package/src/browser/responses/preview.ts +7 -0
- package/src/browser/responses/questionnaireControls.ts +74 -0
- package/src/browser/responses/schema.ts +63 -0
- package/src/browser/responses/setup.ts +132 -0
- package/src/browser/responses/submissionFields.ts +15 -0
- package/src/browser/responses/submit.ts +33 -0
- package/src/cli.ts +14 -22
- package/src/files/changes.ts +2 -1
- package/src/http/respond.ts +2 -0
- package/src/metadata/backlinks.ts +5 -0
- package/src/metadata/problems.ts +3 -0
- package/src/metadata/schema.ts +6 -2
- package/src/page/attentionDetails.ts +2 -1
- package/src/page/metadataDetails.ts +6 -0
- package/src/page/navigationScript.ts +1 -1
- package/src/page/shell.ts +1 -1
- package/src/page/style.ts +3 -1
- package/src/page/swap.ts +3 -0
- package/src/path/Identity.ts +3 -0
- package/src/render/markdown.ts +5 -2
- package/src/render/questionnaireDocuments.ts +12 -0
- package/src/render/visualDocuments.ts +3 -0
- package/src/responses/agent-cli.ts +35 -0
- package/src/responses/agent.ts +59 -0
- package/src/responses/await.ts +21 -0
- package/src/responses/composerHtml.ts +28 -0
- package/src/responses/flags.ts +22 -0
- package/src/responses/history.ts +18 -0
- package/src/responses/locate.ts +60 -0
- package/src/responses/make.ts +17 -0
- package/src/responses/outcome.ts +15 -0
- package/src/responses/page.ts +30 -0
- package/src/responses/publish.ts +53 -0
- package/src/responses/publishIn.ts +64 -0
- package/src/responses/read.ts +45 -0
- package/src/responses/records.ts +55 -0
- package/src/responses/retry.ts +23 -0
- package/src/responses/service.ts +117 -0
- package/src/responses/template.ts +72 -0
- package/src/responses/templateHtml.ts +26 -0
- package/src/responses/templateLinks.ts +43 -0
- package/src/responses/templateNodes.ts +55 -0
- package/src/responses/validateSupersedes.ts +24 -0
- package/src/responses/validatedInput.ts +45 -0
- package/src/responses/waitFor.ts +58 -0
- package/src/rpc/contract.ts +3 -2
- package/src/rpc/responseContract.ts +46 -0
- package/src/rpc/server.ts +8 -1
- package/src/serve-cli.ts +37 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { draftSession } from "./draftSession.ts";
|
|
2
|
+
import { responsePreview } from "./preview.ts";
|
|
3
|
+
import { questionnaireControls } from "./questionnaireControls.ts";
|
|
4
|
+
import { submitResponse } from "./submit.ts";
|
|
5
|
+
|
|
6
|
+
function attach() {
|
|
7
|
+
const doc = document.getElementById("doc");
|
|
8
|
+
if (doc?.dataset.view !== "response") {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const form = doc.querySelector<HTMLFormElement>("#response-form");
|
|
12
|
+
if (!form || form.dataset.bound === "true") {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
form.dataset.bound = "true";
|
|
16
|
+
const status = form.querySelector<HTMLParagraphElement>("#response-status");
|
|
17
|
+
const author = form.elements.namedItem("author");
|
|
18
|
+
const body = form.elements.namedItem("body");
|
|
19
|
+
const type = form.elements.namedItem("type");
|
|
20
|
+
const supersedes = form.elements.namedItem("supersedes");
|
|
21
|
+
const submit = form.querySelector<HTMLButtonElement>("#response-submit");
|
|
22
|
+
const preview = form.querySelector<HTMLPreElement>("#response-preview-content");
|
|
23
|
+
const clear = form.querySelector<HTMLButtonElement>("#response-clear");
|
|
24
|
+
const previewButton = form.querySelector<HTMLButtonElement>("#response-preview");
|
|
25
|
+
if (
|
|
26
|
+
!(
|
|
27
|
+
status
|
|
28
|
+
&& author instanceof HTMLInputElement
|
|
29
|
+
&& body instanceof HTMLTextAreaElement
|
|
30
|
+
&& type instanceof HTMLSelectElement
|
|
31
|
+
&& submit
|
|
32
|
+
&& preview
|
|
33
|
+
&& clear
|
|
34
|
+
&& previewButton
|
|
35
|
+
)
|
|
36
|
+
) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (status.textContent?.includes("JavaScript is required")) {
|
|
40
|
+
status.textContent = "Draft ready. Preview the Markdown before recording it.";
|
|
41
|
+
}
|
|
42
|
+
const item = form.dataset.item ?? "";
|
|
43
|
+
const request = form.dataset.request ?? "";
|
|
44
|
+
const revision = form.dataset.revision ?? "";
|
|
45
|
+
const questionId = form.dataset.question ?? "";
|
|
46
|
+
const key = `${item}/${request}`;
|
|
47
|
+
const storage = `work-board:drafts:${document.documentElement.dataset.workspace}`;
|
|
48
|
+
let storageApi: Storage | undefined;
|
|
49
|
+
try {
|
|
50
|
+
storageApi = window.localStorage;
|
|
51
|
+
} catch {
|
|
52
|
+
storageApi = undefined;
|
|
53
|
+
}
|
|
54
|
+
const drafts = draftSession(key, revision, storageApi, storage, (message) => {
|
|
55
|
+
status.textContent = message;
|
|
56
|
+
});
|
|
57
|
+
let draft = drafts.current();
|
|
58
|
+
const controls = questionnaireControls(form);
|
|
59
|
+
let recovery = controls.restore(draft, revision);
|
|
60
|
+
if (draft.question && draft.question !== questionId && draft.revision === revision) {
|
|
61
|
+
status.textContent = "Source moved since this draft. Text is retained and selections cleared; review the source and preview before responding.";
|
|
62
|
+
}
|
|
63
|
+
if (supersedes instanceof HTMLSelectElement) {
|
|
64
|
+
supersedes.value = draft.supersedes ?? "";
|
|
65
|
+
}
|
|
66
|
+
author.value = draft.author;
|
|
67
|
+
body.value = draft.body;
|
|
68
|
+
type.value = draft.type;
|
|
69
|
+
const fields = { author, body, type };
|
|
70
|
+
function save() {
|
|
71
|
+
draft = drafts.update({
|
|
72
|
+
answers: controls.read(),
|
|
73
|
+
author: fields.author.value,
|
|
74
|
+
body: fields.body.value,
|
|
75
|
+
question: draft.question ?? questionId,
|
|
76
|
+
recovery,
|
|
77
|
+
revision: draft.revision,
|
|
78
|
+
supersedes: supersedes instanceof HTMLSelectElement ? supersedes.value || undefined : undefined,
|
|
79
|
+
type: fields.type.value,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
form.addEventListener("input", () => {
|
|
83
|
+
save();
|
|
84
|
+
preview.hidden = true;
|
|
85
|
+
submit.disabled = true;
|
|
86
|
+
});
|
|
87
|
+
previewButton.disabled = false;
|
|
88
|
+
clear.disabled = false;
|
|
89
|
+
previewButton.addEventListener("click", () => {
|
|
90
|
+
controls.validate(type.value);
|
|
91
|
+
draft = drafts.update({
|
|
92
|
+
answers: controls.read(),
|
|
93
|
+
author: author.value,
|
|
94
|
+
body: body.value,
|
|
95
|
+
id: draft.revision === revision && (!draft.question || draft.question === questionId) ? drafts.current().id : `response.${crypto.randomUUID()}`,
|
|
96
|
+
question: questionId,
|
|
97
|
+
recovery,
|
|
98
|
+
revision,
|
|
99
|
+
supersedes: supersedes instanceof HTMLSelectElement ? supersedes.value || undefined : undefined,
|
|
100
|
+
type: type.value,
|
|
101
|
+
});
|
|
102
|
+
preview.textContent = responsePreview(draft, questionId ?? "", controls.models);
|
|
103
|
+
preview.hidden = false;
|
|
104
|
+
submit.disabled = form.dataset.writable !== "true" || !form.checkValidity();
|
|
105
|
+
});
|
|
106
|
+
clear.addEventListener("click", drafts.clear);
|
|
107
|
+
form.addEventListener("submit", async (event) => {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
if (submit.disabled || draft.revision !== revision || draft.question !== questionId || !form.reportValidity()) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
submit.disabled = true;
|
|
113
|
+
previewButton.disabled = true;
|
|
114
|
+
if (await submitResponse(form, status, drafts, draft)) {
|
|
115
|
+
draft = drafts.current();
|
|
116
|
+
recovery = draft.recovery;
|
|
117
|
+
if (form.isConnected) {
|
|
118
|
+
body.value = "";
|
|
119
|
+
controls.restore(draft, revision);
|
|
120
|
+
if (supersedes instanceof HTMLSelectElement) {
|
|
121
|
+
supersedes.value = "";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
previewButton.disabled = false;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
export function setupResponses() {
|
|
129
|
+
document.addEventListener("board-page", attach);
|
|
130
|
+
window.addEventListener("pageshow", attach);
|
|
131
|
+
attach();
|
|
132
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { Draft } from "./drafts.ts";
|
|
3
|
+
import { ResponseKind } from "./schema.ts";
|
|
4
|
+
|
|
5
|
+
export function submissionFields(pending: Draft, question: string) {
|
|
6
|
+
return {
|
|
7
|
+
...(pending.answers === undefined ? {} : { answers: pending.answers }),
|
|
8
|
+
...(pending.supersedes === undefined ? {} : { supersedes: pending.supersedes }),
|
|
9
|
+
author: pending.author,
|
|
10
|
+
body: pending.body,
|
|
11
|
+
id: pending.id,
|
|
12
|
+
question,
|
|
13
|
+
type: Schema.decodeUnknownSync(ResponseKind)(pending.type),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { session } from "#browser/session.ts";
|
|
2
|
+
import type { draftSession } from "./draftSession.ts";
|
|
3
|
+
import type { Draft } from "./drafts.ts";
|
|
4
|
+
import { ResponseFailed } from "./schema.ts";
|
|
5
|
+
import { submissionFields } from "./submissionFields.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(native.responses.recordResponse.run(submissionFields(pending, question.id)));
|
|
16
|
+
if (form.isConnected) {
|
|
17
|
+
status.dataset.outcome = "saved";
|
|
18
|
+
status.textContent = `Saved: responses/${pending.id}.md. This records feedback, not acceptance.`;
|
|
19
|
+
}
|
|
20
|
+
const cleared = drafts.saved(pending);
|
|
21
|
+
if (!cleared && form.isConnected) {
|
|
22
|
+
status.textContent += " Your current draft remains unsaved.";
|
|
23
|
+
}
|
|
24
|
+
return cleared;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (form.isConnected) {
|
|
27
|
+
const outcome = error instanceof ResponseFailed && error.code !== "Uncertain" ? "rejected" : "uncertain";
|
|
28
|
+
status.dataset.outcome = outcome;
|
|
29
|
+
status.textContent = `${outcome === "rejected" ? "Rejected" : "Uncertain"}: ${String(error)}. Draft retained. Preview and retry with the same identity; saved content will be reconciled.`;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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 {};
|
package/src/files/changes.ts
CHANGED
|
@@ -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
|
});
|
package/src/http/respond.ts
CHANGED
|
@@ -10,6 +10,11 @@ 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) }] : []),
|
|
15
|
+
...(fields.response?.supersedes
|
|
16
|
+
? [{ kind: "superseding response", line: lines.response, target: referenceFile(fields.response.supersedes, model) }]
|
|
17
|
+
: []),
|
|
13
18
|
...(fields.attention?.flatMap((request, index) =>
|
|
14
19
|
request.unblocks.map((target, targetIndex) => ({
|
|
15
20
|
kind: "attention target",
|
package/src/metadata/problems.ts
CHANGED
|
@@ -27,6 +27,9 @@ function criterionProblems(document: MetadataDocument): readonly Diagnostic[] {
|
|
|
27
27
|
function referenceProblems(document: MetadataDocument, ids: ReadonlyMap<string, readonly MetadataDocument[]>): readonly Diagnostic[] {
|
|
28
28
|
const { fields, lines } = document.parsed;
|
|
29
29
|
const references = [
|
|
30
|
+
...(fields.question ? [{ field: "question", key: "question.item", target: fields.question.item }] : []),
|
|
31
|
+
...(fields.response ? [{ field: "response", key: "response.question", target: fields.response.question }] : []),
|
|
32
|
+
...(fields.response?.supersedes ? [{ field: "response", key: "response.supersedes", target: fields.response.supersedes }] : []),
|
|
30
33
|
...(fields.attention?.flatMap((request, index) =>
|
|
31
34
|
request.unblocks.map((target, targetIndex) => ({ field: "attention", key: `attention.${index}.unblocks.${targetIndex}`, target })),
|
|
32
35
|
) ?? []),
|
package/src/metadata/schema.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
import { QuestionRecord, ResponseRecord } from "#browser/responses/schema.ts";
|
|
3
|
+
import { Identity as SourceIdentity } from "#path/Identity.ts";
|
|
2
4
|
|
|
3
|
-
export const Identity =
|
|
5
|
+
export const Identity = SourceIdentity;
|
|
4
6
|
export const Reference = Schema.String.check(Schema.isPattern(/^[a-zA-Z\d][a-zA-Z\d._-]*(?:#[a-zA-Z\d][a-zA-Z\d._-]*)?$/u));
|
|
5
7
|
const Text = Schema.String.check(Schema.isPattern(/\S/u));
|
|
6
8
|
const Attention = Schema.Struct({
|
|
@@ -36,10 +38,12 @@ export const Metadata = Schema.Struct({
|
|
|
36
38
|
evidence: Schema.optional(Schema.Array(Evidence)),
|
|
37
39
|
id: Schema.optional(Identity),
|
|
38
40
|
items: Schema.optional(Schema.Array(Identity)),
|
|
39
|
-
kind: Schema.optional(Schema.Literals(["task", "investigation", "decision", "result", "project", "board"])),
|
|
41
|
+
kind: Schema.optional(Schema.Literals(["task", "investigation", "decision", "result", "project", "board", "question", "response"])),
|
|
40
42
|
nextAction: Schema.optional(Text),
|
|
41
43
|
owner: Schema.optional(Text),
|
|
44
|
+
question: Schema.optional(QuestionRecord),
|
|
42
45
|
relationships: Schema.optional(Schema.Array(Relationship)),
|
|
46
|
+
response: Schema.optional(ResponseRecord),
|
|
43
47
|
status: Schema.optional(Text),
|
|
44
48
|
}).pipe(Schema.encodeKeys({ nextAction: "next_action" }));
|
|
45
49
|
export type Metadata = typeof Metadata.Type;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { attentionSource } from "#attention/requests.ts";
|
|
2
|
+
import { identityUrl } from "#metadata/links.ts";
|
|
2
3
|
import type { MetadataModel } from "#metadata/model.ts";
|
|
3
4
|
import type { ParsedMetadata } from "#metadata/parse.ts";
|
|
4
5
|
import { escapeHtml } from "./escape.ts";
|
|
@@ -12,7 +13,7 @@ export function attentionDetails(parsed: ParsedMetadata, file: string, model: Me
|
|
|
12
13
|
return `<h3>Recorded attention requests</h3><p>Open/closed is source state. Closing a request does not establish acceptance or record an answer.</p><ul>${requests
|
|
13
14
|
.map(
|
|
14
15
|
(request, index) =>
|
|
15
|
-
`<li id="${attentionSource({ file, parsed }, index)}" tabindex="-1"><b>${escapeHtml(request.kind)} · ${escapeHtml(request.id)} · ${escapeHtml(request.state)}</b><p>${escapeHtml(request.reason)}</p><p>Response needed from: ${request.responseFrom.map(escapeHtml).join(", ")}</p><p>Recorded to unblock: ${request.unblocks.map((target) => referenceHtml(target, target, model)).join(", ")}</p><p>Source: ${escapeHtml(file)}:${parsed.lines[`attention.${index}`] ?? parsed.lines.attention ?? 2}</p></li>`,
|
|
16
|
+
`<li id="${attentionSource({ file, parsed }, index)}" tabindex="-1"><b>${escapeHtml(request.kind)} · ${escapeHtml(request.id)} · ${escapeHtml(request.state)}</b><p>${escapeHtml(request.reason)}</p><p>Response needed from: ${request.responseFrom.map(escapeHtml).join(", ")}</p><p>Recorded to unblock: ${request.unblocks.map((target) => referenceHtml(target, target, model)).join(", ")}</p><p><a href="/_board/respond?item=${encodeURIComponent(parsed.fields.id ?? "")}&request=${encodeURIComponent(request.id)}">Respond to this request</a> · <a href="${identityUrl(parsed.fields.id ?? "")}">Reviewed item</a></p><p>Source: ${escapeHtml(file)}:${parsed.lines[`attention.${index}`] ?? parsed.lines.attention ?? 2}</p></li>`,
|
|
16
17
|
)
|
|
17
18
|
.join("")}</ul>`;
|
|
18
19
|
}
|
|
@@ -18,6 +18,12 @@ export function metadataDetails(parsed: ParsedMetadata, file: string, model: Met
|
|
|
18
18
|
? `<h3>Declared board membership</h3><ul>${fields.items.map((id) => `<li>${referenceHtml(id, id, model)}</li>`).join("")}</ul>`
|
|
19
19
|
: "",
|
|
20
20
|
|
|
21
|
+
fields.question
|
|
22
|
+
? `<h3>Reviewed question</h3><p>${referenceHtml(fields.question.item, fields.question.item, model)} / ${escapeHtml(fields.question.request)} · ${escapeHtml(fields.question.reason)}</p><p>Reviewed SHA-256: <code>${fields.question.reviewedRevision}</code>. Registered: ${new Date(fields.question.registeredAt).toISOString()}. Unanswered deadline: ${new Date(fields.question.deadline).toISOString()}.</p>`
|
|
23
|
+
: "",
|
|
24
|
+
fields.response
|
|
25
|
+
? `<h3>Recorded response</h3><p>${referenceHtml(fields.response.question, fields.response.question, model)} · ${escapeHtml(fields.response.author)} · ${escapeHtml(fields.response.type)}</p><p>Reviewed SHA-256: <code>${fields.response.reviewedRevision}</code>. This is authored feedback, not verified acceptance.</p>${fields.response.supersedes ? `<p>Explicitly supersedes ${referenceHtml(fields.response.supersedes, fields.response.supersedes, model)}.</p>` : ""}`
|
|
26
|
+
: "",
|
|
21
27
|
criteriaEvidence(parsed, model),
|
|
22
28
|
attentionDetails(parsed, file, model),
|
|
23
29
|
recordedEvidence(parsed, file, model),
|
|
@@ -88,7 +88,7 @@ document.addEventListener("click", (event) => {
|
|
|
88
88
|
const link = event.target.closest?.("a[href]");
|
|
89
89
|
if (!link || event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey || link.hasAttribute("download") || (link.target && link.target !== "_self")) return;
|
|
90
90
|
const target = new URL(link.href, location.href);
|
|
91
|
-
if (target.origin !== location.origin || !(target.pathname === "/" || (target.pathname === "/_board/start" || target.pathname === "/_board/changes" || target.pathname === "/_board/overview" || target.pathname === "/_board/work" || target.pathname.startsWith("/_board/item/")) || /\\.md$/i.test(target.pathname))) return;
|
|
91
|
+
if (target.origin !== location.origin || !(target.pathname === "/" || (target.pathname === "/_board/respond" || target.pathname === "/_board/start" || target.pathname === "/_board/changes" || target.pathname === "/_board/overview" || target.pathname === "/_board/work" || target.pathname.startsWith("/_board/item/")) || /\\.md$/i.test(target.pathname))) return;
|
|
92
92
|
event.preventDefault();
|
|
93
93
|
navigate(target);
|
|
94
94
|
});
|
package/src/page/shell.ts
CHANGED
|
@@ -17,7 +17,7 @@ export const shell = (
|
|
|
17
17
|
workspace: string,
|
|
18
18
|
board: boolean,
|
|
19
19
|
identity?: string,
|
|
20
|
-
view: boolean | "overview" | "changes" | "start" = false,
|
|
20
|
+
view: boolean | "overview" | "changes" | "start" | "response" = false,
|
|
21
21
|
): string =>
|
|
22
22
|
[
|
|
23
23
|
`<!doctype html><html lang="en" data-workspace="${escapeHtml(workspace)}"><head><meta charset="utf-8">`,
|
package/src/page/style.ts
CHANGED
|
@@ -96,4 +96,6 @@ export const style =
|
|
|
96
96
|
+ workStyle()
|
|
97
97
|
+ attentionStyle()
|
|
98
98
|
+ visualStyles
|
|
99
|
-
+ documents
|
|
99
|
+
+ documents
|
|
100
|
+
+ ".response-panel fieldset, .questionnaire-reading { min-width: 0; border: 1px solid var(--border); border-radius: 12px; padding: 1rem; margin: 1rem 0; } .response-panel legend { font-weight: 650; } .response-panel .question-option { display: grid; grid-template-columns: 1.5rem minmax(0, 1fr); gap: .75rem; padding: 1rem; margin: .5rem 0; border: 1px solid var(--border); border-radius: 8px; } .response-panel .question-option:has(input:checked) { border-color: var(--link); background: var(--surface); } .response-panel .question-option input { width: 1.2rem; height: 1.2rem; margin-top: .2rem; accent-color: var(--link); } .response-panel .question-option label { font-weight: 650; cursor: pointer; } .response-panel .question-option p { margin-top: .5rem; } .response-panel #response-history article { border-top: 1px solid var(--border); margin-top: 1.5rem; padding-top: 1rem; } .response-context, .question-option, .response-panel fieldset { overflow-wrap: anywhere; }"
|
|
101
|
+
+ ".response-panel { max-width: 850px; margin: auto; padding: 1.5rem; } .response-panel form { display: grid; gap: 1rem; } .response-panel label { display: grid; gap: .5rem; } .response-panel input, .response-panel textarea, .response-panel select { width: 100%; box-sizing: border-box; font: inherit; background: var(--surface); color: inherit; padding: .75rem; border: 1px solid var(--border); } .response-panel pre, .response-panel code { white-space: pre-wrap; overflow-wrap: anywhere; }";
|
package/src/page/swap.ts
CHANGED
|
@@ -18,6 +18,9 @@ export const remember = (element) => {
|
|
|
18
18
|
|
|
19
19
|
const normalized = (element) => {
|
|
20
20
|
const copy = element.cloneNode(true);
|
|
21
|
+
for (const form of [copy, ...copy.querySelectorAll("#response-form")]) {
|
|
22
|
+
if (form.id === "response-form") { form.replaceChildren(); delete form.dataset.bound; }
|
|
23
|
+
}
|
|
21
24
|
for (const tool of copy.querySelectorAll("[data-visual-tools], [data-image-error], [data-diagram-error]")) tool.remove();
|
|
22
25
|
for (const node of [copy, ...copy.querySelectorAll("*")]) {
|
|
23
26
|
node.removeAttribute("data-live-change");
|
package/src/render/markdown.ts
CHANGED
|
@@ -14,16 +14,18 @@ import { documentLinks } from "./documentLinks.ts";
|
|
|
14
14
|
import { RenderFailed } from "./failed.ts";
|
|
15
15
|
import { footnoteIds } from "./footnotes.ts";
|
|
16
16
|
import { Highlighter } from "./highlighter.ts";
|
|
17
|
+
import { questionnaireDocuments } from "./questionnaireDocuments.ts";
|
|
17
18
|
|
|
18
19
|
export interface RenderOptions {
|
|
19
20
|
readonly file?: string;
|
|
20
21
|
readonly headings?: ReturnType<typeof documentHeadings>;
|
|
21
22
|
readonly idPrefix?: string;
|
|
23
|
+
readonly safe?: boolean;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export const renderMarkdown = Effect.fn("WorkBoard.renderMarkdown")(function* (source: string, options: RenderOptions = {}) {
|
|
25
27
|
const highlighter = yield* Highlighter;
|
|
26
|
-
const plugins: PluggableList = [rehypeRaw, codeBlocks(highlighter)];
|
|
28
|
+
const plugins: PluggableList = [...(options.safe ? [] : [rehypeRaw]), codeBlocks(highlighter)];
|
|
27
29
|
if (options.idPrefix !== undefined) {
|
|
28
30
|
plugins.push(footnoteIds(options.idPrefix));
|
|
29
31
|
}
|
|
@@ -41,7 +43,8 @@ export const renderMarkdown = Effect.fn("WorkBoard.renderMarkdown")(function* (s
|
|
|
41
43
|
children: source,
|
|
42
44
|
components: { img: ({ node: _node, ...props }) => createElement("img", { ...props, loading: "lazy" }) },
|
|
43
45
|
rehypePlugins: plugins,
|
|
44
|
-
remarkPlugins: [remarkGfm, remarkDirective, visualDocuments, callouts],
|
|
46
|
+
remarkPlugins: [remarkGfm, remarkDirective, questionnaireDocuments, visualDocuments, callouts],
|
|
47
|
+
skipHtml: options.safe,
|
|
45
48
|
}),
|
|
46
49
|
),
|
|
47
50
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Root } from "mdast";
|
|
2
|
+
import { visit } from "unist-util-visit";
|
|
3
|
+
|
|
4
|
+
export function questionnaireDocuments() {
|
|
5
|
+
return (tree: Root) => {
|
|
6
|
+
visit(tree, "containerDirective", (node) => {
|
|
7
|
+
if (node.name === "question" || node.name === "option") {
|
|
8
|
+
node.data = { hName: "section", hProperties: { className: ["questionnaire-reading", `questionnaire-${node.name}`] } };
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -43,6 +43,9 @@ export function visualDocuments() {
|
|
|
43
43
|
if (!isDirective(directive) || index === undefined || parent === undefined) {
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
|
+
if (directive.type === "containerDirective" && ["question", "option"].includes(directive.name)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
46
49
|
const failure = failureOf(directive, source);
|
|
47
50
|
if (failure === undefined) {
|
|
48
51
|
return;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { agentRequest } from "./agent.ts";
|
|
3
|
+
import { flags } from "./flags.ts";
|
|
4
|
+
|
|
5
|
+
export async function runAgent(args: readonly string[]) {
|
|
6
|
+
const [action, request, ...options] = args;
|
|
7
|
+
if ((action !== "wait" && action !== "response" && action !== "question") || !request) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
"Usage: work-board question <item>/<request> | wait <question-id> [--after <response-id>] | response <question-id> [--port 4747]",
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
const parsed = flags(options);
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
function cancel() {
|
|
15
|
+
controller.abort();
|
|
16
|
+
}
|
|
17
|
+
process.once("SIGINT", cancel);
|
|
18
|
+
process.once("SIGTERM", cancel);
|
|
19
|
+
try {
|
|
20
|
+
const result = await Effect.runPromise(
|
|
21
|
+
agentRequest(action, { ...parsed, request }, (text) => process.stderr.write(`${text}\n`)).pipe(Effect.result),
|
|
22
|
+
{ signal: controller.signal },
|
|
23
|
+
);
|
|
24
|
+
if (result._tag === "Failure") {
|
|
25
|
+
if (result.failure instanceof Error && "code" in result.failure && result.failure.code === "Unanswered") {
|
|
26
|
+
process.exitCode = 2;
|
|
27
|
+
}
|
|
28
|
+
throw result.failure;
|
|
29
|
+
}
|
|
30
|
+
process.stdout.write(`${JSON.stringify(result.success)}\n`);
|
|
31
|
+
} finally {
|
|
32
|
+
process.removeListener("SIGINT", cancel);
|
|
33
|
+
process.removeListener("SIGTERM", cancel);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as RpcClient from "effect/unstable/rpc/RpcClient";
|
|
5
|
+
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
|
|
6
|
+
import { responseContract } from "#rpc/responseContract.ts";
|
|
7
|
+
import { waitForResponse } from "./waitFor.ts";
|
|
8
|
+
|
|
9
|
+
export interface AgentOptions {
|
|
10
|
+
readonly after?: string | undefined;
|
|
11
|
+
readonly request: string;
|
|
12
|
+
readonly revision?: string | undefined;
|
|
13
|
+
readonly url: string;
|
|
14
|
+
}
|
|
15
|
+
function protocol(url: string) {
|
|
16
|
+
return RpcClient.layerProtocolHttp({ url: `${url}/_board/rpc` }).pipe(Layer.provide([NodeHttpClient.layerNodeHttp, RpcSerialization.layerNdjson]));
|
|
17
|
+
}
|
|
18
|
+
function validate(url: string) {
|
|
19
|
+
const parsed = new URL(url);
|
|
20
|
+
if (
|
|
21
|
+
parsed.protocol !== "http:"
|
|
22
|
+
|| !["127.0.0.1", "localhost", "[::1]"].includes(parsed.hostname)
|
|
23
|
+
|| parsed.username
|
|
24
|
+
|| parsed.password
|
|
25
|
+
|| parsed.pathname !== "/"
|
|
26
|
+
|| parsed.search
|
|
27
|
+
|| parsed.hash
|
|
28
|
+
) {
|
|
29
|
+
throw new Error("Use a loopback Work Board URL, such as http://127.0.0.1:4747.");
|
|
30
|
+
}
|
|
31
|
+
return parsed.origin;
|
|
32
|
+
}
|
|
33
|
+
export function agentRequest(action: "question" | "response" | "wait", options: AgentOptions, diagnostic: (text: string) => void = () => undefined) {
|
|
34
|
+
return Effect.scoped(
|
|
35
|
+
Effect.gen(function* () {
|
|
36
|
+
const client = yield* RpcClient.make(responseContract, { flatten: true });
|
|
37
|
+
const [item, request, extra] = options.request.split("/");
|
|
38
|
+
if (action === "question") {
|
|
39
|
+
if (!(item && request) || extra) {
|
|
40
|
+
return yield* Effect.fail(new Error("Use question <item>/<request>."));
|
|
41
|
+
}
|
|
42
|
+
return yield* client("work-board.question", { item, request });
|
|
43
|
+
}
|
|
44
|
+
let id = options.request;
|
|
45
|
+
if (request !== undefined) {
|
|
46
|
+
if (action !== "wait" || !item || !request || extra || !options.revision) {
|
|
47
|
+
return yield* Effect.fail(new Error("Use wait <item>/<request> --revision <reviewed SHA-256>, or wait <registered-question-id>."));
|
|
48
|
+
}
|
|
49
|
+
const question = yield* client("work-board.registerQuestion", { item, request, revision: options.revision });
|
|
50
|
+
id = question.id;
|
|
51
|
+
diagnostic(`Registered ${id}; reattach with work-board wait ${id}. Deadline: ${new Date(question.question.deadline).toISOString()}`);
|
|
52
|
+
}
|
|
53
|
+
if (action === "response") {
|
|
54
|
+
return yield* client("work-board.responses", { question: id });
|
|
55
|
+
}
|
|
56
|
+
return yield* waitForResponse(() => client("work-board.awaitResponse", { after: options.after, question: id }), diagnostic);
|
|
57
|
+
}),
|
|
58
|
+
).pipe(Effect.provide(protocol(validate(options.url))));
|
|
59
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Clock, Effect } from "effect";
|
|
2
|
+
import { type Reading, ResponseFailed } from "#browser/responses/schema.ts";
|
|
3
|
+
import { outcome } from "./outcome.ts";
|
|
4
|
+
|
|
5
|
+
export function awaitResponse(read: (id: string) => Effect.Effect<Reading, ResponseFailed>) {
|
|
6
|
+
return Effect.fn("WorkBoard.awaitResponse")(function* (input: { readonly question: string; readonly after?: string | undefined }) {
|
|
7
|
+
const lease = (yield* Clock.currentTimeMillis) + 30_000;
|
|
8
|
+
while (true) {
|
|
9
|
+
const result = yield* read(input.question);
|
|
10
|
+
const now = yield* Clock.currentTimeMillis;
|
|
11
|
+
const state = outcome(result, input.after, now);
|
|
12
|
+
if (state.kind === "missing_cursor") {
|
|
13
|
+
return yield* Effect.fail(new ResponseFailed({ code: "Missing", message: "The next-response cursor is not recorded for this question." }));
|
|
14
|
+
}
|
|
15
|
+
if (state.kind !== "pending" || now >= lease) {
|
|
16
|
+
return { expired: state.kind === "unanswered", question: result.question, response: state.kind === "response" ? state.response : undefined };
|
|
17
|
+
}
|
|
18
|
+
yield* Effect.sleep("1 second");
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { QuestionPreview, RecordedResponse } from "#browser/responses/schema.ts";
|
|
3
|
+
import { fileUrl } from "#files/url.ts";
|
|
4
|
+
import { escapeHtml } from "#page/escape.ts";
|
|
5
|
+
import { renderMarkdown } from "#render/markdown.ts";
|
|
6
|
+
import { templateHtml } from "./templateHtml.ts";
|
|
7
|
+
|
|
8
|
+
export const composerHtml = Effect.fn("WorkBoard.composerHtml")(function* (
|
|
9
|
+
q: QuestionPreview,
|
|
10
|
+
records: readonly RecordedResponse[],
|
|
11
|
+
enabled: boolean,
|
|
12
|
+
) {
|
|
13
|
+
const template = yield* templateHtml(q.context, q.request, q.source);
|
|
14
|
+
const history = yield* Effect.forEach(records, (record) =>
|
|
15
|
+
Effect.map(
|
|
16
|
+
renderMarkdown(record.body, { file: `responses/${record.id}.md`, safe: true }),
|
|
17
|
+
(html) =>
|
|
18
|
+
`<article id="${escapeHtml(record.id)}"><h3><a href="/responses/${record.id}.md">${escapeHtml(record.response.author)} · ${record.response.type}</a></h3><p>${record.response.question === q.id ? "This reviewed revision" : "Earlier reviewed context — does not approve the current source"} · <code>${record.response.reviewedRevision}</code></p>${record.response.supersedes ? `<p>Explicitly supersedes <a href="/responses/${escapeHtml(record.response.supersedes)}.md">${escapeHtml(record.response.supersedes)}</a>.</p>` : ""}${html}</article>`,
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
const prior = records
|
|
22
|
+
.map(
|
|
23
|
+
(record) =>
|
|
24
|
+
`<option value="${escapeHtml(record.id)}">${escapeHtml(record.id)} · ${escapeHtml(record.response.author)} · ${record.response.reviewedRevision.slice(0, 12)}</option>`,
|
|
25
|
+
)
|
|
26
|
+
.join("");
|
|
27
|
+
return `<section class="response-panel"><h1>Respond to ${escapeHtml(q.item)} / ${escapeHtml(q.request)}</h1><p>${escapeHtml(q.reason)}</p><p><a href="${fileUrl(q.source)}">Source: ${escapeHtml(q.source)}</a> · Reviewed SHA-256: <code>${q.reviewedRevision}</code></p><p>Saved replies are authored feedback. They do not close the request or establish acceptance.</p><section class="response-context">${template.background}</section><details><summary>Exact source being reviewed</summary><pre>${escapeHtml(q.context)}</pre></details><form id="response-form" data-question="${q.id}" data-writable="${enabled}" data-item="${escapeHtml(q.item)}" data-request="${escapeHtml(q.request)}" data-revision="${q.reviewedRevision}"><label>Local author label <input name="author" maxlength="200" required></label><label>Response kind <select name="type"><option value="answer">Answer (including no)</option><option value="clarify">Ask back / clarify</option><option value="not_now">Not now</option></select></label>${template.controls}<label>${template.hasPrompts ? "Overall response / next action (optional)" : "Your response"}<textarea name="body" rows="8" maxlength="32768" ${template.hasPrompts ? "" : "required"}></textarea></label><label>Supersedes an earlier response (optional)<select name="supersedes"><option value="">Adds feedback; does not supersede</option>${prior}</select></label><pre id="response-draft-recovery" hidden></pre><p>Submit the whole packet together. Drafts stay in this browser for 30 days after the last edit, with a 2 MiB workspace limit. Saved responses are Markdown files in responses/.</p><button id="response-preview" type="button" disabled>Preview Markdown</button><pre id="response-preview-content" hidden></pre><button id="response-submit" type="submit" disabled>Record response</button><button id="response-clear" type="button" disabled>Clear workspace drafts</button><p id="response-status" role="status">${enabled ? "JavaScript is required to record a response." : "Read-only mode. Restart with --responses to explicitly enable local writes."}</p></form><section id="response-history"><h2>Recorded responses</h2>${history.join("") || "<p>No response is recorded for this source revision.</p>"}</section></section>`;
|
|
28
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function flags(args: readonly string[]) {
|
|
2
|
+
const values = new Map<string, string>();
|
|
3
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
4
|
+
const name = args[index];
|
|
5
|
+
const value = args[index + 1];
|
|
6
|
+
if (!(name && value && ["--port", "--url", "--revision", "--after"].includes(name))) {
|
|
7
|
+
throw new Error("Use --port, --url, --revision or --after, each with a value.");
|
|
8
|
+
}
|
|
9
|
+
if (values.has(name)) {
|
|
10
|
+
throw new Error(`Repeated option ${name}`);
|
|
11
|
+
}
|
|
12
|
+
values.set(name, value);
|
|
13
|
+
}
|
|
14
|
+
const port = values.get("--port") ?? "4747";
|
|
15
|
+
if (/^\d{1,5}$/u.exec(port) === null || Number(port) > 65_535 || Number(port) < 1) {
|
|
16
|
+
throw new Error("Invalid port.");
|
|
17
|
+
}
|
|
18
|
+
if (values.has("--url") && values.has("--port")) {
|
|
19
|
+
throw new Error("Choose --url or --port.");
|
|
20
|
+
}
|
|
21
|
+
return { after: values.get("--after"), revision: values.get("--revision"), url: values.get("--url") ?? `http://127.0.0.1:${port}` };
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { QuestionPreview } from "#browser/responses/schema.ts";
|
|
2
|
+
import type { Snapshot } from "#search/snapshot.ts";
|
|
3
|
+
import { malformedResponse, questionFrom, responsesFor } from "./records.ts";
|
|
4
|
+
|
|
5
|
+
export function responseHistory(data: Snapshot, preview: QuestionPreview) {
|
|
6
|
+
if (data.unavailable.length > 0 || data.documents.some(malformedResponse)) {
|
|
7
|
+
throw new Error("Response history is incomplete or malformed.");
|
|
8
|
+
}
|
|
9
|
+
const questions = data.documents.flatMap((document) => {
|
|
10
|
+
const question = questionFrom(document);
|
|
11
|
+
return question && question.question.item === preview.item && question.question.request === preview.request ? [question] : [];
|
|
12
|
+
});
|
|
13
|
+
const responses = questions.flatMap((question) => responsesFor(data.documents, question).responses);
|
|
14
|
+
if ([...questions, ...responses].some((record) => data.model.ids.get(record.id)?.length !== 1)) {
|
|
15
|
+
throw new Error("Response history includes ambiguous identities.");
|
|
16
|
+
}
|
|
17
|
+
return responses.sort((a, b) => b.response.recordedAt - a.response.recordedAt);
|
|
18
|
+
}
|