toolroll 0.9.101 → 0.9.102
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 +8 -0
- package/dist/browser/terminal.js +9 -9
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +38 -38
- package/dist/browser-workspace.d.ts +47 -13
- package/dist/browser-workspace.js +5 -3
- package/dist/desktop-update.d.ts +1 -1
- package/dist/page-work-contract.d.ts +21 -0
- package/dist/page-work-contract.js +5 -0
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/run-efficiency.d.ts +3 -2
- package/dist/run-efficiency.js +4 -3
- package/dist/serve.js +51 -9
- package/dist/server/chrome.d.ts +10 -1
- package/dist/server/chrome.js +9 -0
- package/dist/server/console.css +19 -4
- package/dist/server/home.d.ts +27 -14
- package/dist/server/home.js +78 -63
- package/dist/server/http.d.ts +6 -0
- package/dist/server/http.js +8 -0
- package/dist/server/in-place.d.ts +15 -0
- package/dist/server/in-place.js +32 -0
- package/dist/server/pages.js +23 -2
- package/dist/server/render-tasks.js +14 -3
- package/dist/server/request-context.d.ts +14 -0
- package/dist/server/request-context.js +3 -0
- package/dist/server/task-views.js +6 -3
- package/dist/server/tasks.d.ts +8 -2
- package/dist/server/tasks.js +31 -17
- package/dist/work-index.d.ts +14 -1
- package/dist/work-index.js +24 -8
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ export { browserCrewFromIndex, browserWorkActionHref } from './browser-crew.js';
|
|
|
9
9
|
import type { Store } from './store.js';
|
|
10
10
|
import type { WorkSummaryAccess } from './work-summary.js';
|
|
11
11
|
import type { StatusTone, StepRow } from './workspace-ui.js';
|
|
12
|
+
import type { PageWork } from './page-work-contract.js';
|
|
12
13
|
import type { Ask, AskChip } from './needs-you.js';
|
|
13
14
|
import type { WorkIndexGroup } from './work-index.js';
|
|
14
15
|
import type { AssignmentCard } from './assignment-ui.js';
|
|
@@ -486,9 +487,12 @@ export type BrowserTaskView = {
|
|
|
486
487
|
}[];
|
|
487
488
|
} | null;
|
|
488
489
|
/** Server cards that may need a person now (stop/resume, scope prompt, plan, live attempt). */
|
|
490
|
+
/** `instance`: what the block is about now (the attempt card: its build). The page installs a block's HTML once;
|
|
491
|
+
* another instance replaces it. */
|
|
489
492
|
lead: {
|
|
490
493
|
key: string;
|
|
491
494
|
html: string;
|
|
495
|
+
instance?: string;
|
|
492
496
|
}[];
|
|
493
497
|
questions: string;
|
|
494
498
|
facts: BrowserTaskFact[];
|
|
@@ -549,6 +553,8 @@ export type BrowserTaskView = {
|
|
|
549
553
|
/** When a remembered password stops counting (ISO); the page words it in the viewer's zone. */
|
|
550
554
|
rememberedUntil: string | null;
|
|
551
555
|
planHtml: string;
|
|
556
|
+
/** The plan sheet alone (no editor, no form): what Home shows beside its Approve (live-pages-and-home.md B2.1). */
|
|
557
|
+
sheetHtml?: string;
|
|
552
558
|
/** The terms line in words, when the rule isn't one of the kit's (easy-start C5: a laptop's own owner). */
|
|
553
559
|
termsWords?: string;
|
|
554
560
|
} | null;
|
|
@@ -1613,6 +1619,15 @@ export type BrowserWorkspace = {
|
|
|
1613
1619
|
user: string;
|
|
1614
1620
|
csrf: string;
|
|
1615
1621
|
sensitive: boolean;
|
|
1622
|
+
/** True when this page may replace another without a document load (docs/plans/live-pages-and-home.md A1.1): the server
|
|
1623
|
+
* sets it on every workspace it sends; absent (an older server, a fixture) is false. */
|
|
1624
|
+
inPlace?: boolean;
|
|
1625
|
+
/** What the app runs for this page in place of a page script (docs/plans/task-page-in-app.md): data, a closed list.
|
|
1626
|
+
* Present only when the app covers the page's whole script; the document then ships no page script string. */
|
|
1627
|
+
work?: readonly PageWork[];
|
|
1628
|
+
/** This document ships a page script string (set on a document load only). Its listeners live as long as the
|
|
1629
|
+
* document, so the app never changes pages in place from it, and never runs `work` in it. */
|
|
1630
|
+
scripted?: boolean;
|
|
1616
1631
|
/** Passwordless is on for this signed-in browser: ceremony password fields are left out (docs/plans/passwordless.md). */
|
|
1617
1632
|
passwordless?: boolean;
|
|
1618
1633
|
/** The session's open project (its path), or null: the sidebar's project picker names it. */
|
|
@@ -1688,13 +1703,14 @@ export type BrowserWorkspace = {
|
|
|
1688
1703
|
command: string;
|
|
1689
1704
|
};
|
|
1690
1705
|
};
|
|
1691
|
-
/** Home ("/"):
|
|
1692
|
-
*
|
|
1706
|
+
/** Home ("/", docs/plans/live-pages-and-home.md B1): the heading said once, one quiet line for what runs and waits, what
|
|
1707
|
+
* needs this person grouped by what is asked (each group with its exact count), the selected one open beside the list,
|
|
1708
|
+
* and the lead's last message above the message box. With nothing waiting: what runs and what finished today. */
|
|
1693
1709
|
export type BrowserHomeView = {
|
|
1694
1710
|
kind: 'home';
|
|
1695
|
-
/** "
|
|
1711
|
+
/** "19 tasks need you", "Nothing needs you", read from the same total as the rail's count. */
|
|
1696
1712
|
say: string;
|
|
1697
|
-
/** The project in view (a path) or every project (null), and the
|
|
1713
|
+
/** The project in view (a path) or every project (null), and the project filter's choices (null with one project). */
|
|
1698
1714
|
project: string | null;
|
|
1699
1715
|
scopes: {
|
|
1700
1716
|
key: string;
|
|
@@ -1702,29 +1718,42 @@ export type BrowserHomeView = {
|
|
|
1702
1718
|
href: string;
|
|
1703
1719
|
active: boolean;
|
|
1704
1720
|
}[] | null;
|
|
1721
|
+
/** The work index totals, each with the Tasks list it opens. */
|
|
1705
1722
|
counts: {
|
|
1706
1723
|
key: 'needs' | 'running' | 'queued' | 'finished';
|
|
1707
1724
|
label: string;
|
|
1708
1725
|
value: number;
|
|
1709
1726
|
href: string;
|
|
1710
1727
|
}[];
|
|
1711
|
-
/**
|
|
1728
|
+
/** What needs you, by what is asked, in this order: approve, answer, wrong, review, other (anything none of the four
|
|
1729
|
+
* fits). `count` is the exact total for that ask, past the rows listed. */
|
|
1712
1730
|
groups: {
|
|
1713
|
-
key:
|
|
1731
|
+
key: BrowserHomeAsk;
|
|
1714
1732
|
label: string;
|
|
1733
|
+
count: number;
|
|
1715
1734
|
rows: BrowserHomeRow[];
|
|
1716
1735
|
}[];
|
|
1717
|
-
/**
|
|
1718
|
-
|
|
1736
|
+
/** What runs, then what waits its turn. */
|
|
1737
|
+
working: BrowserHomeRow[];
|
|
1738
|
+
/** What was completed today: the exact figure, the newest rows, and the list that has them all. */
|
|
1739
|
+
finishedToday: {
|
|
1719
1740
|
count: number;
|
|
1741
|
+
rows: BrowserHomeRow[];
|
|
1720
1742
|
href: string;
|
|
1721
|
-
}
|
|
1743
|
+
};
|
|
1722
1744
|
newTask: string;
|
|
1723
1745
|
/** The lead's message box, where chat is allowed for this person: a POST to /chat in this scope. */
|
|
1724
1746
|
ask: {
|
|
1725
1747
|
project: string | null;
|
|
1726
1748
|
placeholder: string;
|
|
1727
1749
|
} | null;
|
|
1750
|
+
/** The lead's last message in this person's main thread, as one plain line that opens the chat; null when there is
|
|
1751
|
+
* none or this person may not read that thread. */
|
|
1752
|
+
lastLead: {
|
|
1753
|
+
text: string;
|
|
1754
|
+
age: string;
|
|
1755
|
+
href: string;
|
|
1756
|
+
} | null;
|
|
1728
1757
|
empty: {
|
|
1729
1758
|
title: string;
|
|
1730
1759
|
text: string;
|
|
@@ -1732,16 +1761,20 @@ export type BrowserHomeView = {
|
|
|
1732
1761
|
/** While the first run is open (no first result yet): the welcome's address, linked as Finish setup. */
|
|
1733
1762
|
setup?: string | null;
|
|
1734
1763
|
};
|
|
1764
|
+
/** What a waiting row asks (work-index.ts WorkAskGroup). */
|
|
1765
|
+
export type BrowserHomeAsk = 'approve' | 'answer' | 'wrong' | 'review' | 'other';
|
|
1735
1766
|
export type BrowserHomeRow = {
|
|
1736
1767
|
id: string;
|
|
1737
1768
|
title: string;
|
|
1738
1769
|
href: string;
|
|
1739
1770
|
project: string | null;
|
|
1740
1771
|
age: string;
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1772
|
+
/** Which list it is in: it needs you, it runs, it waits its turn, or it finished. */
|
|
1773
|
+
state: 'needs' | 'running' | 'queued' | 'done';
|
|
1774
|
+
/** What it asks of you (its group), or null when it doesn't wait on you. */
|
|
1775
|
+
ask: BrowserHomeAsk | null;
|
|
1776
|
+
/** Why, in one short line: only a row where something went wrong. */
|
|
1777
|
+
reason: string | null;
|
|
1745
1778
|
/** One line: what it asks, or what the agent is doing now. */
|
|
1746
1779
|
line: string | null;
|
|
1747
1780
|
/** The one act that resolves it (a link), or null. */
|
|
@@ -1933,3 +1966,4 @@ export type MergeReadyView = {
|
|
|
1933
1966
|
act: MergeReadyAct | null;
|
|
1934
1967
|
}[];
|
|
1935
1968
|
};
|
|
1969
|
+
export type { PageWork } from './page-work-contract.js';
|
|
@@ -97,11 +97,13 @@ export function browserProjectsOf(projects) {
|
|
|
97
97
|
export function browserNavigationOf(path, project = null, needsYou = 0, tasksProject = project, needsYouLabel) {
|
|
98
98
|
const pathname = path.split('?')[0].split('#')[0];
|
|
99
99
|
const under = (...roots) => roots.some(root => pathname === root || pathname.startsWith(`${root}/`));
|
|
100
|
-
// The rail (UI revamp, wave B): Home carries the one Needs-you count
|
|
101
|
-
// under
|
|
100
|
+
// The rail (UI revamp, wave B; Chat under Home, live-pages-and-home.md B1): Home carries the one Needs-you count and
|
|
101
|
+
// an agent's question lives under it; the lead chat is its own item; a task's pages under Tasks, every computer and
|
|
102
|
+
// terminal under Agents, every setting under Settings.
|
|
102
103
|
return [
|
|
103
|
-
{ label: 'Home', href: '/', active: pathname === '/' ||
|
|
104
|
+
{ label: 'Home', href: '/', active: pathname === '/' || under('/d', '/activity', '/morning'),
|
|
104
105
|
...(needsYou > 0 ? { count: needsYou, ...(needsYouLabel === undefined ? {} : { countLabel: needsYouLabel }) } : {}) },
|
|
106
|
+
{ label: 'Chat', href: '/chat', active: under('/chat') },
|
|
105
107
|
{ label: 'Tasks', href: `/tasks${tasksProject === null ? '' : `?project=${encodeURIComponent(tasksProject)}`}`,
|
|
106
108
|
active: under('/tasks', '/t', '/r', '/review') },
|
|
107
109
|
{ label: 'Flows', href: '/flows', active: under('/flows', '/recipes', '/kits', '/routines') },
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -97,13 +97,13 @@ export declare function sqliteLock(file: string, busyTimeoutMs?: number): Databa
|
|
|
97
97
|
export declare function lockHeld(file: string): boolean;
|
|
98
98
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
99
99
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
100
|
+
error?: never;
|
|
100
101
|
active: boolean;
|
|
101
102
|
phase: string;
|
|
102
103
|
detail: string;
|
|
103
104
|
running: boolean;
|
|
104
105
|
canResume: boolean;
|
|
105
106
|
canCancel: boolean;
|
|
106
|
-
error?: never;
|
|
107
107
|
automaticRecovery?: never;
|
|
108
108
|
recoveryAttempts?: never;
|
|
109
109
|
recoveryRepairs?: never;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One behaviour the app runs for the page on screen instead of a page script string (docs/plans/task-page-in-app.md).
|
|
3
|
+
* `answer`: a question's inline answer (render-tasks.ts decisionAnswerScript). `region`: a server-drawn region kept
|
|
4
|
+
* current from a build's own fragment (chrome.ts regionScript), `/r/<run>?fragment=check|peek`. `result`: reviewing the
|
|
5
|
+
* result the page shows (result-review.ts RESULT_REVIEW_SCRIPT with render-results.ts reviewEvidenceScript). No code,
|
|
6
|
+
* selector or address travels here: the app runs only the kinds it knows.
|
|
7
|
+
*/
|
|
8
|
+
export type PageWork = {
|
|
9
|
+
kind: "answer";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "result";
|
|
12
|
+
} | {
|
|
13
|
+
kind: "region";
|
|
14
|
+
region: "check-progress" | "run-peek";
|
|
15
|
+
run: number;
|
|
16
|
+
};
|
|
17
|
+
/** The work the app and the server agree on. A workspace read says it with `x-toolroll-work`; a reader that does not
|
|
18
|
+
* say exactly this version (an older tab) gets the page as a document with its script string, as before. A hint, never
|
|
19
|
+
* authority. It moves whenever a kind is added: 2 added `result`. */
|
|
20
|
+
export declare const PAGE_WORK_VERSION = 2;
|
|
21
|
+
export declare const PAGE_WORK_HEADER = "x-toolroll-work";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** The work the app and the server agree on. A workspace read says it with `x-toolroll-work`; a reader that does not
|
|
2
|
+
* say exactly this version (an older tab) gets the page as a document with its script string, as before. A hint, never
|
|
3
|
+
* authority. It moves whenever a kind is added: 2 added `result`. */
|
|
4
|
+
export const PAGE_WORK_VERSION = 2;
|
|
5
|
+
export const PAGE_WORK_HEADER = "x-toolroll-work";
|
|
@@ -22,18 +22,19 @@ 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
|
+
title?: never;
|
|
26
|
+
content?: never;
|
|
25
27
|
history?: never;
|
|
26
28
|
revision: number;
|
|
27
29
|
stale: boolean;
|
|
28
30
|
notice: string;
|
|
29
|
-
title?: never;
|
|
30
|
-
content?: never;
|
|
31
31
|
instructions?: never;
|
|
32
32
|
references?: never;
|
|
33
33
|
decisions?: never;
|
|
34
34
|
waiting?: never;
|
|
35
35
|
} | {
|
|
36
|
-
|
|
36
|
+
title?: never;
|
|
37
|
+
content?: never;
|
|
37
38
|
history?: never;
|
|
38
39
|
stale?: never;
|
|
39
40
|
decision: {
|
|
@@ -52,8 +53,7 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
52
53
|
};
|
|
53
54
|
afterwards?: string;
|
|
54
55
|
notice: string;
|
|
55
|
-
|
|
56
|
-
content?: never;
|
|
56
|
+
revision?: never;
|
|
57
57
|
instructions?: never;
|
|
58
58
|
references?: never;
|
|
59
59
|
decisions?: never;
|
|
@@ -70,9 +70,9 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
70
70
|
decisions?: never;
|
|
71
71
|
waiting?: never;
|
|
72
72
|
} | {
|
|
73
|
-
stale?: never;
|
|
74
73
|
title?: never;
|
|
75
74
|
content?: never;
|
|
75
|
+
stale?: never;
|
|
76
76
|
revision: number;
|
|
77
77
|
history: {
|
|
78
78
|
revision: number;
|
package/dist/run-efficiency.d.ts
CHANGED
|
@@ -68,6 +68,7 @@ export declare function deriveRunEfficiency(run: Run & {
|
|
|
68
68
|
}, evidenceRoot: string, now: Date, commands?: boolean): RunEfficiency;
|
|
69
69
|
/**
|
|
70
70
|
* A finished run's How it ran: the saved one when it was derived by this version, else derived now and saved (once:
|
|
71
|
-
* a row whose steps were complete or have expired is final). An unfinished run is derived but never saved
|
|
71
|
+
* a row whose steps were complete or have expired is final). An unfinished run is derived but never saved, and
|
|
72
|
+
* nothing is saved when `save` is false (a page read ahead of its link writes nothing).
|
|
72
73
|
*/
|
|
73
|
-
export declare function runEfficiencyOf(store: Store, evidenceRoot: string, runId: number, now: Date): RunEfficiency | null;
|
|
74
|
+
export declare function runEfficiencyOf(store: Store, evidenceRoot: string, runId: number, now: Date, save?: boolean): RunEfficiency | null;
|
package/dist/run-efficiency.js
CHANGED
|
@@ -199,9 +199,10 @@ export function deriveRunEfficiency(run, evidenceRoot, now, commands = false) {
|
|
|
199
199
|
}
|
|
200
200
|
/**
|
|
201
201
|
* A finished run's How it ran: the saved one when it was derived by this version, else derived now and saved (once:
|
|
202
|
-
* a row whose steps were complete or have expired is final). An unfinished run is derived but never saved
|
|
202
|
+
* a row whose steps were complete or have expired is final). An unfinished run is derived but never saved, and
|
|
203
|
+
* nothing is saved when `save` is false (a page read ahead of its link writes nothing).
|
|
203
204
|
*/
|
|
204
|
-
export function runEfficiencyOf(store, evidenceRoot, runId, now) {
|
|
205
|
+
export function runEfficiencyOf(store, evidenceRoot, runId, now, save = true) {
|
|
205
206
|
const run = store.getRun(runId);
|
|
206
207
|
if (run === null)
|
|
207
208
|
return null;
|
|
@@ -227,7 +228,7 @@ export function runEfficiencyOf(store, evidenceRoot, runId, now) {
|
|
|
227
228
|
const payload = JSON.stringify(derived);
|
|
228
229
|
// A row too large for its column (it can't be: the counts are bounded) is shown, never saved.
|
|
229
230
|
// Saving is a convenience: a store that can't take the row (read-only, busy) still shows what was derived.
|
|
230
|
-
if (payload.length <= 16_384) {
|
|
231
|
+
if (save && payload.length <= 16_384) {
|
|
231
232
|
try {
|
|
232
233
|
store.handle.prepare(`INSERT INTO run_efficiency (run, parser_version, steps, steps_reason, payload_json, derived_at) VALUES (?, ?, ?, ?, ?, ?)
|
|
233
234
|
ON CONFLICT(run) DO UPDATE SET parser_version = excluded.parser_version, steps = excluded.steps, steps_reason = excluded.steps_reason,
|
package/dist/serve.js
CHANGED
|
@@ -27,6 +27,7 @@ const PROXY_HEADER = /^(forwarded|via|x-forwarded-.+|x-real-ip|x-client-ip|x-ori
|
|
|
27
27
|
const BACKGROUND_CHECKS_MS = 30_000;
|
|
28
28
|
const LOCAL_AGENTS_EVERY_MS = 2 * 60_000;
|
|
29
29
|
import { browserCrewFromIndex, browserCrewOf, browserNavigationOf, browserProjectsOf, needsYouLabelOf } from './browser-workspace.js';
|
|
30
|
+
import { PAGE_WORK_HEADER, PAGE_WORK_VERSION } from './page-work-contract.js';
|
|
30
31
|
import { sharedActionPayload } from './chat-actions.js';
|
|
31
32
|
import { prepareCodingContext } from './coding-context.js';
|
|
32
33
|
import { CodingWorkspace } from './coding-workspace.js';
|
|
@@ -155,7 +156,8 @@ import { createFlowsHandlers } from './server/flows.js';
|
|
|
155
156
|
import { createPagesHandlers } from './server/pages.js';
|
|
156
157
|
import { createPeopleHandlers } from './server/people-tokens.js';
|
|
157
158
|
import { createSettingsHandlers } from './server/settings.js';
|
|
158
|
-
import { requestContext } from "./server/request-context.js";
|
|
159
|
+
import { readPurpose, requestContext } from "./server/request-context.js";
|
|
160
|
+
import { inPlaceRoute, prefetchRoute } from "./server/in-place.js";
|
|
159
161
|
import { asLocalOwner, localModeOn, localOwnerRefusal, localOwnerStandsNow } from "./local-owner.js";
|
|
160
162
|
import { defaultAgentOf, fillDefaultRouting } from "./model-choices.js";
|
|
161
163
|
import { chromeScript, DEMO_BANNER, DEMO_BANNER_SHORT, focusDocument, KBD_HELP, pinnedTheme, QUEUE_VIEW, refuse, screen, SENSITIVE_INPUT, shell, sidebarScript } from "./server/chrome.js";
|
|
@@ -878,9 +880,22 @@ export function createDecisionServer(options) {
|
|
|
878
880
|
// Passive polls authenticate without extending the browser session.
|
|
879
881
|
const fragmentName = url.searchParams.get("fragment");
|
|
880
882
|
const workspaceRead = method === "GET" && url.searchParams.get('format') === 'workspace';
|
|
881
|
-
|
|
883
|
+
// A workspace read's purpose (live-pages-and-home.md A1.4): bookkeeping only. Read on workspace GETs alone; on any other
|
|
884
|
+
// request the header means nothing. No authorisation decision below reads it.
|
|
885
|
+
const purpose = workspaceRead ? readPurpose(request.headers['x-toolroll-read']) : 'poll';
|
|
886
|
+
// The reads that never extend the session, whatever their header says.
|
|
887
|
+
const noTouch = edgeRoute?.domain === 'live' || (fragmentName !== null && NO_TOUCH_FRAGMENTS.has(fragmentName)) || /^\/code\/[a-f0-9]{32}\/state$/.test(url.pathname) || url.pathname === '/term/state'
|
|
882
888
|
// The Agents view's Live grid only watches: its polls never extend or write the session.
|
|
883
|
-
|| url.pathname === '/agents/live' || /^\/agents\/live\/[0-9]{1,15}$/.test(url.pathname) || (url.pathname === '/agents' && url.searchParams.get('view') === 'live')
|
|
889
|
+
|| url.pathname === '/agents/live' || /^\/agents\/live\/[0-9]{1,15}$/.test(url.pathname) || (url.pathname === '/agents' && url.searchParams.get('view') === 'live');
|
|
890
|
+
// A followed link (`navigate`) extends the session exactly as the HTML GET of the same address would; every other
|
|
891
|
+
// workspace read (a re-read, a prefetch) does not.
|
|
892
|
+
const touch = !(method === "GET" && (noTouch || (workspaceRead && purpose !== 'navigate')));
|
|
893
|
+
// A read ahead is answered only for a browser session, only for the prefetch allowlist (in-place.ts): anything else
|
|
894
|
+
// hears 204 before any sign-in bookkeeping (a token's last use, a sign-in charge) is written.
|
|
895
|
+
if (purpose === 'prefetch' && (request.headers.authorization !== undefined || !prefetchRoute(matchedRoute?.id, url))) {
|
|
896
|
+
request.resume();
|
|
897
|
+
return respond(response, 204, "text/plain; charset=utf-8", "");
|
|
898
|
+
}
|
|
884
899
|
const refuseBudget = (admitted) => {
|
|
885
900
|
if (admitted.ok)
|
|
886
901
|
return false;
|
|
@@ -906,6 +921,11 @@ export function createDecisionServer(options) {
|
|
|
906
921
|
}
|
|
907
922
|
if (who?.via === "bearer" && refuseBudget(admitBearer(request, who)))
|
|
908
923
|
return;
|
|
924
|
+
// A read ahead from anything but a signed-in browser session (signed out, a token): 204, never the sign-in page.
|
|
925
|
+
if (purpose === 'prefetch' && who?.via !== 'cookie') {
|
|
926
|
+
request.resume();
|
|
927
|
+
return respond(response, 204, "text/plain; charset=utf-8", "");
|
|
928
|
+
}
|
|
909
929
|
const passwordProof = who?.via === 'cookie' ? { name: who.name, generation: who.session.generation } : null;
|
|
910
930
|
const fresh = freshFactsOf(request, who, matchedRoute?.freshness === 'allowlist');
|
|
911
931
|
if (edgeRoute?.domain === 'people')
|
|
@@ -940,6 +960,9 @@ export function createDecisionServer(options) {
|
|
|
940
960
|
get fresh() { return freshFacts.getStore() != null && ssoNowAvailable(who.name); } } : undefined,
|
|
941
961
|
refusal: (answer, status, body) => sendScreen(answer, status, screen(status === 404 ? "Not found" : "Request refused", body, { chrome: chromeFor(who.via === "cookie" ? who.session.project : null, "work") })),
|
|
942
962
|
workspaceRead,
|
|
963
|
+
readPurpose: purpose,
|
|
964
|
+
...(workspaceRead && request.headers[PAGE_WORK_HEADER] === String(PAGE_WORK_VERSION) ? { appWork: true } : {}),
|
|
965
|
+
...(matchedRoute?.stage === 'console' ? { routeId: matchedRoute.id } : {}),
|
|
943
966
|
freshWork: ["request", "task", "result"].some(name => url.searchParams.has(name)),
|
|
944
967
|
...(workspaceRead && typeof request.headers['if-none-match'] === 'string' ? { workspaceIfNoneMatch: request.headers['if-none-match'] } : {}),
|
|
945
968
|
workspaceRequest: url.searchParams.get('request'),
|
|
@@ -952,7 +975,9 @@ export function createDecisionServer(options) {
|
|
|
952
975
|
? { localStepUp: localOwnerRefusal(false, method === "GET" ? passwordSetNext(pagePathOf(url)) : "/").href } : {}),
|
|
953
976
|
reconnect: browserKeys.stale(who, method === "GET" ? safeReturn(pagePathOf(url)) : "/"),
|
|
954
977
|
};
|
|
955
|
-
|
|
978
|
+
// A read ahead writes nothing, not even a slow request's record.
|
|
979
|
+
if (purpose !== 'prefetch')
|
|
980
|
+
response.once("finish", () => keepSlowRequest(store, timeline, matchedRoute?.id ?? "unmatched", response.statusCode, clock()));
|
|
956
981
|
if (method === "GET" || method === "POST")
|
|
957
982
|
return freshFacts.run(fresh, () => provenPasswordAccount.run(passwordProof, () => requestContext.run(requestFacts, async () => {
|
|
958
983
|
if (edgeRoute?.domain === 'live')
|
|
@@ -1669,8 +1694,29 @@ export function createDecisionServer(options) {
|
|
|
1669
1694
|
}
|
|
1670
1695
|
const conversation = extras.conversation ?? null;
|
|
1671
1696
|
const request = requestFacts.workspaceRequest;
|
|
1697
|
+
// React owns the conversation and its refresh/draft lifecycle. Native
|
|
1698
|
+
// guarded forms keep their current behavior after React inserts them.
|
|
1699
|
+
// A page with its own view keeps its script; the chat page runs the result script.
|
|
1700
|
+
// The exact page script the document would run: the JSON arm and the document arm read this one value.
|
|
1701
|
+
const ownScript = conversation === null || extras.view;
|
|
1702
|
+
const pageScript = ownScript ? (s.functional?.script ?? '') : RESULT_REVIEW_SCRIPT;
|
|
1703
|
+
const routeInPlace = inPlaceRoute(requestFacts.routeId, path);
|
|
1704
|
+
// The app runs the page's behaviour itself (docs/plans/task-page-in-app.md §2) only when all of these hold: the
|
|
1705
|
+
// app covers the WHOLE script, no password field (a password page keeps its string path), an in-place route, and a
|
|
1706
|
+
// reader that can run it: a document load (its bundle is this server's, served no-store) or a workspace read that
|
|
1707
|
+
// says so. Anything else is the page exactly as before: the string, no work, a document.
|
|
1708
|
+
const covered = ownScript ? s.functional?.app : undefined;
|
|
1709
|
+
const work = pageScript !== '' && covered !== undefined && covered.length > 0 && !sensitive && routeInPlace
|
|
1710
|
+
&& (!requestFacts.workspaceRead || requestFacts.appWork === true) ? covered : null;
|
|
1711
|
+
const functional = work === null ? pageScript : '';
|
|
1712
|
+
// May replace another page without a document load (live-pages-and-home.md A1.1): an allowlisted route, no password
|
|
1713
|
+
// field, no page script (a page script runs once per document, so its page always loads as one).
|
|
1714
|
+
const inPlace = routeInPlace && !sensitive && functional === '';
|
|
1672
1715
|
const workspace = {
|
|
1673
|
-
version: 1, path: currentPath, title: s.title, user: requestFacts.actor ?? '', csrf: requestFacts.csrf, sensitive,
|
|
1716
|
+
version: 1, path: currentPath, title: s.title, user: requestFacts.actor ?? '', csrf: requestFacts.csrf, sensitive, inPlace,
|
|
1717
|
+
...(work === null ? {} : { work }),
|
|
1718
|
+
// Said on the document that ships it: this document's page script listens for as long as the document lives.
|
|
1719
|
+
...(functional !== '' && !requestFacts.workspaceRead ? { scripted: true } : {}),
|
|
1674
1720
|
// The project picker names the open project; the settings frame carries its breadcrumb.
|
|
1675
1721
|
// Only a project this person may see now: never the raw saved path (settings-kit.ts openProjectFor).
|
|
1676
1722
|
openProject: openProjectFor(requestFacts.openProject ?? null, s.chrome.project, s.chrome.projects ?? []),
|
|
@@ -1761,10 +1807,6 @@ export function createDecisionServer(options) {
|
|
|
1761
1807
|
return respond(response, status, 'application/json; charset=utf-8', body);
|
|
1762
1808
|
}
|
|
1763
1809
|
const nonce = randomBytes(16).toString('base64');
|
|
1764
|
-
// React owns the conversation and its refresh/draft lifecycle. Native
|
|
1765
|
-
// guarded forms keep their current behavior after React inserts them.
|
|
1766
|
-
// A page with its own view keeps its script; the chat page runs the result script.
|
|
1767
|
-
const functional = conversation === null || extras.view ? (s.functional?.script ?? '') : RESULT_REVIEW_SCRIPT;
|
|
1768
1810
|
const document = shell(s.title, s.body, { chrome: s.chrome, sensitive });
|
|
1769
1811
|
// The keys and their list (keymap.ts) work here as on every chromed page; the list sits beside the app React owns.
|
|
1770
1812
|
// A sensitive page keeps to its own script.
|
package/dist/server/chrome.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BrowserSignIn, type BrowserUpdateNotice, type BrowserWorkspace } from "../browser-workspace.js";
|
|
1
|
+
import { type BrowserSignIn, type BrowserUpdateNotice, type BrowserWorkspace, type PageWork } from "../browser-workspace.js";
|
|
2
2
|
import { type Crumb } from "../settings-kit.js";
|
|
3
3
|
import { type Decision, type Run } from "../store.js";
|
|
4
4
|
import { type tally } from "../summary.js";
|
|
@@ -176,6 +176,9 @@ export type Screen = {
|
|
|
176
176
|
functional?: {
|
|
177
177
|
script: string;
|
|
178
178
|
fetches?: boolean;
|
|
179
|
+
/** The app's own version of this WHOLE script (task-page-in-app.md §1): where the app can run it, the document
|
|
180
|
+
* ships no script string and the page may change in place. Added to only through withScript. */
|
|
181
|
+
app?: readonly PageWork[];
|
|
179
182
|
};
|
|
180
183
|
refreshSeconds?: number;
|
|
181
184
|
/** Render sensitive even when no password field is visible — one-time
|
|
@@ -317,3 +320,9 @@ export declare function oneLineOf(text: string, cap: number): string;
|
|
|
317
320
|
* minutes are the answer without script.
|
|
318
321
|
*/
|
|
319
322
|
export declare function boardTickScript(regionId?: string): string;
|
|
323
|
+
/**
|
|
324
|
+
* A page's script with one more added (docs/plans/task-page-in-app.md §1). All or nothing: `app` survives only when
|
|
325
|
+
* every part has it, so a script the app does not cover keeps the whole page a document, and a page never gets half
|
|
326
|
+
* its behaviour from the string and half from the app.
|
|
327
|
+
*/
|
|
328
|
+
export declare function withScript(base: Screen["functional"], add: NonNullable<Screen["functional"]>): NonNullable<Screen["functional"]>;
|
package/dist/server/chrome.js
CHANGED
|
@@ -807,3 +807,12 @@ export function boardTickScript(regionId = "board-region") {
|
|
|
807
807
|
`run();setInterval(function(){if(!document.hidden)run();},1000);document.addEventListener("visibilitychange",function(){if(!document.hidden)run();});` +
|
|
808
808
|
`if(region&&typeof MutationObserver==="function")new MutationObserver(run).observe(region,{childList:true});})();`);
|
|
809
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* A page's script with one more added (docs/plans/task-page-in-app.md §1). All or nothing: `app` survives only when
|
|
812
|
+
* every part has it, so a script the app does not cover keeps the whole page a document, and a page never gets half
|
|
813
|
+
* its behaviour from the string and half from the app.
|
|
814
|
+
*/
|
|
815
|
+
export function withScript(base, add) {
|
|
816
|
+
const app = base === undefined ? add.app : base.app !== undefined && add.app !== undefined ? [...base.app, ...add.app] : undefined;
|
|
817
|
+
return { script: (base?.script ?? "") + add.script, fetches: (base?.fetches ?? false) || (add.fetches ?? false), ...(app === undefined ? {} : { app }) };
|
|
818
|
+
}
|
package/dist/server/console.css
CHANGED
|
@@ -2662,14 +2662,29 @@ button { min-height: 44px; }
|
|
|
2662
2662
|
the same on both, swaps in place instead of fading (each shell names its
|
|
2663
2663
|
own, so a page can never carry the name twice). */
|
|
2664
2664
|
::view-transition-old(root) { animation: so-page-leave 80ms ease-in both; }
|
|
2665
|
-
::view-transition-new(root) { animation: so-page-arrive
|
|
2665
|
+
::view-transition-new(root) { animation: so-page-arrive 160ms ease-out 60ms both; }
|
|
2666
2666
|
@keyframes so-page-leave { to { opacity: 0; } }
|
|
2667
2667
|
@keyframes so-page-arrive { from { opacity: 0; } }
|
|
2668
2668
|
.side { view-transition-name: so-nav-page; }
|
|
2669
2669
|
.so-sidebar { view-transition-name: so-nav-app; }
|
|
2670
|
-
|
|
2670
|
+
.so-ph { view-transition-name: so-phone-header; }
|
|
2671
|
+
::view-transition-group(so-nav-page), ::view-transition-group(so-nav-app), ::view-transition-group(so-phone-header), ::view-transition-group(so-message-box),
|
|
2671
2672
|
::view-transition-old(so-nav-page), ::view-transition-new(so-nav-page),
|
|
2672
|
-
::view-transition-old(so-nav-app), ::view-transition-new(so-nav-app)
|
|
2673
|
+
::view-transition-old(so-nav-app), ::view-transition-new(so-nav-app),
|
|
2674
|
+
::view-transition-old(so-phone-header), ::view-transition-new(so-phone-header),
|
|
2675
|
+
::view-transition-old(so-message-box), ::view-transition-new(so-message-box) { animation: none; }
|
|
2676
|
+
/* A page changed in place (docs/plans/live-pages-and-home.md A2.2, browser/transition.ts): only the page content
|
|
2677
|
+
moves; everything else (the root, the sidebar, the phone header, the message box) swaps where it stands. The same
|
|
2678
|
+
level fades through like a document load (80 ms out, 160 ms in); deeper, the new page also moves 8px in from the
|
|
2679
|
+
right over 200 ms; shallower, from the left. Nothing runs past 220 ms. */
|
|
2680
|
+
html.so-nav #workspace-main { view-transition-name: so-page; }
|
|
2681
|
+
html.so-nav::view-transition-old(root), html.so-nav::view-transition-new(root), html.so-nav::view-transition-group(so-page) { animation: none; }
|
|
2682
|
+
html.so-nav::view-transition-old(so-page) { animation: so-page-leave 80ms ease-in both; }
|
|
2683
|
+
html.so-nav-same::view-transition-new(so-page) { animation: so-page-arrive 160ms ease-out 60ms both; }
|
|
2684
|
+
html.so-nav-deeper::view-transition-new(so-page) { animation: so-page-deeper 200ms cubic-bezier(.2, .8, .2, 1) both; }
|
|
2685
|
+
html.so-nav-shallower::view-transition-new(so-page) { animation: so-page-shallower 200ms cubic-bezier(.2, .8, .2, 1) both; }
|
|
2686
|
+
@keyframes so-page-deeper { from { opacity: 0; transform: translateX(8px); } }
|
|
2687
|
+
@keyframes so-page-shallower { from { opacity: 0; transform: translateX(-8px); } }
|
|
2673
2688
|
/* UI polish 2026-09-13 — the motion contract. Feedback transitions run
|
|
2674
2689
|
140–200 ms on color, border, shadow, opacity, and transform only; an
|
|
2675
2690
|
overlay's entrance is at most 220 ms of opacity + transform. Nothing
|
|
@@ -2699,7 +2714,7 @@ button { min-height: 44px; }
|
|
|
2699
2714
|
@keyframes fade { from { opacity: 0; } }
|
|
2700
2715
|
@media (prefers-reduced-motion: reduce) {
|
|
2701
2716
|
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
2702
|
-
::view-transition-group(*), ::view-transition-old(
|
|
2717
|
+
::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
|
|
2703
2718
|
button:active, :is(.button-link, .side .new-task, .content .new-task, .result-feedback-link, .next-pager .skip):active { transform: none; }
|
|
2704
2719
|
@media (hover: hover) and (pointer: fine) { button:hover, .side nav a:hover, .chat-project-card:hover, .lane-card:hover, .decide-card:hover, .menu-row:hover { transform: none; } }
|
|
2705
2720
|
}
|
package/dist/server/home.d.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Home ("/",
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Home ("/", docs/plans/live-pages-and-home.md B1): what needs this person, grouped by what is asked (Approve a plan,
|
|
3
|
+
* Answer a question, Something went wrong, Review results, and Needs you for the rest), each group with its exact
|
|
4
|
+
* count; one quiet line for what runs and waits; with nothing waiting, what runs and what finished today. The figures
|
|
5
|
+
* are read from the same work index the Tasks list filters, so a count and the list it opens always agree. Reads
|
|
6
|
+
* only: every act keeps its own route and rules (an agent's question is answered through POST /d/<id>/answer, the
|
|
7
|
+
* same one-click act as its own page).
|
|
6
8
|
*/
|
|
7
|
-
import type { BrowserHomeView } from "../browser-workspace.js";
|
|
9
|
+
import type { BrowserHomeAsk, BrowserHomeView } from "../browser-workspace.js";
|
|
8
10
|
import type { Store } from "../store.js";
|
|
9
11
|
import type { WorkSummaryAccess } from "../work-summary.js";
|
|
10
|
-
/** How many waiting rows Home lists;
|
|
12
|
+
/** How many waiting rows Home lists; each group says how many more open in Tasks. */
|
|
11
13
|
export declare const HOME_NEEDS_SHOWN = 30;
|
|
14
|
+
/** How many rows finished today Home lists; its figure counts them all. */
|
|
15
|
+
export declare const HOME_DONE_SHOWN = 8;
|
|
16
|
+
/** Each group's heading, in the order Home shows them. */
|
|
17
|
+
export declare const HOME_ASK_LABEL: Readonly<Record<BrowserHomeAsk, string>>;
|
|
12
18
|
export type HomeFacts = {
|
|
13
19
|
store: Store;
|
|
14
20
|
now: Date;
|
|
@@ -16,7 +22,7 @@ export type HomeFacts = {
|
|
|
16
22
|
evidenceRoot?: string;
|
|
17
23
|
/** The project in view (already admitted), or null for every project. */
|
|
18
24
|
project: string | null;
|
|
19
|
-
/** Every admitted project, for the
|
|
25
|
+
/** Every admitted project, for the project filter. */
|
|
20
26
|
projects: readonly {
|
|
21
27
|
name: string;
|
|
22
28
|
path: string;
|
|
@@ -31,12 +37,19 @@ export type HomeFacts = {
|
|
|
31
37
|
why: string;
|
|
32
38
|
road: "scope" | "agents";
|
|
33
39
|
} | null;
|
|
40
|
+
/** The lead's last message in this person's main thread, already read through the chat's own access check
|
|
41
|
+
* (pages.ts leadLastMessage); null when there is none or this person may not read that thread. */
|
|
42
|
+
leadMessage?: {
|
|
43
|
+
text: string;
|
|
44
|
+
at: string;
|
|
45
|
+
} | null;
|
|
34
46
|
};
|
|
35
|
-
/** "
|
|
36
|
-
export declare function homeSentence(needs: number,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
/** "19 tasks need you": the heading, said once, from the same total as the rail's count. */
|
|
48
|
+
export declare function homeSentence(needs: number, any: boolean): string;
|
|
49
|
+
/** The lead's words as one plain line: no markdown marks, no line breaks, cut to about one line. */
|
|
50
|
+
export declare function leadLineOf(text: string): string;
|
|
51
|
+
/** Midnight today on this computer's clock: "finished today" starts here. */
|
|
52
|
+
export declare function startOfToday(now: Date): Date;
|
|
53
|
+
/** Home's view for one person and scope, from four reads of the work index: what waits on you (with exact per-ask
|
|
54
|
+
* totals), what runs, what waits its turn, and what was completed today (filtered before its limit). */
|
|
42
55
|
export declare function homeView(facts: HomeFacts): BrowserHomeView;
|