@nanocollective/roster 0.1.0-alpha.50 → 0.1.0-alpha.51
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/dist/cli.js +537 -469
- package/docs/portal.md +12 -10
- package/package.json +1 -1
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +20 -8
- package/templates/briefs/amend.md +4 -3
- package/templates/portal/css/health.css +13 -0
- package/templates/portal/css/home.css +3 -1
- package/templates/portal/css/setup.css +3 -2
- package/templates/portal/js/views/health.js +10 -5
- package/templates/portal/js/views/paste.js +87 -79
- package/templates/portal/js/views/prompt.js +90 -39
package/docs/portal.md
CHANGED
|
@@ -427,11 +427,15 @@ half complaints answered neither well.
|
|
|
427
427
|
|
|
428
428
|
### Getting help changing it
|
|
429
429
|
|
|
430
|
-
**
|
|
431
|
-
|
|
432
|
-
self-contained
|
|
433
|
-
and what may and may not be edited.
|
|
434
|
-
|
|
430
|
+
**Change this with your AI** opens a side sheet in three steps:
|
|
431
|
+
1. What you want changed, in a box big enough to say it in.
|
|
432
|
+
2. **Copy the prompt**: a self-contained brief carrying the composed text, every layer with its
|
|
433
|
+
path and blast radius, and what may and may not be edited. It only copies. The prompt as it
|
|
434
|
+
composes today is under it, to read first.
|
|
435
|
+
3. Paste your AI's reply. It hands back whole files, which are checked against the layers this
|
|
436
|
+
prompt is made of and shown as diffs, each with Save. Nothing is written until you press it.
|
|
437
|
+
|
|
438
|
+
`roster brief amend <handle> --want "…"` prints the same brief from the terminal.
|
|
435
439
|
|
|
436
440
|
It carries the state rather than asking for it, because working out which of eight files to
|
|
437
441
|
open is the difficulty being solved. A brief that says "read your layers first" has handed
|
|
@@ -550,11 +554,9 @@ worse than no linter. A finding true of more than one prompt is one row, and it
|
|
|
550
554
|
| it names a file that is not there | an instruction to read something absent is a quiet no-op inside a run nobody watches |
|
|
551
555
|
| an included layer is empty | it contributes nothing and costs a line of includes |
|
|
552
556
|
|
|
553
|
-
**Every finding carries the fix.** "
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
not. The box comes pre-filled with what the finding worked out, so you can add to it rather
|
|
557
|
-
than retype it. "Open the file" takes you to that layer on the Prompt screen.
|
|
557
|
+
**Every finding carries the fix.** "Fix this with your AI" opens the same sheet as the Prompt
|
|
558
|
+
screen's, with the finding at the top and the box pre-filled with what it worked out, so you
|
|
559
|
+
can add to it rather than retype it. Copy the prompt, paste the reply back, save the diff. "Open the file" takes you to that layer on the Prompt screen.
|
|
558
560
|
|
|
559
561
|
**Memory problems** are the same checks `roster lint` runs. Each one has a button that opens an
|
|
560
562
|
issue in that staff member's own repo asking them to fix it, which is usually right, because
|
package/package.json
CHANGED
|
@@ -11,9 +11,19 @@ name: %%STAFF_UPPER%% on a mention
|
|
|
11
11
|
|
|
12
12
|
# What the run is and what started it, as the Actions list shows it. The portal reads this back
|
|
13
13
|
# to link a live run to the request behind it, and the daily limit counts peer runs by it.
|
|
14
|
+
# Most events here are nobody asking (a comment without the mention, this staff member's own
|
|
15
|
+
# reply), and those are named `ignored`, so nothing reads one as work while it waits to be
|
|
16
|
+
# skipped.
|
|
14
17
|
run-name: >-
|
|
15
|
-
%%STAFF%% ${{
|
|
16
|
-
|
|
18
|
+
%%STAFF%% ${{
|
|
19
|
+
(contains(fromJSON('%%HUMAN_LOGINS%%'), github.event.sender.login) &&
|
|
20
|
+
(contains(github.event.comment.body, '%%MENTION%%') ||
|
|
21
|
+
(github.event_name == 'issues' && contains(github.event.issue.body, '%%MENTION%%'))))
|
|
22
|
+
&& 'mention'
|
|
23
|
+
|| ((github.event_name == 'issues' && github.event.action == 'opened' &&
|
|
24
|
+
endsWith(github.event.sender.login, '[bot]') &&
|
|
25
|
+
github.event.sender.login != '%%APP%%[bot]') && 'peer' || 'ignored')
|
|
26
|
+
}} #${{ github.event.issue.number }}
|
|
17
27
|
|
|
18
28
|
on:
|
|
19
29
|
# A mention in the body of a brand new issue, which is often the faster route: one box, rather
|
|
@@ -29,12 +39,6 @@ on:
|
|
|
29
39
|
issue_comment:
|
|
30
40
|
types: [created, edited]
|
|
31
41
|
|
|
32
|
-
# Per issue, not global. Several comments in a row are the normal case and a shared group would
|
|
33
|
-
# silently drop all but one of them.
|
|
34
|
-
concurrency:
|
|
35
|
-
group: %%STAFF%%-mention-${{ github.event.issue.number }}
|
|
36
|
-
cancel-in-progress: false
|
|
37
|
-
|
|
38
42
|
jobs:
|
|
39
43
|
answer:
|
|
40
44
|
# Only a human this org answers to, only on a real mention. A bot quoting the phrase must
|
|
@@ -58,6 +62,14 @@ jobs:
|
|
|
58
62
|
# accounts that can open an issue on a private tracker are the people and Apps it is
|
|
59
63
|
# installed for. A peer run may not file on another peer, and session.yaml holds peer runs
|
|
60
64
|
# to the day's limit, so two staff cannot wake each other forever.
|
|
65
|
+
# Per issue, not global: several comments in a row are the normal case, and a shared group
|
|
66
|
+
# would silently drop all but one of them. On the job rather than the workflow, so the
|
|
67
|
+
# condition below is decided first. At workflow level every event queued here, including
|
|
68
|
+
# this staff member's own replies, and GitHub keeps one waiting run per group, so a reply
|
|
69
|
+
# waiting to be skipped could knock out a real mention waiting behind a run.
|
|
70
|
+
concurrency:
|
|
71
|
+
group: %%STAFF%%-mention-${{ github.event.issue.number }}
|
|
72
|
+
cancel-in-progress: false
|
|
61
73
|
if: >-
|
|
62
74
|
(
|
|
63
75
|
contains(fromJSON('%%HUMAN_LOGINS%%'), github.event.sender.login) &&
|
|
@@ -44,7 +44,8 @@ If what is wanted needs one of these, say so and stop. Do not work around it.
|
|
|
44
44
|
|
|
45
45
|
1. **Say which file, and why that one.** Prefer the narrowest file that achieves it. If the
|
|
46
46
|
change is about %%NAME%% specifically, it does not belong in `org/`.
|
|
47
|
-
2. **
|
|
47
|
+
2. **Hand back the whole file you changed**, in the block described at the end. The tool
|
|
48
|
+
%%HUMAN%% pastes your answer into shows them the diff, so they see exactly what moved.
|
|
48
49
|
3. **Do not restate.** Every layer is already in the composed text below. A rule added to
|
|
49
50
|
`org/voice.md` that `org/operating.md` already states makes the prompt longer and no
|
|
50
51
|
clearer.
|
|
@@ -56,5 +57,5 @@ If what is wanted needs one of these, say so and stop. Do not work around it.
|
|
|
56
57
|
|
|
57
58
|
## Then
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
Outside the block: the one-line reason for the file you chose, and what you cut. Inside it:
|
|
61
|
+
the whole changed file. Nothing is saved until %%HUMAN%% has seen the diff and pressed Save.
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
.hsect i{margin-left:auto;font:11.5px var(--mono);font-style:normal;letter-spacing:0;
|
|
24
24
|
text-transform:none;color:var(--ink-faint)}
|
|
25
25
|
.hbody{padding-top:2px}
|
|
26
|
+
/* Findings sit in a card, as Home's lists do: the marker carries the colour, the words stay
|
|
27
|
+
readable. The row's warn or fail class used to colour the whole sentence amber. */
|
|
28
|
+
.hlist{padding:2px 16px;margin:12px 0 0}
|
|
29
|
+
.hlist .check{color:var(--ink)}
|
|
30
|
+
.hlist .check b{color:var(--ink)}
|
|
26
31
|
/* What the section is, in one line, before any of its findings. */
|
|
27
32
|
.hnote{margin:10px 0 0;font:12.5px/1.55 var(--sans);color:var(--ink-faint)}
|
|
28
33
|
.hacts{margin-top:12px}
|
|
@@ -60,3 +65,11 @@
|
|
|
60
65
|
.ffield label{display:block;font:600 11px var(--sans);letter-spacing:.05em;
|
|
61
66
|
text-transform:uppercase;color:var(--ink-faint);margin-bottom:4px}
|
|
62
67
|
.ffield small{display:block;margin-top:3px;font:11px var(--sans);color:var(--ink-faint)}
|
|
68
|
+
|
|
69
|
+
/* ---------- fixing a prompt with your own AI (amendSheet in prompt.js) ---------- */
|
|
70
|
+
.amend .sheettitle{padding-right:44px}
|
|
71
|
+
.amendlabel{display:block;font:600 13px var(--sans);color:var(--ink);margin:20px 0 8px}
|
|
72
|
+
.amend .pastebox{width:100%;box-sizing:border-box}
|
|
73
|
+
.amendtoday{margin-top:14px;font-size:12.5px;color:var(--ink-dim)}
|
|
74
|
+
.amendtoday summary{cursor:pointer}
|
|
75
|
+
.amendtoday pre{max-height:45vh;overflow:auto;margin-top:8px}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* Home: what needs you, who is working, your requests, closed today. */
|
|
2
2
|
|
|
3
|
-
.home{max-width:820px}
|
|
3
|
+
.home{max-width:820px;margin-top:22px}
|
|
4
|
+
/* The placeholder lines up with the title, as the cards that replace it do. */
|
|
5
|
+
.home > .sk-row{padding-left:0;padding-right:0}
|
|
4
6
|
/* Secondary text on Home is read, not scanned for numbers: the readable face and colour, not
|
|
5
7
|
the faint monospace the rest of the portal uses for metadata. */
|
|
6
8
|
.home .meta{font:12.5px/1.5 var(--sans);color:var(--ink-dim)}
|
|
@@ -41,8 +41,9 @@
|
|
|
41
41
|
.btn:active{opacity:.8}
|
|
42
42
|
/* The full accent, not the dim one. Dim on the page background is pale-green-on-white with
|
|
43
43
|
near-white text: the button that does the main thing on the screen read as disabled. */
|
|
44
|
-
.
|
|
45
|
-
.btn.primary
|
|
44
|
+
/* The same green as every other primary button, so a screen never shows two. */
|
|
45
|
+
.btn.primary{background:var(--accent);border-color:transparent;color:var(--accent-ink);font-weight:600}
|
|
46
|
+
.btn.primary:hover{background:var(--accent);opacity:.88}
|
|
46
47
|
.btn[disabled]{opacity:.45;cursor:default}
|
|
47
48
|
|
|
48
49
|
.cmd{background:var(--bg);border:1px solid var(--line);border-radius:var(--r);padding:10px 12px;
|
|
@@ -7,7 +7,7 @@ import { inline } from "../md.js";
|
|
|
7
7
|
import { render } from "../router.js";
|
|
8
8
|
import { S, staff } from "../state.js";
|
|
9
9
|
import { checklist } from "./checklist.js";
|
|
10
|
-
import {
|
|
10
|
+
import { amendSheet } from "./prompt.js";
|
|
11
11
|
import { runOnce } from "./runonce.js";
|
|
12
12
|
|
|
13
13
|
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
@@ -141,6 +141,8 @@ function memoryProblems({ body, actions, say }, s) {
|
|
|
141
141
|
);
|
|
142
142
|
if (!s.problems.length) return;
|
|
143
143
|
|
|
144
|
+
const list = el("div", { className: "card hlist" });
|
|
145
|
+
body.append(list);
|
|
144
146
|
for (const p of s.problems) {
|
|
145
147
|
const { row, body: text } = finding(
|
|
146
148
|
p.level === "error" ? "error" : "warning",
|
|
@@ -158,7 +160,7 @@ function memoryProblems({ body, actions, say }, s) {
|
|
|
158
160
|
}
|
|
159
161
|
acts.append(status);
|
|
160
162
|
text.append(acts);
|
|
161
|
-
|
|
163
|
+
list.append(row);
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
/* The portal's one power over an agent is that it acts as the human, so "fix this" is an
|
|
@@ -235,13 +237,16 @@ async function promptProblems({ body, say }, s) {
|
|
|
235
237
|
/* Worst first, and each one carrying the sentence that fixes it. Knowing there is a problem
|
|
236
238
|
is the hard part; writing the paragraph that asks for the change is not. */
|
|
237
239
|
const rank = { error: 0, warning: 1, note: 2 };
|
|
240
|
+
const list = el("div", { className: "card hlist" });
|
|
241
|
+
body.append(list);
|
|
238
242
|
for (const p of [...problems].sort((a, b) => (rank[a.level] ?? 3) - (rank[b.level] ?? 3))) {
|
|
239
243
|
const { row, body: text } = finding(p.level, { text: p.title }, [(p.kinds ?? []).join(", ")]);
|
|
240
244
|
text.append(el("p", { textContent: p.detail }));
|
|
241
245
|
|
|
242
246
|
const note = el("span", { className: "meta" });
|
|
243
|
-
const fix = el("button", { className: "ghbtn", textContent: "
|
|
244
|
-
fix.onclick = () =>
|
|
247
|
+
const fix = el("button", { className: "ghbtn", textContent: "Fix this with your AI" });
|
|
248
|
+
fix.onclick = () =>
|
|
249
|
+
amendSheet({ handle: s.handle, name: s.name, kind: p.kind, want: p.want, title: p.title, detail: p.detail });
|
|
245
250
|
const actions = el("div", { className: "row facts" }, [fix]);
|
|
246
251
|
if (p.path) {
|
|
247
252
|
const go = el("button", { className: "ghbtn", textContent: "Open the file" });
|
|
@@ -256,7 +261,7 @@ async function promptProblems({ body, say }, s) {
|
|
|
256
261
|
}
|
|
257
262
|
actions.append(note);
|
|
258
263
|
text.append(actions);
|
|
259
|
-
|
|
264
|
+
list.append(row);
|
|
260
265
|
}
|
|
261
266
|
}
|
|
262
267
|
|
|
@@ -120,89 +120,97 @@ export function paste(opts) {
|
|
|
120
120
|
result.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
|
-
result.
|
|
124
|
-
|
|
125
|
-
/* Problems first, each with the sentence that gets a better answer. A model that replied in
|
|
126
|
-
prose is the common case and it is not the person's mistake, so it reads as a next step
|
|
127
|
-
rather than as an error. */
|
|
128
|
-
for (const p of data.problems ?? []) {
|
|
129
|
-
const note = el("div", { className: "problem" });
|
|
130
|
-
note.append(el("b", { textContent: p.message }));
|
|
131
|
-
if (p.retry) {
|
|
132
|
-
const retry = el("pre", { className: "cmd", textContent: p.retry });
|
|
133
|
-
const again = el("button", { className: "btn", textContent: "Copy this reply" });
|
|
134
|
-
again.onclick = () => toClipboard(p.retry, again, "Copied");
|
|
135
|
-
note.append(retry, again);
|
|
136
|
-
}
|
|
137
|
-
result.append(note);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// What the AI said around the file: its caveats and "check these" are worth reading first.
|
|
141
|
-
const notes = String(answer.value ?? "")
|
|
142
|
-
.replace(/<<<ROSTER FILE [^>]+>>>[\s\S]*?<<<ROSTER END>>>/g, "")
|
|
143
|
-
.trim();
|
|
144
|
-
|
|
145
|
-
for (const file of data.files ?? []) {
|
|
146
|
-
const card = el("div", { className: "pastefile" });
|
|
147
|
-
const name = file.path.split("/").slice(-2).join("/");
|
|
148
|
-
if (file.unchanged) {
|
|
149
|
-
card.append(el("p", { className: "verdict", textContent: "✓ Same as what's already in " + name + ". Nothing to save." }));
|
|
150
|
-
result.append(card);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
card.append(
|
|
154
|
-
el("p", {
|
|
155
|
-
className: "verdict" + (file.writable ? " ok" : " bad"),
|
|
156
|
-
textContent: file.writable
|
|
157
|
-
? "✓ Looks good. Saving replaces " + name + " with this:"
|
|
158
|
-
: "✗ " + file.path + " can't be written from here.",
|
|
159
|
-
}),
|
|
160
|
-
);
|
|
161
|
-
card.append(el("div", { className: "md doc pastepreview", innerHTML: mdlite(file.text) }));
|
|
162
|
-
|
|
163
|
-
if (notes) {
|
|
164
|
-
const said = el("details", { className: "pastenotes", open: true });
|
|
165
|
-
said.append(el("summary", { textContent: "Your AI also said" }), el("div", { className: "md doc", innerHTML: mdlite(notes) }));
|
|
166
|
-
card.append(said);
|
|
167
|
-
}
|
|
123
|
+
showPasteResult(result, data, answer.value, opts.onSaved);
|
|
124
|
+
};
|
|
168
125
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const stat = diffStat(file.before, file.text);
|
|
172
|
-
const toggle = el("button", { className: "ghbtn", textContent: `Show changes (+${stat.added} −${stat.removed})` });
|
|
173
|
-
toggle.onclick = () => {
|
|
174
|
-
diff.hidden = !diff.hidden;
|
|
175
|
-
toggle.textContent = (diff.hidden ? "Show changes" : "Hide changes") + ` (+${stat.added} −${stat.removed})`;
|
|
176
|
-
};
|
|
126
|
+
return box;
|
|
127
|
+
}
|
|
177
128
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
129
|
+
/**
|
|
130
|
+
* What a pasted reply turned into: problems first, then each file with its diff and a Save.
|
|
131
|
+
* Shared by every copy-a-prompt, paste-the-answer loop, whichever endpoint parsed it.
|
|
132
|
+
*/
|
|
133
|
+
export function showPasteResult(result, data, answerText, onSaved) {
|
|
134
|
+
result.replaceChildren();
|
|
135
|
+
|
|
136
|
+
/* Problems first, each with the sentence that gets a better answer. A model that replied in
|
|
137
|
+
prose is the common case and it is not the person's mistake, so it reads as a next step
|
|
138
|
+
rather than as an error. */
|
|
139
|
+
for (const p of data.problems ?? []) {
|
|
140
|
+
const note = el("div", { className: "problem" });
|
|
141
|
+
note.append(el("b", { textContent: p.message }));
|
|
142
|
+
if (p.retry) {
|
|
143
|
+
const retry = el("pre", { className: "cmd", textContent: p.retry });
|
|
144
|
+
const again = el("button", { className: "btn", textContent: "Copy this reply" });
|
|
145
|
+
again.onclick = () => toClipboard(p.retry, again, "Copied");
|
|
146
|
+
note.append(retry, again);
|
|
147
|
+
}
|
|
148
|
+
result.append(note);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// What the AI said around the file: its caveats and "check these" are worth reading first.
|
|
152
|
+
const notes = String(answerText ?? "")
|
|
153
|
+
.replace(/<<<ROSTER FILE [^>]+>>>[\s\S]*?<<<ROSTER END>>>/g, "")
|
|
154
|
+
.trim();
|
|
155
|
+
|
|
156
|
+
for (const file of data.files ?? []) {
|
|
157
|
+
const card = el("div", { className: "pastefile" });
|
|
158
|
+
const name = file.path.split("/").slice(-2).join("/");
|
|
159
|
+
if (file.unchanged) {
|
|
160
|
+
card.append(el("p", { className: "verdict", textContent: "✓ Same as what's already in " + name + ". Nothing to save." }));
|
|
199
161
|
result.append(card);
|
|
162
|
+
continue;
|
|
200
163
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
164
|
+
card.append(
|
|
165
|
+
el("p", {
|
|
166
|
+
className: "verdict" + (file.writable ? " ok" : " bad"),
|
|
167
|
+
textContent: file.writable
|
|
168
|
+
? "✓ Looks good. Saving replaces " + name + " with this:"
|
|
169
|
+
: "✗ " + file.path + " can't be written from here.",
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
card.append(el("div", { className: "md doc pastepreview", innerHTML: mdlite(file.text) }));
|
|
173
|
+
|
|
174
|
+
if (notes) {
|
|
175
|
+
const said = el("details", { className: "pastenotes", open: true });
|
|
176
|
+
said.append(el("summary", { textContent: "Your AI also said" }), el("div", { className: "md doc", innerHTML: mdlite(notes) }));
|
|
177
|
+
card.append(said);
|
|
204
178
|
}
|
|
205
|
-
};
|
|
206
179
|
|
|
207
|
-
|
|
180
|
+
const diff = el("pre", { className: "diff", hidden: true });
|
|
181
|
+
diff.textContent = unifiedDiff(file.before, file.text, file.path);
|
|
182
|
+
const stat = diffStat(file.before, file.text);
|
|
183
|
+
const toggle = el("button", { className: "ghbtn", textContent: `Show changes (+${stat.added} −${stat.removed})` });
|
|
184
|
+
toggle.onclick = () => {
|
|
185
|
+
diff.hidden = !diff.hidden;
|
|
186
|
+
toggle.textContent = (diff.hidden ? "Show changes" : "Hide changes") + ` (+${stat.added} −${stat.removed})`;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const row = el("div", { className: "row" });
|
|
190
|
+
if (file.writable) {
|
|
191
|
+
const save = el("button", { className: "btn primary", textContent: "Save" });
|
|
192
|
+
save.onclick = async () => {
|
|
193
|
+
save.disabled = true;
|
|
194
|
+
save.textContent = "Saving…";
|
|
195
|
+
try {
|
|
196
|
+
await saveFile(file.path, file.text, `portal: write ${file.path}`);
|
|
197
|
+
save.textContent = "Saved";
|
|
198
|
+
card.classList.add("saved");
|
|
199
|
+
onSaved?.();
|
|
200
|
+
} catch (err) {
|
|
201
|
+
save.disabled = false;
|
|
202
|
+
save.textContent = "Save";
|
|
203
|
+
card.append(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
row.append(save);
|
|
207
|
+
}
|
|
208
|
+
row.append(toggle);
|
|
209
|
+
card.append(row, diff);
|
|
210
|
+
result.append(card);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!(data.problems ?? []).length && !(data.files ?? []).length) {
|
|
214
|
+
result.append(el("p", { className: "verdict bad", textContent: "✗ No file found in that reply." }));
|
|
215
|
+
}
|
|
208
216
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { highlighted } from "../mdedit.js";
|
|
13
13
|
import { getFile, post } from "../api.js";
|
|
14
|
-
import {
|
|
14
|
+
import { askYes, sheet } from "../dialog.js";
|
|
15
15
|
import { el, esc, grow, kb, toClipboard } from "../dom.js";
|
|
16
16
|
import { icon } from "../icons.js";
|
|
17
17
|
import { mdlite } from "../md.js";
|
|
@@ -19,6 +19,7 @@ import { render } from "../router.js";
|
|
|
19
19
|
import { S, staff, writeHash } from "../state.js";
|
|
20
20
|
import { diffStat, unifiedDiff } from "../textdiff.js";
|
|
21
21
|
import { renderDiff } from "./changed.js";
|
|
22
|
+
import { showPasteResult } from "./paste.js";
|
|
22
23
|
|
|
23
24
|
const KINDS = [
|
|
24
25
|
["daily", "Daily", "the scheduled run"],
|
|
@@ -26,48 +27,98 @@ const KINDS = [
|
|
|
26
27
|
];
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
|
-
*
|
|
30
|
+
* Fix a prompt with your own AI, in a side sheet: what is wrong, what you want changed, the
|
|
31
|
+
* prompt as it is today, a brief to copy, and a box for the reply. The reply is parsed into
|
|
32
|
+
* whole files and shown as diffs; nothing is saved until you press Save on one.
|
|
30
33
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* the same brief whichever screen you asked from.
|
|
34
|
+
* @param {{handle: string, name?: string, kind: string, want?: string, title?: string,
|
|
35
|
+
* detail?: string}} o
|
|
36
|
+
* @returns {{box: object | null, body: HTMLElement, want: HTMLTextAreaElement}} what it built,
|
|
37
|
+
* so a caller (or a test, where there is no <dialog>) can see it.
|
|
36
38
|
*/
|
|
37
|
-
export
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
export function amendSheet(o) {
|
|
40
|
+
const body = el("div", { className: "amend" });
|
|
41
|
+
const x = el("button", { className: "iconbtn", title: "Close (Esc)", ariaLabel: "Close" });
|
|
42
|
+
x.append(icon("close"));
|
|
43
|
+
body.append(el("div", { className: "sidebar-x" }, [x]));
|
|
44
|
+
|
|
45
|
+
body.append(el("h3", { className: "sheettitle", textContent: o.title ?? "Change what they are told" }));
|
|
46
|
+
if (o.detail) body.append(el("p", { className: "sub", textContent: o.detail }));
|
|
47
|
+
|
|
48
|
+
const want = el("textarea", {
|
|
49
|
+
className: "pastebox",
|
|
50
|
+
rows: 3,
|
|
51
|
+
value: o.want ?? "",
|
|
45
52
|
placeholder: "stop opening decision issues for anything reversible",
|
|
46
|
-
confirm: "Copy the brief",
|
|
47
53
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
btn
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
body.append(el("label", { className: "amendlabel", textContent: "1. What you want changed" }), want);
|
|
55
|
+
|
|
56
|
+
const copy = el("button", { className: "btn primary", textContent: "Copy the prompt" });
|
|
57
|
+
const copied = el("span", { className: "meta" });
|
|
58
|
+
copy.onclick = async () => {
|
|
59
|
+
copy.disabled = true;
|
|
60
|
+
try {
|
|
61
|
+
const url =
|
|
62
|
+
"/api/amend?staff=" + encodeURIComponent(o.handle) +
|
|
63
|
+
"&kind=" + encodeURIComponent(o.kind) +
|
|
64
|
+
"&want=" + encodeURIComponent(want.value);
|
|
65
|
+
const text = await (await fetch(url, { cache: "no-store" })).text();
|
|
66
|
+
await toClipboard(text, copy, "Copy the prompt");
|
|
67
|
+
copied.textContent = "Paste it into Claude, ChatGPT or any other AI.";
|
|
68
|
+
copied.className = "meta";
|
|
69
|
+
} catch (e) {
|
|
70
|
+
copied.textContent = String(e.message || e);
|
|
71
|
+
copied.className = "meta err";
|
|
72
|
+
} finally {
|
|
73
|
+
copy.disabled = false;
|
|
62
74
|
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
};
|
|
76
|
+
body.append(
|
|
77
|
+
el("label", { className: "amendlabel", textContent: "2. Copy the prompt into your AI" }),
|
|
78
|
+
el("div", { className: "row" }, [copy, copied]),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// The prompt this is about, to read before deciding what to ask for.
|
|
82
|
+
const today = el("details", { className: "amendtoday" });
|
|
83
|
+
const summary = el("summary", { textContent: "The " + o.kind + " prompt as it is today" });
|
|
84
|
+
const text = el("pre", { className: "briefpreview", textContent: "Composing…" });
|
|
85
|
+
today.append(summary, text);
|
|
86
|
+
body.append(today);
|
|
87
|
+
fetch("/api/prompt?staff=" + encodeURIComponent(o.handle) + "&kind=" + encodeURIComponent(o.kind), { cache: "no-store" })
|
|
88
|
+
.then((r) => r.json())
|
|
89
|
+
.then((v) => {
|
|
90
|
+
if (v.error) throw new Error(v.error);
|
|
91
|
+
const words = String(v.composed ?? "").split(/\s+/).filter(Boolean).length;
|
|
92
|
+
summary.textContent = "The " + o.kind + " prompt as it is today · " + words.toLocaleString() + " words";
|
|
93
|
+
text.textContent = v.composed ?? "";
|
|
94
|
+
})
|
|
95
|
+
.catch((e) => {
|
|
96
|
+
text.textContent = String(e.message || e);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const answer = el("textarea", { className: "pastebox", rows: 8, placeholder: "Paste its whole reply here." });
|
|
100
|
+
const check = el("button", { className: "btn", textContent: "Check the answer" });
|
|
101
|
+
const result = el("div", { className: "pasteresult" });
|
|
102
|
+
check.onclick = async () => {
|
|
103
|
+
if (!answer.value.trim()) return;
|
|
104
|
+
result.replaceChildren(el("p", { className: "sub", textContent: "Reading…" }));
|
|
105
|
+
try {
|
|
106
|
+
const data = await post({ staff: o.handle, kind: o.kind, answer: answer.value }, "/api/amend/check");
|
|
107
|
+
showPasteResult(result, data, answer.value, () => {});
|
|
108
|
+
} catch (e) {
|
|
109
|
+
result.replaceChildren(el("p", { className: "err", textContent: String(e.message || e) }));
|
|
69
110
|
}
|
|
70
|
-
}
|
|
111
|
+
};
|
|
112
|
+
body.append(
|
|
113
|
+
el("label", { className: "amendlabel", textContent: "3. Paste its reply" }),
|
|
114
|
+
answer,
|
|
115
|
+
el("div", { className: "row" }, [check]),
|
|
116
|
+
result,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const box = sheet({ node: body, side: true });
|
|
120
|
+
if (box) x.onclick = () => box.close();
|
|
121
|
+
return { box, body, want };
|
|
71
122
|
}
|
|
72
123
|
|
|
73
124
|
export function viewPrompt(m) {
|
|
@@ -91,7 +142,7 @@ export function viewPrompt(m) {
|
|
|
91
142
|
};
|
|
92
143
|
/* The point of the screen: you can see the prompt, and you can get help changing it
|
|
93
144
|
without first working out which of eight files to open. */
|
|
94
|
-
const help = el("button", { className: "ghbtn primary", textContent: "
|
|
145
|
+
const help = el("button", { className: "ghbtn primary", textContent: "Change this with your AI" });
|
|
95
146
|
help.onclick = () => copyAmend("");
|
|
96
147
|
const copy = el("button", { className: "ghbtn", textContent: "Copy the prompt" });
|
|
97
148
|
copy.onclick = () => {
|
|
@@ -100,7 +151,7 @@ export function viewPrompt(m) {
|
|
|
100
151
|
const note = el("span", { className: "meta" });
|
|
101
152
|
m.append(el("div", { className: "row", style: "margin-bottom:16px" }, [kind, help, copy, note]));
|
|
102
153
|
|
|
103
|
-
const copyAmend = (want
|
|
154
|
+
const copyAmend = (want) => amendSheet({ handle: s.handle, name: s.name, kind: S.promptKind, want });
|
|
104
155
|
|
|
105
156
|
const split = el("div", { className: "split" });
|
|
106
157
|
const tree = el("div", { className: "tree" });
|