toolroll 0.9.73 → 0.9.75
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 +63 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-turns.js +5 -0
- package/dist/agents-live.d.ts +31 -0
- package/dist/agents-live.js +43 -0
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +27 -8
- package/dist/browser/THIRD_PARTY_NOTICES.txt +40 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -30
- package/dist/browser-workspace.d.ts +7 -1
- package/dist/builder.js +1 -1
- package/dist/chat-acceptance.d.ts +4 -1
- package/dist/chat-acceptance.js +13 -3
- package/dist/chat-continuity.js +10 -0
- package/dist/chat-quiet.js +3 -3
- package/dist/cli.js +1 -1
- package/dist/coding-handoff.js +4 -3
- package/dist/coding-ui.js +2 -2
- package/dist/coding-workspace.js +16 -4
- package/dist/contracts/console-api.d.ts +5 -1
- package/dist/contracts/console-api.js +10 -0
- package/dist/control-ui.d.ts +3 -2
- package/dist/control-ui.js +7 -2
- package/dist/demo.js +5 -3
- package/dist/deploy-gate.js +5 -2
- package/dist/desktop-update.d.ts +4 -1
- package/dist/desktop-update.js +43 -9
- package/dist/dispose.js +35 -2
- package/dist/exec.js +13 -16
- package/dist/first-run.d.ts +20 -1
- package/dist/first-run.js +71 -0
- package/dist/flow-card-task.js +3 -2
- package/dist/flow-unattended.d.ts +2 -0
- package/dist/flow-unattended.js +12 -1
- package/dist/integrations.d.ts +4 -3
- package/dist/integrations.js +5 -6
- package/dist/invoke.d.ts +13 -0
- package/dist/invoke.js +21 -12
- package/dist/knowledge-ui.d.ts +4 -1
- package/dist/knowledge-ui.js +10 -5
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-follow.js +3 -1
- package/dist/lead-progress.d.ts +4 -1
- package/dist/lead-progress.js +3 -1
- package/dist/lead.js +4 -0
- package/dist/live-bus.d.ts +2 -0
- package/dist/live-bus.js +1 -0
- package/dist/live-children-read.d.ts +52 -0
- package/dist/live-children-read.js +333 -0
- package/dist/live-children.d.ts +73 -0
- package/dist/live-children.js +318 -0
- package/dist/live-detail.d.ts +4 -3
- package/dist/live-detail.js +9 -5
- package/dist/live-input.js +7 -8
- package/dist/live-main-read.d.ts +19 -0
- package/dist/live-main-read.js +91 -0
- package/dist/live-open.d.ts +28 -0
- package/dist/live-open.js +125 -0
- package/dist/live-records.d.ts +10 -1
- package/dist/live-records.js +16 -5
- package/dist/live.d.ts +54 -22
- package/dist/live.js +218 -69
- package/dist/openrouter-models.d.ts +5 -2
- package/dist/openrouter-models.js +7 -4
- package/dist/operate.js +5 -2
- package/dist/peek-cli.d.ts +10 -3
- package/dist/peek-cli.js +17 -8
- package/dist/planner.js +1 -1
- package/dist/project-learning.js +44 -6
- package/dist/project-tools.js +19 -1
- package/dist/provider-limits.d.ts +3 -1
- package/dist/provider-limits.js +5 -2
- package/dist/provider.js +6 -12
- package/dist/pull-request-ui.d.ts +1 -1
- package/dist/pull-request-ui.js +5 -1
- package/dist/push.d.ts +2 -0
- package/dist/push.js +24 -19
- package/dist/read-only-request.d.ts +4 -0
- package/dist/read-only-request.js +15 -0
- package/dist/release-adapters.js +4 -0
- package/dist/release.d.ts +3 -0
- package/dist/release.js +3 -0
- package/dist/repository-context-ui.js +2 -1
- package/dist/repository-context.js +6 -1
- package/dist/result-follow-ups.js +20 -1
- package/dist/result-review.js +5 -1
- package/dist/run-efficiency.d.ts +3 -3
- package/dist/run-efficiency.js +20 -8
- package/dist/scout.d.ts +3 -0
- package/dist/scout.js +5 -3
- package/dist/serve.js +196 -29
- package/dist/server/agents-live-filed.d.ts +17 -0
- package/dist/server/agents-live-filed.js +97 -0
- package/dist/server/agents-live-lead.d.ts +47 -0
- package/dist/server/agents-live-lead.js +116 -0
- package/dist/server/agents-live-room.d.ts +23 -0
- package/dist/server/agents-live-room.js +91 -0
- package/dist/server/chat.js +115 -53
- package/dist/server/chrome.js +2 -2
- package/dist/server/console.css +68 -40
- package/dist/server/http.d.ts +5 -0
- package/dist/server/last-checks.d.ts +17 -0
- package/dist/server/last-checks.js +22 -0
- package/dist/server/live.d.ts +4 -0
- package/dist/server/live.js +42 -2
- package/dist/server/pages.js +98 -4
- package/dist/server/people-tokens.js +7 -4
- package/dist/server/render-chat.d.ts +15 -1
- package/dist/server/render-chat.js +14 -7
- package/dist/server/render-pages.d.ts +10 -5
- package/dist/server/render-pages.js +83 -74
- package/dist/server/render-results.d.ts +11 -2
- package/dist/server/render-results.js +54 -12
- package/dist/server/render-settings.js +3 -3
- package/dist/server/render-tasks.js +13 -3
- package/dist/server/route-table.js +19 -2
- package/dist/server/runtime.d.ts +17 -2
- package/dist/server/settings.js +85 -32
- package/dist/server/task-views.js +1 -2
- package/dist/server/tasks.js +70 -43
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +1 -1
- package/dist/storage.js +2 -1
- package/dist/store.d.ts +34 -1
- package/dist/store.js +108 -11
- package/dist/stream-scope.d.ts +18 -0
- package/dist/stream-scope.js +28 -0
- package/dist/subagent-work.js +3 -0
- package/dist/subscription-chat.js +5 -3
- package/dist/task-status.d.ts +28 -1
- package/dist/task-status.js +93 -11
- package/dist/teardown-git.d.ts +27 -1
- package/dist/teardown-git.js +185 -39
- package/dist/telegram-progress.js +4 -4
- package/dist/telegram-status.js +8 -3
- package/dist/tool-secret-store.js +8 -0
- package/dist/toolroll-update-ui.d.ts +2 -0
- package/dist/toolroll-update-ui.js +4 -3
- package/dist/toolroll-update.js +10 -9
- package/dist/work-index.js +29 -12
- package/dist/workspace-ui.d.ts +5 -0
- package/dist/workspace-ui.js +38 -10
- package/package.json +1 -1
package/dist/server/console.css
CHANGED
|
@@ -172,6 +172,30 @@
|
|
|
172
172
|
color: color-mix(in srgb, var(--running) 72%, var(--foreground)); border-color: var(--border);
|
|
173
173
|
}
|
|
174
174
|
.badge-cut { background: var(--muted); }
|
|
175
|
+
/* The status chip (components/ui/badge.tsx `size="chip"`, after ReUI's light badge with a dot): 20px, 6px
|
|
176
|
+
corners, a soft wash, 11.5px words and a 6px dot. Needs you is the amber wash with its words leaned toward ink
|
|
177
|
+
(4.5:1); the dot is decoration and the word carries the meaning. */
|
|
178
|
+
.chip {
|
|
179
|
+
display: inline-flex; align-items: center; gap: .375rem; height: 1.25rem; padding: 0 .375rem; border-radius: var(--so-radius-sm);
|
|
180
|
+
font-family: var(--font-sans); font-size: .71875rem; font-weight: 500; line-height: 1; white-space: nowrap; vertical-align: middle;
|
|
181
|
+
background: var(--so-neutral-soft); color: var(--so-ink);
|
|
182
|
+
}
|
|
183
|
+
/* Ink words, the tone in the dot (the Agents → Live chip); Needs you keeps amber words leaned toward ink. */
|
|
184
|
+
.chip-dot { width: .375rem; height: .375rem; flex-shrink: 0; border-radius: var(--so-radius-full); background: var(--so-neutral-ink); }
|
|
185
|
+
.chip-needs { background: var(--so-warning-soft); color: color-mix(in oklab, var(--so-warning) 85%, var(--so-ink)); }
|
|
186
|
+
.chip-needs .chip-dot { background: var(--so-warning); }
|
|
187
|
+
.chip-live { background: var(--so-info-soft); }
|
|
188
|
+
.chip-live .chip-dot { background: var(--so-info); }
|
|
189
|
+
.chip-done { background: var(--so-success-soft); }
|
|
190
|
+
.chip-done .chip-dot { background: var(--so-success); }
|
|
191
|
+
.chip-failed { background: var(--so-danger-soft); }
|
|
192
|
+
.chip-failed .chip-dot { background: var(--so-danger); }
|
|
193
|
+
/* The frame (components/ui/frame.tsx, after ReUI's Frame): a raised well with a hairline, its heading on the
|
|
194
|
+
well's fill, and paper panels whose corners sit concentric inside it. No shadow on a panel. */
|
|
195
|
+
.frame { display: flex; flex-direction: column; gap: .25rem; padding: .25rem; border: 1px solid var(--so-line); border-radius: var(--so-radius-card); background: var(--so-raised); }
|
|
196
|
+
.frame-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem; min-height: 2rem; padding: .25rem .625rem; }
|
|
197
|
+
.frame-panel { position: relative; border: 1px solid var(--so-line); border-radius: calc(var(--so-radius-card) - .25rem - 1px); background: var(--so-paper); padding: .75rem .875rem; }
|
|
198
|
+
.frame-panel.waits { border-color: var(--so-warning); }
|
|
175
199
|
|
|
176
200
|
.card {
|
|
177
201
|
border: 1px solid var(--glass-border); border-radius: var(--radius); background: var(--glass);
|
|
@@ -1749,13 +1773,13 @@
|
|
|
1749
1773
|
says which way it folds. */
|
|
1750
1774
|
@media (max-width: 760px) {
|
|
1751
1775
|
.board { display: flex; flex-direction: column; align-items: stretch; gap: .5rem; padding-bottom: 0; }
|
|
1752
|
-
.board .lane { min-height: 0; padding: 0 .
|
|
1776
|
+
.board .lane { min-height: 0; padding: 0 .25rem .25rem; }
|
|
1753
1777
|
.board .lane > summary {
|
|
1754
1778
|
cursor: pointer; display: flex; align-items: center; min-height: 2.75rem; margin: 0;
|
|
1755
1779
|
-webkit-tap-highlight-color: transparent;
|
|
1756
1780
|
}
|
|
1757
|
-
.board .lane > summary
|
|
1758
|
-
.board .lane > summary
|
|
1781
|
+
.board .lane > summary { padding: 0 .375rem; }
|
|
1782
|
+
.board .lane > summary h2 { flex: 1; }
|
|
1759
1783
|
.board .lane > summary::after {
|
|
1760
1784
|
content: ""; width: .5rem; height: .5rem; flex: none; margin-right: .25rem;
|
|
1761
1785
|
border-right: 1.5px solid var(--muted-foreground); border-bottom: 1.5px solid var(--muted-foreground);
|
|
@@ -1763,63 +1787,66 @@
|
|
|
1763
1787
|
}
|
|
1764
1788
|
.board .lane[open] > summary::after { transform: rotate(225deg) translateY(-.125rem); }
|
|
1765
1789
|
.board .lane .hint { margin-bottom: .25rem; }
|
|
1766
|
-
.board .lane .lane-card:last-of-type { margin-bottom:
|
|
1790
|
+
.board .lane .lane-card:last-of-type { margin-bottom: 0; }
|
|
1767
1791
|
.board .lane-attention { order: 0; }
|
|
1768
1792
|
.board .lane-building { order: 1; }
|
|
1769
1793
|
.board .lane-queued { order: 2; }
|
|
1770
1794
|
.board .lane-waiting { order: 3; }
|
|
1771
1795
|
.board .lane-done { order: 4; }
|
|
1772
1796
|
}
|
|
1797
|
+
/* A lane is a frame (components/ui/frame.tsx, after ReUI's Frame): a raised well with a hairline, its head a
|
|
1798
|
+
status chip and a mono count on the well's fill, its cards paper panels at concentric corners. Only the lane
|
|
1799
|
+
that waits on a person is amber: its chip, and each card's thin full outline. No fill, no stripe, no shadow. */
|
|
1773
1800
|
.lane {
|
|
1774
|
-
background:
|
|
1775
|
-
border-radius: var(--radius); padding: .
|
|
1801
|
+
background: var(--so-raised); border: 1px solid var(--so-line);
|
|
1802
|
+
border-radius: var(--so-radius-card); padding: .25rem; min-height: 12rem;
|
|
1776
1803
|
}
|
|
1777
|
-
.lane > summary { list-style: none; cursor: default; margin: 0
|
|
1804
|
+
.lane > summary { list-style: none; cursor: default; margin: 0; padding: .375rem .375rem .125rem; }
|
|
1778
1805
|
.lane > summary::-webkit-details-marker { display: none; }
|
|
1779
|
-
.lane h2 { display: flex; align-items: center; gap: .
|
|
1806
|
+
.lane h2 { display: flex; align-items: center; gap: .5rem; margin: 0; font-size: .8125rem; font-weight: 600; }
|
|
1780
1807
|
.lane h2 a { color: inherit; text-decoration: none; }
|
|
1781
1808
|
@media (hover: hover) and (pointer: fine) { .lane h2 a:hover { text-decoration: underline; } }
|
|
1782
|
-
.lane .hint { margin
|
|
1783
|
-
.lane-count { color: var(--muted-foreground); font-weight:
|
|
1784
|
-
.lane h2::before {
|
|
1785
|
-
content: ""; width: .375rem; height: .375rem; border-radius: var(--so-radius-full); flex: none;
|
|
1786
|
-
background: var(--muted-foreground); opacity: .55;
|
|
1787
|
-
}
|
|
1788
|
-
.lane-attention h2::before { background: var(--brand); opacity: 1; }
|
|
1789
|
-
.lane-building h2::before { background: var(--running); opacity: 1; }
|
|
1790
|
-
.lane-done h2::before { background: var(--muted-foreground); opacity: 1; }
|
|
1809
|
+
.lane .hint { margin: 0; padding: .125rem .375rem .375rem; }
|
|
1810
|
+
.lane-count { color: var(--muted-foreground); font-family: var(--font-mono); font-size: .6875rem; font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
1791
1811
|
.lane-card {
|
|
1792
|
-
display:
|
|
1793
|
-
background: var(--
|
|
1794
|
-
border-radius: calc(var(--radius) -
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
.lane-
|
|
1812
|
+
display: flex; flex-direction: column; text-decoration: none; color: inherit;
|
|
1813
|
+
background: var(--so-paper); border: 1px solid var(--so-line);
|
|
1814
|
+
border-radius: calc(var(--so-radius-card) - .25rem - 1px); padding: .625rem .75rem; margin-top: .25rem;
|
|
1815
|
+
}
|
|
1816
|
+
.lane-attention .lane-card { border-color: var(--so-warning); }
|
|
1817
|
+
/* The order view's columns are frames too, with roomier insides (a form, a note) and a quiet dot by the name. */
|
|
1818
|
+
.lane.queue-column { padding: .5rem; }
|
|
1819
|
+
.queue-column > h2::before { content: ""; width: .375rem; height: .375rem; border-radius: var(--so-radius-full); flex: none; background: var(--muted-foreground); opacity: .55; }
|
|
1820
|
+
@media (hover: hover) and (pointer: fine) { .lane-card:hover { background: var(--so-raised); } }
|
|
1821
|
+
/* The title leads; the task id is a machine fact under it, in mono. */
|
|
1822
|
+
.lane-card .t { display: block; font-size: .84375rem; font-weight: 500; line-height: 1.35; }
|
|
1823
|
+
.lane-card .why { display: block; margin-top: .125rem; font-size: .75rem; color: var(--muted-foreground); }
|
|
1824
|
+
.lane-card .id { display: block; margin-top: .375rem; font-family: var(--font-mono); font-size: .75rem; color: var(--muted-foreground); overflow-wrap: anywhere; }
|
|
1825
|
+
.lane-card .who { display: block; margin-top: .125rem; font-size: .75rem; color: var(--muted-foreground); overflow-wrap: anywhere; }
|
|
1826
|
+
.lane-card .who .mono { display: inline; margin: 0; font-family: var(--font-mono); font-size: .6875rem; }
|
|
1799
1827
|
.lane-card .dot { margin-right: .4rem; }
|
|
1800
1828
|
.lane-card .meta, .lane-card .mono { display: block; margin-top: .125rem; font-size: .75rem; }
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
.lane-card .facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: .0625rem .625rem; margin-top: .4rem; font-size: .6875rem; line-height: 1.5; }
|
|
1829
|
+
/* The facts: a muted word, then the value; only a machine's own words (a name, a model, a branch, a clock) are mono. */
|
|
1830
|
+
.lane-card .facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: .0625rem .625rem; margin-top: .25rem; font-size: .75rem; line-height: 1.5; }
|
|
1804
1831
|
.lane-card .fact { display: contents; }
|
|
1805
|
-
.lane-card .fact .k { color: var(--muted-foreground);
|
|
1806
|
-
.lane-card .fact .v {
|
|
1807
|
-
.lane-card .
|
|
1832
|
+
.lane-card .fact .k { color: var(--muted-foreground); }
|
|
1833
|
+
.lane-card .fact .v { color: var(--foreground); overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
|
|
1834
|
+
.lane-card .fact .v.mono { display: inline; margin: 0; font-family: var(--font-mono); font-size: .6875rem; }
|
|
1835
|
+
.lane-card .chips { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .5rem; }
|
|
1808
1836
|
.lane-card .chips .badge { margin: 0; }
|
|
1809
|
-
/* The live strip on a building card: stage
|
|
1810
|
-
the run's own facts, never a percent. */
|
|
1837
|
+
/* The live strip on a building card: the stage in words, the clock in mono, on a sunken well. */
|
|
1811
1838
|
.lane-card .live-line {
|
|
1812
1839
|
display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin-top: .375rem;
|
|
1813
|
-
padding: .3rem .5rem; border-radius:
|
|
1814
|
-
font-
|
|
1840
|
+
padding: .3rem .5rem; border-radius: var(--so-radius-sm); background: var(--so-raised);
|
|
1841
|
+
font-size: .75rem; color: var(--so-info);
|
|
1815
1842
|
}
|
|
1816
|
-
.lane-card .live-line .clock { color: var(--muted-foreground); }
|
|
1843
|
+
.lane-card .live-line .clock { color: var(--muted-foreground); font-family: var(--font-mono); font-size: .6875rem; font-variant-numeric: tabular-nums; }
|
|
1817
1844
|
/* The build's steps — set up, writing, checking and committing: three quiet
|
|
1818
1845
|
ticks, the current one in the running hue. A stage, never a percent. */
|
|
1819
1846
|
.lane-card .steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .1875rem; margin-top: .5rem; }
|
|
1820
|
-
.lane-card .steps i { height: .1875rem; border-radius:
|
|
1821
|
-
.lane-card .steps i.on { background: color-mix(in srgb, var(--
|
|
1822
|
-
.lane-card .steps i.now { background: var(--
|
|
1847
|
+
.lane-card .steps i { height: .1875rem; border-radius: var(--so-radius-full); background: var(--so-line); }
|
|
1848
|
+
.lane-card .steps i.on { background: color-mix(in srgb, var(--so-info) 45%, var(--so-line)); }
|
|
1849
|
+
.lane-card .steps i.now { background: var(--so-info); }
|
|
1823
1850
|
|
|
1824
1851
|
/* The worker strip: one quiet tile per worker above the lanes — what it is
|
|
1825
1852
|
building here, the stage, the clock. Ink and lines only; the running dot
|
|
@@ -2613,8 +2640,9 @@ button { min-height: 44px; }
|
|
|
2613
2640
|
.chat-workspace.projects-open::before { animation: fade 180ms ease-out; }
|
|
2614
2641
|
.switcher[open] .switcher-menu { animation: rise 160ms ease-out; }
|
|
2615
2642
|
@media (hover: hover) and (pointer: fine) {
|
|
2616
|
-
.lane-card, .decide-card, .menu-row, .chat-overview-item, .proposal { transition: border-color .15s, transform .15s, box-shadow .15s; }
|
|
2617
|
-
|
|
2643
|
+
.lane-card, .decide-card, .menu-row, .chat-overview-item, .proposal { transition: border-color .15s, background-color .15s, transform .15s, box-shadow .15s; }
|
|
2644
|
+
/* Board cards are panels in a frame: a hover fills them raised, never a shadow. */
|
|
2645
|
+
.decide-card:hover, .menu-row:hover {
|
|
2618
2646
|
|
|
2619
2647
|
box-shadow: 0 2px 8px -2px rgb(0 0 0 / .35);
|
|
2620
2648
|
}
|
package/dist/server/http.d.ts
CHANGED
|
@@ -165,6 +165,11 @@ export type ServeOptions = {
|
|
|
165
165
|
* the exact part of the ceiling, so an addition or removal shows without a restart. */
|
|
166
166
|
admittedRepos?: () => readonly string[];
|
|
167
167
|
connectionProbe?: typeof execRun;
|
|
168
|
+
/** The console's background checks (docs/plans/get-side-effects.md): sign-in checks, integration checks, model
|
|
169
|
+
* lists and the tidy-ups pages used to do. On for `up` and `serve`; a test turns them on, and `backgroundChecksMs` sets
|
|
170
|
+
* how often they run and ask this computer's agents again. */
|
|
171
|
+
backgroundChecks?: boolean;
|
|
172
|
+
backgroundChecksMs?: number;
|
|
168
173
|
connectionHome?: string;
|
|
169
174
|
/** Test seam for Chat's first tasks: the `gh` and `git grep` reads. */
|
|
170
175
|
firstTaskRunner?: typeof execRun;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The last answer of each check a page shows (docs/plans/get-side-effects.md). A page reads `known`; the background
|
|
3
|
+
* checks and a person's Check now (a POST) call `run`. One per console, held in memory: a restart asks again.
|
|
4
|
+
*/
|
|
5
|
+
export type LastCheck<T> = {
|
|
6
|
+
at: number;
|
|
7
|
+
value: T;
|
|
8
|
+
};
|
|
9
|
+
export declare class LastChecks {
|
|
10
|
+
private readonly answers;
|
|
11
|
+
private readonly running;
|
|
12
|
+
known<T>(key: string): LastCheck<T> | null;
|
|
13
|
+
/** Whether the last answer is older than `everyMs` (or missing). */
|
|
14
|
+
due(key: string, everyMs: number, now?: number): boolean;
|
|
15
|
+
/** Run the check (one at a time per key) and keep its answer. */
|
|
16
|
+
run<T>(key: string, check: () => Promise<T>): Promise<T>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class LastChecks {
|
|
2
|
+
answers = new Map();
|
|
3
|
+
running = new Map();
|
|
4
|
+
known(key) {
|
|
5
|
+
return this.answers.get(key) ?? null;
|
|
6
|
+
}
|
|
7
|
+
/** Whether the last answer is older than `everyMs` (or missing). */
|
|
8
|
+
due(key, everyMs, now = Date.now()) {
|
|
9
|
+
const last = this.answers.get(key);
|
|
10
|
+
return last === undefined || now - last.at >= everyMs;
|
|
11
|
+
}
|
|
12
|
+
/** Run the check (one at a time per key) and keep its answer. */
|
|
13
|
+
run(key, check) {
|
|
14
|
+
const pending = this.running.get(key);
|
|
15
|
+
if (pending !== undefined)
|
|
16
|
+
return pending;
|
|
17
|
+
const started = check().then(value => { this.answers.set(key, { at: Date.now(), value }); return value; })
|
|
18
|
+
.finally(() => { this.running.delete(key); });
|
|
19
|
+
this.running.set(key, started);
|
|
20
|
+
return started;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/server/live.d.ts
CHANGED
|
@@ -45,6 +45,10 @@ export type LiveRoom = {
|
|
|
45
45
|
} | {
|
|
46
46
|
kind: "agent";
|
|
47
47
|
run: number;
|
|
48
|
+
}
|
|
49
|
+
/** The Agents view's Live grid (agents-live-room.ts): per viewer, watching only. */
|
|
50
|
+
| {
|
|
51
|
+
kind: "agents-live";
|
|
48
52
|
};
|
|
49
53
|
/** A room's name, read strictly: anything else is not a room. */
|
|
50
54
|
export declare function parseLiveRoom(name: string): LiveRoom | null;
|
package/dist/server/live.js
CHANGED
|
@@ -9,9 +9,11 @@ import { handlersOf } from "./handler-registry.js";
|
|
|
9
9
|
import { respond } from "./http.js";
|
|
10
10
|
import { adapterPolicy } from "./route-policy.js";
|
|
11
11
|
import { requestContext } from "./request-context.js";
|
|
12
|
+
import { createAgentsLiveRooms } from "./agents-live-room.js";
|
|
13
|
+
import { leadTilesFor } from "./agents-live-lead.js";
|
|
12
14
|
/** A page asks for a few rooms at once: its workspace, what it shows, its conversation. */
|
|
13
15
|
export const LIVE_ROOMS_PER_PAGE = 6;
|
|
14
|
-
const ROOM_NAME = /^(workspace|task|flow|chat|team|agent|summary)(?::([^?]{1,400}))?(?:\?([^#]{0,600}))?$/;
|
|
16
|
+
const ROOM_NAME = /^(workspace|task|flow|chat|team|agent|summary|agents-live)(?::([^?]{1,400}))?(?:\?([^#]{0,600}))?$/;
|
|
15
17
|
/** At most one workspace nudge a second: a burst of writes is one re-read, the first heard at once. */
|
|
16
18
|
const WORKSPACE_SETTLE_MS = 1_000;
|
|
17
19
|
/** A running reply's preview: at most one snapshot every 80 ms; the last one always goes out. */
|
|
@@ -34,6 +36,7 @@ export function parseLiveRoom(name) {
|
|
|
34
36
|
switch (kind) {
|
|
35
37
|
case "workspace": return key === null && query === undefined ? { kind } : null;
|
|
36
38
|
case "summary": return key === null && query === undefined ? { kind } : null;
|
|
39
|
+
case "agents-live": return key === null && query === undefined ? { kind } : null;
|
|
37
40
|
case "task": return key !== null && key !== "" && query === undefined ? { kind, task: key } : null;
|
|
38
41
|
case "flow": {
|
|
39
42
|
const flow = Number(key), card = params.get("card");
|
|
@@ -92,6 +95,11 @@ export function createLiveHandlers(runtime, live) {
|
|
|
92
95
|
const chatViewers = new Set();
|
|
93
96
|
// One agent's open view (Agents board): what it shows, keyed by the exact run.
|
|
94
97
|
const agentRooms = createLiveRooms(run => agentRunFingerprint(store, run), null, { bus: live.bus });
|
|
98
|
+
// The Live grid: one room per viewer and scope, watching only (no lease, no seen mark).
|
|
99
|
+
// Each Live page's own lead tiles, read as its viewer's saved session now (a page's room key carries the page).
|
|
100
|
+
const liveLeads = new Map();
|
|
101
|
+
const agentsLive = createAgentsLiveRooms({ store, bus: live.bus, evidenceRoot: runtime.evidenceRoot, localRunner: runtime.options.localRunner, clock, visible,
|
|
102
|
+
leads: key => liveLeads.get(key)?.() ?? null, forget: key => liveLeads.delete(key) });
|
|
95
103
|
const readBrowser = (request, who) => rereadBrowser(runtime, request, who);
|
|
96
104
|
const sameBrowser = (request, who) => () => readBrowser(request, who) !== null;
|
|
97
105
|
function admitted(viewer, thread) {
|
|
@@ -196,10 +204,11 @@ export function createLiveHandlers(runtime, live) {
|
|
|
196
204
|
if ([...url.searchParams.keys()].some(key => key !== "room") || names.length === 0 || names.length > LIVE_ROOMS_PER_PAGE || new Set(names).size !== names.length)
|
|
197
205
|
return json(response, 400, { error: "room" });
|
|
198
206
|
const rooms = names.map(name => ({ name, room: parseLiveRoom(name) }));
|
|
199
|
-
if (rooms.some(one => one.room === null) || rooms.filter(one => one.room?.kind === "agent").length > 1 || rooms.filter(one => one.room?.kind === "workspace").length > 1 || rooms.filter(one => one.room?.kind === "chat").length > 1 || rooms.filter(one => one.room?.kind === "team").length > 1 || rooms.filter(one => one.room?.kind === "summary").length > 1)
|
|
207
|
+
if (rooms.some(one => one.room === null) || rooms.filter(one => one.room?.kind === "agent").length > 1 || rooms.filter(one => one.room?.kind === "workspace").length > 1 || rooms.filter(one => one.room?.kind === "chat").length > 1 || rooms.filter(one => one.room?.kind === "team").length > 1 || rooms.filter(one => one.room?.kind === "summary").length > 1 || rooms.filter(one => one.room?.kind === "agents-live").length > 1)
|
|
200
208
|
return json(response, 400, { error: "room" });
|
|
201
209
|
// Admit every room before the stream opens, with the answers each page's own stream gave.
|
|
202
210
|
const joins = [];
|
|
211
|
+
let refusedJoin = false;
|
|
203
212
|
for (const { name, room } of rooms) {
|
|
204
213
|
if (room.kind !== "team" && who.via !== "cookie")
|
|
205
214
|
return json(response, 403, { error: "session" });
|
|
@@ -216,6 +225,33 @@ export function createLiveHandlers(runtime, live) {
|
|
|
216
225
|
joins.push(connection => summaryRooms.join(summaryKey(who.name, scope), { name: who.name, room: name, connection,
|
|
217
226
|
valid: () => signedIn() && live.projectScopeFor(who.name) === scope }));
|
|
218
227
|
}
|
|
228
|
+
else if (room.kind === "agents-live") {
|
|
229
|
+
const browser = who;
|
|
230
|
+
const scope = live.projectScopeFor(who.name);
|
|
231
|
+
if (scope === null)
|
|
232
|
+
return json(response, 404, { error: "agents-live" });
|
|
233
|
+
const signedIn = sameBrowser(request, browser);
|
|
234
|
+
const valid = () => signedIn() && live.projectScopeFor(who.name) === scope;
|
|
235
|
+
joins.push(connection => {
|
|
236
|
+
// Proved before the room is made or read: an invalid viewer never causes a look at a file.
|
|
237
|
+
let ok = false;
|
|
238
|
+
try {
|
|
239
|
+
ok = valid();
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
ok = false;
|
|
243
|
+
}
|
|
244
|
+
if (!ok) {
|
|
245
|
+
connection.send(name, "gone");
|
|
246
|
+
refusedJoin = true;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Each page's own room (its key carries this connection): a newcomer is never handed a digest computed earlier.
|
|
250
|
+
const key = agentsLive.keyOf(who.name, scope, randomBytes(9).toString("hex"));
|
|
251
|
+
liveLeads.set(key, () => { const again = readBrowser(request, browser); return again === null ? null : leadTilesFor(runtime, again); });
|
|
252
|
+
agentsLive.rooms.join(key, { name: who.name, room: name, connection, valid });
|
|
253
|
+
});
|
|
254
|
+
}
|
|
219
255
|
else if (room.kind === "task") {
|
|
220
256
|
const browser = who;
|
|
221
257
|
const family = familyOf(room.task);
|
|
@@ -342,6 +378,9 @@ export function createLiveHandlers(runtime, live) {
|
|
|
342
378
|
for (const join of joins)
|
|
343
379
|
if (connection.open)
|
|
344
380
|
join(connection);
|
|
381
|
+
// A page whose only room was refused at its join ends now (`ended`), its keep-alive stopped, not left open.
|
|
382
|
+
if (refusedJoin && connection.rooms === 0)
|
|
383
|
+
connection.refuse(() => connection.close());
|
|
345
384
|
request.once("close", () => connection.close());
|
|
346
385
|
}
|
|
347
386
|
const detachChat = live.bus.subscribe(() => {
|
|
@@ -357,6 +396,7 @@ export function createLiveHandlers(runtime, live) {
|
|
|
357
396
|
summaryRooms.close();
|
|
358
397
|
teamRooms.close();
|
|
359
398
|
agentRooms.close();
|
|
399
|
+
agentsLive.rooms.close();
|
|
360
400
|
for (const viewer of [...chatViewers])
|
|
361
401
|
viewer.connection.close();
|
|
362
402
|
},
|
package/dist/server/pages.js
CHANGED
|
@@ -26,6 +26,11 @@ import { LEDGER_CSV_HEADER, ledgerCsvRows } from "../ledger-csv.js";
|
|
|
26
26
|
import { ledgerBody } from "../ledger-view.js";
|
|
27
27
|
import { agentLanesOf, agentRowOf, agentsBoardOf, agentFactsFor, laneWindowOf, sealedDiffOf } from "../agents-board.js";
|
|
28
28
|
import { markAgentSeen } from "../agent-board.js";
|
|
29
|
+
import { agentsLiveOf } from "../agents-live.js";
|
|
30
|
+
import { readChildWindow } from "../live-children-read.js";
|
|
31
|
+
import { leadFiledLinks } from "./agents-live-filed.js";
|
|
32
|
+
import { readMainWindow } from "../live-main-read.js";
|
|
33
|
+
import { runLiveCommands } from "../store.js";
|
|
29
34
|
import { prometheusMetrics } from "../metrics.js";
|
|
30
35
|
import { readMonitoring } from "../monitoring-settings.js";
|
|
31
36
|
import { targetOf } from "../monitoring.js";
|
|
@@ -38,6 +43,7 @@ import { healthWords } from "../server-telemetry.js";
|
|
|
38
43
|
import { tally } from "../summary.js";
|
|
39
44
|
import { serveInstallAsset } from "./pages-assets.js";
|
|
40
45
|
import { healthz } from "./remote-hooks.js";
|
|
46
|
+
import { leadsStillAdmitted, leadTilesFor } from './agents-live-lead.js';
|
|
41
47
|
import { refuse, regionScript, screen, settingsRows, WORKSPACE_STYLE, workToolRows } from "./chrome.js";
|
|
42
48
|
import { redirect, respond, safeReturn, SAFETY, taskHref } from "./http.js";
|
|
43
49
|
import { assignmentOf } from "../assignment.js";
|
|
@@ -46,8 +52,9 @@ import { browsePage, fleetBody, fleetScript, githubIdentityOf, githubReposPage,
|
|
|
46
52
|
import { capsPage } from "./render-settings.js";
|
|
47
53
|
import { decisionAnswerScript, taskBody } from "./render-tasks.js";
|
|
48
54
|
import {} from "./session.js";
|
|
55
|
+
import { rereadBrowser } from "./live.js";
|
|
49
56
|
export function createPagesHandlers(runtime) {
|
|
50
|
-
const { clock, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession } = runtime;
|
|
57
|
+
const { clock, lastChecks, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession } = runtime;
|
|
51
58
|
// The filesystem browser (operator request): pick a project folder by
|
|
52
59
|
// looking, not typing. CONFINED to exactly what opening allows —
|
|
53
60
|
// explicit --repo mode has an enumerable list and gets no browser at
|
|
@@ -117,7 +124,8 @@ export function createPagesHandlers(runtime) {
|
|
|
117
124
|
const { who, response } = ctx;
|
|
118
125
|
if (store.isDemo())
|
|
119
126
|
return refuse(response, who, 403, "the sandbox never talks to GitHub");
|
|
120
|
-
|
|
127
|
+
// The last listing (docs/plans/get-side-effects.md): List repositories posts; opening the page asks GitHub nothing.
|
|
128
|
+
const listed = lastChecks.known("github-repos")?.value ?? null;
|
|
121
129
|
// What this machine already has, matched by the clone's own recorded
|
|
122
130
|
// origin — registered projects first, then the immediate children of
|
|
123
131
|
// the configured roots. Identities are compared case-insensitively
|
|
@@ -136,7 +144,7 @@ export function createPagesHandlers(runtime) {
|
|
|
136
144
|
// Roots are walked ONLY when there are listed repos to match against
|
|
137
145
|
// (finding 1), through an iterator that stops at 400 entries — a
|
|
138
146
|
// directory with a million children costs 400 stats, not a scan.
|
|
139
|
-
if (listed.ok && listed.repos.length > 0) {
|
|
147
|
+
if (listed !== null && listed.ok && listed.repos.length > 0) {
|
|
140
148
|
for (const root of ceiling.roots) {
|
|
141
149
|
const children = [];
|
|
142
150
|
try {
|
|
@@ -176,6 +184,14 @@ export function createPagesHandlers(runtime) {
|
|
|
176
184
|
openProject: who.via === "cookie" ? who.session.project ?? null : null,
|
|
177
185
|
}));
|
|
178
186
|
}
|
|
187
|
+
// List repositories / Refresh list: ask the GitHub CLI signed in on this machine, then show the page.
|
|
188
|
+
async function projectsGithubList(ctx) {
|
|
189
|
+
const { who, response } = ctx;
|
|
190
|
+
if (store.isDemo())
|
|
191
|
+
return refuse(response, who, 403, "the sandbox never talks to GitHub");
|
|
192
|
+
await lastChecks.run("github-repos", () => (options.ghList ?? listGithubRepos)());
|
|
193
|
+
return redirect(response, "/projects/github");
|
|
194
|
+
}
|
|
179
195
|
async function projectsIndex(ctx) {
|
|
180
196
|
const { url, who, response } = ctx;
|
|
181
197
|
return void projectsScreen(response, who, url.searchParams.get("said"), 200, safeReturn(url.searchParams.get("return")));
|
|
@@ -444,8 +460,14 @@ export function createPagesHandlers(runtime) {
|
|
|
444
460
|
const now = clock();
|
|
445
461
|
const access = workAccess();
|
|
446
462
|
const asked = url.searchParams.get("run");
|
|
463
|
+
// The Live grid only watches (docs/plans/agents-view.md §3): it branches off before an opened agent is built, so
|
|
464
|
+
// viewing it never marks a run seen, takes a watch or offers a composer — a reload of ?view=live&run=… included.
|
|
465
|
+
const watching = url.searchParams.get("view") === "live";
|
|
466
|
+
// Live is a browser session's only, like its reads (no token sees the grid).
|
|
467
|
+
if (watching && who.via !== "cookie")
|
|
468
|
+
return respond(response, 403, "text/plain; charset=utf-8", "The Live view is for a signed-in browser.");
|
|
447
469
|
let selected = null;
|
|
448
|
-
if (asked !== null && /^[1-9][0-9]{0,9}$/.test(asked)) {
|
|
470
|
+
if (!watching && asked !== null && /^[1-9][0-9]{0,9}$/.test(asked)) {
|
|
449
471
|
const run = store.getRun(Number(asked));
|
|
450
472
|
const facts = run === null || !runVisible(run) ? null : agentFactsFor(store, run.id, access, who.name);
|
|
451
473
|
if (facts !== null) {
|
|
@@ -485,6 +507,11 @@ export function createPagesHandlers(runtime) {
|
|
|
485
507
|
};
|
|
486
508
|
}
|
|
487
509
|
}
|
|
510
|
+
// The Live grid is drawn in the browser from its own read-only reads (agents-live-view.tsx): no board facts here.
|
|
511
|
+
if (watching) {
|
|
512
|
+
const view = { kind: "agents", groups: [], selected: null, lanes: null, empty: null, live: true };
|
|
513
|
+
return sendScreen(response, 200, screen("Agents", html `<h1>Agents</h1><p class="meta">Live needs JavaScript.</p>`, { chrome: chromeFor(project, "runs"), workspace: { view } }));
|
|
514
|
+
}
|
|
488
515
|
const groups = agentsBoardOf(store, now, access, who.name, evidenceRoot);
|
|
489
516
|
const lanes = selected === null ? agentLanesOf(store, now, access, who.name, laneWindowOf(url.searchParams.get("window"))) : null;
|
|
490
517
|
const view = { kind: "agents", groups, selected, lanes,
|
|
@@ -989,9 +1016,74 @@ export function createPagesHandlers(runtime) {
|
|
|
989
1016
|
response.setHeader("cache-control", "no-store");
|
|
990
1017
|
return respond(response, 200, "application/json", JSON.stringify({ proof: createHmac("sha256", Buffer.from(identity, "hex")).update(challenge).digest("hex") }));
|
|
991
1018
|
}
|
|
1019
|
+
// The Live grid (docs/plans/agents-view.md §3), kept after every other handler.
|
|
1020
|
+
/** The Live grid's tiles (JSON, read-only): every role's running agents and those that just ended, that this viewer
|
|
1021
|
+
* may see — the console's ceiling and the account's own access, proved per run, on every request. */
|
|
1022
|
+
async function agentsLive(ctx) {
|
|
1023
|
+
const { who, request, response } = ctx;
|
|
1024
|
+
response.setHeader("cache-control", "no-store");
|
|
1025
|
+
// A browser session only (the route says so too): live output is never a token's to read.
|
|
1026
|
+
if (who.via !== "cookie")
|
|
1027
|
+
return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
|
|
1028
|
+
const browser = who;
|
|
1029
|
+
const canSee = (run) => { const repo = taskRepoOf(run.taskRef); return repo !== null && visible(repo) && store.accountCanAccess(who.name, repo); };
|
|
1030
|
+
const tiles = agentsLiveOf(store, clock(), { canSee, localRunner: options.localRunner });
|
|
1031
|
+
// The viewer's own lead turns, admitted as their chat is, from the session as it is saved now.
|
|
1032
|
+
const current = rereadBrowser(runtime, request, browser);
|
|
1033
|
+
const leads = current === null ? [] : leadTilesFor(runtime, current);
|
|
1034
|
+
// Re-proved right before delivery: the session still held, each tile's run still in view from its current row, and
|
|
1035
|
+
// each lead turn still admitted, from the session as it is saved now.
|
|
1036
|
+
const again = current === null ? null : rereadBrowser(runtime, request, browser);
|
|
1037
|
+
if (again === null)
|
|
1038
|
+
return respond(response, 401, "application/json", JSON.stringify({ error: "session" }));
|
|
1039
|
+
const shown = tiles.filter(tile => { const run = store.getRun(tile.runId); return run !== null && canSee(run); });
|
|
1040
|
+
// Work the viewer's own lead filed nests under it (docs/plans/lead-dispatched-tiles.md): proved from the store now,
|
|
1041
|
+
// for the tiles already shown, and only ever for their owner.
|
|
1042
|
+
const filed = again.role === "approver" ? leadFiledLinks(store, again.name, shown.map(tile => tile.runId)) : new Map();
|
|
1043
|
+
return respond(response, 200, "application/json", JSON.stringify({ tiles: shown.map(tile => ({ ...tile, filedBy: filed.get(tile.runId) ?? null })), leads: leadsStillAdmitted(runtime, again, leads) }));
|
|
1044
|
+
}
|
|
1045
|
+
/** This console's key for the Live grid's read cursors: random, in memory only (never where a build can reach), so a
|
|
1046
|
+
* restart makes old cursors start over visibly. A cursor is state, never permission. */
|
|
1047
|
+
const liveCursorKey = randomBytes(32);
|
|
1048
|
+
/** One window of a run's live output for the Live grid (JSON, read-only): its own records or its subagents'. Access
|
|
1049
|
+
* is proved before the read (ceiling, account, this computer's run) and again after it, before a byte is sent. */
|
|
1050
|
+
async function agentsLiveRead(ctx) {
|
|
1051
|
+
const { url, who, response } = ctx;
|
|
1052
|
+
response.setHeader("cache-control", "no-store");
|
|
1053
|
+
// A browser session only (the route says so too): live output is never a token's to read.
|
|
1054
|
+
if (who.via !== "cookie")
|
|
1055
|
+
return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
|
|
1056
|
+
const runId = Number(url.pathname.slice("/agents/live/".length));
|
|
1057
|
+
const file = url.searchParams.get("file");
|
|
1058
|
+
const cursor = url.searchParams.get("cursor");
|
|
1059
|
+
if ((file !== "main" && file !== "sub") || [...url.searchParams.keys()].some(key => key !== "file" && key !== "cursor")) {
|
|
1060
|
+
return respond(response, 400, "application/json", JSON.stringify({ error: "request" }));
|
|
1061
|
+
}
|
|
1062
|
+
const browser = who;
|
|
1063
|
+
// The saved session still held (not just the cookie), and the run still in view from its current row.
|
|
1064
|
+
const allowed = () => {
|
|
1065
|
+
if (rereadBrowser(runtime, ctx.request, browser) === null)
|
|
1066
|
+
return false;
|
|
1067
|
+
const run = store.getRun(runId);
|
|
1068
|
+
const repo = run === null ? null : taskRepoOf(run.taskRef);
|
|
1069
|
+
return run !== null && repo !== null && runVisible(run) && store.accountCanAccess(who.name, repo);
|
|
1070
|
+
};
|
|
1071
|
+
if (!allowed())
|
|
1072
|
+
return respond(response, 404, "application/json", JSON.stringify({ error: "run" }));
|
|
1073
|
+
if (options.localRunner === undefined || store.getRun(runId)?.runner !== options.localRunner) {
|
|
1074
|
+
return allowed() ? respond(response, 200, "application/json", JSON.stringify({ error: "off" })) : respond(response, 404, "application/json", JSON.stringify({ error: "run" }));
|
|
1075
|
+
}
|
|
1076
|
+
const read = { key: liveCursorKey, commands: runLiveCommands(store, runId) };
|
|
1077
|
+
const window = file === "sub" ? readChildWindow(evidenceRoot, runId, cursor, read) : readMainWindow(evidenceRoot, runId, cursor, read);
|
|
1078
|
+
// Re-proved after the read: a narrowed account or a run gone from view gets nothing it read.
|
|
1079
|
+
if (!allowed() || store.getRun(runId)?.runner !== options.localRunner)
|
|
1080
|
+
return respond(response, 404, "application/json", JSON.stringify({ error: "run" }));
|
|
1081
|
+
return respond(response, 200, "application/json", JSON.stringify(window.ok ? window : { error: window.reason }));
|
|
1082
|
+
}
|
|
992
1083
|
const registrations = handlersOf("pages", {
|
|
993
1084
|
"projects.browse": projectsBrowse,
|
|
994
1085
|
"projects.github": projectsGithub,
|
|
1086
|
+
"projects.github-list": projectsGithubList,
|
|
995
1087
|
"projects.page": projectsIndex,
|
|
996
1088
|
"home": homeRedirect,
|
|
997
1089
|
"morning": morningAlias,
|
|
@@ -1001,6 +1093,8 @@ export function createPagesHandlers(runtime) {
|
|
|
1001
1093
|
"system": systemScreen,
|
|
1002
1094
|
"peek": peekPage,
|
|
1003
1095
|
"agents": agentsPage,
|
|
1096
|
+
"agents.live": agentsLive,
|
|
1097
|
+
"agents.live.read": agentsLiveRead,
|
|
1004
1098
|
"fleet": fleetPage,
|
|
1005
1099
|
"ledger.export": ledgerExport,
|
|
1006
1100
|
"menu": menuPage,
|
|
@@ -39,7 +39,7 @@ export function sameVisitCheck(authTime, startedAt, now) {
|
|
|
39
39
|
return Math.min(Math.max(at, startedAt), now);
|
|
40
40
|
}
|
|
41
41
|
export function createPeopleHandlers(runtime) {
|
|
42
|
-
const { joinBySource, ssoOffer, ssoVisits, ssoSettings, options, clock, ssoHandoffs, managedRepos, store, visible, admissionList, sendScreen, chromeFor, restricted, sessions, consoleProjects, authenticateApprover, authenticateCredential, bustBadge, projectOf, signInLinks, linkKey, fromThisComputer, recordSignIn, passwordAllowed, arrival, defaultProject, cookieSecure, consoleOrigin, providerFor, joinSourceOf, signInBudget, signInActor, minutesWords } = runtime;
|
|
42
|
+
const { joinBySource, ssoOffer, ssoVisits, ssoSettings, options, clock, ssoHandoffs, managedRepos, store, visible, admissionList, sendScreen, chromeFor, restricted, sessions, consoleProjects, authenticateApprover, authenticateCredential, bustBadge, projectOf, signInLinks, linkKey, fromThisComputer, recordSignIn, passwordAllowed, arrival, defaultProject, cookieSecure, consoleOrigin, providerFor, providerKnown, joinSourceOf, signInBudget, signInActor, minutesWords } = runtime;
|
|
43
43
|
// v101: Settings → Sessions & tokens: where you're signed in, and your API tokens. An instance operator can see everyone's.
|
|
44
44
|
async function sessionsPage(ctx) {
|
|
45
45
|
const { url, who, response, project } = ctx;
|
|
@@ -353,9 +353,12 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
|
|
|
353
353
|
const origin = consoleOrigin(request.headers.host);
|
|
354
354
|
if (origin === null)
|
|
355
355
|
return page(response, 400, loginPage("Sign in from this computer (localhost) or from your https address.", returnTo, ssoOffer()));
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
356
|
+
// Discovered as the console starts and in the background (docs/plans/get-side-effects.md): following this link
|
|
357
|
+
// fetches nothing.
|
|
358
|
+
const known = providerKnown(settings.issuer);
|
|
359
|
+
if (known === null)
|
|
360
|
+
return page(response, 503, loginPage(`${settings.label} isn't answering yet. Try again in a moment.`, returnTo, ssoOffer()));
|
|
361
|
+
const found = { provider: known };
|
|
359
362
|
const visit = newOidcVisit(), redirectUri = `${origin}${SSO_CALLBACK}`;
|
|
360
363
|
for (const [key, one] of ssoVisits)
|
|
361
364
|
if (one.expires < Date.now())
|
|
@@ -359,8 +359,14 @@ export declare function leadAfterComposerHtml(data: {
|
|
|
359
359
|
pending: LeadTurn | null;
|
|
360
360
|
focusTask: TaskChatFocus | null;
|
|
361
361
|
}): Html;
|
|
362
|
+
/** A conversation shown before it exists (quiet page visits): its page binds sends to this session id, and the first
|
|
363
|
+
* send starts the conversation (chatSend). A visit never starts one. */
|
|
364
|
+
export declare const NOT_STARTED_SESSION = 0;
|
|
362
365
|
export declare function leadPage(chrome: Chrome, data: LeadThreadRows & {
|
|
363
|
-
|
|
366
|
+
/** Null: no conversation yet; the first message starts one. */
|
|
367
|
+
session: LeadSession | null;
|
|
368
|
+
/** Whose conversation this is (the session's approver once it exists). */
|
|
369
|
+
approver: string;
|
|
364
370
|
/** A project's own thread (v77); null for the lead conversation or a task. */
|
|
365
371
|
chatProject?: string | null;
|
|
366
372
|
follow?: {
|
|
@@ -390,6 +396,14 @@ export type LiveTurn = {
|
|
|
390
396
|
ok: boolean;
|
|
391
397
|
listeners: Set<() => void>;
|
|
392
398
|
expiry?: NodeJS.Timeout;
|
|
399
|
+
/** For the Live grid's lead tile (agents-live-lead.ts): the turn's id once it says it, when it started and ended, and
|
|
400
|
+
* when each step began (epoch ms). Never sent to the chat room. */
|
|
401
|
+
turn?: number;
|
|
402
|
+
startedAt?: number;
|
|
403
|
+
endedAt?: number;
|
|
404
|
+
stepAt?: number[];
|
|
405
|
+
/** Per step: its text is the provider's finished answer, secret-checked whole (lead.ts `final`). */
|
|
406
|
+
confirmed?: boolean[];
|
|
393
407
|
};
|
|
394
408
|
export type ChatEnablement = {
|
|
395
409
|
ok: true;
|
|
@@ -952,18 +952,25 @@ export function leadThreadHtml(data) {
|
|
|
952
952
|
export function leadAfterComposerHtml(data) {
|
|
953
953
|
return html `<div id="chat-after-composer" data-chat-region="after">${data.messages.length === 0 && data.pending === null ? leadPromptStarters(data.focusTask) : ""}</div>`;
|
|
954
954
|
}
|
|
955
|
+
/** A conversation shown before it exists (quiet page visits): its page binds sends to this session id, and the first
|
|
956
|
+
* send starts the conversation (chatSend). A visit never starts one. */
|
|
957
|
+
export const NOT_STARTED_SESSION = 0;
|
|
955
958
|
export function leadPage(chrome, data) {
|
|
956
959
|
const subscription = isSubscriptionChatProvider(data.config.provider);
|
|
957
960
|
const chatProject = data.focusTask === null ? data.chatProject ?? null : null;
|
|
958
961
|
const returnTo = data.focusTask !== null ? taskChatHref(data.focusTask.id) : chatProject !== null ? projectChatHref(chatProject) : "/chat";
|
|
962
|
+
const sessionId = data.session?.id ?? NOT_STARTED_SESSION;
|
|
963
|
+
const approver = data.session?.approver ?? data.approver;
|
|
959
964
|
const controlsHtml = joinHtml([
|
|
960
|
-
html `<details class="lead-follow"><summary>Automatic crew updates${data.follow?.enabled ? ' · On' : ''}</summary><p class="meta">${data.follow?.detail ?? 'Automatic crew updates are off.'}</p>${postForm("/chat/mate/follow", html `${data.follow?.enabled ? '' : html `<p>The lead responds when results or decisions arrive. Uses this conversation’s ${subscription ? 'membership usage, with no dollar maximum' : 'remaining spend allowance'} and daily turn limit. Existing task approvals still apply.</p>`}<button type="submit" class="quiet">${data.follow?.enabled ? 'Pause updates' : 'Enable updates'}</button>`, { returnTo, hidden: { enabled: data.follow?.enabled ? 'no' : 'yes' } })}</details>`,
|
|
965
|
+
data.session === null ? "" : html `<details class="lead-follow"><summary>Automatic crew updates${data.follow?.enabled ? ' · On' : ''}</summary><p class="meta">${data.follow?.detail ?? 'Automatic crew updates are off.'}</p>${postForm("/chat/mate/follow", html `${data.follow?.enabled ? '' : html `<p>The lead responds when results or decisions arrive. Uses this conversation’s ${subscription ? 'membership usage, with no dollar maximum' : 'remaining spend allowance'} and daily turn limit. Existing task approvals still apply.</p>`}<button type="submit" class="quiet">${data.follow?.enabled ? 'Pause updates' : 'Enable updates'}</button>`, { returnTo, hidden: { enabled: data.follow?.enabled ? 'no' : 'yes' } })}</details>`,
|
|
961
966
|
html `<details class="chat-limits chat-session-details"><summary>Conversation details<span class="meta">${subscription ? "membership" : data.config.provider}</span></summary>\n${joinHtml([
|
|
962
967
|
html `<div class="chat-budget"><span class="mono">${data.config.provider} · ${data.config.model}</span><span>${data.turnsToday} / ${data.config.dailyTurns} turns today</span>${subscription
|
|
963
968
|
? html `<span>membership login · no dollar ceiling</span>`
|
|
964
|
-
: html `<span>this conversation: ${chatMoney(data.session
|
|
965
|
-
|
|
966
|
-
|
|
969
|
+
: html `<span>this conversation: ${chatMoney(data.session?.spentMicrousd ?? 0)} of ${chatMoney(data.session?.ceilingMicrousd ?? 0)}</span><span>this week ${chatMoney(data.weeklySpent)} of ${chatMoney(data.config.weeklyCeilingMicrousd)}</span>`}</div>`,
|
|
970
|
+
data.session === null
|
|
971
|
+
? html `<p class="meta">No conversation yet. Send a message to start one.</p>`
|
|
972
|
+
: html `<p class="meta">Started ${data.session.mintedAt.slice(0, 16).replace("T", " ")}Z. It stays open until you end it; only bounded recent context is sent to the model.</p>`,
|
|
973
|
+
data.session === null ? "" : postForm("/chat/mate/end", html `<button type="submit" class="quiet">End the conversation and forget the thread</button>`, { attrs: { class: "inline" }, returnTo }),
|
|
967
974
|
data.recent.length === 0
|
|
968
975
|
? ""
|
|
969
976
|
: html `<p class="meta">Recent turns: ${joinHtml(data.recent
|
|
@@ -996,8 +1003,8 @@ export function leadPage(chrome, data) {
|
|
|
996
1003
|
html `<label>Message<textarea id="chat-message" name="message" rows="1" maxlength="${LEAD_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${projectName(chatProject)}…` : "Describe what you want done…"}"></textarea></label>`,
|
|
997
1004
|
html `<button type="submit" aria-label="${data.pending === null ? "send message" : "send message — it waits for the current reply"}">Send</button>`,
|
|
998
1005
|
], "\n"), {
|
|
999
|
-
attrs: { class: "card composer", id: latestReply === null && data.pending === null ? "latest" : "chat-composer", "aria-label": "message the lead", "data-chat-session":
|
|
1000
|
-
hidden: { request: randomBytes(16).toString("hex"), "request-session":
|
|
1006
|
+
attrs: { class: "card composer", id: latestReply === null && data.pending === null ? "latest" : "chat-composer", "aria-label": "message the lead", "data-chat-session": sessionId, "data-chat-task": data.focusTask?.id ?? "", "data-chat-user": approver, "data-chat-busy": data.pending === null ? "0" : "1", "data-chat-version": leadChatVersion(data), "data-chat-approval": data.focusTask?.approval?.digest ?? "" },
|
|
1007
|
+
hidden: { request: randomBytes(16).toString("hex"), "request-session": sessionId, task: data.focusTask?.id, project: chatProject, result: data.resultRunId },
|
|
1001
1008
|
}),
|
|
1002
1009
|
// Concise pass (2026-09-13): the status line speaks only when there is
|
|
1003
1010
|
// a state to report — a reply in progress here, the connection from
|
|
@@ -1009,7 +1016,7 @@ export function leadPage(chrome, data) {
|
|
|
1009
1016
|
return screen("chat", chatWorkspace(joinHtml(conversation, "\n"), data.projects, false, data.focusTask, data.resultPanel ?? null), { chrome, functional: { script: CHAT_CONTINUITY_SCRIPT + CHAT_UI_SCRIPT + (data.focusTask === null ? "" : RESULT_REVIEW_SCRIPT), fetches: true },
|
|
1010
1017
|
workspace: {
|
|
1011
1018
|
conversation: {
|
|
1012
|
-
sessionId
|
|
1019
|
+
sessionId, user: approver, version: leadChatVersion(data),
|
|
1013
1020
|
messages: leadBrowserMessages(data, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
|
|
1014
1021
|
pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: LEAD_MESSAGE_MAX_CHARS,
|
|
1015
1022
|
taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
|