@nanocollective/roster 0.1.0-alpha.50 → 0.1.0-alpha.52

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.
Binary file
Binary file
Binary file
Binary file
Binary file
package/docs/portal.md CHANGED
@@ -80,6 +80,8 @@ goes away; the repo picker lives on under [Org](#org).
80
80
 
81
81
  ## Home
82
82
 
83
+ ![Home: latest reports, what needs you, and the Ask box](images/home.jpg)
84
+
83
85
  Where the portal opens. An Ask box at the top, then these, in order.
84
86
 
85
87
  - **Ask.** Pick a staff member, type what you want, **Send**. It opens an issue on their
@@ -427,11 +429,15 @@ half complaints answered neither well.
427
429
 
428
430
  ### Getting help changing it
429
431
 
430
- **Copy a brief for changing this** asks what you want changed, in a box big enough to say it
431
- in, and copies the same thing: a
432
- self-contained prompt carrying the composed text, every layer with its path and blast radius,
433
- and what may and may not be edited. `roster brief amend <handle> --want "…"` prints the same
434
- from the terminal.
432
+ **Change this with your AI** opens a side sheet in three steps:
433
+ 1. What you want changed, in a box big enough to say it in.
434
+ 2. **Copy the prompt**: a self-contained brief carrying the composed text, every layer with its
435
+ path and blast radius, and what may and may not be edited. It only copies. The prompt as it
436
+ composes today is under it, to read first.
437
+ 3. Paste your AI's reply. It hands back whole files, which are checked against the layers this
438
+ prompt is made of and shown as diffs, each with Save. Nothing is written until you press it.
439
+
440
+ `roster brief amend <handle> --want "…"` prints the same brief from the terminal.
435
441
 
436
442
  It carries the state rather than asking for it, because working out which of eight files to
437
443
  open is the difficulty being solved. A brief that says "read your layers first" has handed
@@ -550,11 +556,9 @@ worse than no linter. A finding true of more than one prompt is one row, and it
550
556
  | 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
557
  | an included layer is empty | it contributes nothing and costs a line of includes |
552
558
 
553
- **Every finding carries the fix.** "Copy a prompt to fix this" builds a brief containing the
554
- finding, the composed prompt, and every layer, and puts it on your clipboard. Paste it into
555
- whatever agent you use. Knowing there is a problem is the hard part; writing the paragraph is
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.
559
+ **Every finding carries the fix.** "Fix this with your AI" opens the same sheet as the Prompt
560
+ screen's, with the finding at the top and the box pre-filled with what it worked out, so you
561
+ 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
562
 
559
563
  **Memory problems** are the same checks `roster lint` runs. Each one has a button that opens an
560
564
  issue in that staff member's own repo asking them to fix it, which is usually right, because
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.50",
3
+ "version": "0.1.0-alpha.52",
4
4
  "description": "An agent-run org, powered by GitHub. Scaffold AI staff members whose brain is a repo.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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%% ${{ contains(fromJSON('%%HUMAN_LOGINS%%'), github.event.sender.login) && 'mention' || 'peer' }}
16
- #${{ github.event.issue.number }}
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. **Show a diff, not a rewritten file.** %%HUMAN%% has to be able to see exactly what moved.
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
- Give %%HUMAN%% the diff, the one-line reason for the file you chose, and what you cut. They
60
- apply it: in the portal's Prompt screen, or by editing the file and committing it.
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)}
@@ -64,10 +66,11 @@
64
66
  .homecard.gone,.homeline.gone{opacity:0;transform:translateX(12px);
65
67
  transition:opacity .2s var(--ease),transform .2s var(--ease)}
66
68
 
67
- /* Activity you have not seen, as Trackers marks it: a blue edge and a bold title. Opening the
68
- thread marks it read, here and on GitHub. */
69
+ /* Activity you have not seen: a blue dot and a bold title, and on a card in Needs you a blue
70
+ edge that follows its corners. A row inside a list card carries no edge, since a bar drawn to
71
+ the card's side squares off its rounded corners. Opening the thread marks it read, here and on
72
+ GitHub. */
69
73
  .homecard.unread{box-shadow:inset 3px 0 0 var(--blue)}
70
- .homeline.unread{box-shadow:inset 3px 0 0 var(--blue);margin:0 -16px;padding-left:13px;padding-right:16px}
71
74
  .unread .homeopen,.unread .homerowtitle{font-weight:700}
72
75
  .unread .homeopen::before,.unread .homerowtitle::before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;
73
76
  background:var(--blue);margin:0 7px 1px 0;vertical-align:middle}
@@ -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
- .btn.primary{background:var(--accent-fill);border-color:transparent;color:var(--accent-ink);font-weight:600}
45
- .btn.primary:hover{background:var(--accent-fill);opacity:.88}
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 { copyAmendBrief } from "./prompt.js";
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
- body.append(row);
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: "Copy a prompt to fix this" });
244
- fix.onclick = () => copyAmendBrief(s.handle, p.kind, p.want, fix, note);
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
- body.append(row);
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.replaceChildren();
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
- const diff = el("pre", { className: "diff", hidden: true });
170
- diff.textContent = unifiedDiff(file.before, file.text, file.path);
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
- const row = el("div", { className: "row" });
179
- if (file.writable) {
180
- const save = el("button", { className: "btn primary", textContent: "Save" });
181
- save.onclick = async () => {
182
- save.disabled = true;
183
- save.textContent = "Saving…";
184
- try {
185
- await saveFile(file.path, file.text, `portal: write ${file.path}`);
186
- save.textContent = "Saved";
187
- card.classList.add("saved");
188
- opts.onSaved?.();
189
- } catch (err) {
190
- save.disabled = false;
191
- save.textContent = "Save";
192
- card.append(el("p", { className: "err", textContent: String(err.message || err) }));
193
- }
194
- };
195
- row.append(save);
196
- }
197
- row.append(toggle);
198
- card.append(row, diff);
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
- if (!(data.problems ?? []).length && !(data.files ?? []).length) {
203
- result.append(el("p", { className: "verdict bad", textContent: "✗ No file found in that reply." }));
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
- return box;
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 { askText, askYes } from "../dialog.js";
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
- * Build the paste-ready brief on the server, where compose.mjs lives, and copy it.
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
- * A finding pre-fills the box rather than skipping it: what it wrote is a starting point, and
32
- * "and keep it in operating.md" is exactly the sort of thing you want to add.
33
- *
34
- * Exported because the findings themselves live on Health now, and the fix for one of them is
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 async function copyAmendBrief(handle, kind, want, btn, note) {
38
- const label = btn.textContent;
39
- const asked = await askText({
40
- title: "What do you want changed?",
41
- hint:
42
- "This goes at the top of a brief carrying the whole prompt and every file it is " +
43
- "made of. Say it the way you would say it to a person.",
44
- value: want,
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
- if (!asked) return;
49
- btn.disabled = true;
50
- btn.textContent = "building…";
51
- try {
52
- const url =
53
- "/api/amend?staff=" + encodeURIComponent(handle) +
54
- "&kind=" + encodeURIComponent(kind) +
55
- "&want=" + encodeURIComponent(asked);
56
- const text = await (await fetch(url, { cache: "no-store" })).text();
57
- btn.disabled = false;
58
- toClipboard(text, btn, label);
59
- if (note) {
60
- note.textContent = "paste it into whatever agent you use";
61
- note.className = "meta";
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
- } catch (e) {
64
- btn.disabled = false;
65
- btn.textContent = label;
66
- if (note) {
67
- note.textContent = e.message;
68
- note.className = "meta err";
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: "Copy a brief for changing this" });
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, btn) => copyAmendBrief(s.handle, S.promptKind, want, btn ?? help, note);
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" });