toolroll 0.9.53 → 0.9.55
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 +17 -0
- package/README.md +5 -4
- package/dist/agent-onboard.js +4 -1
- package/dist/approval-rules-ui.d.ts +3 -2
- package/dist/approval-rules-ui.js +15 -20
- package/dist/assignment-ui.d.ts +3 -2
- package/dist/assignment-ui.js +4 -11
- package/dist/backup-ui.d.ts +2 -1
- package/dist/backup-ui.js +16 -27
- package/dist/brand-mark.d.ts +3 -2
- package/dist/brand-mark.js +4 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-shell.d.ts +2 -1
- package/dist/browser-shell.js +9 -9
- package/dist/browser-workspace.d.ts +2 -3
- package/dist/browser-workspace.js +0 -3
- package/dist/chat-ask.js +8 -8
- package/dist/chat-core.d.ts +171 -0
- package/dist/chat-core.js +288 -0
- package/dist/chat-decide.d.ts +10 -8
- package/dist/chat-decide.js +26 -21
- package/dist/chat-delivery-state.d.ts +51 -75
- package/dist/chat-delivery-state.js +140 -170
- package/dist/chat-delivery.d.ts +21 -2
- package/dist/chat-delivery.js +313 -194
- package/dist/chat-flow.d.ts +29 -1
- package/dist/chat-flow.js +24 -50
- package/dist/chat-messages.d.ts +319 -0
- package/dist/chat-messages.js +841 -0
- package/dist/chat-migration.d.ts +30 -0
- package/dist/chat-migration.js +802 -0
- package/dist/chat-polish.d.ts +5 -4
- package/dist/chat-polish.js +9 -10
- package/dist/chat-quiet.d.ts +3 -0
- package/dist/chat-quiet.js +5 -0
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +26 -53
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +9 -3
- package/dist/coding-shipping-ui.d.ts +2 -1
- package/dist/coding-shipping-ui.js +6 -6
- package/dist/coding-ui.d.ts +3 -1
- package/dist/coding-ui.js +30 -29
- package/dist/contracts/chat-content.d.ts +71 -2
- package/dist/contracts/chat-content.js +22 -3
- package/dist/contracts/chat-tables.d.ts +11 -4
- package/dist/contracts/chat-tables.js +165 -87
- package/dist/contracts/cli.d.ts +1 -3
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +6 -5
- package/dist/contracts/discord-callback.js +2 -1
- package/dist/contracts/health.d.ts +1 -3
- package/dist/contracts/slack-callback.js +3 -2
- package/dist/contracts/store-json.d.ts +1 -5
- package/dist/contracts/store-json.js +1 -4
- package/dist/contracts/teams-callback.js +2 -1
- package/dist/control-ui.d.ts +5 -4
- package/dist/control-ui.js +9 -34
- package/dist/credentials-ui.d.ts +5 -4
- package/dist/credentials-ui.js +23 -24
- package/dist/demo-chat.d.ts +3 -3
- package/dist/demo-chat.js +29 -76
- package/dist/discord-chat.js +19 -38
- package/dist/discord-settings.d.ts +3 -2
- package/dist/discord-settings.js +21 -55
- package/dist/discord.js +4 -14
- package/dist/envelope.js +10 -0
- package/dist/evidence-pack.d.ts +2 -1
- package/dist/evidence-pack.js +45 -49
- package/dist/export-ui.d.ts +7 -2
- package/dist/export-ui.js +4 -10
- package/dist/export.js +4 -4
- package/dist/flow-gallery-ui.d.ts +4 -3
- package/dist/flow-gallery-ui.js +28 -43
- package/dist/flow-starters-ui.d.ts +2 -1
- package/dist/flow-starters-ui.js +12 -15
- package/dist/flow-triggers.d.ts +1 -0
- package/dist/flow-triggers.js +16 -14
- package/dist/flows-ui.d.ts +4 -3
- package/dist/flows-ui.js +19 -30
- package/dist/flows.js +2 -1
- package/dist/guides.js +108 -3
- package/dist/html.d.ts +76 -0
- package/dist/html.js +173 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +19 -23
- package/dist/kits-ui.d.ts +5 -4
- package/dist/kits-ui.js +18 -20
- package/dist/knowledge-ui.d.ts +6 -5
- package/dist/knowledge-ui.js +26 -25
- package/dist/lead-context.d.ts +11 -1
- package/dist/lead-context.js +6 -8
- package/dist/ledger-view.d.ts +2 -1
- package/dist/ledger-view.js +27 -29
- package/dist/limits-ui.d.ts +2 -1
- package/dist/limits-ui.js +7 -7
- package/dist/live-bus.d.ts +50 -8
- package/dist/live-bus.js +133 -50
- package/dist/mate-tools.js +3 -2
- package/dist/mcp-oauth.js +35 -36
- package/dist/models-ui.d.ts +2 -1
- package/dist/models-ui.js +25 -35
- package/dist/modes.d.ts +13 -1
- package/dist/modes.js +17 -1
- package/dist/monitoring-ui.d.ts +4 -3
- package/dist/monitoring-ui.js +13 -26
- package/dist/notification-rows.d.ts +30 -0
- package/dist/notification-rows.js +102 -0
- package/dist/openrouter-models.d.ts +2 -1
- package/dist/openrouter-models.js +5 -9
- package/dist/operate.js +16 -9
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +13 -13
- package/dist/plane-review.js +5 -11
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +17 -23
- package/dist/project-delete-ui.d.ts +4 -3
- package/dist/project-delete-ui.js +15 -27
- package/dist/project-delete.js +21 -26
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +17 -32
- package/dist/recipe-ui.d.ts +8 -7
- package/dist/recipe-ui.js +39 -32
- package/dist/release-adapters.d.ts +60 -0
- package/dist/release-adapters.js +435 -0
- package/dist/release-cli.d.ts +24 -0
- package/dist/release-cli.js +181 -0
- package/dist/release-gate.d.ts +80 -0
- package/dist/release-gate.js +150 -0
- package/dist/release-links.d.ts +29 -0
- package/dist/release-links.js +102 -0
- package/dist/release.d.ts +331 -0
- package/dist/release.js +550 -0
- package/dist/reply-shape.d.ts +2 -1
- package/dist/reply-shape.js +5 -5
- package/dist/repository-context-ui.d.ts +2 -1
- package/dist/repository-context-ui.js +2 -7
- package/dist/request-budget-ui.d.ts +2 -1
- package/dist/request-budget-ui.js +16 -16
- package/dist/result-review.d.ts +2 -3
- package/dist/result-review.js +3 -9
- package/dist/result-shots.js +2 -2
- package/dist/retention-ui.d.ts +7 -1
- package/dist/retention-ui.js +7 -11
- package/dist/retention.js +1 -1
- package/dist/serve.d.ts +11 -2
- package/dist/serve.js +69 -58
- package/dist/server/chat.d.ts +3 -6
- package/dist/server/chat.js +969 -1006
- package/dist/server/chrome.d.ts +291 -0
- package/dist/server/chrome.js +747 -0
- package/dist/server/console.css +2819 -0
- package/dist/server/flows.d.ts +2 -6
- package/dist/server/flows.js +840 -766
- package/dist/server/guards.d.ts +1 -1
- package/dist/server/guards.js +5 -2
- package/dist/server/handler-context.d.ts +1 -1
- package/dist/server/handler-context.js +1 -1
- package/dist/server/handler-registry.d.ts +8 -6
- package/dist/server/handler-registry.js +14 -2
- package/dist/server/http.d.ts +274 -0
- package/dist/server/http.js +239 -0
- package/dist/server/live.d.ts +34 -0
- package/dist/server/live.js +309 -0
- package/dist/server/pages.d.ts +2 -6
- package/dist/server/pages.js +808 -789
- package/dist/server/people-tokens.d.ts +1 -6
- package/dist/server/people-tokens.js +580 -572
- package/dist/server/remote.d.ts +4 -7
- package/dist/server/remote.js +37 -70
- package/dist/server/render-chat.d.ts +402 -0
- package/dist/server/render-chat.js +927 -0
- package/dist/server/render-pages.d.ts +535 -0
- package/dist/server/render-pages.js +1277 -0
- package/dist/server/render-people.d.ts +32 -0
- package/dist/server/render-people.js +121 -0
- package/dist/server/render-results.d.ts +744 -0
- package/dist/server/render-results.js +2237 -0
- package/dist/server/render-settings.d.ts +147 -0
- package/dist/server/render-settings.js +577 -0
- package/dist/server/render-tasks.d.ts +632 -0
- package/dist/server/render-tasks.js +2345 -0
- package/dist/server/request-context.d.ts +46 -0
- package/dist/server/request-context.js +17 -0
- package/dist/server/route-table.d.ts +25 -3
- package/dist/server/route-table.js +163 -75
- package/dist/server/runtime.d.ts +15 -7
- package/dist/server/runtime.js +7 -1
- package/dist/server/session.d.ts +138 -0
- package/dist/server/session.js +94 -0
- package/dist/server/settings.d.ts +1 -5
- package/dist/server/settings.js +2182 -2117
- package/dist/server/task-views.d.ts +11 -4
- package/dist/server/task-views.js +26 -22
- package/dist/server/tasks.d.ts +9 -9
- package/dist/server/tasks.js +1486 -1418
- package/dist/server-telemetry.d.ts +4 -3
- package/dist/server-telemetry.js +4 -8
- package/dist/skills-ui.d.ts +4 -3
- package/dist/skills-ui.js +14 -17
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +2 -1
- package/dist/slack-api.d.ts +5 -1
- package/dist/slack-api.js +2 -2
- package/dist/slack-chat.d.ts +9 -4
- package/dist/slack-chat.js +29 -52
- package/dist/slack-settings.d.ts +3 -2
- package/dist/slack-settings.js +23 -57
- package/dist/slack.js +6 -16
- package/dist/spend-ui.d.ts +2 -1
- package/dist/spend-ui.js +9 -32
- package/dist/sso-ui.d.ts +3 -2
- package/dist/sso-ui.js +26 -27
- package/dist/storage-ui.d.ts +2 -1
- package/dist/storage-ui.js +14 -33
- package/dist/store.d.ts +97 -364
- package/dist/store.js +207 -1531
- package/dist/surface.js +14 -3
- package/dist/task-composer.d.ts +2 -1
- package/dist/task-composer.js +7 -7
- package/dist/task-status.d.ts +4 -3
- package/dist/task-status.js +15 -16
- package/dist/team-http.d.ts +5 -9
- package/dist/team-http.js +5 -63
- package/dist/team-ui.d.ts +2 -1
- package/dist/team-ui.js +2 -5
- package/dist/teammate-question.js +12 -12
- package/dist/teammates-ui.d.ts +5 -4
- package/dist/teammates-ui.js +61 -90
- package/dist/teams-chat.js +12 -9
- package/dist/teams-settings.d.ts +3 -2
- package/dist/teams-settings.js +22 -33
- package/dist/teams.js +4 -5
- package/dist/telegram-flow.js +5 -5
- package/dist/telegram-settings.d.ts +3 -2
- package/dist/telegram-settings.js +14 -25
- package/dist/telegram.js +4 -4
- package/dist/toolroll-update-ui.d.ts +3 -2
- package/dist/toolroll-update-ui.js +35 -39
- package/dist/toolroll-update.d.ts +10 -0
- package/dist/toolroll-update.js +50 -3
- package/dist/tools-ui.d.ts +5 -2
- package/dist/tools-ui.js +31 -31
- package/dist/when-html.d.ts +11 -3
- package/dist/when-html.js +5 -6
- package/dist/workspace-revision.js +3 -1
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +10 -10
- package/package.json +1 -1
- package/dist/server/shared.d.ts +0 -3151
- package/dist/server/shared.js +0 -12001
- package/dist/slack-state.d.ts +0 -13
- package/dist/slack-state.js +0 -10
package/dist/slack.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
4
|
import { SocketModeClient, LogLevel } from "@slack/socket-mode";
|
|
5
5
|
import { loadSlackCredentials, slackApi } from "./slack-api.js";
|
|
6
|
-
import {
|
|
6
|
+
import { ChatState } from "./chat-delivery-state.js";
|
|
7
7
|
import { receiveSlack, processSlackEvent, deliverSlackPart, planSlackNotifications, planSlackRooms, } from "./slack-chat.js";
|
|
8
8
|
/** The bot and socket tokens must belong to the same app, including after reconnect. */
|
|
9
9
|
export class ToolrollSlackSocket extends SocketModeClient {
|
|
@@ -29,7 +29,7 @@ export class ToolrollSlackSocket extends SocketModeClient {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
export async function followSlack(options) {
|
|
32
|
-
const owner = randomBytes(16).toString("hex"), state = new
|
|
32
|
+
const owner = randomBytes(16).toString("hex"), state = new ChatState(options.store, "slack");
|
|
33
33
|
const pause = async (ms) => {
|
|
34
34
|
try {
|
|
35
35
|
await sleep(ms, undefined, { signal: options.signal });
|
|
@@ -55,9 +55,7 @@ export async function followSlack(options) {
|
|
|
55
55
|
current.appToken === credentials.appToken &&
|
|
56
56
|
state.owns(credentials.installation, owner));
|
|
57
57
|
};
|
|
58
|
-
const problem = (text) => state.
|
|
59
|
-
.prepare("UPDATE slack_runtime SET problem=? WHERE installation=? AND owner=?")
|
|
60
|
-
.run(text, credentials.installation, owner);
|
|
58
|
+
const problem = (text) => state.setProblem(credentials.installation, owner, text);
|
|
61
59
|
// SDK log output can include envelopes/tokens. Only fixed status phrases are saved.
|
|
62
60
|
const client = new ToolrollSlackSocket({
|
|
63
61
|
appToken: credentials.appToken,
|
|
@@ -93,9 +91,7 @@ export async function followSlack(options) {
|
|
|
93
91
|
}, 10_000);
|
|
94
92
|
client.on("connected", () => {
|
|
95
93
|
connected = true;
|
|
96
|
-
state.
|
|
97
|
-
.prepare("UPDATE slack_runtime SET connected=?,problem=NULL WHERE installation=? AND owner=?")
|
|
98
|
-
.run(new Date().toISOString(), credentials.installation, owner);
|
|
94
|
+
state.setConnected(credentials.installation, owner, new Date());
|
|
99
95
|
});
|
|
100
96
|
client.on("disconnected", () => {
|
|
101
97
|
connected = false;
|
|
@@ -131,11 +127,7 @@ export async function followSlack(options) {
|
|
|
131
127
|
try {
|
|
132
128
|
await client.start();
|
|
133
129
|
while (same()) {
|
|
134
|
-
|
|
135
|
-
.prepare("SELECT retry_at FROM slack_runtime WHERE installation=?")
|
|
136
|
-
.get(credentials.installation)?.retry_at;
|
|
137
|
-
if (!connected ||
|
|
138
|
-
(typeof retry === "string" && retry > new Date().toISOString())) {
|
|
130
|
+
if (!connected || state.retryAt(credentials.installation) > new Date().toISOString()) {
|
|
139
131
|
await pause(1000);
|
|
140
132
|
continue;
|
|
141
133
|
}
|
|
@@ -158,9 +150,7 @@ export async function followSlack(options) {
|
|
|
158
150
|
alive = false;
|
|
159
151
|
options.signal.removeEventListener("abort", stop);
|
|
160
152
|
await client.disconnect().catch(() => { });
|
|
161
|
-
state.
|
|
162
|
-
.prepare("UPDATE slack_runtime SET owner=NULL,lease_until=NULL,connected=NULL WHERE installation=? AND owner=?")
|
|
163
|
-
.run(credentials.installation, owner);
|
|
153
|
+
state.stop(credentials.installation, owner);
|
|
164
154
|
}
|
|
165
155
|
await pause(5000);
|
|
166
156
|
}
|
package/dist/spend-ui.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* only) stop new work at 100 %. An instance operator's page; budgets change
|
|
5
5
|
* with a step-up.
|
|
6
6
|
*/
|
|
7
|
+
import { type Html } from "./html.js";
|
|
7
8
|
import { type BudgetState, type SpendItem } from "./spend.js";
|
|
8
9
|
export declare const SPEND_CSS: string;
|
|
9
10
|
export type SpendView = {
|
|
@@ -23,6 +24,6 @@ export type SpendView = {
|
|
|
23
24
|
export declare function spendHtml(view: SpendView, notice: {
|
|
24
25
|
said?: string | null;
|
|
25
26
|
problem?: string | null;
|
|
26
|
-
}):
|
|
27
|
+
}): Html;
|
|
27
28
|
/** One row per piece of spend, for a spreadsheet. */
|
|
28
29
|
export declare function spendCsv(items: readonly SpendItem[], teammateNames: Map<number, string>): string;
|
package/dist/spend-ui.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* only) stop new work at 100 %. An instance operator's page; budgets change
|
|
5
5
|
* with a step-up.
|
|
6
6
|
*/
|
|
7
|
+
import { html, postForm } from "./html.js";
|
|
7
8
|
import { budgetLabel, usd } from "./spend.js";
|
|
8
|
-
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
9
9
|
const projectName = (repo) => repo.split("/").filter(Boolean).pop() ?? repo;
|
|
10
10
|
export const SPEND_CSS = `.spend{max-width:960px;min-width:0}.spend-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}.spend-head h1{margin:0}` +
|
|
11
11
|
`.spend-month{display:flex;gap:10px;align-items:baseline;font-size:.875rem}.spend-total{font-size:1.5rem;font-weight:600;margin:12px 0 2px;font-variant-numeric:tabular-nums}` +
|
|
@@ -35,10 +35,10 @@ function breakdown(items, keyOf, top = 12) {
|
|
|
35
35
|
function table(title, rows, name) {
|
|
36
36
|
if (rows.length === 0)
|
|
37
37
|
return "";
|
|
38
|
-
return `<section><h2>${
|
|
38
|
+
return html `<section><h2>${title}</h2><table><tbody>${rows.map(row => html `<tr><td>${name(row.key)}</td><td class="n">${row.count}</td><td class="n">${usd(row.microusd)}${row.unpriced > 0 ? html ` <span class="meta">+${row.unpriced} unpriced</span>` : ""}</td></tr>`)}</tbody></table></section>`;
|
|
39
39
|
}
|
|
40
40
|
export function spendHtml(view, notice) {
|
|
41
|
-
const note = notice.problem ? `<p class="problem" role="alert">${
|
|
41
|
+
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
42
42
|
const total = view.items.reduce((sum, item) => sum + (item.microusd ?? 0), 0);
|
|
43
43
|
const unpriced = view.items.filter(item => item.microusd === null && (item.tokensIn !== null || item.kind !== "run")).length;
|
|
44
44
|
const counts = { run: view.items.filter(item => item.kind === "run").length, teammate: view.items.filter(item => item.kind === "teammate").length, chat: view.items.filter(item => item.kind === "chat").length, sort: view.items.filter(item => item.kind === "sort").length };
|
|
@@ -48,38 +48,15 @@ export function spendHtml(view, notice) {
|
|
|
48
48
|
const width = Math.min(100, Math.max(0, budget.percent));
|
|
49
49
|
const over = budget.spentMicrousd >= budget.limitMicrousd;
|
|
50
50
|
const label = budgetLabel(budget, budget.scope === "teammate" ? nameOfTeammate(budget.key) : undefined).replace(/'s$/, "");
|
|
51
|
-
return `<div class="budget${over ? " over" : ""}" data-budget="${budget.id}"><span class="name">${
|
|
52
|
-
|
|
53
|
-
`<span class="bar" aria-hidden="true"><span style="width:${width}%"></span></span>` +
|
|
54
|
-
`<span class="meta">${over && budget.hardStop ? "Used up: new API work waits until next month or a higher budget." : budget.hardStop ? "Stops API work at 100%." : "Alerts only."}${budget.unpriced > 0 ? ` ${budget.unpriced} unpriced` : ""}</span>` +
|
|
55
|
-
`<details><summary>Change</summary><form method="post" action="/spend/budget" class="budget-form"><input type="hidden" name="csrf" value="${e(view.csrf)}"><input type="hidden" name="target" value="${e(`${budget.scope}:${budget.key}`)}">` +
|
|
56
|
-
`<label>Monthly limit (US dollars)<input type="number" name="usd" min="1" step="1" value="${Math.round(budget.limitMicrousd / 1_000_000)}" required></label>` +
|
|
57
|
-
`<label class="choice"><input type="checkbox" name="stop" value="1"${budget.hardStop ? " checked" : ""}> Stop new work at 100%</label>` +
|
|
58
|
-
`<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>` +
|
|
59
|
-
`<button type="submit" name="action" value="save">Save</button> <button type="submit" name="action" value="remove">Remove</button></form></details></div>`;
|
|
60
|
-
}).join("");
|
|
51
|
+
return html `<div class="budget${over ? " over" : ""}" data-budget="${budget.id}"><span class="name">${label}</span><span class="figures">${usd(budget.spentMicrousd)} of ${usd(budget.limitMicrousd)} · ${budget.percent}%</span><span class="bar" aria-hidden="true"><span style="width:${width}%"></span></span><span class="meta">${over && budget.hardStop ? "Used up: new API work waits until next month or a higher budget." : budget.hardStop ? "Stops API work at 100%." : "Alerts only."}${budget.unpriced > 0 ? ` ${budget.unpriced} unpriced` : ""}</span><details><summary>Change</summary>${postForm("/spend/budget", html `<label>Monthly limit (US dollars)<input type="number" name="usd" min="1" step="1" value="${Math.round(budget.limitMicrousd / 1_000_000)}" required></label><label class="choice"><input type="checkbox" name="stop" value="1"${budget.hardStop ? html ` checked` : ""}> Stop new work at 100%</label><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label><button type="submit" name="action" value="save">Save</button> <button type="submit" name="action" value="remove">Remove</button>`, { attrs: { class: "budget-form" }, hidden: { target: `${budget.scope}:${budget.key}` } })}</details></div>`;
|
|
52
|
+
});
|
|
61
53
|
const groups = ["Everything", "Projects", "People", "Teammates"];
|
|
62
54
|
const options = groups.map(group => {
|
|
63
55
|
const members = view.targets.filter(one => one.group === group);
|
|
64
|
-
return members.length === 0 ? "" : `<optgroup label="${group}">${members.map(one => `<option value="${
|
|
65
|
-
})
|
|
66
|
-
const add = `<details${view.budgets.length === 0 ?
|
|
67
|
-
|
|
68
|
-
`<label>Monthly limit (US dollars)<input type="number" name="usd" min="1" step="1" required></label>` +
|
|
69
|
-
`<label class="choice"><input type="checkbox" name="stop" value="1" checked> Stop new work at 100%</label>` +
|
|
70
|
-
`<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>` +
|
|
71
|
-
`<button type="submit" name="action" value="save">Add budget</button></form></details>`;
|
|
72
|
-
const csv = `/spend?month=${e(view.month)}&format=csv`;
|
|
73
|
-
return `<article class="spend">` +
|
|
74
|
-
`<div class="spend-head"><h1>Spend</h1><p class="spend-month"><a href="/spend?month=${e(view.previous)}">← ${e(view.previous)}</a><strong>${e(view.month)}</strong>${view.next === null ? "" : `<a href="/spend?month=${e(view.next)}">${e(view.next)} →</a>`}<a href="${csv}" download>CSV</a></p></div>${note}` +
|
|
75
|
-
`<p class="spend-total" data-spend-total="${total}">${usd(total)}</p><p class="meta">${parts.join(" · ")}${unpriced > 0 ? ` · ${unpriced} unpriced (no reported cost or catalogue price)` : ""} · UTC month</p>` +
|
|
76
|
-
`<section><h2>Budgets</h2>${budgets === "" ? `<p class="meta">No budgets yet.</p>` : budgets}${add}</section>` +
|
|
77
|
-
`<div class="spend-grid">` +
|
|
78
|
-
table("By project", breakdown(view.items, item => item.project), projectName) +
|
|
79
|
-
table("By person", breakdown(view.items, item => item.person), key => key) +
|
|
80
|
-
table("By teammate", breakdown(view.items, item => item.teammate === null ? null : String(item.teammate)), nameOfTeammate) +
|
|
81
|
-
table("By model", breakdown(view.items, item => `${item.provider}${item.model === null ? "" : ` · ${item.model}`}`), key => key) +
|
|
82
|
-
`</div><p class="meta">Subscription work is $0; its limits are on Tasks. API work is what the provider reported, or its tokens at the prices in Settings → Models.</p></article>`;
|
|
56
|
+
return members.length === 0 ? "" : html `<optgroup label="${group}">${members.map(one => html `<option value="${one.value}">${one.label}</option>`)}</optgroup>`;
|
|
57
|
+
});
|
|
58
|
+
const add = html `<details${view.budgets.length === 0 ? html ` open` : ""}><summary>Add a budget</summary>${postForm("/spend/budget", html `<label>For<select name="target" required>${options}</select></label><label>Monthly limit (US dollars)<input type="number" name="usd" min="1" step="1" required></label><label class="choice"><input type="checkbox" name="stop" value="1" checked> Stop new work at 100%</label><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label><button type="submit" name="action" value="save">Add budget</button>`, { attrs: { class: "budget-form" } })}</details>`;
|
|
59
|
+
return html `<article class="spend"><div class="spend-head"><h1>Spend</h1><p class="spend-month"><a href="/spend?month=${view.previous}">← ${view.previous}</a><strong>${view.month}</strong>${view.next === null ? "" : html `<a href="/spend?month=${view.next}">${view.next} →</a>`}<a href="/spend?month=${view.month}&format=csv" download>CSV</a></p></div>${note}<p class="spend-total" data-spend-total="${total}">${usd(total)}</p><p class="meta">${parts.join(" · ")}${unpriced > 0 ? ` · ${unpriced} unpriced (no reported cost or catalogue price)` : ""} · UTC month</p><section><h2>Budgets</h2>${budgets.length === 0 ? html `<p class="meta">No budgets yet.</p>` : budgets}${add}</section><div class="spend-grid">${table("By project", breakdown(view.items, item => item.project), projectName)}${table("By person", breakdown(view.items, item => item.person), key => key)}${table("By teammate", breakdown(view.items, item => item.teammate === null ? null : String(item.teammate)), nameOfTeammate)}${table("By model", breakdown(view.items, item => `${item.provider}${item.model === null ? "" : ` · ${item.model}`}`), key => key)}</div><p class="meta">Subscription work is $0; its limits are on Tasks. API work is what the provider reported, or its tokens at the prices in Settings → Models.</p></article>`;
|
|
83
60
|
}
|
|
84
61
|
/** One row per piece of spend, for a spreadsheet. */
|
|
85
62
|
export function spendCsv(items, teammateNames) {
|
package/dist/sso-ui.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* operator's page; changes take their password.
|
|
5
5
|
*/
|
|
6
6
|
import type { OidcSettings } from "./oidc.js";
|
|
7
|
+
import { type Html } from "./html.js";
|
|
7
8
|
export declare const SSO_CSS: string;
|
|
8
9
|
export type SsoSettingsView = {
|
|
9
10
|
settings: OidcSettings | null;
|
|
@@ -16,7 +17,7 @@ export type SsoSettingsView = {
|
|
|
16
17
|
/** Whether the person looking is linked to the provider already. */
|
|
17
18
|
linked: boolean;
|
|
18
19
|
};
|
|
19
|
-
export declare function ssoSettingsHtml(view: SsoSettingsView,
|
|
20
|
+
export declare function ssoSettingsHtml(view: SsoSettingsView, notice: {
|
|
20
21
|
said?: string | null;
|
|
21
22
|
problem?: string | null;
|
|
22
|
-
}):
|
|
23
|
+
}): Html;
|
package/dist/sso-ui.js
CHANGED
|
@@ -1,39 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
import { html, postForm } from "./html.js";
|
|
2
2
|
export const SSO_CSS = `.sign-in{max-width:780px;min-width:0}.sign-in form{display:grid;gap:12px;margin:0}.sign-in label{display:grid;gap:6px}.sign-in input,.sign-in select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}` +
|
|
3
3
|
`.sign-in .card{padding:16px 18px;margin:12px 0}.sign-in .card h2{font-size:1.05rem;margin:0 0 6px}.sign-in .redirect code{user-select:all;overflow-wrap:anywhere}.sign-in button{justify-self:start;min-height:44px}` +
|
|
4
4
|
`.sign-in .rules{display:grid;gap:8px}.sign-in .rule{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr) minmax(0,2fr);gap:8px;align-items:start}.sign-in .rule select[multiple]{min-height:5.5rem}` +
|
|
5
5
|
`.sign-in .row{display:flex;gap:12px;flex-wrap:wrap;align-items:center}.sign-in fieldset{border:0;padding:0;margin:0;display:grid;gap:6px}.sign-in fieldset label{display:flex;gap:8px;align-items:center}.sign-in fieldset input{width:auto}` +
|
|
6
6
|
`@media(max-width:600px){.sign-in .rule{grid-template-columns:1fr}.sign-in input,.sign-in select{font-size:16px}}`;
|
|
7
|
-
const password =
|
|
7
|
+
const password = html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`;
|
|
8
8
|
function ruleRow(index, rule, projects) {
|
|
9
9
|
const chosen = rule === null ? [] : rule.projects === "all" ? ["all"] : rule.projects;
|
|
10
|
-
return `<div class="rule"><label>Group<input type="text" name="group-${index}" value="${
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
return html `<div class="rule"><label>Group<input type="text" name="group-${index}" value="${rule?.group ?? ""}" placeholder="${index === 0 ? "for example: engineering" : ""}" autocomplete="off"></label>\
|
|
11
|
+
<label>Role<select name="role-${index}"><option value="operator"${rule?.role === "operator" ? " selected" : ""}>Operator</option><option value="viewer"${rule?.role === "viewer" ? " selected" : ""}>Viewer</option></select></label>\
|
|
12
|
+
<label>Projects<select name="projects-${index}" multiple><option value="all"${chosen.includes("all") ? " selected" : ""}>All projects</option>${projects.map(one => html `<option value="${one.path}"${chosen.includes(one.path) ? " selected" : ""}>${one.name}</option>`)}</select></label></div>`;
|
|
13
13
|
}
|
|
14
|
-
export function ssoSettingsHtml(view,
|
|
14
|
+
export function ssoSettingsHtml(view, notice) {
|
|
15
15
|
const s = view.settings;
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
|
|
16
|
+
const rows = [...(s?.rules ?? []), null, null].slice(0, Math.max(3, (s?.rules.length ?? 0) + 1)).map((rule, index) => ruleRow(index, rule, view.projects));
|
|
17
|
+
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
19
18
|
const redirect = view.redirect === null
|
|
20
|
-
? `<p class="meta">Open this page from the address people use (your https address) to see the redirect address to register.</p>`
|
|
21
|
-
: `<p class="redirect">Register this redirect address with the provider: <code>${
|
|
22
|
-
const form =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const test = s === null ? "" :
|
|
19
|
+
? html `<p class="meta">Open this page from the address people use (your https address) to see the redirect address to register.</p>`
|
|
20
|
+
: html `<p class="redirect">Register this redirect address with the provider: <code>${view.redirect}</code></p>`;
|
|
21
|
+
const form = postForm("/settings/sign-in", html `\
|
|
22
|
+
<label>Provider address (issuer)<input type="url" name="issuer" value="${s?.issuer ?? ""}" placeholder="https://acme.okta.com/oauth2/default" required inputmode="url" autocomplete="off"></label>\
|
|
23
|
+
<label>Client ID<input type="text" name="client-id" value="${s?.clientId ?? ""}" required autocomplete="off"></label>\
|
|
24
|
+
<label>Client secret<input type="password" name="client-secret" autocomplete="off" placeholder="${s?.clientSecret ? "Saved. Leave blank to keep it" : "Leave blank for a public client"}"></label>\
|
|
25
|
+
<details><summary>More</summary><label>Button says “Sign in with …”<input type="text" name="label" value="${s?.label ?? ""}" placeholder="Okta"></label>\
|
|
26
|
+
<label>Scopes<input type="text" name="scopes" value="${s?.scopes ?? "openid email profile"}"></label><label>Groups claim<input type="text" name="groups-claim" value="${s?.groupsClaim ?? "groups"}"></label></details>\
|
|
27
|
+
<h2>Who may sign in</h2><p class="meta">A person's first matching group sets their role and projects, again at every sign-in. No match, no entry. Use * for everyone.</p><div class="rules">${rows}</div>\
|
|
28
|
+
<fieldset><legend>Passwords</legend><label><input type="radio" name="passwords" value="everyone"${s?.passwords !== "operators" ? " checked" : ""}> Everyone may still use a password</label>\
|
|
29
|
+
<label><input type="radio" name="passwords" value="operators"${s?.passwords === "operators" ? " checked" : ""}> Only instance operators (a way in if the provider is down)</label></fieldset>\
|
|
30
|
+
${password}<button>${s === null ? "Turn on sign-in with the provider" : "Save"}</button>`, { hidden: { action: "save" } });
|
|
31
|
+
const test = s === null ? "" : postForm("/settings/sign-in", html `<button class="secondary">Check the provider</button>`, { attrs: { class: "row" }, hidden: { action: "test" } });
|
|
33
32
|
const link = s === null ? "" : view.linked
|
|
34
|
-
? `<p class="meta">You sign in with ${
|
|
35
|
-
: `<section class="card"><h2>Your account</h2><p>Sign in with ${
|
|
36
|
-
const off = s === null ? "" : `<details class="card"><summary>Turn off</summary
|
|
37
|
-
return `<section class="sign-in">${note}<p>People sign in with your organisation's identity provider (Okta, Microsoft Entra, Google, anything that speaks OpenID Connect), and its groups decide what they may do.</p
|
|
38
|
-
|
|
33
|
+
? html `<p class="meta">You sign in with ${s.label}.</p>`
|
|
34
|
+
: html `<section class="card"><h2>Your account</h2><p>Sign in with ${s.label} as this account from now on.</p><p><a class="button-link" href="/login/sso?link=1">Link to ${s.label}</a></p></section>`;
|
|
35
|
+
const off = s === null ? "" : html `<details class="card"><summary>Turn off</summary>${postForm("/settings/sign-in", html `<p>People who signed in with ${s.label} can't sign in until it's on again, unless they have a password.</p>${password}<button class="danger">Turn off sign-in with ${s.label}</button>`, { hidden: { action: "remove" } })}</details>`;
|
|
36
|
+
return html `<section class="sign-in">${note}<p>People sign in with your organisation's identity provider (Okta, Microsoft Entra, Google, anything that speaks OpenID Connect), and its groups decide what they may do.</p>\
|
|
37
|
+
${redirect}<section class="card">${form}</section>${test}${link}${off}</section>`;
|
|
39
38
|
}
|
package/dist/storage-ui.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Html } from "./html.js";
|
|
1
2
|
import { type CheckoutPlan } from "./checkout-cleanup.js";
|
|
2
3
|
import { type SweepRecord } from "./storage-sweep.js";
|
|
3
4
|
export declare const STORAGE_CSS: string;
|
|
@@ -12,4 +13,4 @@ export type StorageView = {
|
|
|
12
13
|
export declare function storageHtml(view: StorageView, notice: {
|
|
13
14
|
said?: string | null;
|
|
14
15
|
problem?: string | null;
|
|
15
|
-
}):
|
|
16
|
+
}): Html;
|
package/dist/storage-ui.js
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { basename } from "node:path";
|
|
9
|
+
import { html, postForm } from "./html.js";
|
|
9
10
|
import { previewDigest, whyWords } from "./checkout-cleanup.js";
|
|
10
11
|
import { CLEANUP_CHOICES, bytesWords } from "./storage.js";
|
|
11
12
|
import { sweepDetails, sweepWords } from "./storage-sweep.js";
|
|
12
|
-
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
13
13
|
const plural = (count, one, many = `${one}s`) => `${count} ${count === 1 ? one : many}`;
|
|
14
14
|
export const STORAGE_CSS = `.storage{max-width:720px;min-width:0;overflow-wrap:anywhere}.storage h2{font-size:1.0625rem;margin:24px 0 4px}` +
|
|
15
15
|
`.storage .storage-state{display:grid;gap:12px}.storage .storage-state h2{margin:0}.storage .storage-state p{margin:0}` +
|
|
@@ -45,49 +45,30 @@ function whenWords(at) {
|
|
|
45
45
|
function sweepHtml(sweep) {
|
|
46
46
|
const last = sweep.last;
|
|
47
47
|
if (last === null)
|
|
48
|
-
return `<p class="meta" data-last-sweep="">${sweep.off ? "Automatic sweep is off." :
|
|
49
|
-
const who = last.source === "manual" ? `Cleaned up by ${
|
|
48
|
+
return html `<p class="meta" data-last-sweep="">${sweep.off ? "Automatic sweep is off." : html `Toolroll sweeps leftovers once a day. It hasn't run yet.`}</p>`;
|
|
49
|
+
const who = last.source === "manual" ? `Cleaned up by ${last.actor}` : "Swept";
|
|
50
50
|
const details = sweepDetails(last.parts);
|
|
51
51
|
const problem = last.parts.some(one => one.failed > 0 || one.problem !== undefined);
|
|
52
|
-
return `<div class="sweep" data-last-sweep="${
|
|
53
|
-
(details.length === 0 ? "" : `<details class="sweep-details"><summary>Details</summary><ul>${details.map(line => `<li>${e(homeAsTilde(line))}</li>`).join("")}</ul></details>`) + `</div>`;
|
|
52
|
+
return html `<div class="sweep" data-last-sweep="${last.at}"><p${problem ? html ` class="problem"` : ""}>${who} <time datetime="${last.at}">${whenWords(last.at)}</time>. ${sweepWords(last.parts)}</p>${details.length === 0 ? "" : html `<details class="sweep-details"><summary>Details</summary><ul>${details.map(line => html `<li>${homeAsTilde(line)}</li>`)}</ul></details>`}</div>`;
|
|
54
53
|
}
|
|
55
|
-
function item(one
|
|
54
|
+
function item(one) {
|
|
56
55
|
const discard = one.why === "has changes"
|
|
57
|
-
? `<details class="discard"><summary>Discard</summary
|
|
58
|
-
`<p class="meta">Throws away this checkout and its uncommitted changes. Its branch stays.</p>` +
|
|
59
|
-
`<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button type="submit" class="danger">Discard changes</button></form></details>`
|
|
56
|
+
? html `<details class="discard"><summary>Discard</summary>${postForm("/settings/storage/discard", html `<p class="meta">Throws away this checkout and its uncommitted changes. Its branch stays.</p><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button type="submit" class="danger">Discard changes</button>`, { hidden: { path: one.path } })}</details>`
|
|
60
57
|
: "";
|
|
61
58
|
// The task it was for, then where it is (the home folder as ~), then why it stays.
|
|
62
59
|
const home = homedir();
|
|
63
60
|
const where = home !== "" && one.path.startsWith(`${home}/`) ? `~${one.path.slice(home.length)}` : one.path;
|
|
64
|
-
return `<li data-checkout="${
|
|
65
|
-
`<span class="path">${e(where)}</span>` +
|
|
66
|
-
(one.why === null ? "" : `<span class="why">${e(whyWords(one)[0].toUpperCase() + whyWords(one).slice(1))}</span>`) + discard + `</li>`;
|
|
61
|
+
return html `<li data-checkout="${one.path}"><span class="name">${one.taskId ?? basename(one.path)}</span><span class="size">${bytesWords(one.bytes)}</span><span class="path">${where}</span>${one.why === null ? "" : html `<span class="why">${whyWords(one)[0].toUpperCase() + whyWords(one).slice(1)}</span>`}${discard}</li>`;
|
|
67
62
|
}
|
|
68
63
|
export function storageHtml(view, notice) {
|
|
69
|
-
const { plan
|
|
70
|
-
const note = notice.problem ? `<p class="problem" role="alert">${
|
|
71
|
-
const hidden = `<input type="hidden" name="csrf" value="${e(csrf)}">`;
|
|
64
|
+
const { plan } = view;
|
|
65
|
+
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
72
66
|
const facts = [plural(plan.count, "checkout"), `${plan.waitingReview} waiting for review`, `${plan.withChanges} kept for their changes`].join(" · ");
|
|
73
67
|
const frees = plan.go.length === 0 ? "Nothing to clean up now." : `Cleaning up now frees about ${bytesWords(plan.freeBytes)}.`;
|
|
74
68
|
const remove = plan.go.length === 0 ? "" :
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
`<h3>Removes ${plural(plan.go.length, "checkout")}, about ${e(bytesWords(plan.freeBytes))}</h3><p class="meta">Their branches stay, so nothing committed is lost.</p>` +
|
|
81
|
-
`<ul class="checkouts" data-goes>${plan.go.map(one => item(one, csrf)).join("")}</ul>`) +
|
|
82
|
-
(plan.stay.length === 0 ? "" : `<h3>Stays (${plan.stay.length})</h3><ul class="checkouts" data-stays>${plan.stay.map(one => item(one, csrf)).join("")}</ul>`) +
|
|
83
|
-
remove + `</div></details>`;
|
|
84
|
-
const options = CLEANUP_CHOICES.map(one => `<option value="${one.value}"${one.value === plan.cleanup ? " selected" : ""}>${e(one.label)}</option>`).join("");
|
|
85
|
-
return `<section class="storage">${note}` +
|
|
86
|
-
`<div class="card storage-state"><h2 data-checkout-bytes="${plan.totalBytes}">Checkouts use ${e(bytesWords(plan.totalBytes))}</h2><p class="meta">${e(facts)}</p>` +
|
|
87
|
-
`<p data-clean-bytes="${plan.freeBytes}">${e(frees)}</p>${view.sweep === undefined ? "" : sweepHtml(view.sweep)}${preview}</div>` +
|
|
88
|
-
`<h2>Automatic cleanup</h2>` +
|
|
89
|
-
`<form method="post" action="/settings/storage" class="setting">${hidden}` +
|
|
90
|
-
`<label>Remove a finished task's clean checkout<select name="cleanup">${options}</select></label>` +
|
|
91
|
-
`<p class="meta">Its branch always stays. Checkouts with changes, commits on no branch, in use, or waiting for review are kept.</p>` +
|
|
92
|
-
`<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button type="submit">Save</button></form></section>`;
|
|
69
|
+
postForm("/settings/storage/clean", html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button type="submit">Remove ${plural(plan.go.length, "checkout")}</button>`, { attrs: { class: "remove" }, hidden: { preview: previewDigest(plan) } });
|
|
70
|
+
const preview = html `<details class="clean-up" data-clean-up${notice.problem?.startsWith("Enter your Toolroll password to clean") ? html ` open` : ""}><summary>Clean up</summary><div class="preview">${plan.go.length === 0 ? html `<p>Nothing to remove now.</p>` :
|
|
71
|
+
html `<h3>Removes ${plural(plan.go.length, "checkout")}, about ${bytesWords(plan.freeBytes)}</h3><p class="meta">Their branches stay, so nothing committed is lost.</p><ul class="checkouts" data-goes>${plan.go.map(one => item(one))}</ul>`}${plan.stay.length === 0 ? "" : html `<h3>Stays (${plan.stay.length})</h3><ul class="checkouts" data-stays>${plan.stay.map(one => item(one))}</ul>`}${remove}</div></details>`;
|
|
72
|
+
const options = CLEANUP_CHOICES.map(one => html `<option value="${one.value}"${one.value === plan.cleanup ? html ` selected` : ""}>${one.label}</option>`);
|
|
73
|
+
return html `<section class="storage">${note}<div class="card storage-state"><h2 data-checkout-bytes="${plan.totalBytes}">Checkouts use ${bytesWords(plan.totalBytes)}</h2><p class="meta">${facts}</p><p data-clean-bytes="${plan.freeBytes}">${frees}</p>${view.sweep === undefined ? "" : sweepHtml(view.sweep)}${preview}</div><h2>Automatic cleanup</h2>${postForm("/settings/storage", html `<label>Remove a finished task's clean checkout<select name="cleanup">${options}</select></label><p class="meta">Its branch always stays. Checkouts with changes, commits on no branch, in use, or waiting for review are kept.</p><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button type="submit">Save</button>`, { attrs: { class: "setting" } })}</section>`;
|
|
93
74
|
}
|