toolroll 0.9.100 → 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 +15 -0
- package/dist/accent-colors.js +1 -1
- package/dist/activity-line.d.ts +1 -1
- package/dist/activity-line.js +1 -1
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +40 -40
- package/dist/browser-workspace.d.ts +78 -15
- package/dist/browser-workspace.js +5 -3
- package/dist/design/kit.css +94 -2
- package/dist/design/kit.d.ts +12 -4
- package/dist/design/kit.js +8 -2
- package/dist/design/tokens.css +5 -5
- package/dist/desktop-update.d.ts +1 -1
- package/dist/first-run.d.ts +10 -6
- package/dist/first-run.js +19 -18
- package/dist/guides.js +1 -1
- package/dist/operate.js +2 -0
- 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/scope.d.ts +5 -0
- package/dist/scope.js +8 -1
- package/dist/serve.js +51 -9
- package/dist/server/chrome.d.ts +11 -2
- package/dist/server/chrome.js +10 -1
- package/dist/server/console.css +65 -21
- 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/kit.d.ts +36 -3
- package/dist/server/kit.js +33 -4
- package/dist/server/pages.js +23 -2
- package/dist/server/render-chat.d.ts +2 -0
- package/dist/server/render-chat.js +1 -0
- package/dist/server/render-pages.js +1 -1
- package/dist/server/render-results.js +3 -3
- package/dist/server/render-tasks.d.ts +10 -0
- package/dist/server/render-tasks.js +245 -65
- package/dist/server/request-context.d.ts +14 -0
- package/dist/server/request-context.js +3 -0
- package/dist/server/task-views.js +11 -4
- package/dist/server/tasks.d.ts +8 -2
- package/dist/server/tasks.js +31 -17
- package/dist/settings-groups-ui.js +1 -1
- package/dist/store.js +14 -3
- package/dist/task-archive.d.ts +3 -0
- package/dist/task-archive.js +5 -0
- package/dist/task-status.d.ts +2 -2
- package/dist/task-status.js +3 -3
- package/dist/tui.js +1 -1
- package/dist/work-index.d.ts +14 -1
- package/dist/work-index.js +24 -8
- package/dist/workspace-ui.d.ts +69 -0
- package/dist/workspace-ui.js +103 -0
- package/package.json +1 -1
package/dist/work-index.js
CHANGED
|
@@ -22,10 +22,12 @@ import { LANDED_ACTOR, landedReasonOf } from './result-completion.js';
|
|
|
22
22
|
import { ASKS, CODE_READING, STORAGE_HOLD_PREFIX, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, isWaitCode, processNeedOf, resultHoldUpSentence } from './needs-you.js';
|
|
23
23
|
import { retryWhenWords } from './dispatch.js';
|
|
24
24
|
import { LEAD_CLAIM_SQL, LEAD_IDLE_MS } from './lead-voice.js';
|
|
25
|
+
export const WORK_ASK_GROUPS = ['approve', 'answer', 'wrong', 'review', 'other'];
|
|
25
26
|
export const WORK_INDEX_PAGE_LIMIT = 40;
|
|
26
27
|
export const WORK_INDEX_MAX_LIMIT = 100;
|
|
27
28
|
const EMPTY = { all: 0, 'needs-you': 0, ready: 0, running: 0, queued: 0, completed: 0, archived: 0 };
|
|
28
29
|
const NO_GROUPS = { decide: 0, review: 0, unblock: 0, building: 0, rest: 0 };
|
|
30
|
+
const NO_ASKS = { approve: 0, answer: 0, wrong: 0, review: 0, other: 0 };
|
|
29
31
|
const FAMILY_PROBLEM = 'Task history is unavailable or incomplete. This task is shown separately.';
|
|
30
32
|
const VIEWS = ['all', 'needs-you', 'ready', 'running', 'queued', 'completed', 'archived'];
|
|
31
33
|
/** The family's current version is put away (v145): it waits on nobody and only the Archived view lists it. */
|
|
@@ -70,6 +72,11 @@ export function askOfCode(code, custody = false) {
|
|
|
70
72
|
const codesOf = (ask) => PERSON_CODES.filter(code => askOfCode(code) === ask).map(code => `'${code}'`).join(',');
|
|
71
73
|
/** The ask as its place in the list (ASKS order), read by the same code the row's status reads. */
|
|
72
74
|
const ASK_RANK = `CASE WHEN code IN (${codesOf('decide')}) THEN 0 WHEN code IN (${codesOf('review')}) AND NOT custody_unresolved THEN 1 ELSE 2 END`;
|
|
75
|
+
/** Home's ask group of a row, by its code alone, so a row's group and its group's count are one rule: a plan waiting
|
|
76
|
+
* for its yes, a question, a build that failed or stopped unexpectedly, a saved result to review (ASK_RANK's Review),
|
|
77
|
+
* and everything else that waits on a person. Read on Needs you rows only. */
|
|
78
|
+
const ASK_GROUP_SQL = `CASE WHEN code='needs-approval' THEN 'approve' WHEN code IN ('waiting-decision','decision-queue') THEN 'answer'
|
|
79
|
+
WHEN code IN ('failed','waiting-incident','vanished-run') THEN 'wrong' WHEN code IN (${codesOf('review')}) AND NOT custody_unresolved THEN 'review' ELSE 'other' END`;
|
|
73
80
|
/** Where a row sits in the list: Decide, Review, Unblock, then building, then the rest by recency. */
|
|
74
81
|
const ORDER = `(CASE WHEN rank=0 THEN ask_rank WHEN rank=1 THEN 3 ELSE 4 END)`;
|
|
75
82
|
/** A row whose build may still be running (the store's reading, custodyReadings): Waiting, not Needs you. */
|
|
@@ -261,7 +268,7 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
261
268
|
${ARCHIVED} archived,
|
|
262
269
|
CASE WHEN code='complete' THEN 3 WHEN code='cancelled' THEN 4 WHEN code='running' THEN 1
|
|
263
270
|
WHEN ${CUSTODY_WAITS} OR code IN (${WAITING_SQL.replace(/'(?:cancelled|complete|running)',/g, '')}) THEN 2 ELSE 0 END rank,
|
|
264
|
-
${ASK_RANK} ask_rank,
|
|
271
|
+
${ASK_RANK} ask_rank,${ASK_GROUP_SQL} ask_group,
|
|
265
272
|
CASE WHEN code='complete' THEN completed.at ELSE family_updated END sort_at,
|
|
266
273
|
completed.actor checked_actor,completed.at checked_at,completed.outcome checked_digest,completed.detail checked_detail
|
|
267
274
|
FROM claimed c LEFT JOIN action_ledger completed ON completed.id=c.checked_id WHERE ($state IS NULL OR c.state=$state) AND ($leadId IS NULL OR EXISTS(SELECT 1 FROM team_task_owner own WHERE own.task_ref=c.root_ref AND own.lead=$leadId))
|
|
@@ -364,6 +371,12 @@ const TOTALS = `COALESCE(SUM(archived=0),0) all_count,COALESCE(SUM(needs),0) nee
|
|
|
364
371
|
+ `COALESCE(SUM(${QUEUED}),0) queued_count,COALESCE(SUM(code='complete' AND archived=0),0) completed_count,COALESCE(SUM(archived),0) archived_count`;
|
|
365
372
|
const GROUP_TOTALS = `COALESCE(SUM(rank=0 AND ask_rank=0 AND archived=0),0) decide_count,COALESCE(SUM(rank=0 AND ask_rank=1 AND archived=0),0) review_count,COALESCE(SUM(rank=0 AND ask_rank=2 AND archived=0),0) unblock_count,`
|
|
366
373
|
+ `COALESCE(SUM(rank=1 AND archived=0),0) building_count,COALESCE(SUM(rank>1 AND archived=0),0) rest_count`;
|
|
374
|
+
/** The exact Needs you totals per ask group, and what was completed since the page's `completedSince`: page reads only. */
|
|
375
|
+
const ASK_TOTALS = WORK_ASK_GROUPS.map(group => `COALESCE(SUM(needs AND ask_group='${group}'),0) ask_${group}_count`).join(',')
|
|
376
|
+
+ `,COALESCE(SUM(code='complete' AND archived=0 AND $completedSince IS NOT NULL AND sort_at>=$completedSince),0) completed_since_count`;
|
|
377
|
+
function askCounts(row) {
|
|
378
|
+
return Object.fromEntries(WORK_ASK_GROUPS.map(group => [group, n(row, `ask_${group}_count`)]));
|
|
379
|
+
}
|
|
367
380
|
function groupCounts(row) {
|
|
368
381
|
return { decide: n(row, 'decide_count'), review: n(row, 'review_count'), unblock: n(row, 'unblock_count'), building: n(row, 'building_count'), rest: n(row, 'rest_count') };
|
|
369
382
|
}
|
|
@@ -374,7 +387,8 @@ function cursorScope(access, options, view) {
|
|
|
374
387
|
return createHash('sha256').update(JSON.stringify({ principal: access.principal,
|
|
375
388
|
repos: access.repos === null ? null : [...new Set(access.repos)].sort(),
|
|
376
389
|
unplaced: access.principal === 'operator' && access.includeUnplaced === true,
|
|
377
|
-
leadId: options.leadId ?? null, project: options.project ?? null, state: options.state ?? null, view
|
|
390
|
+
leadId: options.leadId ?? null, project: options.project ?? null, state: options.state ?? null, view,
|
|
391
|
+
...(options.completedSince === undefined ? {} : { completedSince: options.completedSince }) })).digest('hex');
|
|
378
392
|
}
|
|
379
393
|
function readCursor(value, scope) {
|
|
380
394
|
if (value == null)
|
|
@@ -413,18 +427,18 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
413
427
|
const view = VIEWS.includes(options.view ?? 'all') ? options.view ?? 'all' : 'all';
|
|
414
428
|
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(WORK_INDEX_MAX_LIMIT, Math.floor(options.limit))) : WORK_INDEX_PAGE_LIMIT;
|
|
415
429
|
const scope = cursorScope(access, options, view), cursor = readCursor(options.cursor, scope);
|
|
416
|
-
const filter = view === 'archived' ? 'archived=1' : view === 'needs-you' ? 'needs=1' : view === 'ready' ? READY : view === 'running' ? 'family_running=1 AND archived=0' : view === 'queued' ? QUEUED : view === 'completed' ? "code='complete' AND archived=0" : 'archived=0';
|
|
430
|
+
const filter = view === 'archived' ? 'archived=1' : view === 'needs-you' ? 'needs=1' : view === 'ready' ? READY : view === 'running' ? 'family_running=1 AND archived=0' : view === 'queued' ? QUEUED : view === 'completed' ? "code='complete' AND archived=0 AND ($completedSince IS NULL OR sort_at>=$completedSince)" : 'archived=0';
|
|
417
431
|
const rows = prepared(store, `${projection}, selected_page AS MATERIALIZED (
|
|
418
432
|
SELECT * FROM ranked WHERE ($taskId IS NOT NULL OR ${filter}) AND ($taskId IS NULL OR root_ref IN (SELECT root_ref FROM members WHERE id=$taskId)) AND ($roots IS NULL OR root_id IN (SELECT value FROM json_each($roots))) AND ($cursorRoot=0 OR
|
|
419
433
|
${ORDER}>$cursorRank OR (${ORDER}=$cursorRank AND (sort_at<$cursorAt OR (sort_at=$cursorAt AND root_ref<$cursorRoot))))
|
|
420
434
|
ORDER BY ${ORDER},sort_at DESC,root_ref DESC LIMIT $limit
|
|
421
|
-
), page AS (SELECT p.*,COALESCE(p.result_id,(SELECT id FROM run WHERE task_ref=p.ref_id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1)) page_result_id FROM selected_page p), totals AS (SELECT ${TOTALS},${GROUP_TOTALS} FROM ranked), project_totals AS (
|
|
435
|
+
), page AS (SELECT p.*,COALESCE(p.result_id,(SELECT id FROM run WHERE task_ref=p.ref_id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1)) page_result_id FROM selected_page p), totals AS (SELECT ${TOTALS},${GROUP_TOTALS},${ASK_TOTALS} FROM ranked), project_totals AS (
|
|
422
436
|
SELECT repo,${TOTALS},SUM(state='queued' AND family_running=0 AND archived=0) queued,
|
|
423
437
|
SUM(state='done' AND family_running=0 AND archived=0 AND updated_at>=$recent) done_recently FROM ranked GROUP BY repo
|
|
424
438
|
)
|
|
425
439
|
SELECT 0 record_kind,totals.*,(SELECT json_group_array(json_object('repo',repo,'all_count',all_count,'needs_count',needs_count,'ready_count',ready_count,
|
|
426
440
|
'running_count',running_count,'queued_count',queued_count,'completed_count',completed_count,'archived_count',archived_count,'queued',queued,'done_recently',done_recently)) FROM project_totals) row_json FROM totals
|
|
427
|
-
UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,0,0,0,json_object('archived',archived,'archived_by',(SELECT by FROM task_archive WHERE task_ref=page.ref_id),
|
|
441
|
+
UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,0,0,0,${WORK_ASK_GROUPS.map(() => '0,').join('')}0,json_object('archived',archived,'archived_by',(SELECT by FROM task_archive WHERE task_ref=page.ref_id),
|
|
428
442
|
'archived_kind',(SELECT by_kind FROM task_archive WHERE task_ref=page.ref_id),'archived_reason',(SELECT reason FROM task_archive WHERE task_ref=page.ref_id),
|
|
429
443
|
'archived_at',(SELECT at FROM task_archive WHERE task_ref=page.ref_id),'root_ref',root_ref,'root_id',root_id,'id',id,'root_title',root_title,'repo',repo,
|
|
430
444
|
'state',state,'created_at',created_at,'family_updated',family_updated,'version_count',version_count,'earlier_active',earlier_active,
|
|
@@ -436,7 +450,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
436
450
|
'check_follows',(SELECT json_group_array(q.outcome||' '||COALESCE((SELECT substr(f.outcome,instr(f.outcome,':')+1) FROM action_ledger f WHERE f.repo IS page.repo AND f.run_id=page_result_id
|
|
437
451
|
AND f.action='checks finished' AND f.outcome LIKE q.id||':%' LIMIT 1),'open')) FROM (SELECT id,outcome FROM action_ledger WHERE repo IS page.repo AND run_id=page_result_id AND action='checks requested' ORDER BY id) q),
|
|
438
452
|
'proof_verdict',(SELECT COALESCE(machine_verdict,verdict) FROM proof_verdict WHERE run=page_result_id),
|
|
439
|
-
'proof_failed_check',(SELECT verdict='refuted' AND reasons_json LIKE '%approved verification command exited%' FROM proof_verdict WHERE run=page_result_id),'code',code,'needs',needs,'person_need',person_need,'lead_claim',lead_claim,'family_running',family_running,'rank',rank,'ask_rank',ask_rank,'list_order',${ORDER},'sort_at',sort_at,
|
|
453
|
+
'proof_failed_check',(SELECT verdict='refuted' AND reasons_json LIKE '%approved verification command exited%' FROM proof_verdict WHERE run=page_result_id),'code',code,'ask_group',ask_group,'needs',needs,'person_need',person_need,'lead_claim',lead_claim,'family_running',family_running,'rank',rank,'ask_rank',ask_rank,'list_order',${ORDER},'sort_at',sort_at,
|
|
440
454
|
'checked_actor',checked_actor,'checked_at',checked_at,'checked_digest',checked_digest,'checked_detail',checked_detail,'question_id',question_id,
|
|
441
455
|
'dependency_id',dependency_id,'dependency_state',dependency_state,'hold_until',hold_until,'attempt_run',(SELECT id FROM run WHERE task_ref IN (SELECT ref_id FROM members WHERE root_ref=page.root_ref) AND (finished_at IS NOT NULL OR outcome IS NOT NULL) AND role NOT IN ('planner','reviewer') ORDER BY id DESC LIMIT 1),'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,600),'hold_id',hold_id,'live_lease',live_lease IS NOT NULL,'approved',COALESCE(approved,0),
|
|
442
456
|
'plan_changed',scope_digest IS NOT NULL AND digest IS NOT scope_digest,'head_ok',length(head_revision)=40 AND head_revision NOT GLOB '*[^a-f0-9]*','stop_run',stop_run,'unfinished',unfinished,'signed_out',signed_out,
|
|
@@ -444,7 +458,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
444
458
|
'agent_ask_run',(SELECT run FROM agent_question WHERE id=page.agent_ask),'agent_ask_text',(SELECT substr(question,1,300) FROM agent_question WHERE id=page.agent_ask),'replaced_by',(SELECT successor FROM task_replacement WHERE task_ref=page.ref_id),
|
|
445
459
|
'completed_by_lead',(SELECT lead FROM task_act WHERE task_ref=page.root_ref AND act='completed' ORDER BY id DESC LIMIT 1),'machine_check',COALESCE(machine_check,0),
|
|
446
460
|
'question_task',(SELECT r.external_id FROM decision d JOIN run ON run.id=d.run JOIN task_ref r ON r.id=run.task_ref WHERE d.id=page.question_id)) FROM page`)
|
|
447
|
-
.all({ ...parameters(now, access, options, custody), $taskId: taskId, $roots: options.roots === undefined ? null : JSON.stringify(options.roots), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
|
|
461
|
+
.all({ ...parameters(now, access, options, custody), $taskId: taskId, $roots: options.roots === undefined ? null : JSON.stringify(options.roots), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $completedSince: options.completedSince ?? null, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
|
|
448
462
|
const selected = rows.slice(1).map(row => JSON.parse(String(row['row_json'])));
|
|
449
463
|
const page = selected.slice(0, limit), last = page.at(-1);
|
|
450
464
|
const nextCursor = selected.length > limit && last !== undefined ? Buffer.from(JSON.stringify({ version: 2, scope,
|
|
@@ -462,7 +476,8 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
462
476
|
const memo = new Map();
|
|
463
477
|
const readiness = (row, checks) => resultReadinessFacts(store, now, options.root, row, checks, memo);
|
|
464
478
|
return { items: page.map(row => itemOf(row, access.principal, probe, leadName, readiness, options.root !== undefined, now)), projects, totals: rows[0] === undefined ? { ...EMPTY } : counts(rows[0]),
|
|
465
|
-
groups: rows[0] === undefined ? { ...NO_GROUPS } : groupCounts(rows[0]),
|
|
479
|
+
groups: rows[0] === undefined ? { ...NO_GROUPS } : groupCounts(rows[0]), asks: rows[0] === undefined ? { ...NO_ASKS } : askCounts(rows[0]),
|
|
480
|
+
completedSince: rows[0] === undefined ? 0 : n(rows[0], 'completed_since_count'), nextCursor, limit, view };
|
|
466
481
|
}
|
|
467
482
|
export function workCountsByProject(store, now, access) {
|
|
468
483
|
const projection = registerValidators(store);
|
|
@@ -757,6 +772,7 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false, no
|
|
|
757
772
|
status: { token: `assignment-${assignmentState}`, label, detail, tone: workToneOf(shared.headline),
|
|
758
773
|
action: primaryAction === null ? null : { label: actionLabel, kind: actionCode === 'open-result' ? 'open-result' : 'open-task' }, views, rank: n(row, 'rank') },
|
|
759
774
|
ask: need ? ASKS[n(row, 'ask_rank')] ?? 'unblock' : null,
|
|
775
|
+
askGroup: need ? WORK_ASK_GROUPS.find(group => group === s(row, 'ask_group')) ?? 'other' : null,
|
|
760
776
|
chip: need ? askChipOf({ headline: shared.headline, need: shared.need?.key ?? null, planChanged, mismatch: verdict === 'refuted' && n(row, 'proof_failed_check') !== 1, ask: ASKS[n(row, 'ask_rank')] ?? 'unblock' }) : null,
|
|
761
777
|
primaryAction, completion: code === 'complete' ? { actor: String(row['checked_actor']), at: String(row['checked_at']), digest: String(row['checked_digest']), ...(landed === null ? {} : { landed }) } : null,
|
|
762
778
|
evidence: 'recorded', ...(lead === 'on-it' ? { lead: leadOnIt(leadName) } : {}), ...(archived === null ? {} : { archived }) };
|
package/dist/workspace-ui.d.ts
CHANGED
|
@@ -314,3 +314,72 @@ export type BuildProgress = {
|
|
|
314
314
|
export declare function buildProgressOf(steps: readonly BuildStep[] | null | undefined): BuildProgress | null;
|
|
315
315
|
/** Earlier attempts that stopped before the one now running, in one quiet line. */
|
|
316
316
|
export declare function earlierAttemptsWords(count: number): string;
|
|
317
|
+
/** One row of a task page's steps (docs/plans/task-page-steps.md): the step's label, how its marker reads, one plain
|
|
318
|
+
* sentence and a time, whether it has something to open, and whether it starts open. `mark`: done (an ink disc with
|
|
319
|
+
* a check), needs (waits on this person: ink), live (something runs now: blue), wait (current, waiting on nothing
|
|
320
|
+
* this person does), stuck (stopped there: vermilion), skipped (a dashed ring), todo (not reached: grey). */
|
|
321
|
+
export type StepRow = {
|
|
322
|
+
key: "plan" | "approve" | "build" | "checks" | "ready";
|
|
323
|
+
label: string;
|
|
324
|
+
state: "done" | "current" | "stuck" | "skipped" | "todo";
|
|
325
|
+
mark: "done" | "needs" | "live" | "wait" | "stuck" | "skipped" | "todo";
|
|
326
|
+
sentence: string;
|
|
327
|
+
/** When the step happened (ISO), shown as a clock time; null when nothing recorded says. */
|
|
328
|
+
at: string | null;
|
|
329
|
+
needsYou: boolean;
|
|
330
|
+
/** It holds something to read or do: a finished row opens on press, the current one is open. */
|
|
331
|
+
opens: boolean;
|
|
332
|
+
open: boolean;
|
|
333
|
+
};
|
|
334
|
+
/** What `stepRowsOf` reads: facts the task page's view already carries. */
|
|
335
|
+
export type StepRowFacts = {
|
|
336
|
+
journey?: readonly {
|
|
337
|
+
key: StepRow["key"];
|
|
338
|
+
label: string;
|
|
339
|
+
state: StepRow["state"];
|
|
340
|
+
needsYou?: true;
|
|
341
|
+
say?: string;
|
|
342
|
+
}[] | null;
|
|
343
|
+
scout: boolean;
|
|
344
|
+
status: {
|
|
345
|
+
status: {
|
|
346
|
+
headline: string;
|
|
347
|
+
};
|
|
348
|
+
attempts: readonly unknown[];
|
|
349
|
+
} | null;
|
|
350
|
+
/** Builds that stopped before this one: with more than one version, the Build row's history. */
|
|
351
|
+
earlier?: {
|
|
352
|
+
attempts: readonly unknown[];
|
|
353
|
+
} | null;
|
|
354
|
+
approved?: {
|
|
355
|
+
by: string;
|
|
356
|
+
at: string;
|
|
357
|
+
automatic: boolean;
|
|
358
|
+
rule: string | null;
|
|
359
|
+
} | null;
|
|
360
|
+
approve?: {
|
|
361
|
+
heading: string;
|
|
362
|
+
} | null;
|
|
363
|
+
approval?: string;
|
|
364
|
+
progress?: {
|
|
365
|
+
line: string;
|
|
366
|
+
} | null;
|
|
367
|
+
plan?: {
|
|
368
|
+
lead: string | null;
|
|
369
|
+
by: string | null;
|
|
370
|
+
at: string | null;
|
|
371
|
+
planning: boolean;
|
|
372
|
+
updating?: boolean;
|
|
373
|
+
} | null;
|
|
374
|
+
result?: {
|
|
375
|
+
selected: unknown;
|
|
376
|
+
} | null;
|
|
377
|
+
thread?: readonly {
|
|
378
|
+
kind: string;
|
|
379
|
+
at: string;
|
|
380
|
+
}[];
|
|
381
|
+
release?: unknown;
|
|
382
|
+
};
|
|
383
|
+
/** The task page's rows, from its road (first-run.ts taskRoad) and the facts beside it. Null when the task has no road
|
|
384
|
+
* (cancelled, or no status): the page keeps its status block then. Pure: no clock, no store. */
|
|
385
|
+
export declare function stepRowsOf(view: StepRowFacts): StepRow[] | null;
|
package/dist/workspace-ui.js
CHANGED
|
@@ -624,3 +624,106 @@ export function buildProgressOf(steps) {
|
|
|
624
624
|
export function earlierAttemptsWords(count) {
|
|
625
625
|
return `${count} earlier attempt${count === 1 ? "" : "s"} stopped`;
|
|
626
626
|
}
|
|
627
|
+
/** The task page's rows, from its road (first-run.ts taskRoad) and the facts beside it. Null when the task has no road
|
|
628
|
+
* (cancelled, or no status): the page keeps its status block then. Pure: no clock, no store. */
|
|
629
|
+
export function stepRowsOf(view) {
|
|
630
|
+
const road = view.journey ?? null;
|
|
631
|
+
if (road === null || road.length === 0)
|
|
632
|
+
return null;
|
|
633
|
+
const headline = view.status?.status.headline ?? null;
|
|
634
|
+
const runs = headline === "Planning" || headline === "Building";
|
|
635
|
+
const approvalOpen = view.approve != null || road.some(step => step.key === "approve" && step.state === "current");
|
|
636
|
+
const hasResult = view.result != null;
|
|
637
|
+
// Earlier versions or stopped builds: the Build row holds them, and opens for them before the next build starts.
|
|
638
|
+
const hasHistory = (view.status?.attempts.length ?? 0) > 1 || (view.earlier?.attempts.length ?? 0) > 0;
|
|
639
|
+
const lastResult = [...(view.thread ?? [])].filter(one => one.kind === "result").map(one => one.at).sort().at(-1) ?? null;
|
|
640
|
+
const checksStuck = road.some(step => step.key === "checks" && step.state === "stuck");
|
|
641
|
+
// A step stopped there, in a word (its box under it says why).
|
|
642
|
+
const stoppedWord = headline === "Failed" ? "Failed" : headline === "Stopped" ? "Stopped" : headline === "Waiting" || headline === "Queued" ? "Waiting" : "Stopped here";
|
|
643
|
+
const words = (step) => {
|
|
644
|
+
const here = step.state === "current" || step.state === "stuck";
|
|
645
|
+
switch (step.key) {
|
|
646
|
+
case "plan":
|
|
647
|
+
if (step.state === "done" || step.state === "skipped") {
|
|
648
|
+
const by = view.plan?.by ?? null;
|
|
649
|
+
if (approvalOpen)
|
|
650
|
+
return { sentence: by === null ? "Shown below" : `Written by ${by} · shown below`, at: view.plan?.at ?? null };
|
|
651
|
+
return { sentence: view.plan?.lead ?? (by === null ? "Plan written" : `Written by ${by}`), at: view.plan?.at ?? null };
|
|
652
|
+
}
|
|
653
|
+
// Stopped at Plan with a plan already written (a held task whose plan waits for approval): it says it stopped, never "No plan yet".
|
|
654
|
+
if (here)
|
|
655
|
+
return { sentence: view.plan?.updating === true ? "Updating the plan" : view.plan?.planning === true || headline === "Planning" ? "Writing the plan" : step.needsYou ? ""
|
|
656
|
+
: step.state === "stuck" && view.approve != null ? stoppedWord : "No plan yet", at: null };
|
|
657
|
+
return { sentence: "A plan for you to approve", at: null };
|
|
658
|
+
case "approve":
|
|
659
|
+
if (step.state === "done") {
|
|
660
|
+
const by = view.approved ?? null;
|
|
661
|
+
return { sentence: by === null ? "Approved" : by.automatic ? "Approved automatically" : `Approved by ${by.by}`, at: by?.at ?? null };
|
|
662
|
+
}
|
|
663
|
+
if (step.state === "skipped")
|
|
664
|
+
return { sentence: "No approval was asked for", at: null };
|
|
665
|
+
if (here)
|
|
666
|
+
return { sentence: step.needsYou ? "" : "Waiting for approval", at: null };
|
|
667
|
+
return { sentence: view.scout ? "You approve before it starts" : "You approve before anything is built", at: null };
|
|
668
|
+
case "build":
|
|
669
|
+
if (step.state === "done")
|
|
670
|
+
return { sentence: view.scout ? "The investigation finished" : "The build finished", at: lastResult };
|
|
671
|
+
if (step.state === "skipped")
|
|
672
|
+
return { sentence: "Didn't run", at: null };
|
|
673
|
+
// The open row's box says the rest: its head says only how the step stands.
|
|
674
|
+
if (here)
|
|
675
|
+
return { sentence: step.needsYou ? "" : runs ? "Working now" : step.state === "stuck" ? stoppedWord : "Waiting to start", at: null };
|
|
676
|
+
return { sentence: `${approvalOpen ? "Starts when you approve" : "Starts after approval"}${hasHistory ? ". Earlier attempts are here." : ""}`, at: null };
|
|
677
|
+
case "checks": {
|
|
678
|
+
const said = step.say ?? null;
|
|
679
|
+
if (step.state === "done")
|
|
680
|
+
return { sentence: "Project checks passed", at: null };
|
|
681
|
+
if (step.state === "skipped")
|
|
682
|
+
return { sentence: said === "Checks not run" ? "The checks didn't run" : said === "Checks at release" ? "Checked at release" : "No checks were recorded", at: null };
|
|
683
|
+
if (step.state === "stuck")
|
|
684
|
+
return { sentence: step.needsYou && said === null ? "" : said === "Checks unreadable" ? "The checks couldn't be read" : said === "Checks failed" ? "The checks failed" : stoppedWord, at: null };
|
|
685
|
+
if (step.state === "current")
|
|
686
|
+
return { sentence: said === "Checks running" ? "The checks are running" : step.needsYou ? "" : "Waiting for the checks", at: null };
|
|
687
|
+
return { sentence: "The project's checks run on the result", at: null };
|
|
688
|
+
}
|
|
689
|
+
case "ready":
|
|
690
|
+
if (step.state === "done")
|
|
691
|
+
return { sentence: headline === "Complete" ? "Finished" : view.scout ? "Report ready" : "Ready", at: lastResult };
|
|
692
|
+
if (here)
|
|
693
|
+
return { sentence: step.needsYou ? "" : step.state === "stuck" ? stoppedWord : "Waiting", at: null };
|
|
694
|
+
if (hasResult)
|
|
695
|
+
return { sentence: checksStuck ? "Not ready: the checks didn't pass. The result is here." : "Not ready yet. The result so far is here.", at: null };
|
|
696
|
+
return { sentence: view.scout ? "A report for you to read" : "A result for you to review", at: null };
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
const rows = road.map((step) => {
|
|
700
|
+
const said = words(step);
|
|
701
|
+
const here = step.state === "current" || step.state === "stuck";
|
|
702
|
+
const needsYou = step.needsYou === true && here;
|
|
703
|
+
const mark = step.state === "done" ? "done" : step.state === "skipped" ? "skipped" : step.state === "todo" ? "todo"
|
|
704
|
+
// Vermilion only for a failure: a stopped or held step waits, in grey.
|
|
705
|
+
: needsYou ? "needs" : step.state === "stuck" ? (headline === "Failed" || step.say === "Checks failed" || step.say === "Checks unreadable" ? "stuck" : "wait")
|
|
706
|
+
: runs || step.say === "Checks running" ? "live" : "wait";
|
|
707
|
+
// A step not reached opens only when it holds something already there: a result before Ready was reached, or
|
|
708
|
+
// earlier attempts before the next build starts.
|
|
709
|
+
// An approval this person can give is always on the page, open, wherever the road stands (a held task stops at
|
|
710
|
+
// Plan with its plan still waiting for a yes): the Approval row holds its form.
|
|
711
|
+
const approvalHere = step.key === "approve" && view.approve != null;
|
|
712
|
+
const opens = step.state !== "todo" || (step.key === "ready" && hasResult) || (step.key === "build" && hasHistory) || approvalHere;
|
|
713
|
+
return { key: step.key, label: step.label, state: step.state, mark, sentence: said.sentence, at: said.at, needsYou, opens, open: here || approvalHere };
|
|
714
|
+
});
|
|
715
|
+
// A finished road whose task still waits on this person (a result to review): the last step it reached is theirs.
|
|
716
|
+
const waitsOnViewer = headline === "Ready for review" || headline === "Needs you";
|
|
717
|
+
if (waitsOnViewer && !rows.some(row => row.state === "current" || row.state === "stuck")) {
|
|
718
|
+
const last = [...rows].reverse().find(row => row.state === "done");
|
|
719
|
+
if (last !== undefined)
|
|
720
|
+
Object.assign(last, { state: "current", mark: "needs", needsYou: true, sentence: "", at: null, open: true, opens: true });
|
|
721
|
+
}
|
|
722
|
+
// Nothing happening (a finished task): the last step it reached is the one open.
|
|
723
|
+
if (!rows.some(row => row.open)) {
|
|
724
|
+
const last = [...rows].reverse().find(row => row.opens && row.state !== "todo");
|
|
725
|
+
if (last !== undefined)
|
|
726
|
+
last.open = true;
|
|
727
|
+
}
|
|
728
|
+
return rows;
|
|
729
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.102",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|