toolroll 0.9.46 → 0.9.47
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 +15 -0
- package/dist/activity-line.d.ts +26 -0
- package/dist/activity-line.js +47 -0
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-workspace.d.ts +18 -1
- package/dist/browser-workspace.js +9 -3
- package/dist/builder.js +4 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/flow-card-task.d.ts +2 -0
- package/dist/flow-card-task.js +57 -0
- package/dist/flow-live.d.ts +2 -2
- package/dist/flow-live.js +69 -8
- package/dist/flows-ui.js +15 -1
- package/dist/live.d.ts +14 -0
- package/dist/live.js +71 -22
- package/dist/operate.js +6 -1
- package/dist/planner.js +4 -3
- package/dist/serve.d.ts +5 -0
- package/dist/serve.js +91 -39
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +16 -0
- package/dist/store.js +34 -0
- package/dist/task-activity.d.ts +27 -0
- package/dist/task-activity.js +53 -0
- package/dist/task-live.d.ts +17 -0
- package/dist/task-live.js +144 -0
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -8,7 +8,7 @@ import { projectBatchChecks, setProjectBatchChecks } from './batch-policy.js';
|
|
|
8
8
|
import { CHECK_LEVEL_WORDS, isCheckLevel, liveQuickCommand, projectCheckLevel, quickVerifyKey, setProjectCheckLevel, suggestQuickCommand } from './check-levels.js';
|
|
9
9
|
import { repositoryContextHtml } from './repository-context-ui.js';
|
|
10
10
|
import { browserAssetsAvailable, browserWorkspaceDocument, serveBrowserAsset } from './browser-shell.js';
|
|
11
|
-
import { browserCrewOf, browserCrewFromIndex, browserWorkActionHref, browserProjectsOf, browserNavigationOf } from './browser-workspace.js';
|
|
11
|
+
import { browserCrewOf, browserCrewFromIndex, browserWorkActionHref, browserProjectsOf, browserNavigationOf, needsYouLabelOf } from './browser-workspace.js';
|
|
12
12
|
import { configureLeadFollow, leadFollowStatus, runLeadFollowPass } from './lead-follow.js';
|
|
13
13
|
import { cancelCommitment, conditionWords, openCommitments } from './lead-commitments.js';
|
|
14
14
|
import { startMaintenance } from './maintenance.js';
|
|
@@ -44,6 +44,8 @@ import { addRoutine, removeRoutine, runRoutine } from "./teammate-desk.js";
|
|
|
44
44
|
import { sendTeammateWeeklies, undoCall } from "./teammate-week.js";
|
|
45
45
|
import { answerTeammateQuestion } from "./teammate-work.js";
|
|
46
46
|
import { createFlowRooms, flowFingerprint } from "./flow-live.js";
|
|
47
|
+
import { createTaskRooms, taskFingerprint } from "./task-live.js";
|
|
48
|
+
import { runActivityOf } from "./task-activity.js";
|
|
47
49
|
import { disconnectGoogle, finishGoogleConsent, GOOGLE_CALLBACK, googleConnected, googleConsent, readGoogleMail, saveGoogleClient } from "./google-mail.js";
|
|
48
50
|
import { mailboxAccess, readThroughImap } from "./mailbox.js";
|
|
49
51
|
import { readEmailSettings, saveEmailSettings, sendingAccount, sendThroughServer, setFlowSecret } from "./flow-actions.js";
|
|
@@ -749,6 +751,7 @@ export function createDecisionServer(options) {
|
|
|
749
751
|
const connectVisits = new Map();
|
|
750
752
|
/** Flows open in a browser (v88): who's here, and a nudge when one changes. */
|
|
751
753
|
const flowRooms = createFlowRooms(flow => flowFingerprint(store, flow));
|
|
754
|
+
const taskRooms = createTaskRooms(root => taskFingerprint(store, root, clock()));
|
|
752
755
|
const teamChatProvider = () => { const enabled = chatEnablement(); return enabled.ok ? { config: enabled.config, key: enabled.key } : null; };
|
|
753
756
|
const team = createTeamRuntime({ store, repos: codingProjects, evidenceRoot, clock, workspaceRevision,
|
|
754
757
|
...(options.chatFetcher ? { fetcher: options.chatFetcher } : {}),
|
|
@@ -1831,9 +1834,9 @@ export function createDecisionServer(options) {
|
|
|
1831
1834
|
}
|
|
1832
1835
|
const read = new Set(["/settings/flows", "/settings/skills", "/settings/knowledge", "/settings", "/settings/learning", "/recipes", "/recipes/run", "/recipes/start", "/recipes/new", "/recipes/edit", "/recipes/from-task", "/recipes/preview", "/recipes/export", "/", "/inbox", "/work", "/projects", "/people", "/ledger", "/ledger/export", "/next", "/board", "/tasks", "/tasks/new", "/runs", "/review", "/done", "/routines", "/menu"]);
|
|
1833
1836
|
const write = new Set(["/settings/flows/on", "/settings/skills/import", "/settings/skills/change", "/settings/skills/revise", "/settings/knowledge/change", "/settings/knowledge/refresh", "/settings/learning/change", "/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/projects/select", "/tasks/add", "/routines/add"]);
|
|
1834
|
-
const task = matchTaskPath(path, request.method === "GET" ? "(/evidence)?$" : "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|next|reopen|steer|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
|
|
1837
|
+
const task = matchTaskPath(path, request.method === "GET" ? "(/evidence|/live)?$" : "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|next|reopen|steer|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
|
|
1835
1838
|
const resource = request.method === "GET"
|
|
1836
|
-
? /^\/(?:r|d)\/[0-9]{1,15}(?:\/evidence\/[0-9]{1,15})?$/.test(path) || /^\/routines\/[0-9]{1,15}$/.test(path) || path === "/flows" || /^\/flows\/[0-9]{1,15}(\/insights|\/export|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(path) || path === "/flows/new" || /^\/flows\/new\/[a-z-]{1,40}$/.test(path)
|
|
1839
|
+
? /^\/(?:r|d)\/[0-9]{1,15}(?:\/evidence\/[0-9]{1,15})?$/.test(path) || /^\/routines\/[0-9]{1,15}$/.test(path) || path === "/flows" || /^\/flows\/[0-9]{1,15}(\/insights|\/export|\/live|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(path) || path === "/flows/new" || /^\/flows\/new\/[a-z-]{1,40}$/.test(path)
|
|
1837
1840
|
: /^\/d\/[0-9]{1,15}\/answer$/.test(path) || /^\/routines\/[0-9]{1,15}\/(approve|refresh|pause|resume|run-now)$/.test(path) || path === "/flows/new" || /^\/flows\/new\/[a-z-]{1,40}$/.test(path) || path === "/flows/import" || /^\/flows\/[0-9]{1,15}\/(save|cards|archive|scripts)$/.test(path) || /^\/flows\/[0-9]{1,15}\/cards\/[0-9]{1,15}\/(move|decide|cancel|comment|assign|watch)$/.test(path) || /^\/flows\/[0-9]{1,15}\/triggers(\/[0-9]{1,15}\/(pause|resume|remove|check|press|renew|secret|share|unshare))?$/.test(path) || /^\/r\/[0-9]{1,15}\/(note|comment|revise|draft-repair|checks|add-tests)$/.test(path);
|
|
1838
1841
|
if (!(request.method === "GET" ? read : write).has(path) && task === null && !resource) {
|
|
1839
1842
|
refuse(response, who, 403, "This area requires instance access. Your account operates within its assigned projects.", "/projects");
|
|
@@ -1891,6 +1894,10 @@ export function createDecisionServer(options) {
|
|
|
1891
1894
|
}
|
|
1892
1895
|
project = wanted;
|
|
1893
1896
|
}
|
|
1897
|
+
// Every page's Tasks count covers this one project view, whatever project the page itself shows.
|
|
1898
|
+
const facts = requestContext.getStore();
|
|
1899
|
+
if (facts !== undefined)
|
|
1900
|
+
facts.lens = project;
|
|
1894
1901
|
// Exact result links carry their own read context. Check the stored
|
|
1895
1902
|
// placement against both account and instance access before using it;
|
|
1896
1903
|
// viewing a result never changes the session's selected project, and
|
|
@@ -1975,7 +1982,7 @@ export function createDecisionServer(options) {
|
|
|
1975
1982
|
url.pathname !== "/menu" &&
|
|
1976
1983
|
url.pathname !== "/recipes" &&
|
|
1977
1984
|
// A flow names its own project; the list spans every project.
|
|
1978
|
-
url.pathname !== "/flows" && url.pathname !== "/flows/import" && !/^\/flows\/[0-9]{1,15}(\/insights|\/export|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(url.pathname) &&
|
|
1985
|
+
url.pathname !== "/flows" && url.pathname !== "/flows/import" && !/^\/flows\/[0-9]{1,15}(\/insights|\/export|\/live|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(url.pathname) &&
|
|
1979
1986
|
// The gallery (Flows → New) asks for its project on each template's page.
|
|
1980
1987
|
url.pathname !== "/flows/new" && !/^\/flows\/new\/[a-z-]{1,40}$/.test(url.pathname) &&
|
|
1981
1988
|
// Teammates (v92) name their own project, like flows.
|
|
@@ -1983,7 +1990,7 @@ export function createDecisionServer(options) {
|
|
|
1983
1990
|
// New work names its project in the form (a dropdown of known
|
|
1984
1991
|
// projects); /tasks/add still admits the posted repo on its own.
|
|
1985
1992
|
url.pathname !== "/tasks/new" && url.pathname !== "/tasks/add" &&
|
|
1986
|
-
!/^\/t\/[^/]+(\/evidence)?$/.test(url.pathname) &&
|
|
1993
|
+
!/^\/t\/[^/]+(\/evidence|\/live)?$/.test(url.pathname) &&
|
|
1987
1994
|
!/^\/r\/[0-9]{1,15}(?:\/evidence\/[0-9]{1,15})?$/.test(url.pathname) &&
|
|
1988
1995
|
!url.pathname.startsWith("/d/") && !url.pathname.startsWith("/contest/") &&
|
|
1989
1996
|
url.pathname !== "/projects" &&
|
|
@@ -2276,6 +2283,8 @@ export function createDecisionServer(options) {
|
|
|
2276
2283
|
const docked = projectThread === null ? null : dockedConversation(who, null, projectThread, now, `/work?project=${encodeURIComponent(projectThread)}`);
|
|
2277
2284
|
if (docked !== null)
|
|
2278
2285
|
page.workspace = { ...page.workspace, conversation: docked, pageHtml: page.body };
|
|
2286
|
+
// Without a docked chat the list reads itself: 10 s while any of its tasks is building, else 30 s.
|
|
2287
|
+
page.refreshSeconds = work.totals.running > 0 ? 10 : 30;
|
|
2279
2288
|
return sendScreen(response, 200, page);
|
|
2280
2289
|
}
|
|
2281
2290
|
if (url.pathname === "/next") {
|
|
@@ -3057,6 +3066,26 @@ export function createDecisionServer(options) {
|
|
|
3057
3066
|
const chrome = chromeFor(project, "tasks");
|
|
3058
3067
|
return sendScreen(response, 200, newTaskPage(chrome, project, csrf, revision, null, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? []));
|
|
3059
3068
|
}
|
|
3069
|
+
const taskLive = matchTaskPath(url.pathname, "/live$");
|
|
3070
|
+
if (taskLive !== null) {
|
|
3071
|
+
// The live task page: a nudge the moment the task changes, and who else
|
|
3072
|
+
// has it open. Hints only — the page reads the task the usual way.
|
|
3073
|
+
if (who.via !== "cookie")
|
|
3074
|
+
return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
|
|
3075
|
+
const family = familyOf(taskLive.taskId);
|
|
3076
|
+
if (family === null || family.problem !== null)
|
|
3077
|
+
return respond(response, 404, "application/json", JSON.stringify({ error: "task" }));
|
|
3078
|
+
response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", "x-content-type-options": "nosniff", "x-accel-buffering": "no" });
|
|
3079
|
+
const name = who.name, repo = family.root.repo;
|
|
3080
|
+
taskRooms.join(family.root.id, {
|
|
3081
|
+
name, response,
|
|
3082
|
+
// Rechecked while open, from the cookie and the account (never a request's context): a sign-out or a narrowed account stops it.
|
|
3083
|
+
valid: () => { const again = identify(request, false); return again !== null && again.name === name && rowVisible(liveCeiling(), repo) && store.accountCanAccess(name, repo); },
|
|
3084
|
+
});
|
|
3085
|
+
request.once("close", () => { if (!response.writableEnded)
|
|
3086
|
+
response.end(); });
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3060
3089
|
const task = matchTaskPath(url.pathname, "");
|
|
3061
3090
|
if (task !== null) {
|
|
3062
3091
|
const family = familyOf(task.taskId);
|
|
@@ -3841,7 +3870,8 @@ export function createDecisionServer(options) {
|
|
|
3841
3870
|
// Console v2: the landing's live view (who is working, four counts, Catch up in tabs).
|
|
3842
3871
|
const home = focusTask === null && chatProject === null && !roomId ? chatHomeOf(who, repos, now) : null;
|
|
3843
3872
|
const withFirstRun = (shown) => firstRun === undefined && phone === undefined && home === null ? shown
|
|
3844
|
-
: { ...shown,
|
|
3873
|
+
: { ...shown, ...(home === null ? {} : { refreshSeconds: liveRefreshSeconds() }),
|
|
3874
|
+
workspace: { ...shown.workspace, ...(firstRun === undefined ? {} : { firstRun }), ...(phone === undefined ? {} : { phone }), ...(home === null ? {} : { home }) } };
|
|
3845
3875
|
if (enabled.ok && mateSession !== null && principal !== null && !ceilingStale) {
|
|
3846
3876
|
{
|
|
3847
3877
|
const said = takeMateNote(who.session.csrf, mateSession.id);
|
|
@@ -5173,17 +5203,24 @@ export function createDecisionServer(options) {
|
|
|
5173
5203
|
catch {
|
|
5174
5204
|
notices.push('Crew updates are unavailable. Open Tasks to inspect saved work.');
|
|
5175
5205
|
}
|
|
5176
|
-
//
|
|
5177
|
-
// waiting on a person behind its Tasks link — the open project's, or
|
|
5178
|
-
// everything this person may see — so it matches that page's Needs you tab.
|
|
5179
|
-
let needsYou = 0;
|
|
5206
|
+
// A running row says what its agent did last, and when (the task page's own line).
|
|
5180
5207
|
try {
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5208
|
+
const now = clock(), live = new Map();
|
|
5209
|
+
if (crew.crew.length > 0)
|
|
5210
|
+
for (const run of store.liveRuns(now)) {
|
|
5211
|
+
const root = familyOf(run.taskId)?.root.id;
|
|
5212
|
+
if (root !== undefined && !live.has(root))
|
|
5213
|
+
live.set(root, runActivityOf(store, run, now));
|
|
5214
|
+
}
|
|
5215
|
+
if (live.size > 0)
|
|
5216
|
+
crew = { ...crew, crew: crew.crew.map(item => { const activity = live.get(item.id); return activity === undefined ? item : { ...item, activity }; }) };
|
|
5217
|
+
}
|
|
5218
|
+
catch { /* the rows keep their status; the line returns on the next read */ }
|
|
5219
|
+
// "Wake me only for these": the navigation carries the sidebar's own
|
|
5220
|
+
// Tasks count (chromeFor) and links to the project view it covers, so
|
|
5221
|
+
// it matches that page's Needs you tab on every screen.
|
|
5222
|
+
const needsYou = s.chrome.inboxCount;
|
|
5223
|
+
const tasksProject = s.chrome.inboxProject === undefined ? s.chrome.project : s.chrome.inboxProject;
|
|
5187
5224
|
// The person's own project and task conversations (v77), for the
|
|
5188
5225
|
// sidebar's chat list; a thread whose project is out of view is left out.
|
|
5189
5226
|
let chats = [];
|
|
@@ -5219,7 +5256,7 @@ export function createDecisionServer(options) {
|
|
|
5219
5256
|
catchUpHtml: extras.catchUpHtml ?? '', controlsHtml: extras.controlsHtml ?? '', notices, view: extras.view ?? null,
|
|
5220
5257
|
...(s.chrome.demo ? { demo: { text: DEMO_BANNER, short: DEMO_BANNER_SHORT } } : {}),
|
|
5221
5258
|
pageHtml: extras.pageHtml === undefined ? (conversation === null ? pageHtml : null) : extras.pageHtml,
|
|
5222
|
-
navigation: [...browserNavigationOf(currentPath, s.chrome.project, needsYou), { label: 'Workspace tools', href: '/menu', active: path.pathname === '/menu' }],
|
|
5259
|
+
navigation: [...browserNavigationOf(currentPath, s.chrome.project, needsYou, tasksProject, s.chrome.inboxLabel), { label: 'Workspace tools', href: '/menu', active: path.pathname === '/menu' }],
|
|
5223
5260
|
chats,
|
|
5224
5261
|
...(s.refreshSeconds === undefined ? {} : { refreshSeconds: Math.max(5, Math.floor(s.refreshSeconds)) }),
|
|
5225
5262
|
...(s.chrome.signIn === undefined ? {} : { signIn: s.chrome.signIn }),
|
|
@@ -5274,12 +5311,6 @@ export function createDecisionServer(options) {
|
|
|
5274
5311
|
// functional script polls.
|
|
5275
5312
|
return page(response, status, html, nonce, s.functional?.fetches === true || chromeLayer || sensitiveChrome !== "");
|
|
5276
5313
|
}
|
|
5277
|
-
/** The badge cache: five seconds per project — mutations invalidate it. */
|
|
5278
|
-
const badgeCache = new Map();
|
|
5279
|
-
/** The needs-you count every surface wears for a project (workspace
|
|
5280
|
-
* package 1): Work's own Needs-you membership, cached per viewer and
|
|
5281
|
-
* project, so the rail, the phone tab, the scope bar, the project cards,
|
|
5282
|
-
* the chat overview, and the Needs-you tab never disagree. */
|
|
5283
5314
|
/** The Chat landing's live view: each agent at work now with its task and
|
|
5284
5315
|
* phase, four counts, plan-window use (never dollars: subscriptions don't
|
|
5285
5316
|
* bill per run) and Catch up's items, each tagged with its tab. Reads only. */
|
|
@@ -5289,7 +5320,8 @@ export function createDecisionServer(options) {
|
|
|
5289
5320
|
const agents = store.liveRuns(now).filter(run => admitted(run.repo)).map(run => {
|
|
5290
5321
|
const root = familyOf(run.taskId)?.root ?? null;
|
|
5291
5322
|
return { runId: run.id, taskId: root?.id ?? run.taskId, title: root?.title ?? run.title, href: taskHref(root?.id ?? run.taskId),
|
|
5292
|
-
agent: `${providerName(run.provider)} on ${run.runner}`, phase: homePhaseWords(run), project: run.repo === null ? null : projectName(run.repo), since: run.startedAt
|
|
5323
|
+
agent: `${providerName(run.provider)} on ${run.runner}`, phase: homePhaseWords(run), project: run.repo === null ? null : projectName(run.repo), since: run.startedAt,
|
|
5324
|
+
activity: runActivityOf(store, run, now) };
|
|
5293
5325
|
});
|
|
5294
5326
|
const all = workIndexPage(store, now, access, { view: "all", limit: WORK_INDEX_MAX_LIMIT });
|
|
5295
5327
|
const needs = workIndexPage(store, now, access, { view: "needs-you", limit: WORK_INDEX_MAX_LIMIT });
|
|
@@ -5323,19 +5355,13 @@ export function createDecisionServer(options) {
|
|
|
5323
5355
|
planUse: (windows?.tiles ?? []).map(one => ({ name: one.name, window: one.window, percent: one.percent, detail: one.detail, tone: one.tone })),
|
|
5324
5356
|
};
|
|
5325
5357
|
}
|
|
5358
|
+
/** The needs-you count every surface wears, read from this request's own
|
|
5359
|
+
* Work counts (one query per request, never a cached earlier one), so a
|
|
5360
|
+
* live page's badge and its Needs you tab always agree. */
|
|
5326
5361
|
function needsYouBadge(project) {
|
|
5327
|
-
|
|
5328
|
-
const key = `${actor ?? ""}:${actor === undefined ? "" : store.accountOf(actor)?.generation}:${project ?? ""}`;
|
|
5329
|
-
const cached = badgeCache.get(key);
|
|
5330
|
-
if (cached !== undefined && Date.now() - cached.at <= 5_000)
|
|
5331
|
-
return cached;
|
|
5332
|
-
const counted = needsYouCount(project);
|
|
5333
|
-
const badge = { at: Date.now(), count: counted.count, saturated: counted.saturated };
|
|
5334
|
-
badgeCache.set(key, badge);
|
|
5335
|
-
return badge;
|
|
5362
|
+
return needsYouCount(project);
|
|
5336
5363
|
}
|
|
5337
5364
|
const bustBadge = () => {
|
|
5338
|
-
badgeCache.clear();
|
|
5339
5365
|
paletteCache.clear();
|
|
5340
5366
|
};
|
|
5341
5367
|
/** The sidebar's facts for this request. */
|
|
@@ -5343,7 +5369,12 @@ export function createDecisionServer(options) {
|
|
|
5343
5369
|
if (restricted() && !visible(project))
|
|
5344
5370
|
project = null;
|
|
5345
5371
|
const actor = requestContext.getStore()?.actor;
|
|
5346
|
-
|
|
5372
|
+
// One Tasks count per request: Needs you in the request's project view
|
|
5373
|
+
// (the open project, else every admitted project), never the page's own
|
|
5374
|
+
// project — a flow or task in another project shows the same number.
|
|
5375
|
+
const lens = requestContext.getStore()?.lens;
|
|
5376
|
+
const inboxProject = lens !== undefined && (!restricted() || visible(lens)) ? lens : project;
|
|
5377
|
+
const badge = needsYouBadge(inboxProject);
|
|
5347
5378
|
const liveMode = project === null ? null : store.activeMode(project, clock());
|
|
5348
5379
|
const liveModeTerms = liveMode === null ? null : modeTermsFromJson(liveMode.termsJson);
|
|
5349
5380
|
let projectPeek = project === null ? null : undefined;
|
|
@@ -5383,6 +5414,9 @@ export function createDecisionServer(options) {
|
|
|
5383
5414
|
...(facts === undefined ? {} : { csrf: facts.csrf, returnTo: facts.returnTo }),
|
|
5384
5415
|
inboxCount: badge.count,
|
|
5385
5416
|
inboxSaturated: badge.saturated,
|
|
5417
|
+
inboxProject,
|
|
5418
|
+
inboxLabel: needsYouLabelOf(badge.count, inboxProject === null ? null
|
|
5419
|
+
: store.listProjects().find(one => one.path === inboxProject)?.name ?? projectName(inboxProject), badge.saturated),
|
|
5386
5420
|
settings: true,
|
|
5387
5421
|
...(store.isDemo() ? { demo: true } : {}),
|
|
5388
5422
|
...(() => { const signIn = signInNotices(store); return signIn.length === 0 ? {} : { signIn }; })(),
|
|
@@ -5436,6 +5470,10 @@ export function createDecisionServer(options) {
|
|
|
5436
5470
|
.reduce((sum, one) => sum + one.totals['needs-you'], 0);
|
|
5437
5471
|
return { count, saturated: false };
|
|
5438
5472
|
}
|
|
5473
|
+
/** A live page's own beat without a docked chat: 10 s while any admitted task is building, else 30 s. */
|
|
5474
|
+
function liveRefreshSeconds() {
|
|
5475
|
+
return projectCounts(clock()).some(one => one.totals.running > 0) ? 10 : 30;
|
|
5476
|
+
}
|
|
5439
5477
|
function projectFamilyPeek(repo, now) {
|
|
5440
5478
|
const row = projectCounts(now).find(one => one.repo === repo);
|
|
5441
5479
|
return { waiting: row?.totals['needs-you'] ?? 0, running: row?.totals.running ?? 0,
|
|
@@ -6239,6 +6277,9 @@ export function createDecisionServer(options) {
|
|
|
6239
6277
|
// What went wrong reads the family's latest finished attempt, as the Tasks row does.
|
|
6240
6278
|
presentedData.failure = failureOf(presentedData.family?.runs ?? data.runs, data.assignment ?? null);
|
|
6241
6279
|
presentedData.runChecks = runChecksHere(data.assignment?.receipt?.runId ?? null, who, taskHref(taskId));
|
|
6280
|
+
const liveRun = data.liveRunId == null ? undefined : data.runs.find(one => one.id === data.liveRunId);
|
|
6281
|
+
if (liveRun !== undefined)
|
|
6282
|
+
presentedData.activity = runActivityOf(store, liveRun, clock());
|
|
6242
6283
|
presentedData.demo = store.isDemo();
|
|
6243
6284
|
const paneProject = restricted() ? store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
|
|
6244
6285
|
const page = taskPage(paneProject === null && !unscopedMode
|
|
@@ -6249,11 +6290,19 @@ export function createDecisionServer(options) {
|
|
|
6249
6290
|
const docked = focus === null ? null : dockedConversation(who, focus, null, clock(), taskHref(focus.id));
|
|
6250
6291
|
// The conversation is the page's own thread here, so the page drops its Ask tab;
|
|
6251
6292
|
// only an approver is offered a road to message the agent.
|
|
6252
|
-
const taskView = page.workspace?.view?.kind === "task" ? { ...page.workspace.view, ...(docked === null ? {} : { tabs: [] }), ...(who.role === "approver" ? {} : { chatHref: null })
|
|
6293
|
+
const taskView = page.workspace?.view?.kind === "task" ? { ...page.workspace.view, ...(docked === null ? {} : { tabs: [] }), ...(who.role === "approver" ? {} : { chatHref: null }),
|
|
6294
|
+
// A signed-in page hears the moment the task changes, and who else has it open.
|
|
6295
|
+
...(who.via === "cookie" ? { live: { href: `${taskHref(family?.root.id ?? taskId)}/live`, at: (() => { try {
|
|
6296
|
+
return taskFingerprint(store, family?.root.id ?? taskId, clock());
|
|
6297
|
+
}
|
|
6298
|
+
catch {
|
|
6299
|
+
return null;
|
|
6300
|
+
} })() } } : {}) } : page.workspace?.view;
|
|
6253
6301
|
if (docked !== null)
|
|
6254
6302
|
page.workspace = { ...page.workspace, ...(taskView === undefined ? {} : { view: taskView }), conversation: docked, pageHtml: page.body };
|
|
6255
6303
|
else if (taskView !== undefined && page.workspace !== undefined)
|
|
6256
6304
|
page.workspace = { ...page.workspace, view: taskView };
|
|
6305
|
+
page.refreshSeconds = liveRefreshSeconds();
|
|
6257
6306
|
return sendScreen(response, status, page);
|
|
6258
6307
|
}
|
|
6259
6308
|
/** The routine screen: the standing order restated, its verbs, its ledger. */
|
|
@@ -12509,6 +12558,7 @@ export function createDecisionServer(options) {
|
|
|
12509
12558
|
stream.end();
|
|
12510
12559
|
chatStreams.clear();
|
|
12511
12560
|
flowRooms.close();
|
|
12561
|
+
taskRooms.close();
|
|
12512
12562
|
void Promise.all([closeCoding(), bounded(team.close()), bounded(leadMaintenance?.stop())]).then(() => closeServer(callback)).catch(error => {
|
|
12513
12563
|
if (callback)
|
|
12514
12564
|
callback(error instanceof Error ? error : Error('Coding session shutdown failed.'));
|
|
@@ -16781,9 +16831,9 @@ function shell(title, body, options = {}) {
|
|
|
16781
16831
|
// destination it now lives under. The count rides Work: it is the
|
|
16782
16832
|
// saturated needs-you count, exactly as the inbox row wore it.
|
|
16783
16833
|
const primary = chrome.active === "code" ? "work" : primaryDestinationOf(chrome.active);
|
|
16784
|
-
const primaryItem = (key, href, label, count) => `<a href="${href}" aria-label="${escape(label)}" title="${escape(label)}"${primary === key ? ' class="active" aria-current="page"' : ""}${key === "work" && count !== undefined ? ` data-waiting="${count}"` : ""}>` +
|
|
16834
|
+
const primaryItem = (key, href, label, count) => `<a href="${href}" aria-label="${escape(key === "work" && count !== undefined && count > 0 && chrome.inboxLabel !== undefined ? `${label}, ${chrome.inboxLabel}` : label)}" title="${escape(label)}"${primary === key ? ' class="active" aria-current="page"' : ""}${key === "work" && count !== undefined ? ` data-waiting="${count}"` : ""}>` +
|
|
16785
16835
|
`<span class="glyph">${NAV_ICONS[key] ?? ""}</span>${label}` +
|
|
16786
|
-
`${count !== undefined && count > 0 ? ` <span class="count badge badge-open">${count}${chrome.inboxSaturated ? "+" : ""}</span>` : ""}</a>`;
|
|
16836
|
+
`${count !== undefined && count > 0 ? ` <span${chrome.inboxLabel === undefined ? "" : ` aria-label="${escape(chrome.inboxLabel)}" title="${escape(chrome.inboxLabel)}"`} class="count badge badge-open">${count}${chrome.inboxSaturated ? "+" : ""}</span>` : ""}</a>`;
|
|
16787
16837
|
const side = [
|
|
16788
16838
|
`<aside class="side">`,
|
|
16789
16839
|
`<div class="side-head"><a class="brand" href="${chrome.chat === true ? "/chat" : "/work"}"><span class="brand-long">Toolroll</span><span class="brand-short">T</span></a>`,
|
|
@@ -16869,7 +16919,7 @@ function shell(title, body, options = {}) {
|
|
|
16869
16919
|
// A phone tab says THAT something waits, with a dot; the number is on
|
|
16870
16920
|
// the Work views themselves (Linear Mobile's rule — one tap away).
|
|
16871
16921
|
`<a href="${href}"${primary === key ? ' class="active" aria-current="page"' : ""}><span class="glyph">${TAB_ICONS[key]}</span>${label}` +
|
|
16872
|
-
`${count !== undefined && count > 0 ? `<span class="dot-badge" role="img" aria-label="${count} waiting"></span>` : ""}</a>`;
|
|
16922
|
+
`${count !== undefined && count > 0 ? `<span class="dot-badge" role="img" aria-label="${escape(chrome.inboxLabel ?? `${count} waiting`)}"></span>` : ""}</a>`;
|
|
16873
16923
|
// Three primary tabs, the same three as the rail: chat where allowed,
|
|
16874
16924
|
// work, projects. Tools and settings sit behind the header's menu action.
|
|
16875
16925
|
const tabbar = [
|
|
@@ -22037,6 +22087,8 @@ function taskBodyParts(data) {
|
|
|
22037
22087
|
retry,
|
|
22038
22088
|
runChecks,
|
|
22039
22089
|
progress,
|
|
22090
|
+
// One quiet line under the step: what the agent did last, and when.
|
|
22091
|
+
activity: liveRun === undefined ? null : data.activity ?? null,
|
|
22040
22092
|
stop,
|
|
22041
22093
|
record,
|
|
22042
22094
|
earlier,
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** Browser installation may write only this known file inside its project. */
|
|
17
17
|
export declare function previewProjectInstructions(repo: string): {
|
|
18
|
+
content?: never;
|
|
18
19
|
ok: false;
|
|
19
20
|
message: string;
|
|
20
21
|
plan?: never;
|
|
21
|
-
content?: never;
|
|
22
22
|
fingerprint?: never;
|
|
23
23
|
installed?: never;
|
|
24
24
|
} | {
|
|
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
30
30
|
installed: boolean;
|
|
31
31
|
};
|
|
32
32
|
export declare function addProjectInstructions(repo: string, fingerprint: string): {
|
|
33
|
+
content?: never;
|
|
33
34
|
ok: false;
|
|
34
35
|
message: string;
|
|
35
36
|
plan?: never;
|
|
36
|
-
content?: never;
|
|
37
37
|
fingerprint?: never;
|
|
38
38
|
installed?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|
package/dist/store.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { DecisionOption } from "./contracts/decision.js";
|
|
|
2
2
|
import { type SavedToolAction, type SavedToolRule } from "./contracts/store-json.js";
|
|
3
3
|
import type { FlowCardChange, FlowCardFields } from "./contracts/flow-card.js";
|
|
4
4
|
import { type Actor } from "./actor.js";
|
|
5
|
+
import { type LiveActivityKind } from "./live.js";
|
|
5
6
|
import { type TokenPurpose } from "./api-tokens.js";
|
|
6
7
|
import { type CriterionMatrixRow, type CriterionJudgementWord } from "./proof.js";
|
|
7
8
|
import { type ExecutionProfile, type ChainEntry, type UnattendedPermissionMode, type AcceptanceCriterion } from "./scope.js";
|
|
@@ -69,6 +70,10 @@ export declare const FLOW_SEND_SCHEMA = "\nCREATE TABLE IF NOT EXISTS flow_send
|
|
|
69
70
|
/** An owner's lasting choice to approve plans and merges from their paired chat (chat-approval.ts), for all their
|
|
70
71
|
* projects (scope '*') or one (its path), with the limits they agreed to. Off unless a row says on; a project's own row
|
|
71
72
|
* outranks the all-projects one. `generation` is the account's at the change: a reset password ends it. */
|
|
73
|
+
/** What a running agent did last (live tasks): one row per run, a fixed kind from live.ts's
|
|
74
|
+
* vocabulary and when — never text, tool names or arguments. Display state, not evidence;
|
|
75
|
+
* a finished run's row goes with the live log's own retention (sweepRunActivity). */
|
|
76
|
+
export declare const RUN_ACTIVITY_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_activity (\n run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,\n kind TEXT NOT NULL CHECK (kind IN ('session','message','edit','command','read','search','lookup','plan','delegate','tool')),\n at TEXT NOT NULL\n);\n";
|
|
72
77
|
export declare const CHAT_APPROVAL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS chat_approval_setting (\n approver TEXT NOT NULL,\n scope TEXT NOT NULL,\n enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),\n full_access INTEGER NOT NULL DEFAULT 0 CHECK (full_access IN (0,1)),\n attempt_cap_microusd INTEGER CHECK (attempt_cap_microusd IS NULL OR attempt_cap_microusd >= 0),\n generation INTEGER NOT NULL,\n digest TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (approver, scope)\n);\n";
|
|
73
78
|
/** The second tap on a flow decision in Telegram (telegram-flow.ts): Approve arms Yes and Cancel, bound to the binding,
|
|
74
79
|
* chat, message and exact visit of the card; Yes decides it once. */
|
|
@@ -5601,6 +5606,17 @@ export declare class Store {
|
|
|
5601
5606
|
* page and focused chat render live, including once the run that wrote
|
|
5602
5607
|
* it has ended. */
|
|
5603
5608
|
latestCheckpointForTask(taskRef: number): RunCheckpoint | null;
|
|
5609
|
+
/** What the agent did last on this run (live tasks): one fixed kind and when, replacing the last. */
|
|
5610
|
+
recordRunActivity(run: number, kind: LiveActivityKind, now: Date): void;
|
|
5611
|
+
/** A run's last recorded activity and its newest progress checkpoint's time, in one read. */
|
|
5612
|
+
runActivity(run: number): {
|
|
5613
|
+
kind: LiveActivityKind | null;
|
|
5614
|
+
at: string | null;
|
|
5615
|
+
progressAt: string | null;
|
|
5616
|
+
};
|
|
5617
|
+
/** The live log's own retention: a finished run's activity goes once older than `retainMs`, and beyond the newest
|
|
5618
|
+
* `bound` finished runs; a live run's always stays. */
|
|
5619
|
+
sweepRunActivity(now: Date, retainMs: number, bound: number): number;
|
|
5604
5620
|
/** One run's full checkpoint history, oldest first. */
|
|
5605
5621
|
checkpointHistory(run: number): RunCheckpoint[];
|
|
5606
5622
|
/** The newest scout run's report for a task (v34), when one exists. */
|
package/dist/store.js
CHANGED
|
@@ -12,6 +12,7 @@ import { cardOutputsForStore, cardOutputsFromStore } from "./contracts/stage-out
|
|
|
12
12
|
import { chatControlHref, chatResultHref } from "./chat-controls.js";
|
|
13
13
|
import { actorLabel, currentActor, leadSecretMatches, mintLeadToken, parseLeadToken } from "./actor.js";
|
|
14
14
|
import { scanForSecrets } from "./evidence.js";
|
|
15
|
+
import { LIVE_ACTIVITY_KINDS } from "./live.js";
|
|
15
16
|
import { tokenPurpose, tokenRotationProblem } from "./api-tokens.js";
|
|
16
17
|
/**
|
|
17
18
|
* The database: a small task store, and the operational overlay beside it.
|
|
@@ -359,6 +360,16 @@ CREATE INDEX IF NOT EXISTS telegram_flow_choice_visit ON telegram_flow_choice (c
|
|
|
359
360
|
/** An owner's lasting choice to approve plans and merges from their paired chat (chat-approval.ts), for all their
|
|
360
361
|
* projects (scope '*') or one (its path), with the limits they agreed to. Off unless a row says on; a project's own row
|
|
361
362
|
* outranks the all-projects one. `generation` is the account's at the change: a reset password ends it. */
|
|
363
|
+
/** What a running agent did last (live tasks): one row per run, a fixed kind from live.ts's
|
|
364
|
+
* vocabulary and when — never text, tool names or arguments. Display state, not evidence;
|
|
365
|
+
* a finished run's row goes with the live log's own retention (sweepRunActivity). */
|
|
366
|
+
export const RUN_ACTIVITY_SCHEMA = `
|
|
367
|
+
CREATE TABLE IF NOT EXISTS run_activity (
|
|
368
|
+
run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,
|
|
369
|
+
kind TEXT NOT NULL CHECK (kind IN ('session','message','edit','command','read','search','lookup','plan','delegate','tool')),
|
|
370
|
+
at TEXT NOT NULL
|
|
371
|
+
);
|
|
372
|
+
`;
|
|
362
373
|
export const CHAT_APPROVAL_SCHEMA = `
|
|
363
374
|
CREATE TABLE IF NOT EXISTS chat_approval_setting (
|
|
364
375
|
approver TEXT NOT NULL,
|
|
@@ -4090,6 +4101,8 @@ function initializeStore(db, file) {
|
|
|
4090
4101
|
db.exec(CHAT_DECIDE_SCHEMA);
|
|
4091
4102
|
// An owner's lasting chat-approval setting (no version bump: additive only; absent means off).
|
|
4092
4103
|
db.exec(CHAT_APPROVAL_SCHEMA);
|
|
4104
|
+
// What a running agent did last (no version bump: additive only; absent means no word yet).
|
|
4105
|
+
db.exec(RUN_ACTIVITY_SCHEMA);
|
|
4093
4106
|
db.exec(TELEGRAM_FLOW_CONFIRM_SCHEMA);
|
|
4094
4107
|
// A flow's "Send to me" and "Person chooses" zones (no version bump: additive only).
|
|
4095
4108
|
db.exec(FLOW_SEND_SCHEMA);
|
|
@@ -17523,6 +17536,27 @@ export class Store {
|
|
|
17523
17536
|
const row = this.db.prepare("SELECT * FROM run_checkpoint WHERE task_ref = ? ORDER BY id DESC LIMIT 1").get(taskRef);
|
|
17524
17537
|
return row === undefined ? null : readRunCheckpoint(row);
|
|
17525
17538
|
}
|
|
17539
|
+
/** What the agent did last on this run (live tasks): one fixed kind and when, replacing the last. */
|
|
17540
|
+
recordRunActivity(run, kind, now) {
|
|
17541
|
+
this.db.prepare("INSERT INTO run_activity (run, kind, at) VALUES (?, ?, ?) ON CONFLICT (run) DO UPDATE SET kind = excluded.kind, at = excluded.at")
|
|
17542
|
+
.run(run, kind, now.toISOString());
|
|
17543
|
+
}
|
|
17544
|
+
/** A run's last recorded activity and its newest progress checkpoint's time, in one read. */
|
|
17545
|
+
runActivity(run) {
|
|
17546
|
+
const row = this.db.prepare(`SELECT (SELECT kind FROM run_activity WHERE run = ?1) AS kind, (SELECT at FROM run_activity WHERE run = ?1) AS at,
|
|
17547
|
+
(SELECT created_at FROM run_checkpoint WHERE run = ?1 ORDER BY id DESC LIMIT 1) AS progress_at`).get(run);
|
|
17548
|
+
const kind = typeof row["kind"] === "string" && LIVE_ACTIVITY_KINDS.includes(row["kind"]) ? row["kind"] : null;
|
|
17549
|
+
return { kind, at: kind === null ? null : String(row["at"]), progressAt: row["progress_at"] == null ? null : String(row["progress_at"]) };
|
|
17550
|
+
}
|
|
17551
|
+
/** The live log's own retention: a finished run's activity goes once older than `retainMs`, and beyond the newest
|
|
17552
|
+
* `bound` finished runs; a live run's always stays. */
|
|
17553
|
+
sweepRunActivity(now, retainMs, bound) {
|
|
17554
|
+
const finished = "run IN (SELECT id FROM run WHERE outcome IS NOT NULL)";
|
|
17555
|
+
const aged = this.db.prepare(`DELETE FROM run_activity WHERE at < ? AND ${finished}`).run(new Date(now.getTime() - retainMs).toISOString()).changes;
|
|
17556
|
+
const over = this.db.prepare(`DELETE FROM run_activity WHERE ${finished} AND run NOT IN (SELECT run FROM run_activity WHERE ${finished} ORDER BY at DESC, run DESC LIMIT ?)`)
|
|
17557
|
+
.run(Math.max(0, Math.floor(bound))).changes;
|
|
17558
|
+
return Number(aged) + Number(over);
|
|
17559
|
+
}
|
|
17526
17560
|
/** One run's full checkpoint history, oldest first. */
|
|
17527
17561
|
checkpointHistory(run) {
|
|
17528
17562
|
return this.db.prepare("SELECT * FROM run_checkpoint WHERE run = ? ORDER BY id ASC").all(run).map(readRunCheckpoint);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a running agent did last, recorded and read (live tasks).
|
|
3
|
+
*
|
|
4
|
+
* The recorder sits beside the live log on the agent's stream and keeps one
|
|
5
|
+
* row per run: a fixed kind from live.ts's vocabulary and when. No text, no
|
|
6
|
+
* tool names, no arguments are ever kept, so nothing here can leak a secret.
|
|
7
|
+
* The reader picks the newer of that row and the run's latest progress
|
|
8
|
+
* checkpoint, and asks the runner's heartbeat whether the worker still
|
|
9
|
+
* answers. Neither side reads or stats a live-log file.
|
|
10
|
+
*/
|
|
11
|
+
import { type RunActivity } from "./activity-line.js";
|
|
12
|
+
import type { Store } from "./store.js";
|
|
13
|
+
type Observer = {
|
|
14
|
+
observe(event: Record<string, unknown>): void;
|
|
15
|
+
close(): void;
|
|
16
|
+
};
|
|
17
|
+
/** Watch a run's stream for what the agent does, alongside its live log (which may be null). Fail-soft: a write
|
|
18
|
+
* that fails never touches the run. */
|
|
19
|
+
export declare function withRunActivity(liveLog: Observer | null, store: Pick<Store, "recordRunActivity">, run: number, clock: () => Date): Observer;
|
|
20
|
+
/** A live run's last activity: the newer of its recorded kind and its latest progress checkpoint, else when it
|
|
21
|
+
* started; and "offline" only when its registered worker's heartbeat has lapsed. */
|
|
22
|
+
export declare function runActivityOf(store: Store, run: {
|
|
23
|
+
id: number;
|
|
24
|
+
runner: string;
|
|
25
|
+
startedAt: string;
|
|
26
|
+
}, now: Date): RunActivity;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a running agent did last, recorded and read (live tasks).
|
|
3
|
+
*
|
|
4
|
+
* The recorder sits beside the live log on the agent's stream and keeps one
|
|
5
|
+
* row per run: a fixed kind from live.ts's vocabulary and when. No text, no
|
|
6
|
+
* tool names, no arguments are ever kept, so nothing here can leak a secret.
|
|
7
|
+
* The reader picks the newer of that row and the run's latest progress
|
|
8
|
+
* checkpoint, and asks the runner's heartbeat whether the worker still
|
|
9
|
+
* answers. Neither side reads or stats a live-log file.
|
|
10
|
+
*/
|
|
11
|
+
import { ACTIVITY_WORDS } from "./activity-line.js";
|
|
12
|
+
import { liveActivityKind } from "./live.js";
|
|
13
|
+
import { isAlive } from "./runner.js";
|
|
14
|
+
/** At most one write per run this often while the kind stays the same: the age is in seconds, not milliseconds. */
|
|
15
|
+
const SAME_KIND_EVERY_MS = 5_000;
|
|
16
|
+
/** Watch a run's stream for what the agent does, alongside its live log (which may be null). Fail-soft: a write
|
|
17
|
+
* that fails never touches the run. */
|
|
18
|
+
export function withRunActivity(liveLog, store, run, clock) {
|
|
19
|
+
let last = null;
|
|
20
|
+
return {
|
|
21
|
+
observe(event) {
|
|
22
|
+
liveLog?.observe(event);
|
|
23
|
+
const kind = liveActivityKind(event);
|
|
24
|
+
if (kind === null)
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
const now = clock();
|
|
28
|
+
if (last !== null && last.kind === kind && now.getTime() - last.at < SAME_KIND_EVERY_MS)
|
|
29
|
+
return;
|
|
30
|
+
store.recordRunActivity(run, kind, now);
|
|
31
|
+
last = { kind, at: now.getTime() };
|
|
32
|
+
}
|
|
33
|
+
catch { /* display state: the next event tries again */ }
|
|
34
|
+
},
|
|
35
|
+
close() { liveLog?.close(); },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** A live run's last activity: the newer of its recorded kind and its latest progress checkpoint, else when it
|
|
39
|
+
* started; and "offline" only when its registered worker's heartbeat has lapsed. */
|
|
40
|
+
export function runActivityOf(store, run, now) {
|
|
41
|
+
const recorded = store.runActivity(run.id);
|
|
42
|
+
let what = ACTIVITY_WORDS["started"], at = run.startedAt;
|
|
43
|
+
if (recorded.kind !== null && recorded.at !== null && recorded.at >= at) {
|
|
44
|
+
what = ACTIVITY_WORDS[recorded.kind] ?? ACTIVITY_WORDS["tool"];
|
|
45
|
+
at = recorded.at;
|
|
46
|
+
}
|
|
47
|
+
if (recorded.progressAt !== null && recorded.progressAt > at) {
|
|
48
|
+
what = ACTIVITY_WORDS["progress"];
|
|
49
|
+
at = recorded.progressAt;
|
|
50
|
+
}
|
|
51
|
+
const worker = store.getRunner(run.runner)?.runner;
|
|
52
|
+
return { what, at, worker: worker === undefined || isAlive(worker, now) ? "online" : "offline" };
|
|
53
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import { type Store } from "./store.js";
|
|
3
|
+
export type TaskViewer = {
|
|
4
|
+
name: string;
|
|
5
|
+
response: ServerResponse;
|
|
6
|
+
valid: () => boolean;
|
|
7
|
+
};
|
|
8
|
+
/** What changes when anything on the task page changes: one read over the family, kept short and opaque. */
|
|
9
|
+
export declare function taskFingerprint(store: Store, root: string, now: Date, livenessMs?: number): string | null;
|
|
10
|
+
export type TaskRooms = {
|
|
11
|
+
/** Keep this page's stream on the task family until it closes; the first events say where it stands and who's here. */
|
|
12
|
+
join: (root: string, viewer: TaskViewer) => void;
|
|
13
|
+
/** Every stream, ended (the server is closing). */
|
|
14
|
+
close: () => void;
|
|
15
|
+
size: () => number;
|
|
16
|
+
};
|
|
17
|
+
export declare function createTaskRooms(fingerprint: (root: string) => string | null, tickMs?: number): TaskRooms;
|