toolroll 0.9.83 → 0.9.84
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 +27 -0
- package/README.md +4 -3
- package/dist/assignment-ui.js +5 -1
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +22 -22
- package/dist/browser-crew.js +5 -1
- package/dist/browser-workspace.d.ts +7 -0
- package/dist/contracts/console-api.d.ts +2 -1
- package/dist/contracts/console-api.js +5 -1
- 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/folder-look.d.ts +3 -0
- package/dist/folder-look.js +8 -4
- package/dist/guides.js +4 -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 +21 -0
- package/dist/operate.js +96 -26
- package/dist/project-admission.d.ts +4 -1
- package/dist/project-admission.js +23 -2
- package/dist/project-identity.d.ts +9 -2
- package/dist/project-identity.js +80 -33
- package/dist/project-knowledge.d.ts +6 -6
- 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 +5 -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 +42 -6
- package/dist/server/people-tokens.js +40 -20
- package/dist/server/render-pages.d.ts +10 -0
- package/dist/server/render-pages.js +29 -14
- package/dist/server/render-people.d.ts +14 -2
- package/dist/server/render-people.js +35 -12
- package/dist/server/render-settings.js +1 -0
- package/dist/server/render-tasks.js +7 -14
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/route-table.js +8 -1
- package/dist/server/settings.d.ts +58 -1
- package/dist/server/settings.js +198 -69
- package/dist/server/signup-gate.d.ts +9 -0
- package/dist/server/signup-gate.js +40 -0
- package/dist/server/tasks.js +12 -2
- 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/task-sizing.d.ts +5 -3
- package/dist/task-sizing.js +4 -4
- package/package.json +1 -1
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Project identities as the console uses them, without touching a person's folders on its thread.
|
|
3
3
|
*
|
|
4
4
|
* A project's identity is its real path (project.ts canonicalProject). Resolving one reads the folder, and a folder can
|
|
5
|
-
* stop answering (folder-look.ts), so the console never resolves one on a page or at startup on its own thread
|
|
5
|
+
* stop answering (folder-look.ts), so the console never resolves one on a page or at startup on its own thread, and
|
|
6
|
+
* never waits for one before it answers:
|
|
6
7
|
*
|
|
7
8
|
* - `hot` (display and grouping only): the last answer, else the path as saved. Nothing is started by asking.
|
|
8
9
|
* - `strict` (visibility and authorization): only a resolved identity; an unknown one fails closed until the next
|
|
@@ -10,7 +11,8 @@
|
|
|
10
11
|
* - `vouched` (a path a request names): only a path whose identity is known, or one that is exactly a saved identity.
|
|
11
12
|
* Anything else is refused this time and resolved by the next background pass, never resolved inline.
|
|
12
13
|
* - `resolveNow` (an action that needs a path checked now, a POST): resolved off the thread under the look's deadline.
|
|
13
|
-
* - `refresh` (the console's
|
|
14
|
+
* - `refresh` (the console's background pass, the first started right after it listens): resolves what was asked about
|
|
15
|
+
* and what has gone stale. The console answers before any of it does; until a path answers it fails closed.
|
|
14
16
|
*
|
|
15
17
|
* An embedder or test that passes no identities gets `syncProjectIdentities`: the old synchronous reads, unchanged.
|
|
16
18
|
*/
|
|
@@ -31,7 +33,12 @@ export type ProjectIdentities = {
|
|
|
31
33
|
resolveNow: (path: string) => Promise<string | null | "no-answer">;
|
|
32
34
|
/** Resolve `paths`, everything asked about since, and answers older than a minute. False when some didn't answer. */
|
|
33
35
|
refresh: (paths: Iterable<string>) => Promise<boolean>;
|
|
36
|
+
/** Moves whenever a known identity changes or goes: a decision made before an await is re-checked against it right
|
|
37
|
+
* before its write. */
|
|
38
|
+
generation: () => number;
|
|
34
39
|
};
|
|
40
|
+
/** How many folders a pass looks at side by side when they're looked at one by one (within FOLDER_LOOK_CHILDREN). */
|
|
41
|
+
export declare const IDENTITY_LOOKS = 4;
|
|
35
42
|
export declare function projectIdentities(look?: FolderLookOptions, now?: () => number): ProjectIdentities;
|
|
36
43
|
/** The synchronous reads, as before: for embedders and tests that pass no identities. */
|
|
37
44
|
export declare function syncProjectIdentities(): ProjectIdentities;
|
package/dist/project-identity.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Project identities as the console uses them, without touching a person's folders on its thread.
|
|
3
3
|
*
|
|
4
4
|
* A project's identity is its real path (project.ts canonicalProject). Resolving one reads the folder, and a folder can
|
|
5
|
-
* stop answering (folder-look.ts), so the console never resolves one on a page or at startup on its own thread
|
|
5
|
+
* stop answering (folder-look.ts), so the console never resolves one on a page or at startup on its own thread, and
|
|
6
|
+
* never waits for one before it answers:
|
|
6
7
|
*
|
|
7
8
|
* - `hot` (display and grouping only): the last answer, else the path as saved. Nothing is started by asking.
|
|
8
9
|
* - `strict` (visibility and authorization): only a resolved identity; an unknown one fails closed until the next
|
|
@@ -10,7 +11,8 @@
|
|
|
10
11
|
* - `vouched` (a path a request names): only a path whose identity is known, or one that is exactly a saved identity.
|
|
11
12
|
* Anything else is refused this time and resolved by the next background pass, never resolved inline.
|
|
12
13
|
* - `resolveNow` (an action that needs a path checked now, a POST): resolved off the thread under the look's deadline.
|
|
13
|
-
* - `refresh` (the console's
|
|
14
|
+
* - `refresh` (the console's background pass, the first started right after it listens): resolves what was asked about
|
|
15
|
+
* and what has gone stale. The console answers before any of it does; until a path answers it fails closed.
|
|
14
16
|
*
|
|
15
17
|
* An embedder or test that passes no identities gets `syncProjectIdentities`: the old synchronous reads, unchanged.
|
|
16
18
|
*/
|
|
@@ -19,27 +21,86 @@ import { lookIdentities } from "./folder-look.js";
|
|
|
19
21
|
import { canonicalProject } from "./project.js";
|
|
20
22
|
/** How long an answer stands before the background pass resolves the path again. */
|
|
21
23
|
const STALE_MS = 60_000;
|
|
24
|
+
/** How many folders a pass looks at side by side when they're looked at one by one (within FOLDER_LOOK_CHILDREN). */
|
|
25
|
+
export const IDENTITY_LOOKS = 4;
|
|
22
26
|
export function projectIdentities(look = {}, now = Date.now) {
|
|
27
|
+
/** Each answer with when the look that answered it started (folder-look.ts `started`, kept across joined looks): an
|
|
28
|
+
* answer from a look started earlier never overwrites one from a look started later. */
|
|
23
29
|
const answers = new Map();
|
|
24
30
|
const wanted = new Set();
|
|
31
|
+
/** Paths whose last look didn't answer: looked at only on their own (joining a look still under way), never in a
|
|
32
|
+
* batch, so one folder that never answers holds one child, not one per differently made-up batch. */
|
|
33
|
+
const suspect = new Set();
|
|
34
|
+
let generation = 0;
|
|
25
35
|
const known = (path) => answers.get(resolve(path))?.real;
|
|
26
|
-
const
|
|
36
|
+
const put = (key, real, at, seq) => {
|
|
37
|
+
const before = answers.get(key);
|
|
38
|
+
// A known identity that changes (or goes) moves authority: anything decided on the old one is out of date.
|
|
39
|
+
if (before !== undefined && before.real !== real)
|
|
40
|
+
generation++;
|
|
41
|
+
answers.set(key, { real, at, seq });
|
|
42
|
+
};
|
|
43
|
+
const store = (found, seq) => {
|
|
27
44
|
const at = now();
|
|
28
45
|
for (const [path, real] of found) {
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
const key = resolve(path);
|
|
47
|
+
// Superseded: a later lookup already answered for this path. Its real path's entry goes with it.
|
|
48
|
+
if ((answers.get(key)?.seq ?? -1) > seq)
|
|
49
|
+
continue;
|
|
50
|
+
put(key, real, at, seq);
|
|
51
|
+
wanted.delete(key);
|
|
31
52
|
// A real path is its own identity.
|
|
32
|
-
if (real !== null)
|
|
33
|
-
|
|
53
|
+
if (real !== null && (answers.get(real)?.seq ?? -1) <= seq)
|
|
54
|
+
put(real, real, at, seq);
|
|
34
55
|
}
|
|
35
56
|
};
|
|
36
57
|
const resolveNow = async (path) => {
|
|
37
|
-
const
|
|
38
|
-
|
|
58
|
+
const key = resolve(path);
|
|
59
|
+
const looked = await lookIdentities([key], look);
|
|
60
|
+
if (!looked.ok) {
|
|
61
|
+
suspect.add(key);
|
|
39
62
|
return "no-answer";
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
}
|
|
64
|
+
suspect.delete(key);
|
|
65
|
+
// No start known: older than every answer, so it only fills what nothing answered yet.
|
|
66
|
+
store(looked.value, looked.started ?? 0);
|
|
67
|
+
return answers.get(key)?.real ?? null;
|
|
68
|
+
};
|
|
69
|
+
const refreshNow = async (paths) => {
|
|
70
|
+
const at = now();
|
|
71
|
+
const due = new Set(wanted);
|
|
72
|
+
for (const one of paths) {
|
|
73
|
+
const key = resolve(one);
|
|
74
|
+
const answer = answers.get(key);
|
|
75
|
+
if (answer === undefined || at - answer.at >= STALE_MS)
|
|
76
|
+
due.add(key);
|
|
77
|
+
}
|
|
78
|
+
for (const [key, answer] of answers)
|
|
79
|
+
if (at - answer.at >= STALE_MS)
|
|
80
|
+
due.add(key);
|
|
81
|
+
if (due.size === 0)
|
|
82
|
+
return true;
|
|
83
|
+
const alone = [...due].filter(one => suspect.has(one));
|
|
84
|
+
const batched = [...due].filter(one => !suspect.has(one));
|
|
85
|
+
if (batched.length > 0) {
|
|
86
|
+
const batch = await lookIdentities(batched, look);
|
|
87
|
+
if (batch.ok)
|
|
88
|
+
store(batch.value, batch.started ?? 0);
|
|
89
|
+
else
|
|
90
|
+
alone.push(...batched);
|
|
91
|
+
}
|
|
92
|
+
// One folder that doesn't answer mustn't hold the others: each on its own, a few at a time (inside the looks'
|
|
93
|
+
// child cap, leaving room for a person's actions), each under its deadline, each stored as soon as it answers.
|
|
94
|
+
let missed = false;
|
|
95
|
+
const worker = async () => {
|
|
96
|
+
for (let one = alone.shift(); one !== undefined; one = alone.shift())
|
|
97
|
+
if (await resolveNow(one) === "no-answer")
|
|
98
|
+
missed = true;
|
|
99
|
+
};
|
|
100
|
+
await Promise.all(Array.from({ length: Math.min(IDENTITY_LOOKS, alone.length) }, worker));
|
|
101
|
+
return !missed;
|
|
42
102
|
};
|
|
103
|
+
let passes = Promise.resolve();
|
|
43
104
|
return {
|
|
44
105
|
hot: path => {
|
|
45
106
|
const key = resolve(path);
|
|
@@ -68,28 +129,13 @@ export function projectIdentities(look = {}, now = Date.now) {
|
|
|
68
129
|
return undefined;
|
|
69
130
|
},
|
|
70
131
|
resolveNow,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
due.add(key);
|
|
79
|
-
}
|
|
80
|
-
for (const [key, answer] of answers)
|
|
81
|
-
if (at - answer.at >= STALE_MS)
|
|
82
|
-
due.add(key);
|
|
83
|
-
if (due.size === 0)
|
|
84
|
-
return true;
|
|
85
|
-
const batch = await lookIdentities([...due], look);
|
|
86
|
-
if (batch.ok) {
|
|
87
|
-
store(batch.value);
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
// One folder that doesn't answer mustn't hold the others: each on its own, in parallel (the looks are capped).
|
|
91
|
-
const each = await Promise.all([...due].map(one => resolveNow(one)));
|
|
92
|
-
return each.every(one => one !== "no-answer");
|
|
132
|
+
generation: () => generation,
|
|
133
|
+
refresh: paths => {
|
|
134
|
+
// One pass at a time: a pass asked for while another runs waits for it, then resolves only what is still due.
|
|
135
|
+
const asked = [...paths];
|
|
136
|
+
const pass = passes.then(() => refreshNow(asked));
|
|
137
|
+
passes = pass.then(() => undefined, () => undefined);
|
|
138
|
+
return pass;
|
|
93
139
|
},
|
|
94
140
|
};
|
|
95
141
|
}
|
|
@@ -101,6 +147,7 @@ export function syncProjectIdentities() {
|
|
|
101
147
|
strict: path => canonicalProject(path),
|
|
102
148
|
vouched: path => canonicalProject(path) ?? undefined,
|
|
103
149
|
resolveNow: async (path) => canonicalProject(path),
|
|
150
|
+
generation: () => 0,
|
|
104
151
|
refresh: async () => true,
|
|
105
152
|
};
|
|
106
153
|
}
|
|
@@ -22,18 +22,18 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
|
|
|
22
22
|
export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
|
|
23
23
|
/** Chat reads only an admitted project. Keep the full source in the project UI. */
|
|
24
24
|
export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
|
|
25
|
-
|
|
25
|
+
title?: never;
|
|
26
26
|
revision: number;
|
|
27
27
|
stale: boolean;
|
|
28
28
|
notice: string;
|
|
29
29
|
decision?: never;
|
|
30
|
-
title?: never;
|
|
31
30
|
content?: never;
|
|
31
|
+
history?: never;
|
|
32
32
|
instructions?: never;
|
|
33
33
|
references?: never;
|
|
34
34
|
decisions?: never;
|
|
35
35
|
} | {
|
|
36
|
-
|
|
36
|
+
title?: never;
|
|
37
37
|
stale?: never;
|
|
38
38
|
decision: {
|
|
39
39
|
repo: string;
|
|
@@ -50,27 +50,27 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
50
50
|
id: number;
|
|
51
51
|
};
|
|
52
52
|
notice: string;
|
|
53
|
-
title?: never;
|
|
54
53
|
content?: never;
|
|
55
54
|
revision?: never;
|
|
55
|
+
history?: never;
|
|
56
56
|
instructions?: never;
|
|
57
57
|
references?: never;
|
|
58
58
|
decisions?: never;
|
|
59
59
|
} | {
|
|
60
|
-
history?: never;
|
|
61
60
|
stale?: never;
|
|
62
61
|
decision?: never;
|
|
63
62
|
revision: number;
|
|
64
63
|
title: string;
|
|
65
64
|
content: string;
|
|
66
65
|
notice: string;
|
|
66
|
+
history?: never;
|
|
67
67
|
instructions?: never;
|
|
68
68
|
references?: never;
|
|
69
69
|
decisions?: never;
|
|
70
70
|
} | {
|
|
71
|
+
title?: never;
|
|
71
72
|
stale?: never;
|
|
72
73
|
decision?: never;
|
|
73
|
-
title?: never;
|
|
74
74
|
content?: never;
|
|
75
75
|
revision: number;
|
|
76
76
|
history: {
|
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); }
|
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);
|