toolroll 0.9.26 → 0.9.27

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.
@@ -19,6 +19,7 @@ import { imageDimensions, readVerifiedProofForRun, readVerifiedReport, RETENTION
19
19
  import { resultImageFileName, verifyResultImage } from "./chat-evidence.js";
20
20
  import { publicChatText } from "./chat-display.js";
21
21
  import { chatTitle } from "./chat-voice.js";
22
+ import { itemCaption, readFlowItems } from "./flow-items.js";
22
23
  import { FLOW_SHOTS_KIND, RESULT_SHOTS_KIND } from "./store.js";
23
24
  /** "Up to 4": the most screenshots one result ever sends. */
24
25
  export const RESULT_SHOTS_MAX = 4;
@@ -58,6 +59,28 @@ function reportCaptions(store, root, taskRef, run) {
58
59
  }
59
60
  return captions;
60
61
  }
62
+ /** A flow visit's item screenshots from this run, in item order, each with its numbered caption (flow-items.ts). */
63
+ function itemCaptions(store, dedupeKey, run) {
64
+ const captions = new Map();
65
+ const visit = /^flow-shots:([1-9][0-9]{0,14}):([1-9][0-9]{0,9})$/.exec(dedupeKey ?? "");
66
+ const kept = visit === null ? null : store.flowSend(Number(visit[1]), Number(visit[2]));
67
+ if (kept === null)
68
+ return captions;
69
+ let content;
70
+ try {
71
+ content = JSON.parse(kept.contentJson);
72
+ }
73
+ catch {
74
+ return captions;
75
+ }
76
+ if (content.shots?.run !== run)
77
+ return captions;
78
+ readFlowItems(content.items).forEach((item, index) => {
79
+ if (item.shot !== null && !captions.has(item.shot))
80
+ captions.set(item.shot, publicChatText(itemCaption(index + 1, item), 200).trim());
81
+ });
82
+ return captions;
83
+ }
61
84
  function isPhoto(bytes, format) {
62
85
  const size = imageDimensions(bytes, format);
63
86
  if (size === null || size.width === 0 || size.height === 0)
@@ -92,7 +115,13 @@ export function resultShotsFor(store, evidenceRoot, repos, row, sent = new Set()
92
115
  const latest = store.runsFor(row.taskRef).find(one => one.finishedAt !== null && ["builder", "repair", "scout"].includes(one.role));
93
116
  if (family?.current.id !== row.taskId || latest?.id !== row.run)
94
117
  return line("a newer result replaced this one");
95
- const chosen = store.artifactsFor(row.run).filter(one => one.kind === "screenshot").slice(0, limit);
118
+ // After research, a flow's message numbers the report's items (flow-items.ts): their screenshots go first, in that
119
+ // order, each captioned with its item's number, so "2." here is item 2 in the text.
120
+ const items = row.kind === FLOW_SHOTS_KIND ? itemCaptions(store, row.dedupeKey, row.run) : new Map();
121
+ const order = [...items.keys()];
122
+ const rank = (id) => { const at = order.indexOf(id); return at < 0 ? order.length : at; };
123
+ const chosen = store.artifactsFor(row.run).filter(one => one.kind === "screenshot")
124
+ .map((one, at) => ({ one, at })).sort((a, b) => rank(a.one.id) - rank(b.one.id) || a.at - b.at).map(({ one }) => one).slice(0, limit);
96
125
  if (chosen.length === 0)
97
126
  return { kind: "none", why: "none" };
98
127
  const captions = ownCaptions(store, evidenceRoot, row.run);
@@ -107,11 +136,12 @@ export function resultShotsFor(store, evidenceRoot, repos, row, sent = new Set()
107
136
  return line(verified.problem);
108
137
  }
109
138
  const own = captions.get(SCREENSHOT_CAPTURE.exec(artifact.capture)?.[1] ?? "") ?? reported.get(artifact.id) ?? null;
139
+ const item = items.get(artifact.id);
110
140
  shots.push({
111
141
  artifact: artifact.id, sha256: artifact.sha256, bytes: verified.bytes, format: verified.format,
112
142
  fileName: resultImageFileName(row.taskId, row.run, artifact.id, verified.format),
113
- caption: index === 0 ? (own === null ? title : `${title} · ${own}`) : own ?? `${title} · screenshot ${index + 1}`,
114
- photo: isPhoto(verified.bytes, verified.format),
143
+ caption: item !== undefined ? item : index === 0 ? (own === null ? title : `${title} · ${own}`) : own ?? `${title} · screenshot ${index + 1}`,
144
+ photo: isPhoto(verified.bytes, verified.format), ...(item === undefined ? {} : { numbered: true }),
115
145
  });
116
146
  }
117
147
  const remaining = shots.filter(one => !sent.has(one.artifact));
package/dist/scout.js CHANGED
@@ -163,7 +163,7 @@ function scoutBrief(title, goal, outOfScope, mailbox, reportFile, answers, struc
163
163
  ' "summary": "one paragraph the operator reads first",',
164
164
  ' "report": "the report as markdown: what you found, the evidence, the risks",',
165
165
  ' "followUps": [{ "title": "one line", "goal": "what success looks like" }],',
166
- ' "items": [{ "title": "one line", "why": "why it matters", "url": "https://…", "image": "home.png" }],',
166
+ ' "items": [{ "title": "one line", "why": "the pattern; why it fits us; how it plugs in", "url": "https://…", "image": "home.png" }],',
167
167
  ' "images": [{ "file": "home.png", "caption": "one line", "url": "the page it shows" }]',
168
168
  "}",
169
169
  `Caps: title ${REPORT_LIMITS.title}, summary ${REPORT_LIMITS.summary}, report ${REPORT_LIMITS.document} bytes,`,
package/dist/serve.d.ts CHANGED
@@ -569,6 +569,8 @@ export declare function needActionOf(assignment: AssignmentSnapshot | null, stat
569
569
  * stop marker, so an open tab quits refetching a dead build (round-4
570
570
  * finding 15). */
571
571
  export declare function runFactsFragment(run: Run, taskId: string, live: boolean, route?: RouteStamp | null): string;
572
+ /** Settings destinations under short headings: an icon and a name each, and a chat app's logo and state. */
573
+ export declare function settingsTiles(groups: BrowserSettingsGroup[]): string;
572
574
  /**
573
575
  * The settings groups, with each chat app's state from what the server already holds: not set up,
574
576
  * connected, or getting alerts (only when that service was chosen, or is the only one).
package/dist/serve.js CHANGED
@@ -227,6 +227,7 @@ import { readSsoSettings, removeSsoSettings, saveSsoSettings, SSO_CALLBACK, ssoC
227
227
  import { monitoringChange, readMonitoring, saveMonitoring } from "./monitoring-settings.js";
228
228
  import { MONITORING_CSS, monitoringHtml, signingSecretHtml } from "./monitoring-ui.js";
229
229
  import { INTEGRATIONS_CSS, integrationsHtml } from "./integrations-ui.js";
230
+ import { BRAND_MARK_CSS, brandIconHtml } from "./brand-mark.js";
230
231
  import { createIntegrationMonitor } from "./integrations.js";
231
232
  import { BACKUP_CSS, backupHtml } from "./backup-ui.js";
232
233
  import { BACKUP_EVERY_HOURS, MAX_KEEP, backupFolderOf, backupNow, checkBackupFolder, defaultBackupFolder } from "./backup.js";
@@ -15932,7 +15933,7 @@ button.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }
15932
15933
  }
15933
15934
  `;
15934
15935
  /** Appearance: a three-way segmented switch, one tap per choice. */
15935
- const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
15936
+ const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles a>svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
15936
15937
  /** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
15937
15938
  export const PAGE_CSS = STYLE + THEME_CONTROLS_CSS;
15938
15939
  /** The Inbox tabs (console v2): a segmented track of real links; the dot marks a tab with something new, on phones only. */
@@ -15944,7 +15945,7 @@ const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width:100%;o
15944
15945
  '.inbox-ask{margin:18px 0 0}.inbox-ask>h2{display:flex;align-items:baseline;gap:8px;margin:0 0 4px;font-size:15px;font-weight:600}.inbox-ask>h2 .count{font:500 12px var(--so-mono,ui-monospace,monospace);font-variant-numeric:tabular-nums;color:var(--so-muted)}.inbox-ask h3{font-size:13px;font-weight:600;margin:12px 0 4px}' +
15945
15946
  '.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
15946
15947
  '@media (max-width:760px){.inbox-tabs{display:flex;width:100%}.inbox-tabs a{flex:1;justify-content:center;min-height:44px;padding:0 6px}.inbox-unread{display:block}}';
15947
- const WORKSPACE_STYLE = styleAsset(STYLE + INBOX_TABS_CSS + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + STARTERS_CSS + GALLERY_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + TASK_STATUS_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + CHECK_SETTINGS_CSS + DISCLOSURE_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
15948
+ const WORKSPACE_STYLE = styleAsset(STYLE + BRAND_MARK_CSS + INBOX_TABS_CSS + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + STARTERS_CSS + GALLERY_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + TASK_STATUS_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + CHECK_SETTINGS_CSS + DISCLOSURE_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
15948
15949
  /**
15949
15950
  * The board's liveness: fetch this page's own fragment on a timer and swap
15950
15951
  * it in place — no flicker, no scroll reset, no long-lived stream to manage,
@@ -24322,9 +24323,9 @@ async function latestReleaseFor(latest) {
24322
24323
  latestSeen = { at: Date.now(), value };
24323
24324
  return value;
24324
24325
  }
24325
- /** Settings destinations under short headings: an icon and a name each, and a chat app's state. */
24326
- function settingsTiles(groups) {
24327
- return `<nav class="settings-tiles" aria-label="Settings sections">${groups.map(group => `<section aria-label="${escape(group.title)}"><h2>${escape(group.title)}</h2><div>${group.tiles.map(tile => `<a href="${escape(tile.href)}">${strokeIcon(SETTINGS_TILE_ICONS.get(tile.href) ?? "")}<span>${escape(tile.label)}${tile.status === undefined ? "" :
24326
+ /** Settings destinations under short headings: an icon and a name each, and a chat app's logo and state. */
24327
+ export function settingsTiles(groups) {
24328
+ return `<nav class="settings-tiles" aria-label="Settings sections">${groups.map(group => `<section aria-label="${escape(group.title)}"><h2>${escape(group.title)}</h2><div>${group.tiles.map(tile => `<a href="${escape(tile.href)}">${tile.brand === undefined ? strokeIcon(SETTINGS_TILE_ICONS.get(tile.href) ?? "") : brandIconHtml(tile.brand)}<span>${escape(tile.label)}${tile.status === undefined ? "" :
24328
24329
  `<span class="provider-status provider-status--${tile.status.tone}"><i aria-hidden="true"></i>${escape(tile.status.words)}</span>`}</span></a>`).join("")}</div></section>`).join("")}</nav>`;
24329
24330
  }
24330
24331
  /** Each destination once, in order: a group's heading, then its [href, label, icon] tiles. */
@@ -24342,10 +24343,10 @@ const SETTINGS_GROUPS = [
24342
24343
  ["/settings/integrations", "Integrations", `<path d="M9 2v6M15 2v6"/><path d="M6 8h12v4a6 6 0 0 1-12 0z"/><path d="M12 18v4"/>`],
24343
24344
  ]],
24344
24345
  ["Chat apps", [
24345
- ["/settings/telegram", "Telegram", `<path d="m22 2-7 20-4-9-9-4z"/><path d="M22 2 11 13"/>`],
24346
- ["/settings/slack", "Slack", `<path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18"/>`],
24347
- ["/settings/discord", "Discord", `<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>`],
24348
- ["/settings/teams", "Teams", `<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8"/>`],
24346
+ ["/settings/telegram", "Telegram", ""],
24347
+ ["/settings/slack", "Slack", ""],
24348
+ ["/settings/discord", "Discord", ""],
24349
+ ["/settings/teams", "Teams", ""],
24349
24350
  ]],
24350
24351
  ["Access and rules", [
24351
24352
  ["/settings/sign-in", "Sign-in", `<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>`],
@@ -24376,13 +24377,15 @@ export function settingsGroups(messaging, telegramFailing = false) {
24376
24377
  title,
24377
24378
  tiles: tiles.map(([href, label]) => {
24378
24379
  const app = CHAT_APP_TILES[href];
24379
- if (app === undefined || messaging === null)
24380
+ if (app === undefined)
24380
24381
  return { href, label };
24382
+ if (messaging === null)
24383
+ return { href, label, brand: app };
24381
24384
  if (!messaging.configured.includes(app))
24382
- return { href, label, status: { tone: "off", words: "Not set up" } };
24385
+ return { href, label, brand: app, status: { tone: "off", words: "Not set up" } };
24383
24386
  if (app === "telegram" && telegramFailing)
24384
- return { href, label, status: { tone: "warn", words: "Has a problem" } };
24385
- return { href, label, status: { tone: "ok", words: messaging.channel === app && !messaging.implicit ? "Gets alerts" : "Connected" } };
24387
+ return { href, label, brand: app, status: { tone: "warn", words: "Has a problem" } };
24388
+ return { href, label, brand: app, status: { tone: "ok", words: messaging.channel === app && !messaging.implicit ? "Gets alerts" : "Connected" } };
24386
24389
  }),
24387
24390
  }));
24388
24391
  }
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
15
15
  } | null;
16
16
  /** Browser installation may write only this known file inside its project. */
17
17
  export declare function previewProjectInstructions(repo: string): {
18
+ content?: never;
18
19
  ok: false;
19
20
  message: string;
20
21
  plan?: never;
21
- content?: never;
22
22
  fingerprint?: never;
23
23
  installed?: never;
24
24
  } | {
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
30
30
  installed: boolean;
31
31
  };
32
32
  export declare function addProjectInstructions(repo: string, fingerprint: string): {
33
+ content?: never;
33
34
  ok: false;
34
35
  message: string;
35
36
  plan?: never;
36
- content?: never;
37
37
  fingerprint?: never;
38
38
  installed?: never;
39
39
  } | import("./skills.js").InstallResult;
package/dist/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
- const parts = split(offer?.text ?? `${notificationIdentity(notification, title !== undefined && mentions(words, title) ? undefined : title)}${words}`);
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
  }
@@ -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:baseline;justify-content:space-between;gap:12px}.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:32px 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 i{grid-row:span 2;width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-style:normal;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.tools .connect-tile strong{font-weight:600;line-height:1.3}.tools .connect-tile span{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{color:var(--so-success)}.tools .connect-tile:disabled{cursor:default;opacity:.6}.tools .connect-local{grid-column:1/-1}.tools .connect-local span{-webkit-line-clamp:unset}.tools .connect-problem{margin:0}.tools .connect-problem:focus{outline:2px solid var(--so-accent);outline-offset:2px}@media(max-width:600px){.tools .connect-grid{grid-template-columns:1fr 1fr}.tools .connect-tile:not(.connect-local) span{display:none}.tools .connect-local span{grid-column:2}.tools .connect-tile{grid-template-columns:28px 1fr;min-height:52px;padding:8px 10px}.tools .connect-tile i{grid-row:auto;width:28px;height:28px}.tools input,.tools select{font-size:16px}.tools .card{padding:14px}.tools .tool-heading{align-items:flex-start}}";
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 => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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:baseline;justify-content:space-between;gap:12px}.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:32px 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 i{grid-row:span 2;width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-style:normal;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.tools .connect-tile strong{font-weight:600;line-height:1.3}.tools .connect-tile span{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{color:var(--so-success)}.tools .connect-tile:disabled{cursor:default;opacity:.6}.tools .connect-local{grid-column:1/-1}.tools .connect-local span{-webkit-line-clamp:unset}.tools .connect-problem{margin:0}.tools .connect-problem:focus{outline:2px solid var(--so-accent);outline-offset:2px}@media(max-width:600px){.tools .connect-grid{grid-template-columns:1fr 1fr}.tools .connect-tile:not(.connect-local) span{display:none}.tools .connect-local span{grid-column:2}.tools .connect-tile{grid-template-columns:28px 1fr;min-height:52px;padding:8px 10px}.tools .connect-tile i{grid-row:auto;width:28px;height:28px}.tools input,.tools select{font-size:16px}.tools .card{padding:14px}.tools .tool-heading{align-items:flex-start}}`;
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"><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
+ 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><i aria-hidden="true">${e(one.label.charAt(0))}</i><strong>${e(one.label)}</strong><span>Added another way</span></button>`
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}`}"><i aria-hidden="true">${e(one.label.charAt(0))}</i><strong>${e(one.label)}</strong><span>${one.state === "connected" ? "Connected" : e(one.about)}</span></button>`).join("");
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.26",
3
+ "version": "0.9.27",
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",