toolroll 0.9.26 → 0.9.28
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 +25 -0
- package/THIRD_PARTY_NOTICES.md +41 -0
- package/dist/agentconfig.js +71 -3
- package/dist/brand-icons.d.ts +168 -0
- package/dist/brand-icons.js +32 -0
- package/dist/brand-mark.d.ts +27 -0
- package/dist/brand-mark.js +49 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +41 -0
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +21 -23
- package/dist/browser-workspace.d.ts +17 -0
- package/dist/build-review.js +3 -2
- package/dist/builder.js +10 -8
- package/dist/chat-actions.js +7 -4
- package/dist/chat-delivery.js +3 -3
- package/dist/chat-flow.d.ts +4 -2
- package/dist/chat-flow.js +36 -10
- package/dist/converse.js +3 -2
- package/dist/coordinator-proposals.js +2 -2
- package/dist/decision.d.ts +3 -3
- package/dist/decision.js +4 -3
- package/dist/desktop-update.d.ts +2 -2
- package/dist/evidence.js +1 -1
- package/dist/flow-actions.d.ts +1 -0
- package/dist/flow-actions.js +6 -7
- package/dist/flow-code.d.ts +3 -2
- package/dist/flow-code.js +6 -4
- package/dist/flow-draft.d.ts +10 -4
- package/dist/flow-draft.js +19 -8
- package/dist/flow-engine.d.ts +0 -3
- package/dist/flow-engine.js +17 -10
- package/dist/flow-items.d.ts +46 -0
- package/dist/flow-items.js +152 -0
- package/dist/flow-people.d.ts +0 -1
- package/dist/flow-people.js +9 -1
- package/dist/flow-send.d.ts +5 -0
- package/dist/flow-send.js +33 -6
- package/dist/flow-steps.js +33 -10
- package/dist/flows-ui.js +5 -1
- package/dist/flows.d.ts +23 -8
- package/dist/flows.js +106 -52
- package/dist/guides.js +1 -1
- package/dist/integrations-ui.js +10 -3
- package/dist/integrations.d.ts +2 -0
- package/dist/integrations.js +2 -2
- package/dist/lead-context.d.ts +3 -9
- package/dist/lead-context.js +21 -5
- package/dist/mate-doors.js +8 -2
- package/dist/mate-tools.js +27 -10
- package/dist/mate.js +13 -2
- package/dist/operate.d.ts +3 -0
- package/dist/operate.js +122 -8
- package/dist/phase-routing.d.ts +86 -5
- package/dist/phase-routing.js +228 -16
- package/dist/project-skills.d.ts +10 -0
- package/dist/proposal.d.ts +10 -2
- package/dist/proposal.js +25 -10
- package/dist/recipe-ui.js +2 -1
- package/dist/result-shots.d.ts +5 -1
- package/dist/result-shots.js +33 -3
- package/dist/scout-report.d.ts +10 -4
- package/dist/scout-report.js +23 -6
- package/dist/scout.d.ts +2 -0
- package/dist/scout.js +84 -11
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +76 -23
- package/dist/store.d.ts +64 -5
- package/dist/store.js +192 -7
- package/dist/task-sizing.d.ts +78 -0
- package/dist/task-sizing.js +228 -0
- package/dist/task-text.d.ts +4 -4
- package/dist/task-text.js +10 -6
- package/dist/teammate-work.js +31 -7
- package/dist/teammates.d.ts +20 -1
- package/dist/teammates.js +18 -5
- package/dist/teams-chat.d.ts +4 -1
- package/dist/teams-chat.js +17 -6
- package/dist/telegram-flow.js +6 -2
- package/dist/telegram.js +37 -6
- package/dist/test-temp.js +1 -1
- package/dist/text-limits.d.ts +67 -0
- package/dist/text-limits.js +73 -0
- package/dist/tier-report.d.ts +44 -0
- package/dist/tier-report.js +71 -0
- package/dist/tools-ui.d.ts +1 -3
- package/dist/tools-ui.js +5 -4
- package/package.json +3 -1
package/dist/telegram.js
CHANGED
|
@@ -35,7 +35,9 @@ import { BATCH_MS, chatText, chatTitle, factLinkLabel, mentions, nameTelegramBot
|
|
|
35
35
|
import { LEAD_SAY_KIND, enqueueLeadLapses, leadSayEarlier, leadSayText, leadSubjectOf } from "./lead-voice.js";
|
|
36
36
|
import { applyTeamInbound, deliverTeamChats, teamCommand } from "./telegram-team.js";
|
|
37
37
|
import { applyFlowChoiceTap, applyFlowReply, applyFlowTap, FLOW_DECIDE_KEY, flowButtons, flowChoiceButtons, flowDecisionAt, flowSendKeyboardRow, flowSentContent } from "./telegram-flow.js";
|
|
38
|
-
import { flowChoiceAt, FLOW_CHOOSE_KEY, FLOW_SEND_KEY } from "./flow-send.js";
|
|
38
|
+
import { flowChoiceAt, flowSendTail, FLOW_CHOOSE_KEY, FLOW_SEND_KEY } from "./flow-send.js";
|
|
39
|
+
import { cleanFlowMessage, fitFlowMessage } from "./flow-items.js";
|
|
40
|
+
import { telegramReply } from "./reply-shape.js";
|
|
39
41
|
import { connectChannel, FLOW_WORDS, takeChannelMessage, watchedChannel } from "./chat-inbox.js";
|
|
40
42
|
import { focusContextFor, taskInCeiling } from "./chat-channel.js";
|
|
41
43
|
import { answersPrompt, applyDecideFeedback, applyDecideTap, decideFallbackLink, dropDecideTokens, hasLiveDecideTokens, linksFor, placeDecidePrompt, decideOffer, decideTargetOf, isDecideToken, mergedText, mintDecideButtons, offerFingerprint, openPromptFor, placeDecideTokens, recordChatMerge, retireDecideTokens } from "./chat-decide.js";
|
|
@@ -54,6 +56,8 @@ export const BRIDGE_LEASE_MS = 2 * 60_000;
|
|
|
54
56
|
export const DELIVERY_CLAIM_MS = 2 * 60_000;
|
|
55
57
|
/** Telegram's own message ceiling, with room for our part headers. */
|
|
56
58
|
const PART_CAP = 3_900;
|
|
59
|
+
/** Telegram's own limit on one message's text (Bot API sendMessage), less room for the line a tap adds under it. */
|
|
60
|
+
const TELEGRAM_TEXT_MAX = 4_096 - 300;
|
|
57
61
|
/** Pages of getUpdates one pass will read before reporting a backlog. */
|
|
58
62
|
const PAGE_BUDGET = 10;
|
|
59
63
|
// ---- pushed updates (v98) ----------------------------------------------------
|
|
@@ -840,7 +844,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
840
844
|
let answer;
|
|
841
845
|
try {
|
|
842
846
|
answer = batch.length > 1
|
|
843
|
-
? await transport("sendMediaGroup", { chat_id: binding.chatId, media: batch.map((one, index) => ({ type: "photo", media: `attach://shot${index}`, ...(index === 0 ? { caption: one.caption } : {}) })) }, undefined, upload(batch[0], "shot0"), batch.slice(1).map((one, index) => upload(one, `shot${index + 1}`)))
|
|
847
|
+
? await transport("sendMediaGroup", { chat_id: binding.chatId, media: batch.map((one, index) => ({ type: "photo", media: `attach://shot${index}`, ...(index === 0 || one.numbered === true ? { caption: one.caption } : {}) })) }, undefined, upload(batch[0], "shot0"), batch.slice(1).map((one, index) => upload(one, `shot${index + 1}`)))
|
|
844
848
|
: batch[0].photo
|
|
845
849
|
? await transport("sendPhoto", { chat_id: binding.chatId, caption: batch[0].caption }, undefined, upload(batch[0], "photo"))
|
|
846
850
|
: await transport("sendDocument", { chat_id: binding.chatId, caption: batch[0].caption }, undefined, upload(batch[0], "document"));
|
|
@@ -1192,7 +1196,7 @@ async function deliverOne(store, botId, binding, sender, notification, clock, ph
|
|
|
1192
1196
|
const target = alreadyAccepted || !current ? null : decideTargetOf(notification);
|
|
1193
1197
|
const offer = decideOfferFor(store, binding, target, projects, clock(), evidenceRoot);
|
|
1194
1198
|
const decided = offer === null ? null : mintDecideButtons(store, decideSeat(binding), target, offer, clock());
|
|
1195
|
-
|
|
1199
|
+
let parts = split(offer?.text ?? `${notificationIdentity(notification, title !== undefined && mentions(words, title) ? undefined : title)}${words}`);
|
|
1196
1200
|
const fallback = offer === null ? decideFallbackLink(target) : null;
|
|
1197
1201
|
let button = fallback !== null ? (() => { try {
|
|
1198
1202
|
return phoneLinkButton(phoneOrigin?.() ?? null, fallback);
|
|
@@ -1218,6 +1222,17 @@ async function deliverOne(store, botId, binding, sender, notification, clock, ph
|
|
|
1218
1222
|
} })();
|
|
1219
1223
|
if (sentRow !== null)
|
|
1220
1224
|
button = sentRow.length === 0 ? null : sentRow;
|
|
1225
|
+
// After research (flow-items.ts): each item numbered under the summary, its words cleaned as the rest of the message's
|
|
1226
|
+
// are, within Telegram's limit with every link kept — one message, unless head, links and tail alone overflow it.
|
|
1227
|
+
let entities = [];
|
|
1228
|
+
if (sent?.items !== undefined && sent.items.length > 0 && offer === null) {
|
|
1229
|
+
const subject = chatText(leadSubjectOf(store, notification, binding.approver), task);
|
|
1230
|
+
const head = `${notificationIdentity(notification, title !== undefined && mentions(subject, title) ? undefined : title)}${subject}`;
|
|
1231
|
+
const message = cleanFlowMessage({ head, summary: sent.summary, items: sent.items, tail: flowSendTail(sent) }, words => chatText(words, task));
|
|
1232
|
+
const voiced = fitFlowMessage(message, TELEGRAM_TEXT_MAX, shaped => telegramReply(shaped).text.length).map(telegramReply);
|
|
1233
|
+
parts = voiced.map(one => one.text);
|
|
1234
|
+
entities = voiced.map(one => one.entities);
|
|
1235
|
+
}
|
|
1221
1236
|
// A teammate's question (v93): its options and "Answer in words", for the person it asks.
|
|
1222
1237
|
const asked = flowKeys === null && choiceKeys === null ? openQuestionOf(store, notification.dedupeKey) : null;
|
|
1223
1238
|
const questionKeys = asked === null ? null : telegramQuestionButtons(store, binding, asked, clock());
|
|
@@ -1227,7 +1242,7 @@ async function deliverOne(store, botId, binding, sender, notification, clock, ph
|
|
|
1227
1242
|
const final = index === parts.length - 1;
|
|
1228
1243
|
const decideKeys = decided === null ? [] : decideKeyboard(phoneOrigin, decided.rows);
|
|
1229
1244
|
const keyboard = !final ? undefined : keys !== null ? [...keys.keyboard, ...(button === null ? [] : [button])] : decideKeys.length > 0 ? decideKeys : button !== null ? [button] : undefined;
|
|
1230
|
-
const sent = await sender(part, keyboard);
|
|
1245
|
+
const sent = await sender(part, keyboard, undefined, entities[index]?.length ? entities[index] : undefined);
|
|
1231
1246
|
if (!sent.ok) {
|
|
1232
1247
|
if (decided !== null)
|
|
1233
1248
|
dropDecideTokens(store, decided.tokens, clock());
|
|
@@ -1358,6 +1373,19 @@ async function editProgress(transport, chatId, messageId, text, keyboard, entiti
|
|
|
1358
1373
|
const confirmed = answer.result?.message_id;
|
|
1359
1374
|
return String(confirmed) === messageId ? { ok: true, messageId } : { ok: false, error: "Telegram did not confirm the progress message identity" };
|
|
1360
1375
|
}
|
|
1376
|
+
/** A tapped message's own formatting, read back for its edit: only bold and links that still fall inside the text. */
|
|
1377
|
+
function keptEntities(raw, length) {
|
|
1378
|
+
return (raw ?? []).flatMap((one) => {
|
|
1379
|
+
if (typeof one !== "object" || one === null)
|
|
1380
|
+
return [];
|
|
1381
|
+
const { type, offset, length: size, url } = one;
|
|
1382
|
+
if (typeof offset !== "number" || typeof size !== "number" || !Number.isSafeInteger(offset) || !Number.isSafeInteger(size) || offset < 0 || size <= 0 || offset + size > length)
|
|
1383
|
+
return [];
|
|
1384
|
+
if (type === "bold")
|
|
1385
|
+
return [{ type, offset, length: size }];
|
|
1386
|
+
return type === "text_link" && typeof url === "string" && /^https?:\/\/\S+$/.test(url) ? [{ type, offset, length: size, url }] : [];
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1361
1389
|
function receiptFor(botId, chatId, messageId) {
|
|
1362
1390
|
return `telegram:${botId}:${chatId}:${messageId ?? "?"}`;
|
|
1363
1391
|
}
|
|
@@ -2037,7 +2065,7 @@ function applyCallback(context, update, effects) {
|
|
|
2037
2065
|
});
|
|
2038
2066
|
});
|
|
2039
2067
|
};
|
|
2040
|
-
const editText = (text, keyboard) => {
|
|
2068
|
+
const editText = (text, keyboard, entities) => {
|
|
2041
2069
|
if (message === undefined)
|
|
2042
2070
|
return;
|
|
2043
2071
|
const chatId = message.chat === undefined ? null : String(message.chat.id);
|
|
@@ -2049,6 +2077,7 @@ function applyCallback(context, update, effects) {
|
|
|
2049
2077
|
chat_id: chatId,
|
|
2050
2078
|
message_id: messageId,
|
|
2051
2079
|
text,
|
|
2080
|
+
...(entities === undefined || entities.length === 0 ? {} : { entities }),
|
|
2052
2081
|
link_preview_options: { is_disabled: true },
|
|
2053
2082
|
...(keyboard === undefined ? {} : { reply_markup: { inline_keyboard: keyboard } }),
|
|
2054
2083
|
});
|
|
@@ -2166,11 +2195,13 @@ function applyCallback(context, update, effects) {
|
|
|
2166
2195
|
return;
|
|
2167
2196
|
}
|
|
2168
2197
|
const repos = context.projects === null ? null : telegramConversationRepos(store, binding.approver, context.projects);
|
|
2198
|
+
const listed = (flowSentContent(store, choiceAction.card, choiceAction.entry)?.items?.length ?? 0) > 0;
|
|
2169
2199
|
for (const effect of applyFlowChoiceTap(store, binding, choiceAction, { text: message.text ?? "" }, repos, clock())) {
|
|
2170
2200
|
if (effect.kind === "ack")
|
|
2171
2201
|
ack(effect.text);
|
|
2202
|
+
// An item list's message (flow-items.ts) keeps its bold titles and labelled links under what was chosen; any other stays plain.
|
|
2172
2203
|
else if (effect.kind === "edit")
|
|
2173
|
-
editText(effect.text);
|
|
2204
|
+
editText(effect.text, undefined, listed ? keptEntities(message.entities, effect.text.length) : undefined);
|
|
2174
2205
|
}
|
|
2175
2206
|
return;
|
|
2176
2207
|
}
|
package/dist/test-temp.js
CHANGED
|
@@ -18,7 +18,7 @@ export const TEST_TEMP_PREFIXES = [
|
|
|
18
18
|
"flow-goal-", "flow-scratch", "flows-chat-", "invoke-attest-", "knowledge-test-", "learning-", "peek-", "phone-origin-",
|
|
19
19
|
"proposal-repo-", "race-", "refresh-migration-", "refresh-refusal-", "release-check-", "shared-chat-", "skills-test-",
|
|
20
20
|
"task-status-surfaces-", "template-repo-", "template-test-", "v14-migrate-", "v17-migrate-", "watch-anchor-",
|
|
21
|
-
"needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-", "deploy-before-swap-", "one-vocabulary-", "accept-finishes-", "lead-promises-", "lead-people-",
|
|
21
|
+
"needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-", "deploy-before-swap-", "one-vocabulary-", "accept-finishes-", "lead-promises-", "lead-people-", "sizing-repo-",
|
|
22
22
|
// Playwright's browser profiles and artifacts.
|
|
23
23
|
"playwright_chromiumdev_profile-", "playwright_firefoxdev_profile-", "playwright-artifacts-",
|
|
24
24
|
];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text limits, in one place, and the one way a limit is met: a model is told the exact limit before it writes, an
|
|
3
|
+
* answer still over it is asked once to shorten (one repair turn), and an answer still over after that is kept whole
|
|
4
|
+
* in the record and linked, never chopped. Chat platforms' own message limits are met by splitting across messages.
|
|
5
|
+
*
|
|
6
|
+
* Limits apply when text is written. Reading never re-validates length, so a runtime with lower limits (0.9.23 to
|
|
7
|
+
* 0.9.26) still opens a store holding longer text (scripts/upgrade-path.mjs, the rollback leg).
|
|
8
|
+
*/
|
|
9
|
+
/** UTF-16 code units, the JavaScript and HTML maxlength measure, unless a name says bytes. */
|
|
10
|
+
export declare const TEXT_LIMITS: {
|
|
11
|
+
/** A task's goal and its out-of-scope text. */
|
|
12
|
+
readonly goal: 8000;
|
|
13
|
+
readonly goalBytes: 32000;
|
|
14
|
+
/** Revise feedback, steering and decision notes. */
|
|
15
|
+
readonly note: 4000;
|
|
16
|
+
readonly noteBytes: 16000;
|
|
17
|
+
/** A flow work zone's instructions; nothing is held back for the card's details (they are attached when long). */
|
|
18
|
+
readonly flowInstructions: 8000;
|
|
19
|
+
/** A research report's summary, in UTF-8 bytes. */
|
|
20
|
+
readonly reportSummary: 2500;
|
|
21
|
+
/** What one flow step passes on to the steps after it. */
|
|
22
|
+
readonly stageOutput: 12000;
|
|
23
|
+
};
|
|
24
|
+
/** One chat message on each platform. Longer text is split across messages, never cut. Teams: the text one Adaptive Card
|
|
25
|
+
* holds, well inside its 28 KB message. */
|
|
26
|
+
export declare const PLATFORM_LIMITS: {
|
|
27
|
+
readonly telegram: 4096;
|
|
28
|
+
readonly discord: 2000;
|
|
29
|
+
readonly slack: 4000;
|
|
30
|
+
readonly teams: 6000;
|
|
31
|
+
};
|
|
32
|
+
export type Platform = keyof typeof PLATFORM_LIMITS;
|
|
33
|
+
/** The line a model reads before it writes a field with a limit. */
|
|
34
|
+
export declare function limitRule(what: string, limit: number, unit?: "characters" | "bytes"): string;
|
|
35
|
+
/** One field over its limit: what it is, its limit, and how long it came back. */
|
|
36
|
+
export type Overrun = {
|
|
37
|
+
field: string;
|
|
38
|
+
limit: number;
|
|
39
|
+
length: number;
|
|
40
|
+
unit?: "characters" | "bytes";
|
|
41
|
+
};
|
|
42
|
+
/** How long a text is in a limit's unit. */
|
|
43
|
+
export declare const lengthIn: (text: string, unit?: "characters" | "bytes") => number;
|
|
44
|
+
/** The fields of `values` over their limits. */
|
|
45
|
+
export declare function overruns(values: Record<string, string>, limits: Record<string, number>, unit?: "characters" | "bytes"): Overrun[];
|
|
46
|
+
/** The repair turn's ask: each field over its limit, by how much, and the same answer again within it. */
|
|
47
|
+
export declare function shortenAsk(over: readonly Overrun[]): string;
|
|
48
|
+
/**
|
|
49
|
+
* Ask, check, and ask once more to shorten. `write` gets null the first time and the shorten ask the second.
|
|
50
|
+
* The second answer is returned whatever its length: the caller keeps it whole (and links it when it must).
|
|
51
|
+
*/
|
|
52
|
+
export declare function writeWithin<T>(write: (shorten: string | null) => Promise<T>, check: (answer: T) => Overrun[]): Promise<{
|
|
53
|
+
answer: T;
|
|
54
|
+
over: Overrun[];
|
|
55
|
+
repaired: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Text passed on under a limit: whole when it fits; otherwise kept whole in the record named by `where` and passed on
|
|
59
|
+
* as a pointer to it. Never a cut.
|
|
60
|
+
*/
|
|
61
|
+
export declare function passOn(text: string, limit: number, where: {
|
|
62
|
+
label: string;
|
|
63
|
+
href: string | null;
|
|
64
|
+
}, holds?: string): {
|
|
65
|
+
text: string;
|
|
66
|
+
kept: boolean;
|
|
67
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text limits, in one place, and the one way a limit is met: a model is told the exact limit before it writes, an
|
|
3
|
+
* answer still over it is asked once to shorten (one repair turn), and an answer still over after that is kept whole
|
|
4
|
+
* in the record and linked, never chopped. Chat platforms' own message limits are met by splitting across messages.
|
|
5
|
+
*
|
|
6
|
+
* Limits apply when text is written. Reading never re-validates length, so a runtime with lower limits (0.9.23 to
|
|
7
|
+
* 0.9.26) still opens a store holding longer text (scripts/upgrade-path.mjs, the rollback leg).
|
|
8
|
+
*/
|
|
9
|
+
/** UTF-16 code units, the JavaScript and HTML maxlength measure, unless a name says bytes. */
|
|
10
|
+
export const TEXT_LIMITS = {
|
|
11
|
+
/** A task's goal and its out-of-scope text. */
|
|
12
|
+
goal: 8_000,
|
|
13
|
+
goalBytes: 32_000,
|
|
14
|
+
/** Revise feedback, steering and decision notes. */
|
|
15
|
+
note: 4_000,
|
|
16
|
+
noteBytes: 16_000,
|
|
17
|
+
/** A flow work zone's instructions; nothing is held back for the card's details (they are attached when long). */
|
|
18
|
+
flowInstructions: 8_000,
|
|
19
|
+
/** A research report's summary, in UTF-8 bytes. */
|
|
20
|
+
reportSummary: 2_500,
|
|
21
|
+
/** What one flow step passes on to the steps after it. */
|
|
22
|
+
stageOutput: 12_000,
|
|
23
|
+
};
|
|
24
|
+
/** One chat message on each platform. Longer text is split across messages, never cut. Teams: the text one Adaptive Card
|
|
25
|
+
* holds, well inside its 28 KB message. */
|
|
26
|
+
export const PLATFORM_LIMITS = { telegram: 4_096, discord: 2_000, slack: 4_000, teams: 6_000 };
|
|
27
|
+
const count = (n) => n.toLocaleString("en-US");
|
|
28
|
+
/** The line a model reads before it writes a field with a limit. */
|
|
29
|
+
export function limitRule(what, limit, unit = "characters") {
|
|
30
|
+
return `${what}: at most ${count(limit)} ${unit}. Put the most important part first. Longer text is not cut: you will be asked to shorten it.`;
|
|
31
|
+
}
|
|
32
|
+
/** How long a text is in a limit's unit. */
|
|
33
|
+
export const lengthIn = (text, unit = "characters") => unit === "bytes" ? Buffer.byteLength(text, "utf8") : text.length;
|
|
34
|
+
/** The fields of `values` over their limits. */
|
|
35
|
+
export function overruns(values, limits, unit = "characters") {
|
|
36
|
+
return Object.entries(limits).flatMap(([field, limit]) => {
|
|
37
|
+
const value = values[field];
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return [];
|
|
40
|
+
const length = lengthIn(value, unit);
|
|
41
|
+
return length > limit ? [{ field, limit, length, ...(unit === "bytes" ? { unit } : {}) }] : [];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** The repair turn's ask: each field over its limit, by how much, and the same answer again within it. */
|
|
45
|
+
export function shortenAsk(over) {
|
|
46
|
+
return [
|
|
47
|
+
"Your answer is over a limit:",
|
|
48
|
+
...over.map(one => `- ${one.field} is ${count(one.length)} ${one.unit ?? "characters"}; the limit is ${count(one.limit)}.`),
|
|
49
|
+
"Answer again with the same decision and meaning, each field within its limit. Shorten by dropping repetition and detail, not facts that matter.",
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Ask, check, and ask once more to shorten. `write` gets null the first time and the shorten ask the second.
|
|
54
|
+
* The second answer is returned whatever its length: the caller keeps it whole (and links it when it must).
|
|
55
|
+
*/
|
|
56
|
+
export async function writeWithin(write, check) {
|
|
57
|
+
const first = await write(null);
|
|
58
|
+
const over = check(first);
|
|
59
|
+
if (over.length === 0)
|
|
60
|
+
return { answer: first, over, repaired: false };
|
|
61
|
+
const second = await write(shortenAsk(over));
|
|
62
|
+
return { answer: second, over: check(second), repaired: true };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Text passed on under a limit: whole when it fits; otherwise kept whole in the record named by `where` and passed on
|
|
66
|
+
* as a pointer to it. Never a cut.
|
|
67
|
+
*/
|
|
68
|
+
export function passOn(text, limit, where, holds = "a step passes on") {
|
|
69
|
+
if (text.length <= limit)
|
|
70
|
+
return { text, kept: false };
|
|
71
|
+
const at = where.href === null ? where.label : `${where.label}: ${where.href}`;
|
|
72
|
+
return { text: `This is ${count(text.length)} characters, more than the ${count(limit)} ${holds}, so it is kept whole on ${at}.`, kept: true };
|
|
73
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What sized routing saved (v2 routing), for `toolroll spend`: each task's
|
|
3
|
+
* tier (recorded on the task when its route is filed), how long it took
|
|
4
|
+
* from its first run to its first result, and whether it used a planner —
|
|
5
|
+
* grouped by tier, with what the light tier saved: the plans small changes
|
|
6
|
+
* skipped, results that came sooner, and spend below the everyday tier.
|
|
7
|
+
*/
|
|
8
|
+
import type { SpendItem } from "./spend.js";
|
|
9
|
+
import type { Database } from "./store.js";
|
|
10
|
+
export type TierTask = {
|
|
11
|
+
taskId: string;
|
|
12
|
+
tier: string;
|
|
13
|
+
size: string | null;
|
|
14
|
+
planRuns: number;
|
|
15
|
+
minutesToResult: number | null;
|
|
16
|
+
microusd: number;
|
|
17
|
+
};
|
|
18
|
+
export type TierRow = {
|
|
19
|
+
tier: string;
|
|
20
|
+
tasks: number;
|
|
21
|
+
results: number;
|
|
22
|
+
medianMinutesToResult: number | null;
|
|
23
|
+
planRuns: number;
|
|
24
|
+
microusd: number;
|
|
25
|
+
};
|
|
26
|
+
export type TierSaved = {
|
|
27
|
+
/** Small changes that built with no planner run. */
|
|
28
|
+
plansSkipped: number;
|
|
29
|
+
/** Those skipped plans at this month's median planner run, in minutes; null when no planner run was measured. */
|
|
30
|
+
planMinutesSaved: number | null;
|
|
31
|
+
/** How much sooner a light result came than an everyday one (medians), in minutes; null without both. */
|
|
32
|
+
minutesSoonerEach: number | null;
|
|
33
|
+
/** Light tasks' spend below the everyday tier's average task, in micro-USD; null when either is unpriced or $0. */
|
|
34
|
+
microusdSaved: number | null;
|
|
35
|
+
};
|
|
36
|
+
export type TierReport = {
|
|
37
|
+
tasks: TierTask[];
|
|
38
|
+
rows: TierRow[];
|
|
39
|
+
saved: TierSaved;
|
|
40
|
+
};
|
|
41
|
+
/** Every routed task with a run in [from, to), by tier, and what the light tier saved. `items` are the month's spend. */
|
|
42
|
+
export declare function tierReport(db: Database, from: string, to: string, items: readonly SpendItem[]): TierReport;
|
|
43
|
+
/** The spend report's lines: one per tier, then what the tiers saved. Empty when no routed task ran this month. */
|
|
44
|
+
export declare function tierLines(report: TierReport, usd: (microusd: number) => string): string[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const TIER_ORDER = ["light", "routine", "strong", "override", "pinned"];
|
|
2
|
+
function median(values) {
|
|
3
|
+
if (values.length === 0)
|
|
4
|
+
return null;
|
|
5
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
6
|
+
const mid = Math.floor(sorted.length / 2);
|
|
7
|
+
return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
8
|
+
}
|
|
9
|
+
const minutesBetween = (from, to) => {
|
|
10
|
+
const ms = Date.parse(to) - Date.parse(from);
|
|
11
|
+
return Number.isFinite(ms) && ms >= 0 ? Math.round(ms / 6_000) / 10 : null;
|
|
12
|
+
};
|
|
13
|
+
/** Every routed task with a run in [from, to), by tier, and what the light tier saved. `items` are the month's spend. */
|
|
14
|
+
export function tierReport(db, from, to, items) {
|
|
15
|
+
const spendOf = new Map();
|
|
16
|
+
for (const item of items)
|
|
17
|
+
if (item.taskId !== null)
|
|
18
|
+
spendOf.set(item.taskId, (spendOf.get(item.taskId) ?? 0) + (item.microusd ?? 0));
|
|
19
|
+
const rows = db.prepare(`SELECT r.external_id AS task, r.route_tier AS tier, r.size AS size,
|
|
20
|
+
(SELECT COUNT(*) FROM run p WHERE p.task_ref = r.id AND p.role = 'planner') AS plan_runs,
|
|
21
|
+
(SELECT MIN(f.started_at) FROM run f WHERE f.task_ref = r.id) AS first_started,
|
|
22
|
+
(SELECT MIN(b.finished_at) FROM run b WHERE b.task_ref = r.id AND b.role = 'builder' AND b.outcome IN ('built', 'no-change') AND b.finished_at IS NOT NULL) AS result_at
|
|
23
|
+
FROM task_ref r
|
|
24
|
+
WHERE r.route_tier IS NOT NULL AND EXISTS (SELECT 1 FROM run x WHERE x.task_ref = r.id AND x.started_at >= ? AND x.started_at < ?)
|
|
25
|
+
ORDER BY r.id`).all(from, to);
|
|
26
|
+
const tasks = rows.map(row => ({
|
|
27
|
+
taskId: String(row["task"]),
|
|
28
|
+
tier: String(row["tier"]),
|
|
29
|
+
size: row["size"] == null ? null : String(row["size"]),
|
|
30
|
+
planRuns: Number(row["plan_runs"] ?? 0),
|
|
31
|
+
minutesToResult: row["first_started"] == null || row["result_at"] == null ? null : minutesBetween(String(row["first_started"]), String(row["result_at"])),
|
|
32
|
+
microusd: spendOf.get(String(row["task"])) ?? 0,
|
|
33
|
+
}));
|
|
34
|
+
const tiers = [...new Set(tasks.map(one => one.tier))].sort((a, b) => (TIER_ORDER.indexOf(a) + 1 || 99) - (TIER_ORDER.indexOf(b) + 1 || 99));
|
|
35
|
+
const byTier = tiers.map(tier => {
|
|
36
|
+
const mine = tasks.filter(one => one.tier === tier);
|
|
37
|
+
const done = mine.flatMap(one => (one.minutesToResult === null ? [] : [one.minutesToResult]));
|
|
38
|
+
return { tier, tasks: mine.length, results: done.length, medianMinutesToResult: median(done), planRuns: mine.reduce((sum, one) => sum + one.planRuns, 0), microusd: mine.reduce((sum, one) => sum + one.microusd, 0) };
|
|
39
|
+
});
|
|
40
|
+
const plansSkipped = tasks.filter(one => one.size === "small" && one.planRuns === 0).length;
|
|
41
|
+
const plannerMinutes = db.prepare("SELECT started_at, finished_at FROM run WHERE role = 'planner' AND finished_at IS NOT NULL AND started_at >= ? AND started_at < ?").all(from, to)
|
|
42
|
+
.flatMap(row => { const m = minutesBetween(String(row["started_at"]), String(row["finished_at"])); return m === null ? [] : [m]; });
|
|
43
|
+
const perPlan = median(plannerMinutes);
|
|
44
|
+
const light = byTier.find(one => one.tier === "light") ?? null;
|
|
45
|
+
const routine = byTier.find(one => one.tier === "routine") ?? null;
|
|
46
|
+
const sooner = light?.medianMinutesToResult != null && routine?.medianMinutesToResult != null && routine.medianMinutesToResult > light.medianMinutesToResult
|
|
47
|
+
? Math.round((routine.medianMinutesToResult - light.medianMinutesToResult) * 10) / 10 : null;
|
|
48
|
+
const perTask = (row) => (row === null || row.tasks === 0 || row.microusd <= 0 ? null : row.microusd / row.tasks);
|
|
49
|
+
const lightEach = light === null || light.tasks === 0 ? null : light.microusd / light.tasks;
|
|
50
|
+
const routineEach = perTask(routine);
|
|
51
|
+
const microusdSaved = light !== null && lightEach !== null && routineEach !== null && routineEach > lightEach ? Math.round((routineEach - lightEach) * light.tasks) : null;
|
|
52
|
+
return {
|
|
53
|
+
tasks,
|
|
54
|
+
rows: byTier,
|
|
55
|
+
saved: { plansSkipped, planMinutesSaved: perPlan === null || plansSkipped === 0 ? null : Math.round(perPlan * plansSkipped), minutesSoonerEach: sooner, microusdSaved },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const minutesWords = (minutes) => (minutes >= 90 ? `${Math.round(minutes / 6) / 10}h` : `${Math.round(minutes)}m`);
|
|
59
|
+
/** The spend report's lines: one per tier, then what the tiers saved. Empty when no routed task ran this month. */
|
|
60
|
+
export function tierLines(report, usd) {
|
|
61
|
+
if (report.rows.length === 0)
|
|
62
|
+
return [];
|
|
63
|
+
const lines = report.rows.map(row => ` ${`${row.tier} tier`.padEnd(24)} ${row.tasks} ${row.tasks === 1 ? "task" : "tasks"} · ${row.medianMinutesToResult === null ? "no result yet" : `${minutesWords(row.medianMinutesToResult)} to a result`} · ${row.planRuns} ${row.planRuns === 1 ? "plan" : "plans"} · ${usd(row.microusd)}`);
|
|
64
|
+
const saved = report.saved;
|
|
65
|
+
const parts = [
|
|
66
|
+
...(saved.plansSkipped === 0 ? [] : [`${saved.plansSkipped} ${saved.plansSkipped === 1 ? "plan" : "plans"} skipped${saved.planMinutesSaved === null ? "" : ` (~${minutesWords(saved.planMinutesSaved)} of planning)`}`]),
|
|
67
|
+
...(saved.minutesSoonerEach === null ? [] : [`light results ~${minutesWords(saved.minutesSoonerEach)} sooner each`]),
|
|
68
|
+
...(saved.microusdSaved === null ? [] : [`~${usd(saved.microusdSaved)} below the everyday tier`]),
|
|
69
|
+
];
|
|
70
|
+
return [...lines, ` ${"tiers saved".padEnd(24)} ${parts.length === 0 ? "nothing yet — no small change has run on the light tier" : parts.join(", ")}`];
|
|
71
|
+
}
|
package/dist/tools-ui.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
/** Settings → Tools: one project's MCP servers, in plain words. Secrets are
|
|
2
|
-
* entered here and nowhere else, and never shown again. */
|
|
3
1
|
import { type FoundTool, type ProjectTool, type ToolSpec } from "./project-tools.js";
|
|
4
|
-
export declare const TOOLS_CSS = ".tools{max-width:780px;min-width:0;overflow-wrap:anywhere}.tools .card{padding:16px 18px;margin:12px 0;min-width:0}.tools .card h2{font-size:1.05rem;margin:0;color:var(--so-ink);font-weight:600}.tools .tool-heading{display:flex;align-items:
|
|
2
|
+
export declare const TOOLS_CSS = ".tools{max-width:780px;min-width:0;overflow-wrap:anywhere}.tools .card{padding:16px 18px;margin:12px 0;min-width:0}.tools .card h2{font-size:1.05rem;margin:0;color:var(--so-ink);font-weight:600}.tools .tool-heading{display:flex;align-items:center;gap:10px}.tools .tool-heading h2{flex:1;min-width:0}.tools .tool-state{font-size:.85rem;flex-shrink:0;color:var(--so-muted)}.tools .tool-state[data-ready=\"false\"]{color:var(--so-warning)}.tools .card>p{margin:6px 0 0}.tools code{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-size:.85rem}.tools form{display:grid;gap:10px;margin:0}.tools label{display:grid;gap:6px}.tools input,.tools select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.tools button{justify-self:start;white-space:nowrap;min-height:44px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.tools details:not(.card){margin:6px 0 0;padding:0;border:0;border-radius:0;background:none;box-shadow:none}.tools details:not(.card)>summary{min-height:36px;display:flex;align-items:center;font-size:.9rem;color:var(--so-muted);cursor:pointer}.tools details:not(.card)[open]>summary{color:var(--so-ink)}.tools details.card>summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.tools summary{list-style:none;gap:10px}.tools summary::-webkit-details-marker{display:none}.tools summary::before{content:\"\";flex-shrink:0;width:6px;height:6px;margin:0 2px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .15s}.tools details[open]>summary::before{transform:rotate(45deg)}.tools details:not(.card) form{padding:4px 0 10px}.tools .tool-facts{margin:0 0 8px;display:grid;gap:4px;font-size:.9rem}.tools .tool-facts p{margin:0}.tools .tool-controls{display:flex;gap:12px;flex-wrap:wrap;margin-top:12px}.tools .tool-found{display:flex;gap:10px;align-items:flex-start}.tools .tool-found input{flex:0 0 auto;width:18px;height:18px;margin-top:3px}.tools .tool-found span{min-width:0}.tools .problem{padding:10px 12px;margin:8px 0 0}.tools .tool-connect{scroll-margin-top:72px}.tools .connect-wanted{background:var(--so-accent);color:var(--so-on-accent);border-color:var(--so-accent)}.tools .connect-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}.tools .connect-tile{display:grid;grid-template-columns:auto 1fr;column-gap:10px;align-items:center;justify-self:stretch;text-align:left;white-space:normal;padding:10px 12px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);color:var(--so-ink);min-height:60px;font:inherit;cursor:pointer;scroll-margin-top:96px}.tools .connect-tile:hover{border-color:var(--so-input-line);background:var(--so-raised)}.tools .connect-tile:target,.tools .connect-tile:focus-visible{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile .brand-mark{grid-row:span 2}.tools .connect-tile strong{font-weight:600;line-height:1.3}.tools .connect-tile>span:not(.brand-mark){font-size:.8rem;color:var(--so-muted);line-height:1.35;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.tools .connect-tile[data-state=\"connected\"]>span:not(.brand-mark){color:var(--so-success)}.tools .connect-local{grid-column:1/-1}.tools .connect-local>span:not(.brand-mark){-webkit-line-clamp:unset}.tools .connect-problem{margin:0}.tools .connect-problem:focus{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile:disabled{cursor:default;opacity:.6}@media(max-width:600px){.tools .connect-grid{grid-template-columns:1fr 1fr}.tools .connect-tile:not(.connect-local)>span:not(.brand-mark){display:none}.tools .connect-local>span:not(.brand-mark){grid-column:2}.tools .connect-tile{min-height:52px;padding:8px 10px}.tools .connect-tile .brand-mark{grid-row:auto}.tools input,.tools select{font-size:16px}.tools .card{padding:14px}}";
|
|
5
3
|
export type ToolsView = {
|
|
6
4
|
repo: string;
|
|
7
5
|
project: string;
|
package/dist/tools-ui.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Settings → Tools: one project's MCP servers, in plain words. Secrets are
|
|
2
2
|
* entered here and nowhere else, and never shown again. */
|
|
3
|
+
import { brandMarkHtml, TILE_PHONE } from "./brand-mark.js";
|
|
3
4
|
import { toolCommandLine, toolStanding } from "./project-tools.js";
|
|
4
5
|
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
5
6
|
const hidden = (data) => Object.entries(data).map(([k, v]) => `<input type="hidden" name="${e(k)}" value="${e(v)}">`).join("");
|
|
6
|
-
export const TOOLS_CSS = `.tools{max-width:780px;min-width:0;overflow-wrap:anywhere}.tools .card{padding:16px 18px;margin:12px 0;min-width:0}.tools .card h2{font-size:1.05rem;margin:0;color:var(--so-ink);font-weight:600}.tools .tool-heading{display:flex;align-items:
|
|
7
|
+
export const TOOLS_CSS = `.tools{max-width:780px;min-width:0;overflow-wrap:anywhere}.tools .card{padding:16px 18px;margin:12px 0;min-width:0}.tools .card h2{font-size:1.05rem;margin:0;color:var(--so-ink);font-weight:600}.tools .tool-heading{display:flex;align-items:center;gap:10px}.tools .tool-heading h2{flex:1;min-width:0}.tools .tool-state{font-size:.85rem;flex-shrink:0;color:var(--so-muted)}.tools .tool-state[data-ready="false"]{color:var(--so-warning)}.tools .card>p{margin:6px 0 0}.tools code{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-size:.85rem}.tools form{display:grid;gap:10px;margin:0}.tools label{display:grid;gap:6px}.tools input,.tools select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.tools button{justify-self:start;white-space:nowrap;min-height:44px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.tools details:not(.card){margin:6px 0 0;padding:0;border:0;border-radius:0;background:none;box-shadow:none}.tools details:not(.card)>summary{min-height:36px;display:flex;align-items:center;font-size:.9rem;color:var(--so-muted);cursor:pointer}.tools details:not(.card)[open]>summary{color:var(--so-ink)}.tools details.card>summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.tools summary{list-style:none;gap:10px}.tools summary::-webkit-details-marker{display:none}.tools summary::before{content:"";flex-shrink:0;width:6px;height:6px;margin:0 2px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .15s}.tools details[open]>summary::before{transform:rotate(45deg)}.tools details:not(.card) form{padding:4px 0 10px}.tools .tool-facts{margin:0 0 8px;display:grid;gap:4px;font-size:.9rem}.tools .tool-facts p{margin:0}.tools .tool-controls{display:flex;gap:12px;flex-wrap:wrap;margin-top:12px}.tools .tool-found{display:flex;gap:10px;align-items:flex-start}.tools .tool-found input{flex:0 0 auto;width:18px;height:18px;margin-top:3px}.tools .tool-found span{min-width:0}.tools .problem{padding:10px 12px;margin:8px 0 0}.tools .tool-connect{scroll-margin-top:72px}.tools .connect-wanted{background:var(--so-accent);color:var(--so-on-accent);border-color:var(--so-accent)}.tools .connect-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}.tools .connect-tile{display:grid;grid-template-columns:auto 1fr;column-gap:10px;align-items:center;justify-self:stretch;text-align:left;white-space:normal;padding:10px 12px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);color:var(--so-ink);min-height:60px;font:inherit;cursor:pointer;scroll-margin-top:96px}.tools .connect-tile:hover{border-color:var(--so-input-line);background:var(--so-raised)}.tools .connect-tile:target,.tools .connect-tile:focus-visible{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile .brand-mark{grid-row:span 2}.tools .connect-tile strong{font-weight:600;line-height:1.3}.tools .connect-tile>span:not(.brand-mark){font-size:.8rem;color:var(--so-muted);line-height:1.35;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.tools .connect-tile[data-state="connected"]>span:not(.brand-mark){color:var(--so-success)}.tools .connect-local{grid-column:1/-1}.tools .connect-local>span:not(.brand-mark){-webkit-line-clamp:unset}.tools .connect-problem{margin:0}.tools .connect-problem:focus{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile:disabled{cursor:default;opacity:.6}${TILE_PHONE}{.tools .connect-grid{grid-template-columns:1fr 1fr}.tools .connect-tile:not(.connect-local)>span:not(.brand-mark){display:none}.tools .connect-local>span:not(.brand-mark){grid-column:2}.tools .connect-tile{min-height:52px;padding:8px 10px}.tools .connect-tile .brand-mark{grid-row:auto}.tools input,.tools select{font-size:16px}.tools .card{padding:14px}}`;
|
|
7
8
|
/** The project chooser: it opens the chosen project at once; without scripts, its button does. */
|
|
8
9
|
export function toolsProjectPicker(projects, chosen, keep = {}) {
|
|
9
10
|
if (projects.length < 2)
|
|
@@ -26,7 +27,7 @@ function toolCard(one, base, manage, project) {
|
|
|
26
27
|
const details = `<details><summary>Details</summary><div class="tool-facts"><p>Starts: <code>${e(toolCommandLine(tool.spec))}</code></p><p>From ${e(tool.source)} · added by ${e(tool.createdBy)}</p>${tested}</div></details>`;
|
|
27
28
|
const failed = tool.lastTest !== null && !tool.lastTest.ok ? `<p class="problem" role="status">${e(tool.lastTest.problem ?? "The last test failed.")}</p>` : "";
|
|
28
29
|
const controls = manage ? `<div class="tool-controls"><form method="post" action="/settings/tools/change">${hidden({ ...base, action: "test", name: tool.name })}<button aria-label="Test ${e(tool.name)} in ${e(project)}">Test</button></form><form method="post" action="/settings/tools/change">${hidden({ ...base, action: "remove", name: tool.name })}<button class="secondary">Remove from ${e(project)}</button></form></div>` : "";
|
|
29
|
-
return `<article class="card" id="tool-${e(tool.name)}"><div class="tool-heading"
|
|
30
|
+
return `<article class="card" id="tool-${e(tool.name)}"><div class="tool-heading">${brandMarkHtml(tool.source === "custom" ? null : tool.name, tool.name, standing.ready)}<h2>${e(tool.name)}</h2><span class="tool-state" data-ready="${standing.ready}">${e(standing.words)}</span></div><p>${e(tool.spec.about)}</p>${failed}${secrets}${details}${controls}</article>`;
|
|
30
31
|
}
|
|
31
32
|
/** A service connected here, offered to the person's other projects: each button signs in again for that project (tokens are never copied). */
|
|
32
33
|
function elsewhere(view, base) {
|
|
@@ -46,8 +47,8 @@ export function toolsHtml(view, csrf, canManage, message = {}) {
|
|
|
46
47
|
const custom = `<details><summary>Add your own</summary><form method="post" action="/settings/tools/change">${hidden({ ...base, action: "add-custom" })}<label>Name<input name="name" pattern="[a-z0-9][a-z0-9_\\-]{0,39}" maxlength="40" required placeholder="for example: postgres"></label><label>It runs as<select name="transport"><option value="stdio">A program on this computer</option><option value="http">A web address</option></select></label><label>Command or address<input name="target" required placeholder="npx -y some-mcp-server@1.2.3, or https://…"></label><label>Secrets it needs (names, comma separated)<input name="secrets" placeholder="for example: DATABASE_URL"></label><p class="meta">For a web address, the first secret is sent as its sign-in token. Set each secret's value after adding.</p>${password}<button>Add${to}</button></form></details>`;
|
|
47
48
|
// One click: sign in on the service's own page. The password is the same as for adding any tool.
|
|
48
49
|
const tiles = view.connections.map(one => one.state === "taken"
|
|
49
|
-
? `<button type="button" class="connect-tile" id="connect-${e(one.id)}" data-state="taken" disabled
|
|
50
|
-
: `<button name="service" value="${e(one.id)}" class="connect-tile${one.local ? " connect-local" : ""}" id="connect-${e(one.id)}" data-state="${one.state}" aria-label="${one.state === "connected" ? `${e(one.label)}, connected${to}. ${one.local ? "Connect again" : "Sign in again"}` : `Connect ${e(one.label)}${to}`}"
|
|
50
|
+
? `<button type="button" class="connect-tile" id="connect-${e(one.id)}" data-state="taken" disabled>${brandMarkHtml(one.id, one.label, false)}<strong>${e(one.label)}</strong><span>Added another way</span></button>`
|
|
51
|
+
: `<button name="service" value="${e(one.id)}" class="connect-tile${one.local ? " connect-local" : ""}" id="connect-${e(one.id)}" data-state="${one.state}" aria-label="${one.state === "connected" ? `${e(one.label)}, connected${to}. ${one.local ? "Connect again" : "Sign in again"}` : `Connect ${e(one.label)}${to}`}">${brandMarkHtml(one.id.replace(/-desktop$/, ""), one.label, one.state === "connected")}<strong>${e(one.label)}</strong><span>${one.state === "connected" ? "Connected" : e(one.about)}</span></button>`).join("");
|
|
51
52
|
const wanted = view.connections.find(one => one.id === view.wanted && one.state !== "taken");
|
|
52
53
|
const first = wanted === undefined ? "" : `<button name="service" value="${e(wanted.id)}" class="connect-wanted">${wanted.state === "connected" && !wanted.local ? `Sign in to ${e(wanted.label)} again for ${e(view.project)}` : `Connect ${e(wanted.label)}${to}`}</button>`;
|
|
53
54
|
const connecting = manage && view.connections.length > 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.28",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|
|
@@ -70,6 +70,8 @@
|
|
|
70
70
|
"@types/react-dom": "19.3.0",
|
|
71
71
|
"esbuild": "0.28.2",
|
|
72
72
|
"happy-dom": "20.12.0",
|
|
73
|
+
"paper": "0.12.18",
|
|
74
|
+
"simple-icons": "16.34.0",
|
|
73
75
|
"tailwindcss": "4.3.3",
|
|
74
76
|
"tsx": "4.23.12",
|
|
75
77
|
"typescript": "7.0.2",
|