toolroll 0.9.83 → 0.9.85
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 +69 -0
- package/README.md +94 -1192
- package/dist/agent-fence.d.ts +3 -3
- package/dist/agent-fence.js +3 -3
- package/dist/agent-onboard.d.ts +2 -0
- package/dist/agent-onboard.js +3 -0
- package/dist/assignment-ui.js +5 -1
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/browser-crew.js +5 -1
- package/dist/browser-workspace.d.ts +12 -0
- package/dist/chat-channel.d.ts +10 -1
- package/dist/chat-channel.js +35 -5
- package/dist/chat-decide.d.ts +16 -4
- package/dist/chat-decide.js +239 -31
- package/dist/chat-messages.d.ts +5 -0
- package/dist/chat-messages.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-review.d.ts +4 -0
- package/dist/chat-review.js +13 -4
- package/dist/chat-terms.d.ts +9 -0
- package/dist/chat-terms.js +46 -0
- package/dist/contracts/console-api.d.ts +4 -2
- package/dist/contracts/console-api.js +7 -2
- package/dist/contracts/lead-tools.d.ts +30 -0
- package/dist/contracts/lead-tools.js +3 -0
- package/dist/contracts/youiyoux-request.d.ts +248 -0
- package/dist/contracts/youiyoux-request.js +152 -0
- package/dist/control-setup.d.ts +9 -0
- package/dist/control-setup.js +31 -4
- package/dist/control-ui.d.ts +7 -1
- package/dist/control-ui.js +3 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispatch.d.ts +15 -0
- package/dist/dispatch.js +26 -4
- package/dist/first-run.d.ts +5 -2
- package/dist/first-run.js +13 -2
- package/dist/flow-cadence.d.ts +8 -2
- package/dist/flow-cadence.js +16 -3
- package/dist/flow-card-task.d.ts +9 -0
- package/dist/flow-card-task.js +15 -6
- package/dist/folder-look.d.ts +3 -0
- package/dist/folder-look.js +8 -4
- package/dist/guides.js +4 -3
- package/dist/integrations-ui.js +9 -4
- package/dist/integrations.d.ts +9 -1
- package/dist/integrations.js +43 -2
- package/dist/landed-cleanup.d.ts +4 -0
- package/dist/landed-cleanup.js +3 -2
- package/dist/lead-capabilities.js +2 -1
- package/dist/lead-cli.js +3 -1
- package/dist/lead-tools.d.ts +2 -0
- package/dist/lead-tools.js +30 -4
- package/dist/lead.d.ts +2 -0
- package/dist/lead.js +3 -3
- package/dist/model-catalog.d.ts +5 -0
- package/dist/model-catalog.js +11 -2
- package/dist/model-choices.d.ts +188 -0
- package/dist/model-choices.js +405 -0
- package/dist/model-names.d.ts +6 -0
- package/dist/model-names.js +34 -0
- package/dist/models-ui.d.ts +69 -5
- package/dist/models-ui.js +53 -10
- package/dist/needs-you.d.ts +8 -0
- package/dist/needs-you.js +8 -0
- package/dist/operate.d.ts +22 -2
- package/dist/operate.js +107 -41
- package/dist/project-activity.d.ts +20 -0
- package/dist/project-activity.js +23 -0
- package/dist/project-admission.d.ts +4 -1
- package/dist/project-admission.js +23 -2
- package/dist/project-cli.d.ts +1 -0
- package/dist/project-cli.js +36 -19
- package/dist/project-identity.d.ts +9 -2
- package/dist/project-identity.js +80 -33
- package/dist/project-knowledge.d.ts +9 -9
- package/dist/project-outcomes.d.ts +119 -0
- package/dist/project-outcomes.js +79 -0
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +41 -9
- package/dist/result-review.d.ts +9 -0
- package/dist/result-review.js +45 -5
- package/dist/serve.js +60 -13
- package/dist/server/chat.js +5 -4
- package/dist/server/chrome.d.ts +3 -0
- package/dist/server/chrome.js +5 -2
- package/dist/server/console.css +17 -2
- package/dist/server/flows.js +4 -0
- package/dist/server/http.d.ts +1 -0
- package/dist/server/http.js +6 -0
- package/dist/server/pages.js +53 -7
- package/dist/server/people-tokens.js +40 -20
- package/dist/server/render-chat.js +3 -2
- package/dist/server/render-pages.d.ts +12 -2
- package/dist/server/render-pages.js +36 -16
- package/dist/server/render-people.d.ts +14 -2
- package/dist/server/render-people.js +35 -12
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +20 -7
- package/dist/server/render-settings.js +1 -0
- package/dist/server/render-tasks.d.ts +1 -0
- package/dist/server/render-tasks.js +22 -19
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/route-table.js +9 -1
- package/dist/server/settings.d.ts +58 -1
- package/dist/server/settings.js +216 -70
- package/dist/server/signup-gate.d.ts +9 -0
- package/dist/server/signup-gate.js +40 -0
- package/dist/server/task-views.js +6 -0
- package/dist/server/tasks.js +54 -8
- package/dist/settings-kit-script.d.ts +21 -0
- package/dist/settings-kit-script.js +67 -0
- package/dist/settings-kit.d.ts +137 -0
- package/dist/settings-kit.js +186 -0
- package/dist/settings-proofs.d.ts +64 -0
- package/dist/settings-proofs.js +101 -0
- package/dist/setup-guide.js +4 -2
- package/dist/spend-report.d.ts +87 -0
- package/dist/spend-report.js +265 -0
- package/dist/spend-ui.d.ts +29 -8
- package/dist/spend-ui.js +74 -39
- package/dist/store.d.ts +46 -3
- package/dist/store.js +165 -6
- package/dist/task-sizing.d.ts +5 -3
- package/dist/task-sizing.js +4 -4
- package/dist/task-source.d.ts +13 -1
- package/dist/task-source.js +17 -1
- package/dist/telegram-lead.js +7 -5
- package/dist/telegram.js +1 -1
- package/dist/toolroll-update.js +2 -2
- package/dist/webhooks.d.ts +6 -0
- package/dist/webhooks.js +25 -0
- package/dist/work-index.d.ts +4 -0
- package/dist/work-index.js +32 -0
- package/dist/youiyoux-intake.d.ts +234 -0
- package/dist/youiyoux-intake.js +908 -0
- package/package.json +11 -8
package/dist/result-actions.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
|
+
import { verifyResultImage } from "./chat-evidence.js";
|
|
1
2
|
/** Shared result-to-revision action for the result form and chat.
|
|
2
3
|
* The caller authenticates; this service checks project access and the exact
|
|
3
4
|
* displayed note batch, then uses the existing atomic revision/approval boundary.
|
|
4
5
|
*/
|
|
5
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
7
|
import { EVIDENCE_CAPS, readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
|
|
7
|
-
import { isRevisionFeedback, parseRevisionBatch, revisionSourceOf, commentSourceKey, REQUEST_TOKEN } from "./result-review.js";
|
|
8
|
+
import { parseFeedbackImage, sameFeedbackImage, isRevisionFeedback, parseRevisionBatch, revisionSourceOf, commentSourceKey, REQUEST_TOKEN } from "./result-review.js";
|
|
8
9
|
import { validateNote, hasForbiddenControls } from "./decision.js";
|
|
9
10
|
import { TASK_TEXT_LIMITS, validateTaskText } from "./task-text.js";
|
|
10
11
|
import { modeFilingCoverage } from "./scope.js";
|
|
12
|
+
/** Re-prove image custody and current result at each write. No file paths supplied by callers. */
|
|
13
|
+
export function feedbackImageProblem(store, root, runId, image) {
|
|
14
|
+
if (image == null)
|
|
15
|
+
return null;
|
|
16
|
+
const parsed = parseFeedbackImage(image);
|
|
17
|
+
if (!parsed || image.run !== runId)
|
|
18
|
+
return "Choose a screenshot from this result. Your draft is unchanged.";
|
|
19
|
+
const run = store.getRun(runId);
|
|
20
|
+
const task = run === null ? null : store.externalIdFor(run.taskRef);
|
|
21
|
+
const repo = run === null ? null : store.refById(run.taskRef)?.repo;
|
|
22
|
+
if (!task || !repo)
|
|
23
|
+
return "This screenshot is no longer available. Your draft is unchanged.";
|
|
24
|
+
const family = store.taskFamilyOf(task, [repo], false);
|
|
25
|
+
const latest = store.runsFor(run.taskRef).find(one => ["builder", "repair", "scout"].includes(one.role) && one.outcome !== null);
|
|
26
|
+
if (family?.problem !== null || family.current.id !== task || latest?.id !== runId)
|
|
27
|
+
return "A newer result is current. Open it before commenting on a screenshot. Your draft is unchanged.";
|
|
28
|
+
const verified = verifyResultImage(store, root, [repo], { taskId: task, ...image });
|
|
29
|
+
return verified.ok ? null : "This screenshot is unavailable or changed. Restore it or remove it from your feedback. Your draft is unchanged.";
|
|
30
|
+
}
|
|
11
31
|
/** One user action: add the typed change and seal exactly the displayed
|
|
12
32
|
* saved notes. A refusal rolls back the note; a replay returns the same task. */
|
|
13
33
|
export function requestResultChanges(store, evidenceRoot, input, now) {
|
|
@@ -15,8 +35,8 @@ export function requestResultChanges(store, evidenceRoot, input, now) {
|
|
|
15
35
|
return store.transact(() => store.savepoint(() => {
|
|
16
36
|
const fail = (status, message) => { throw new ChangeRequestRefusal(status, message); };
|
|
17
37
|
if (!input.note.trim()) {
|
|
18
|
-
if (input.path.trim() || input.line.trim())
|
|
19
|
-
return fail(400, "Describe the change before attaching
|
|
38
|
+
if (input.path.trim() || input.line.trim() || input.image != null)
|
|
39
|
+
return fail(400, "Describe the change before attaching an image, file or line.");
|
|
20
40
|
if (!input.batch)
|
|
21
41
|
return fail(400, "Describe a change or use a saved note.");
|
|
22
42
|
return createResultRevision(store, evidenceRoot, input, now);
|
|
@@ -43,13 +63,19 @@ export function requestResultChanges(store, evidenceRoot, input, now) {
|
|
|
43
63
|
if (!artifact || !readVerifiedArtifact(evidenceRoot, artifact).ok)
|
|
44
64
|
return fail(409, "The saved changes could not be verified. Restore the result before requesting changes.");
|
|
45
65
|
const sourceKey = commentSourceKey(input.actor, input.request);
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
const earlier = store.diffCommentBySourceKey(sourceKey);
|
|
67
|
+
if (earlier && (earlier.run !== run.id || earlier.path !== path || earlier.line !== line || earlier.note !== valid.note || !sameFeedbackImage(earlier.image, input.image)))
|
|
68
|
+
return fail(409, "This submission already saved different feedback. Reopen the result and try again.");
|
|
69
|
+
// An exact consumed replay only returns its existing revision. Every new
|
|
70
|
+
// use proves the image before insertion, so malformed targets refuse cleanly.
|
|
71
|
+
if (earlier?.consumedBy == null) {
|
|
72
|
+
const problem = feedbackImageProblem(store, evidenceRoot, run.id, input.image);
|
|
73
|
+
if (problem !== null)
|
|
74
|
+
return fail(409, problem);
|
|
52
75
|
}
|
|
76
|
+
const noteId = earlier?.id ?? store.addDiffComment({ artifactId: artifact.id, runId: run.id, path, line, note: valid.note, author: input.actor, sourceKey, image: input.image ?? null }, now);
|
|
77
|
+
if (noteId === null)
|
|
78
|
+
return fail(409, "This feedback changed while saving. Your draft is unchanged; reopen the result.");
|
|
53
79
|
const result = createResultRevision(store, evidenceRoot, { ...input, batch: [...new Set([...batch, noteId])].sort((a, b) => a - b).join(",") }, now);
|
|
54
80
|
if (!result.ok)
|
|
55
81
|
return fail(result.status, result.message);
|
|
@@ -141,6 +167,11 @@ export function createResultRevision(store, evidenceRoot, input, now) {
|
|
|
141
167
|
}
|
|
142
168
|
if (batch.some(one => !isRevisionFeedback(one)))
|
|
143
169
|
return fail(409, "Review observations are not requested changes. Use Request change to add your feedback first.");
|
|
170
|
+
for (const one of batch) {
|
|
171
|
+
const problem = feedbackImageProblem(store, evidenceRoot, id, one.image);
|
|
172
|
+
if (problem !== null)
|
|
173
|
+
return fail(409, problem);
|
|
174
|
+
}
|
|
144
175
|
const comments = batch;
|
|
145
176
|
// Keep the subject recognizable, including when revising a revision.
|
|
146
177
|
// Reserve the suffix before truncating, at whole grapheme boundaries.
|
|
@@ -180,6 +211,7 @@ export function createResultRevision(store, evidenceRoot, input, now) {
|
|
|
180
211
|
note: one.note,
|
|
181
212
|
author: one.author,
|
|
182
213
|
createdAt: one.createdAt,
|
|
214
|
+
...(one.image == null ? {} : { image: one.image }),
|
|
183
215
|
})),
|
|
184
216
|
};
|
|
185
217
|
const briefBytes = Buffer.from(JSON.stringify(brief, null, 2), "utf8");
|
package/dist/result-review.d.ts
CHANGED
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
* selected tab, and the reading position in this tab's sessionStorage,
|
|
20
20
|
* keyed by the server-named account, the task, and the run. */
|
|
21
21
|
import { type ResultFacts, type PublicationFacts, type DisplayStatus } from "./workspace-ui.js";
|
|
22
|
+
/** Optional context for feedback. The sealed diff remains the revision authority. */
|
|
23
|
+
export type FeedbackImage = {
|
|
24
|
+
run: number;
|
|
25
|
+
artifact: number;
|
|
26
|
+
sha256: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function parseFeedbackImage(value: unknown): FeedbackImage | null | false;
|
|
29
|
+
export declare function sameFeedbackImage(a: FeedbackImage | null | undefined, b: FeedbackImage | null | undefined): boolean;
|
|
22
30
|
/** Add re-read evidence health to the existing result/review projection.
|
|
23
31
|
* This is presentation only: the recorded verdict and acceptance stay intact. */
|
|
24
32
|
export declare function evidenceResultStatusOf(result: ResultFacts, publication: PublicationFacts, evidence: EvidenceHealth): DisplayStatus;
|
|
@@ -49,6 +57,7 @@ export type ResultScreenshot = {
|
|
|
49
57
|
path: string;
|
|
50
58
|
caption: string;
|
|
51
59
|
artifactId: number;
|
|
60
|
+
sha256?: string;
|
|
52
61
|
problem: string | null;
|
|
53
62
|
};
|
|
54
63
|
export type ResultEvidenceInput = {
|
package/dist/result-review.js
CHANGED
|
@@ -19,6 +19,26 @@
|
|
|
19
19
|
* selected tab, and the reading position in this tab's sessionStorage,
|
|
20
20
|
* keyed by the server-named account, the task, and the run. */
|
|
21
21
|
import { resultStatusOf } from "./workspace-ui.js";
|
|
22
|
+
export function parseFeedbackImage(value) {
|
|
23
|
+
if (value === undefined || value === null || value === "")
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const image = typeof value === "string" ? JSON.parse(value) : value;
|
|
27
|
+
if (image === null || typeof image !== "object" || Array.isArray(image)
|
|
28
|
+
|| !Number.isSafeInteger(image.run) || image.run < 1
|
|
29
|
+
|| !Number.isSafeInteger(image.artifact) || image.artifact < 1
|
|
30
|
+
|| typeof image.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(image.sha256)
|
|
31
|
+
|| Object.keys(image).some(key => !["run", "artifact", "sha256"].includes(key)))
|
|
32
|
+
return false;
|
|
33
|
+
return { run: image.run, artifact: image.artifact, sha256: image.sha256 };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function sameFeedbackImage(a, b) {
|
|
40
|
+
return (a == null && b == null) || (a != null && b != null && a.run === b.run && a.artifact === b.artifact && a.sha256 === b.sha256);
|
|
41
|
+
}
|
|
22
42
|
/** Add re-read evidence health to the existing result/review projection.
|
|
23
43
|
* This is presentation only: the recorded verdict and acceptance stay intact. */
|
|
24
44
|
export function evidenceResultStatusOf(result, publication, evidence) {
|
|
@@ -155,6 +175,8 @@ export function resultFactsFromHtml(html) {
|
|
|
155
175
|
* page. Matched exactly — never a bare open redirect. */
|
|
156
176
|
export function resultReturnTarget(raw, runId, tasks = []) {
|
|
157
177
|
if (raw !== null && raw !== undefined) {
|
|
178
|
+
if (raw === `/r/${runId}?record=1`)
|
|
179
|
+
return raw;
|
|
158
180
|
const review = /^\/review\?result=[A-Za-z0-9._~%-]{1,200}(?:&run=([0-9]{1,15}))?(?:&tab=(?:summary|changes|checks))?$/.exec(raw);
|
|
159
181
|
if (review !== null && (review[1] === undefined || Number(review[1]) === runId))
|
|
160
182
|
return raw;
|
|
@@ -299,6 +321,22 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
299
321
|
document.addEventListener('click',function(ev){var link=ev.target&&ev.target.closest?ev.target.closest('a[href="#request-changes"]'):null;if(link)openForm();});
|
|
300
322
|
if(form){
|
|
301
323
|
var noteBox=form.querySelector('[name=note]'),pathBox=form.querySelector('[name=path]'),lineBox=form.querySelector('[name=line]'),pin=form.querySelector('details.result-pin');
|
|
324
|
+
var imageBox=form.querySelector('[name=image]'),imagePreview=form.querySelector('[data-feedback-image]');
|
|
325
|
+
function showImage(){
|
|
326
|
+
if(!imageBox||!imagePreview)return;
|
|
327
|
+
imagePreview.hidden=!imageBox.value;
|
|
328
|
+
var img=imagePreview.querySelector('img'),caption=imagePreview.querySelector('figcaption'),selected=null;
|
|
329
|
+
document.querySelectorAll('[data-comment-image]').forEach(function(button){if(button.getAttribute('data-comment-image')===imageBox.value)selected=button;});
|
|
330
|
+
img.hidden=!selected;
|
|
331
|
+
if(selected){var identity=JSON.parse(imageBox.value);img.src='/r/'+identity.run+'/evidence/'+identity.artifact;img.alt=selected.getAttribute('data-image-caption');caption.textContent=img.alt+' · Screenshot '+identity.artifact;}
|
|
332
|
+
else{img.removeAttribute('src');caption.textContent='This screenshot is unavailable. Restore it or remove it from your feedback.';}
|
|
333
|
+
}
|
|
334
|
+
document.addEventListener('click',function(event){
|
|
335
|
+
var button=event.target&&event.target.closest?event.target.closest('[data-comment-image],[data-remove-image]'):null;
|
|
336
|
+
if(!button||!imageBox)return;
|
|
337
|
+
event.preventDefault();imageBox.value=button.getAttribute('data-comment-image')||'';showImage();openForm();save();
|
|
338
|
+
if(noteBox){noteBox.dispatchEvent(new Event('input',{bubbles:true}));noteBox.scrollIntoView({block:'center',behavior:'instant'});noteBox.focus();}
|
|
339
|
+
});
|
|
302
340
|
var limit=document.getElementById('comment-note-limit'),requestBox=form.querySelector('[name=request]');
|
|
303
341
|
function updateActions(){
|
|
304
342
|
var typed=!!(noteBox&¬eBox.value.trim()),batch=form.querySelector('[name=batch]');
|
|
@@ -333,7 +371,7 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
333
371
|
// A fresh request identity: 16 random bytes as hex, the server's own shape.
|
|
334
372
|
function mint(){var bytes=new Uint8Array(16);try{crypto.getRandomValues(bytes);}catch(e){for(var i=0;i<16;i++)bytes[i]=Math.floor(Math.random()*256);}
|
|
335
373
|
var hex='';for(var j=0;j<16;j++)hex+=(bytes[j]<16?'0':'')+bytes[j].toString(16);return hex;}
|
|
336
|
-
function payload(){return JSON.stringify([noteBox?noteBox.value:'',pathBox?pathBox.value:'',lineBox?lineBox.value:'']);}
|
|
374
|
+
function payload(){return JSON.stringify([noteBox?noteBox.value:'',pathBox?pathBox.value:'',lineBox?lineBox.value:'',imageBox?imageBox.value:'']);}
|
|
337
375
|
// What the current token was last submitted with; null = never sent.
|
|
338
376
|
var sent=null,bound=null;
|
|
339
377
|
// The server refused this token for a different note or another result:
|
|
@@ -347,7 +385,7 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
347
385
|
var focusNote=function(){setTimeout(function(){noteBox.focus({preventScroll:true});},0);};
|
|
348
386
|
if(document.readyState==='complete')focusNote();else window.addEventListener('load',focusNote);
|
|
349
387
|
}
|
|
350
|
-
if(saved&&typeof saved.note==='string'&&saved.note.length<=
|
|
388
|
+
if(saved&&typeof saved.note==='string'&&saved.note.length<=(noteBox?noteBox.maxLength:4000)){
|
|
351
389
|
if(noteBox&&!noteBox.value)noteBox.value=saved.note;
|
|
352
390
|
if(saved.note)openForm();
|
|
353
391
|
if(pathBox&&!pathBox.value&&typeof saved.path==='string')pathBox.value=saved.path;
|
|
@@ -355,7 +393,9 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
355
393
|
if(pin&&(pathBox&&pathBox.value||lineBox&&lineBox.value))pin.open=true;
|
|
356
394
|
if(typeof saved.request==='string'&&/^[a-f0-9]{32}$/.test(saved.request)&&requestBox)requestBox.value=saved.request;
|
|
357
395
|
if(typeof saved.sent==='string')sent=saved.sent;
|
|
358
|
-
|
|
396
|
+
if(imageBox&&typeof saved.image==='string'&&saved.image.length<250)imageBox.value=saved.image;
|
|
397
|
+
showImage();
|
|
398
|
+
bound=JSON.stringify([saved.note,saved.path||'',saved.line||'',saved.image||'']);
|
|
359
399
|
if(noteBox)noteBox.dispatchEvent(new Event('input'));
|
|
360
400
|
}
|
|
361
401
|
function save(){
|
|
@@ -365,8 +405,8 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
365
405
|
var current=payload();
|
|
366
406
|
if((bound!==null&¤t!==bound)||(sent!==null&¤t!==sent)){sent=null;if(requestBox)requestBox.value=mint();}
|
|
367
407
|
bound=current;
|
|
368
|
-
if(!note&&!path&&!line){write(draftKey,null);return;}
|
|
369
|
-
write(draftKey,{note:note,path:path,line:line,request:requestBox?requestBox.value:'',sent:sent,at:Date.now()});
|
|
408
|
+
if(!note&&!path&&!line&&!(imageBox&&imageBox.value)){write(draftKey,null);return;}
|
|
409
|
+
write(draftKey,{note:note,path:path,line:line,image:imageBox?imageBox.value:'',request:requestBox?requestBox.value:'',sent:sent,at:Date.now()});
|
|
370
410
|
if(!storage&&limit)limit.textContent='Draft stays on this page only. Browser storage is unavailable.';
|
|
371
411
|
}
|
|
372
412
|
form.addEventListener('input',save);
|
package/dist/serve.js
CHANGED
|
@@ -146,6 +146,7 @@ import { createChatHandlers } from './server/chat.js';
|
|
|
146
146
|
import { createLiveHandlers } from './server/live.js';
|
|
147
147
|
import { createTerminalHandlers, terminalHostFor } from './server/terminals.js';
|
|
148
148
|
import { passwordlessOn } from './passwordless.js';
|
|
149
|
+
import { keepTypedFields, openProjectFor, settingsCrumbs } from './settings-kit.js';
|
|
149
150
|
import { createSweepHandlers } from './server/sweeps.js';
|
|
150
151
|
import { createFlowsHandlers } from './server/flows.js';
|
|
151
152
|
import { createPagesHandlers } from './server/pages.js';
|
|
@@ -200,7 +201,7 @@ export function createDecisionServer(options) {
|
|
|
200
201
|
get managedRepos() { return managedRepos; },
|
|
201
202
|
get ceiling() { return ceiling; },
|
|
202
203
|
get allowedHost() { return allowedHost; },
|
|
203
|
-
get defaultProject() { return defaultProject; },
|
|
204
|
+
get defaultProject() { return defaultProject(); },
|
|
204
205
|
get actionTarget() { return actionTarget; },
|
|
205
206
|
get team() { return team; },
|
|
206
207
|
get joinSourceOf() { return joinSourceOf; },
|
|
@@ -453,15 +454,14 @@ export function createDecisionServer(options) {
|
|
|
453
454
|
};
|
|
454
455
|
let joinGlobal = { tokens: 30, refilledAt: Date.now() };
|
|
455
456
|
const approvalNonces = new Map();
|
|
456
|
-
//
|
|
457
|
-
// Native additions come only from the controller after its admission checks.
|
|
458
|
-
const { ceiling
|
|
459
|
-
const ceiling = { ...startCeiling, canon: identities.strict };
|
|
460
|
-
/** The project every fresh session opens with: the sole configured repo, else none. */
|
|
461
|
-
const defaultProject = ceiling.repos.length === 1 && ceiling.roots.length === 0 ? ceiling.repos[0] : null;
|
|
457
|
+
// Startup authority: the configured repos and roots as their identities say (startupCeiling). Missing or unresolved
|
|
458
|
+
// paths remain restrictive. Native additions come only from the controller after its admission checks.
|
|
459
|
+
const { ceiling, unresolved: unresolvedRepos, defaultProject, configured, configuredPaths } = startupCeiling([...(options.repo === undefined ? [] : [options.repo]), ...(options.repos ?? [])], options.projectRoots ?? [], options.identities === undefined ? undefined : identities, identities.strict);
|
|
462
460
|
/** No ceiling configured at all: the legacy trust-everything mode, named. */
|
|
463
461
|
// An `up` console (it is handed its admitted set) is never unscoped: with every saved project away it is empty.
|
|
464
|
-
|
|
462
|
+
// Judged by what was configured, never by what has resolved so far: a console whose folders haven't answered yet is
|
|
463
|
+
// scoped and empty, not unscoped.
|
|
464
|
+
const unscopedMode = !configured && options.admittedRepos === undefined;
|
|
465
465
|
/** Per-row visibility under the ceiling — the authorization question for reads. */
|
|
466
466
|
/** The exact repositories this console serves now: `up`'s live admitted set when it supplies one, else startup's. */
|
|
467
467
|
const exactRepos = () => options.admittedRepos?.() ?? ceiling.repos;
|
|
@@ -889,6 +889,7 @@ export function createDecisionServer(options) {
|
|
|
889
889
|
browser: who.via === 'cookie',
|
|
890
890
|
// Passwordless (read once for rendering; every act proves it again at the act): ceremony fields render as nothing.
|
|
891
891
|
passwordless: who.via === 'cookie' && passwordlessOn(store),
|
|
892
|
+
openProject: who.via === 'cookie' ? who.session.project : null,
|
|
892
893
|
// v100: signed in with the identity provider: its label, and whether it checked them recently enough to stand in for a password.
|
|
893
894
|
// Cockpit B2: "fresh" is an explicit re-check made on request and not yet spent (read live; one step-up uses it).
|
|
894
895
|
sso: who.via === 'cookie' && who.session.sso !== undefined ? { label: ssoSettings()?.label ?? "your identity provider",
|
|
@@ -1327,8 +1328,8 @@ export function createDecisionServer(options) {
|
|
|
1327
1328
|
return { ok: false, code: "demo", why: "this is a demo database — chat cannot contact an external model" };
|
|
1328
1329
|
if (unscopedMode)
|
|
1329
1330
|
return { ok: false, code: "unscoped", why: "chat needs at least one added project" };
|
|
1330
|
-
if (options.currentRepos === undefined && unresolvedRepos.length > 0) {
|
|
1331
|
-
return { ok: false, code: "unresolved", why: "a configured project
|
|
1331
|
+
if (options.currentRepos === undefined && unresolvedRepos().length > 0) {
|
|
1332
|
+
return { ok: false, code: "unresolved", why: "a configured project folder hasn't been found yet — chat runs once it answers; if its path is wrong, fix it and restart" };
|
|
1332
1333
|
}
|
|
1333
1334
|
if (managedRepos().length === 0)
|
|
1334
1335
|
return { ok: false, code: "empty", why: "add a project first — chat will include it automatically" };
|
|
@@ -1483,13 +1484,20 @@ export function createDecisionServer(options) {
|
|
|
1483
1484
|
const ssoFacts = requestContext.getStore()?.sso;
|
|
1484
1485
|
if (ssoFacts !== undefined) {
|
|
1485
1486
|
const back = requestContext.getStore()?.returnTo ?? "/";
|
|
1486
|
-
|
|
1487
|
+
// A typed-only proof (settings-kit.ts) stays a password field: its handler takes nothing else.
|
|
1488
|
+
s = { ...s, body: keepTypedFields(s.body, ssoStepUps, ssoFacts, back), ...(s.workspace?.pageHtml == null ? {} : { workspace: { ...s.workspace, pageHtml: keepTypedFields(s.workspace.pageHtml, ssoStepUps, ssoFacts, back) } }) };
|
|
1487
1489
|
if (s.workspace?.view?.kind === "flow")
|
|
1488
1490
|
s = { ...s, workspace: { ...s.workspace, view: { ...s.workspace.view, stepUp: { label: ssoFacts.label, fresh: ssoFacts.fresh, confirmHref: `/login/sso?reauth=1&return=${encodeURIComponent(back)}` } } } };
|
|
1489
1491
|
}
|
|
1490
1492
|
// D5: team chat (the central team service) is deprecated; /chat?team=1 still opens this release, but nothing links to it.
|
|
1491
1493
|
const sensitive = authored || s.forceSensitive === true;
|
|
1492
1494
|
const requestFacts = requestContext.getStore();
|
|
1495
|
+
// The settings frame (settings-kit.ts): its breadcrumb, for the shell and the workspace alike.
|
|
1496
|
+
if (s.chrome !== undefined && s.chrome.crumbs === undefined) {
|
|
1497
|
+
const crumbs = settingsCrumbs(new URL(requestFacts?.returnTo ?? '/', 'http://standing-orders.local').pathname, s.title, htmlString(s.body));
|
|
1498
|
+
if (crumbs !== undefined)
|
|
1499
|
+
s = { ...s, chrome: { ...s.chrome, crumbs } };
|
|
1500
|
+
}
|
|
1493
1501
|
if (requestFacts?.browser && s.chrome && !s.forceSensitive && (requestFacts.workspaceRead || browserAssetsAvailable())) {
|
|
1494
1502
|
const path = new URL(requestFacts.returnTo, 'http://standing-orders.local');
|
|
1495
1503
|
path.searchParams.delete('format');
|
|
@@ -1564,6 +1572,10 @@ export function createDecisionServer(options) {
|
|
|
1564
1572
|
const request = requestFacts.workspaceRequest;
|
|
1565
1573
|
const workspace = {
|
|
1566
1574
|
version: 1, path: currentPath, title: s.title, user: requestFacts.actor ?? '', csrf: requestFacts.csrf, sensitive,
|
|
1575
|
+
// The project picker names the open project; the settings frame carries its breadcrumb.
|
|
1576
|
+
// Only a project this person may see now: never the raw saved path (settings-kit.ts openProjectFor).
|
|
1577
|
+
openProject: openProjectFor(requestFacts.openProject ?? null, s.chrome.project, s.chrome.projects ?? []),
|
|
1578
|
+
...(s.chrome.crumbs === undefined ? {} : { crumbs: s.chrome.crumbs }),
|
|
1567
1579
|
...(requestFacts.passwordless === true ? { passwordless: true } : {}),
|
|
1568
1580
|
...(requestFacts.actor ? { leadName: leadNameOf(store, requestFacts.actor) } : {}),
|
|
1569
1581
|
refreshUrl: path.pathname + path.search,
|
|
@@ -1966,7 +1978,7 @@ export function createDecisionServer(options) {
|
|
|
1966
1978
|
waits.push(connectionCheck(provider).catch(() => undefined));
|
|
1967
1979
|
}
|
|
1968
1980
|
// Project identities, resolved off the thread: what pages asked about, and answers gone stale.
|
|
1969
|
-
waits.push(identities.refresh([...exactRepos(), ...(options.currentRepos?.() ?? []), ...store.listProjects().map(one => one.path), ...store.knownRepos()
|
|
1981
|
+
waits.push(identities.refresh([...configuredPaths, ...exactRepos(), ...(options.currentRepos?.() ?? []), ...store.listProjects().map(one => one.path), ...store.knownRepos()]));
|
|
1970
1982
|
integrations.refreshIfDue();
|
|
1971
1983
|
const sso = ssoSettings();
|
|
1972
1984
|
if (sso !== null && (ssoProvider === null || ssoProvider.issuer !== sso.issuer || Date.now() - ssoProvider.at >= 10 * 60_000))
|
|
@@ -2204,7 +2216,7 @@ export function createDecisionServer(options) {
|
|
|
2204
2216
|
get providerFor() { return providerFor; },
|
|
2205
2217
|
get providerKnown() { return providerKnown; },
|
|
2206
2218
|
get connectVisits() { return connectVisits; },
|
|
2207
|
-
get defaultProject() { return defaultProject; },
|
|
2219
|
+
get defaultProject() { return defaultProject(); },
|
|
2208
2220
|
get googleVisits() { return googleVisits; },
|
|
2209
2221
|
get setupAttemptsLeft() { return setupAttemptsLeft; },
|
|
2210
2222
|
set setupAttemptsLeft(value) { setupAttemptsLeft = value; },
|
|
@@ -2324,3 +2336,38 @@ export { diffFileAnchor, editorFileHref, needActionOf, orderChangedFiles, plainC
|
|
|
2324
2336
|
export { settingsGroups, settingsTiles } from "./server/render-settings.js";
|
|
2325
2337
|
export { decisionAnswerScript, earlierVersionsWords, revisionLineageWords } from "./server/render-tasks.js";
|
|
2326
2338
|
export { SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH } from "./server/session.js";
|
|
2339
|
+
/**
|
|
2340
|
+
* The console's configured repos and roots. Without identities (embedders and tests) they resolve once, synchronously,
|
|
2341
|
+
* as before. With them, the console resolves them in the background after it listens (operate.ts startConsole), so
|
|
2342
|
+
* each is read from the last answer: a configured path counts by its real path from the moment its folder answers,
|
|
2343
|
+
* and until then only by its spelling, which the strict `canon` never matches for an unresolved row.
|
|
2344
|
+
*/
|
|
2345
|
+
function startupCeiling(repos, roots, identities, canon) {
|
|
2346
|
+
const configured = repos.length > 0 || roots.length > 0;
|
|
2347
|
+
const configuredPaths = [...repos, ...roots];
|
|
2348
|
+
if (identities === undefined) {
|
|
2349
|
+
const once = resolveCeiling(repos, roots);
|
|
2350
|
+
return {
|
|
2351
|
+
ceiling: { ...once.ceiling, canon },
|
|
2352
|
+
unresolved: () => once.unresolved,
|
|
2353
|
+
defaultProject: () => once.ceiling.repos.length === 1 && once.ceiling.roots.length === 0 ? once.ceiling.repos[0] : null,
|
|
2354
|
+
configured,
|
|
2355
|
+
configuredPaths,
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
// Only resolved entries authorize: an unresolved configured path is in no list, so it matches nothing (not even its
|
|
2359
|
+
// own spelling) and is no session's default until its folder answers.
|
|
2360
|
+
const resolved = (paths) => [...new Set(paths.flatMap(path => { const real = identities.known(path); return typeof real === "string" ? [real] : []; }))];
|
|
2361
|
+
const ceiling = {
|
|
2362
|
+
get repos() { return resolved(repos); },
|
|
2363
|
+
get roots() { return resolved(roots); },
|
|
2364
|
+
canon,
|
|
2365
|
+
};
|
|
2366
|
+
return {
|
|
2367
|
+
ceiling,
|
|
2368
|
+
unresolved: () => [...repos, ...roots].filter(path => typeof identities.known(path) !== "string"),
|
|
2369
|
+
defaultProject: () => repos.length === 1 && roots.length === 0 ? resolved(repos)[0] ?? null : null,
|
|
2370
|
+
configured,
|
|
2371
|
+
configuredPaths,
|
|
2372
|
+
};
|
|
2373
|
+
}
|
package/dist/server/chat.js
CHANGED
|
@@ -12,10 +12,10 @@ import { CHAT_ACTIONS, mintSharedActionReview, sharedActionPayload, sharedAction
|
|
|
12
12
|
import { CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
13
13
|
import { buildDataDocument, composeRequest, isSubscriptionChatProvider, parseAssistantEnvelope, performChatRequest, plausibleChatKey, priceOf, settleForPrice, TURN_WALL_CLOCK_MS, worstCaseForPrice } from "../converse.js";
|
|
14
14
|
import { DEMO_CHAT_SCRIPT, demoChatHtml, demoThreadHtml } from "../demo-chat.js";
|
|
15
|
-
import { withDispatchDiagnoses } from "../dispatch.js";
|
|
15
|
+
import { builderReadyFor, firstRunBuilderRepo, withDispatchDiagnoses } from "../dispatch.js";
|
|
16
16
|
import { readVerifiedArtifact, scanForSecrets } from "../evidence.js";
|
|
17
17
|
import { run as execRun } from "../exec.js";
|
|
18
|
-
import { findFirstTasks, firstTaskSuggestions, HOW_IT_WORKS, SANDBOX_COMMAND } from "../first-run.js";
|
|
18
|
+
import { findFirstTasks, firstTaskSuggestions, HOW_IT_WORKS, SANDBOX_COMMAND, withBuilderStep } from "../first-run.js";
|
|
19
19
|
import { leadBriefHtml } from '../lead-context.js';
|
|
20
20
|
import { configureLeadFollow, leadFollowStatus } from '../lead-follow.js';
|
|
21
21
|
import { leadActivity } from "../lead-voice.js";
|
|
@@ -1158,11 +1158,12 @@ export function createChatHandlers(runtime) {
|
|
|
1158
1158
|
}
|
|
1159
1159
|
/** Chat's first run: the steps, first tasks until the first task exists, and the sandbox while no agent is signed in. */
|
|
1160
1160
|
function chatFirstRun(now, project) {
|
|
1161
|
-
const
|
|
1161
|
+
const repo = project ?? managedRepos()[0] ?? null;
|
|
1162
|
+
// Done only when a task in this project could start: a live builder bound to it, as dispatch decides.
|
|
1163
|
+
const steps = withBuilderStep(firstRunStepsNow(now), builderReadyFor(store, firstRunBuilderRepo(store, project, visible, managedRepos()), now));
|
|
1162
1164
|
if (steps === null)
|
|
1163
1165
|
return undefined;
|
|
1164
1166
|
const taskFiled = steps.find(one => one.key === "task")?.done ?? true;
|
|
1165
|
-
const repo = project ?? managedRepos()[0] ?? null;
|
|
1166
1167
|
const agent = steps.find(one => one.key === "agent");
|
|
1167
1168
|
const lead = leadWords();
|
|
1168
1169
|
return {
|
package/dist/server/chrome.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type BrowserSignIn, type BrowserUpdateNotice, type BrowserWorkspace } from "../browser-workspace.js";
|
|
2
|
+
import { type Crumb } from "../settings-kit.js";
|
|
2
3
|
import { type Decision, type Run } from "../store.js";
|
|
3
4
|
import { type tally } from "../summary.js";
|
|
4
5
|
import { type ServerResponse } from "node:http";
|
|
@@ -115,6 +116,8 @@ export type Chrome = {
|
|
|
115
116
|
csrf?: string;
|
|
116
117
|
/** Where a switch made on this screen returns to. */
|
|
117
118
|
returnTo?: string;
|
|
119
|
+
/** The settings frame's breadcrumb ("Settings / <page>", settings-kit.ts), set by sendScreen; absent elsewhere. */
|
|
120
|
+
crumbs?: Crumb[];
|
|
118
121
|
};
|
|
119
122
|
/**
|
|
120
123
|
* A server-rendered region that stays current: when the page's live stream
|
package/dist/server/chrome.js
CHANGED
|
@@ -33,6 +33,7 @@ import { PULL_REQUEST_SETTINGS_CSS } from "../pull-request-ui.js";
|
|
|
33
33
|
import { RECIPE_CSS } from "../recipe-ui.js";
|
|
34
34
|
import { REQUEST_LIMITS_CSS } from "../request-budget-ui.js";
|
|
35
35
|
import { RETENTION_CSS } from "../retention-ui.js";
|
|
36
|
+
import { crumbsHtml, SETTINGS_KIT_CSS } from "../settings-kit.js";
|
|
36
37
|
import { SKILLS_CSS } from "../skills-ui.js";
|
|
37
38
|
import { SPEND_CSS } from "../spend-ui.js";
|
|
38
39
|
import { SSO_CSS } from "../sso-ui.js";
|
|
@@ -148,7 +149,7 @@ export const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width
|
|
|
148
149
|
'.inbox-ask{margin:18px 0 0}.inbox-ask>h2{display:flex;align-items:baseline;gap:8px;margin:0 0 4px;font-size:15px;font-weight:600}.inbox-ask>h2 .count{font:500 12px var(--so-mono,ui-monospace,monospace);font-variant-numeric:tabular-nums;color:var(--so-muted)}.inbox-ask h3{font-size:13px;font-weight:600;margin:12px 0 4px}' +
|
|
149
150
|
'.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
|
|
150
151
|
'@media (max-width:760px){.inbox-tabs{display:flex;width:100%}.inbox-tabs a{flex:1;justify-content:center;min-height:44px;padding:0 6px}.inbox-unread{display:block}}';
|
|
151
|
-
export const WORKSPACE_STYLE = styleAsset(STYLE + BRAND_MARK_CSS + INBOX_TABS_CSS + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + SUBAGENT_CSS + KITS_CSS + STARTERS_CSS + GALLERY_CSS + SSO_CSS + CREDENTIALS_CSS + REQUEST_LIMITS_CSS + PEOPLE_AUDIT_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + TASK_STATUS_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + CHECK_SETTINGS_CSS + DISCLOSURE_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
|
|
152
|
+
export const WORKSPACE_STYLE = styleAsset(STYLE + BRAND_MARK_CSS + INBOX_TABS_CSS + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + SUBAGENT_CSS + KITS_CSS + STARTERS_CSS + GALLERY_CSS + SSO_CSS + CREDENTIALS_CSS + REQUEST_LIMITS_CSS + PEOPLE_AUDIT_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + TASK_STATUS_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + CHECK_SETTINGS_CSS + DISCLOSURE_CSS + SETTINGS_KIT_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
|
|
152
153
|
/**
|
|
153
154
|
* A server-rendered region that stays current: when the page's live stream
|
|
154
155
|
* says something changed (GET /live, live-bus.ts), fetch this page's own
|
|
@@ -579,8 +580,10 @@ ${update === undefined ? "" : html `<div class="banner update-banner" data-updat
|
|
|
579
580
|
${postForm(update.dismissHref, html `<button type="submit">Dismiss</button>`, { attrs: { class: "inline" }, hidden: { version: update.version } })}</div>`}`;
|
|
580
581
|
// The scope bar sits between the banners and the main/split body, so it
|
|
581
582
|
// can never disappear with a responsive pane (portfolio arc §1).
|
|
583
|
+
// The settings frame's breadcrumb sits above the page's own h1 (its only h1).
|
|
584
|
+
const crumbs = chrome.crumbs === undefined ? "" : crumbsHtml(chrome.crumbs);
|
|
582
585
|
const content = chrome.listPane === undefined
|
|
583
|
-
? html `<div class="content">${demoBanner}${scopeBar}<main>${main}</main></div>`
|
|
586
|
+
? html `<div class="content">${demoBanner}${scopeBar}${crumbs}<main>${main}</main></div>`
|
|
584
587
|
: html `<div class="content">${demoBanner}${scopeBar}<div class="split">\
|
|
585
588
|
<div class="list-pane">${chrome.listPane}</div>\
|
|
586
589
|
<div class="detail"><main>${main}</main></div>\
|
package/dist/server/console.css
CHANGED
|
@@ -265,12 +265,15 @@
|
|
|
265
265
|
.project-card .project-name:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: .375rem; }
|
|
266
266
|
|
|
267
267
|
label { display: block; font-size: 0.8125rem; font-weight: 500; margin: .75rem 0 0; color: var(--foreground); }
|
|
268
|
-
input[type=text], input[type=password], input[type=number], input[type=url], input[type=email], input[type=search], input[type=time], textarea, select {
|
|
269
|
-
width: 100%; margin: .35rem 0 0; padding: .45rem .7rem;
|
|
268
|
+
:where(input[type=text], input[type=password], input[type=number], input[type=url], input[type=email], input[type=search], input[type=time], textarea, select) {
|
|
269
|
+
width: 100%; margin: .35rem 0 0; padding: .45rem .7rem;
|
|
270
270
|
color: var(--foreground); background: var(--card); min-height: 2.125rem;
|
|
271
271
|
border: 1px solid var(--input); border-radius: calc(var(--radius) - 3px);
|
|
272
272
|
transition: border-color .15s, box-shadow .15s;
|
|
273
273
|
}
|
|
274
|
+
/* The box above is in :where() so a surface's own rules (the workspace's 44px phone targets) win; the type keeps
|
|
275
|
+
its old weight, so the phone's 16px rule still loses to it as before. */
|
|
276
|
+
input[type=text], input[type=password], input[type=number], input[type=url], input[type=email], input[type=search], input[type=time], textarea, select { font: 400 0.875rem/1.4 var(--font-sans); }
|
|
274
277
|
@media (hover: hover) and (pointer: fine) { input:hover, textarea:hover, select:hover { border-color: var(--muted-foreground); } }
|
|
275
278
|
input[type=number] { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
276
279
|
input[type=radio], input[type=checkbox] { accent-color: var(--ring); }
|
|
@@ -2727,3 +2730,15 @@ html.so-swap::view-transition-old(*):only-child { animation: so-card-out 160ms e
|
|
|
2727
2730
|
html.so-swap::view-transition-group(*), html.so-swap::view-transition-new(*), html.so-swap::view-transition-old(*) { animation: none !important; }
|
|
2728
2731
|
.worker.flash { animation: none; }
|
|
2729
2732
|
}
|
|
2733
|
+
|
|
2734
|
+
/* Exact-image feedback shares the existing comment form. */
|
|
2735
|
+
.result-image { margin: 0; min-width: 0; }
|
|
2736
|
+
.result-image > button { margin-top: .5rem; min-height: 44px; white-space: nowrap; }
|
|
2737
|
+
.feedback-image { display: flex; align-items: center; flex-wrap: wrap; gap: .75rem; margin: .5rem 0; min-width: 0; }
|
|
2738
|
+
.feedback-image[hidden], .feedback-image img[hidden] { display: none; }
|
|
2739
|
+
.feedback-image img { width: 5rem; max-height: 5rem; object-fit: contain; border: 1px solid var(--border); border-radius: var(--radius); }
|
|
2740
|
+
.feedback-image figcaption { flex: 1 1 12rem; overflow-wrap: anywhere; font-size: .875rem; }
|
|
2741
|
+
.feedback-image button { min-height: 44px; white-space: nowrap; }
|
|
2742
|
+
|
|
2743
|
+
.result-feedback-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
|
|
2744
|
+
.result-feedback-actions button { min-height: 44px; white-space: nowrap; }
|
package/dist/server/flows.js
CHANGED
|
@@ -839,6 +839,7 @@ export function createFlowsHandlers(runtime) {
|
|
|
839
839
|
if (body.get("repo") !== project || (who.via === "cookie" && body.get("projectRevision") !== String(who.session.projectRevision)))
|
|
840
840
|
throw new RecipeError("The open project changed. Reopen this recipe in the intended project.", 409);
|
|
841
841
|
const rootMode = !unscopedMode && ceiling.roots.length > 0;
|
|
842
|
+
const decidedOn = identities.generation();
|
|
842
843
|
if (rootMode) {
|
|
843
844
|
// A mutation's target is resolved now, off the console's thread, and must still be the project it names.
|
|
844
845
|
const resolved = await identities.resolveNow(project);
|
|
@@ -852,6 +853,9 @@ export function createFlowsHandlers(runtime) {
|
|
|
852
853
|
throw new RecipeError(rechecked.message, rechecked.status);
|
|
853
854
|
if (projectOf(who, request) !== project)
|
|
854
855
|
throw new RecipeError("The open project changed. Preview the intended project again.", 409);
|
|
856
|
+
// An identity that changed during the awaits above undoes what they proved.
|
|
857
|
+
if (identities.generation() !== decidedOn)
|
|
858
|
+
throw new RecipeError("A project folder changed while this was being checked. Try again.", 409);
|
|
855
859
|
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
856
860
|
if (step === "prepare") {
|
|
857
861
|
const id = body.get("recipe") ?? "", version = body.get("recipeRevision") ?? "";
|
package/dist/server/http.d.ts
CHANGED
|
@@ -226,6 +226,7 @@ export declare function matchTaskPath(pathname: string, suffixPattern: string):
|
|
|
226
226
|
verb: string;
|
|
227
227
|
} | null;
|
|
228
228
|
export declare function taskHref(taskId: string): string;
|
|
229
|
+
export declare function runHref(runId: number): string;
|
|
229
230
|
export declare const SAFETY: {
|
|
230
231
|
readonly "Content-Security-Policy": "default-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; worker-src 'self'; img-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'";
|
|
231
232
|
readonly "X-Content-Type-Options": "nosniff";
|
package/dist/server/http.js
CHANGED
|
@@ -51,6 +51,9 @@ export function matchTaskPath(pathname, suffixPattern) {
|
|
|
51
51
|
export function taskHref(taskId) {
|
|
52
52
|
return `/t/${encodeURIComponent(taskId)}`;
|
|
53
53
|
}
|
|
54
|
+
export function runHref(runId) {
|
|
55
|
+
return `/r/${Math.trunc(runId)}`;
|
|
56
|
+
}
|
|
54
57
|
// ---- rendering -------------------------------------------------------------
|
|
55
58
|
export const SAFETY = {
|
|
56
59
|
"Content-Security-Policy": "default-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; worker-src 'self'; img-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
|
|
@@ -64,10 +67,13 @@ export function respond(response, status, type, body) {
|
|
|
64
67
|
// writeHead's headers would otherwise win.
|
|
65
68
|
const own = response.getHeader("content-security-policy");
|
|
66
69
|
const cache = response.getHeader("cache-control");
|
|
70
|
+
// The first-account form's page sends `same-origin` so its own post names this console (signup-gate.ts).
|
|
71
|
+
const referrer = response.getHeader("referrer-policy");
|
|
67
72
|
response.writeHead(status, {
|
|
68
73
|
...SAFETY,
|
|
69
74
|
...(own === undefined ? {} : { "content-security-policy": own }),
|
|
70
75
|
...(cache === undefined ? {} : { "Cache-Control": cache }),
|
|
76
|
+
...(referrer === "same-origin" ? { "Referrer-Policy": "same-origin" } : {}),
|
|
71
77
|
"Content-Type": type,
|
|
72
78
|
});
|
|
73
79
|
response.end(isHtml(body) ? htmlString(body) : body);
|