toolroll 0.9.54 → 0.9.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +5 -4
- package/dist/agent-onboard.js +4 -1
- package/dist/approval-rules-ui.d.ts +3 -2
- package/dist/approval-rules-ui.js +15 -20
- package/dist/assignment-ui.d.ts +3 -2
- package/dist/assignment-ui.js +4 -11
- package/dist/backup-ui.d.ts +2 -1
- package/dist/backup-ui.js +16 -27
- package/dist/brand-mark.d.ts +3 -2
- package/dist/brand-mark.js +4 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-shell.d.ts +2 -1
- package/dist/browser-shell.js +9 -9
- package/dist/browser-workspace.d.ts +2 -3
- package/dist/browser-workspace.js +0 -3
- package/dist/chat-polish.d.ts +5 -4
- package/dist/chat-polish.js +9 -10
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +26 -53
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +9 -3
- package/dist/coding-shipping-ui.d.ts +2 -1
- package/dist/coding-shipping-ui.js +6 -6
- package/dist/coding-ui.d.ts +3 -1
- package/dist/coding-ui.js +30 -29
- package/dist/contracts/cli.d.ts +1 -3
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +6 -5
- package/dist/contracts/health.d.ts +1 -3
- package/dist/control-ui.d.ts +5 -4
- package/dist/control-ui.js +9 -34
- package/dist/credentials-ui.d.ts +5 -4
- package/dist/credentials-ui.js +23 -24
- package/dist/demo-chat.d.ts +3 -3
- package/dist/demo-chat.js +29 -76
- package/dist/discord-settings.d.ts +3 -2
- package/dist/discord-settings.js +17 -42
- package/dist/envelope.js +10 -0
- package/dist/evidence-pack.d.ts +2 -1
- package/dist/evidence-pack.js +45 -49
- package/dist/export-ui.d.ts +7 -2
- package/dist/export-ui.js +4 -10
- package/dist/flow-gallery-ui.d.ts +4 -3
- package/dist/flow-gallery-ui.js +28 -43
- package/dist/flow-starters-ui.d.ts +2 -1
- package/dist/flow-starters-ui.js +12 -15
- package/dist/flow-triggers.d.ts +1 -0
- package/dist/flow-triggers.js +16 -14
- package/dist/flows-ui.d.ts +4 -3
- package/dist/flows-ui.js +19 -30
- package/dist/flows.js +2 -1
- package/dist/guides.js +108 -3
- package/dist/html.d.ts +76 -0
- package/dist/html.js +173 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +19 -23
- package/dist/kits-ui.d.ts +5 -4
- package/dist/kits-ui.js +18 -20
- package/dist/knowledge-ui.d.ts +6 -5
- package/dist/knowledge-ui.js +26 -25
- package/dist/lead-context.d.ts +11 -1
- package/dist/lead-context.js +6 -8
- package/dist/ledger-view.d.ts +2 -1
- package/dist/ledger-view.js +27 -29
- package/dist/limits-ui.d.ts +2 -1
- package/dist/limits-ui.js +7 -7
- package/dist/live-bus.d.ts +50 -8
- package/dist/live-bus.js +133 -50
- package/dist/mcp-oauth.js +35 -36
- package/dist/models-ui.d.ts +2 -1
- package/dist/models-ui.js +25 -35
- package/dist/monitoring-ui.d.ts +4 -3
- package/dist/monitoring-ui.js +13 -26
- package/dist/openrouter-models.d.ts +2 -1
- package/dist/openrouter-models.js +5 -9
- package/dist/operate.js +11 -7
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +13 -13
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +17 -23
- package/dist/project-delete-ui.d.ts +4 -3
- package/dist/project-delete-ui.js +15 -27
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +17 -32
- package/dist/recipe-ui.d.ts +8 -7
- package/dist/recipe-ui.js +39 -32
- package/dist/release-adapters.d.ts +60 -0
- package/dist/release-adapters.js +435 -0
- package/dist/release-cli.d.ts +24 -0
- package/dist/release-cli.js +181 -0
- package/dist/release-gate.d.ts +80 -0
- package/dist/release-gate.js +150 -0
- package/dist/release-links.d.ts +29 -0
- package/dist/release-links.js +102 -0
- package/dist/release.d.ts +331 -0
- package/dist/release.js +550 -0
- package/dist/reply-shape.d.ts +2 -1
- package/dist/reply-shape.js +5 -5
- package/dist/repository-context-ui.d.ts +2 -1
- package/dist/repository-context-ui.js +2 -7
- package/dist/request-budget-ui.d.ts +2 -1
- package/dist/request-budget-ui.js +16 -16
- package/dist/result-review.d.ts +2 -3
- package/dist/result-review.js +3 -9
- package/dist/retention-ui.d.ts +7 -1
- package/dist/retention-ui.js +7 -11
- package/dist/serve.d.ts +11 -2
- package/dist/serve.js +69 -58
- package/dist/server/chat.d.ts +3 -6
- package/dist/server/chat.js +969 -1006
- package/dist/server/chrome.d.ts +291 -0
- package/dist/server/chrome.js +747 -0
- package/dist/server/console.css +2819 -0
- package/dist/server/flows.d.ts +2 -6
- package/dist/server/flows.js +840 -766
- package/dist/server/guards.d.ts +1 -1
- package/dist/server/guards.js +5 -2
- package/dist/server/handler-context.d.ts +1 -1
- package/dist/server/handler-context.js +1 -1
- package/dist/server/handler-registry.d.ts +8 -6
- package/dist/server/handler-registry.js +14 -2
- package/dist/server/http.d.ts +274 -0
- package/dist/server/http.js +239 -0
- package/dist/server/live.d.ts +34 -0
- package/dist/server/live.js +309 -0
- package/dist/server/pages.d.ts +2 -6
- package/dist/server/pages.js +808 -789
- package/dist/server/people-tokens.d.ts +1 -6
- package/dist/server/people-tokens.js +580 -572
- package/dist/server/remote.d.ts +4 -7
- package/dist/server/remote.js +37 -70
- package/dist/server/render-chat.d.ts +402 -0
- package/dist/server/render-chat.js +927 -0
- package/dist/server/render-pages.d.ts +535 -0
- package/dist/server/render-pages.js +1277 -0
- package/dist/server/render-people.d.ts +32 -0
- package/dist/server/render-people.js +121 -0
- package/dist/server/render-results.d.ts +744 -0
- package/dist/server/render-results.js +2237 -0
- package/dist/server/render-settings.d.ts +147 -0
- package/dist/server/render-settings.js +577 -0
- package/dist/server/render-tasks.d.ts +632 -0
- package/dist/server/render-tasks.js +2345 -0
- package/dist/server/request-context.d.ts +46 -0
- package/dist/server/request-context.js +17 -0
- package/dist/server/route-table.d.ts +25 -3
- package/dist/server/route-table.js +163 -75
- package/dist/server/runtime.d.ts +15 -7
- package/dist/server/runtime.js +7 -1
- package/dist/server/session.d.ts +138 -0
- package/dist/server/session.js +94 -0
- package/dist/server/settings.d.ts +1 -5
- package/dist/server/settings.js +2181 -2117
- package/dist/server/task-views.d.ts +11 -4
- package/dist/server/task-views.js +26 -22
- package/dist/server/tasks.d.ts +9 -9
- package/dist/server/tasks.js +1486 -1418
- package/dist/server-telemetry.d.ts +4 -3
- package/dist/server-telemetry.js +4 -8
- package/dist/skills-ui.d.ts +4 -3
- package/dist/skills-ui.js +14 -17
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +2 -1
- package/dist/slack-settings.d.ts +3 -2
- package/dist/slack-settings.js +17 -42
- package/dist/spend-ui.d.ts +2 -1
- package/dist/spend-ui.js +9 -32
- package/dist/sso-ui.d.ts +3 -2
- package/dist/sso-ui.js +26 -27
- package/dist/storage-ui.d.ts +2 -1
- package/dist/storage-ui.js +14 -33
- package/dist/surface.js +14 -3
- package/dist/task-composer.d.ts +2 -1
- package/dist/task-composer.js +7 -7
- package/dist/task-status.d.ts +4 -3
- package/dist/task-status.js +15 -16
- package/dist/team-http.d.ts +5 -9
- package/dist/team-http.js +5 -63
- package/dist/team-ui.d.ts +2 -1
- package/dist/team-ui.js +2 -5
- package/dist/teammates-ui.d.ts +5 -4
- package/dist/teammates-ui.js +61 -90
- package/dist/teams-settings.d.ts +3 -2
- package/dist/teams-settings.js +20 -31
- package/dist/telegram-settings.d.ts +3 -2
- package/dist/telegram-settings.js +14 -25
- package/dist/toolroll-update-ui.d.ts +3 -2
- package/dist/toolroll-update-ui.js +35 -39
- package/dist/tools-ui.d.ts +5 -2
- package/dist/tools-ui.js +31 -31
- package/dist/when-html.d.ts +11 -3
- package/dist/when-html.js +5 -6
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +10 -10
- package/package.json +1 -1
- package/dist/server/shared.d.ts +0 -3151
- package/dist/server/shared.js +0 -12001
package/dist/teammates-ui.js
CHANGED
|
@@ -6,11 +6,12 @@ import { MEMORY_CHARS, searchMemories } from "./teammate-memory.js";
|
|
|
6
6
|
import { deskOf, localZone, routinesOf } from "./teammate-desk.js";
|
|
7
7
|
import { weekOf, weekWords } from "./teammate-week.js";
|
|
8
8
|
import { describeSchedule, parseSchedule } from "./flow-schedule.js";
|
|
9
|
+
import { html, joinHtml, postForm } from "./html.js";
|
|
9
10
|
/** v94: the Tools section's rule rows; a limit's fields show only while "Up to a limit" is chosen. */
|
|
10
11
|
export const TEAMMATE_CSS = `.teammate-tools .tool-grant{border-top:1px solid var(--so-line);padding-top:12px;margin-top:12px}.teammate-tools .tool-grant:first-of-type{border-top:0;margin-top:0;padding-top:0}.teammate-tools h3{font-size:1rem;margin:0 0 4px}.teammate-tools .tool-rule{display:grid;gap:6px;padding:10px 0;border-bottom:1px solid var(--so-line)}.teammate-tools .tool-rule:last-of-type{border-bottom:0}.teammate-tools .tool-rule .meta{display:block}.teammate-tools select,.teammate-tools input{max-width:100%;box-sizing:border-box}.teammate-tools .tool-limit{display:none;flex-wrap:wrap;align-items:center;gap:6px}.teammate-tools .tool-rule:has(option[value="limit"]:checked) .tool-limit{display:flex}.teammate-tools .tool-rule>select{width:auto;min-width:16em}.teammate-tools .tool-limit select{width:auto}.teammate-tools .tool-limit input{width:7em}.teammate-tools .tool-buttons{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}.teammate-tools button{min-height:44px}.teammate-activity li{overflow-wrap:anywhere}.teammate-memory .memory-tell,.teammate-memory .memory-search{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;margin:0 0 12px}.teammate-memory .memory-tell textarea{flex:1 1 280px}.teammate-memory .memory-search input{flex:1 1 220px}.teammate-memory button{min-height:44px}.teammate-memories{list-style:none;padding:0;margin:0}.teammate-memories li{padding:10px 0;border-top:1px solid var(--so-line);overflow-wrap:anywhere}.teammate-memories li:first-child{border-top:0}.teammate-memories .meta{display:block;font-size:.85rem}.teammate-memories details.memory-edit{border:0;padding:0;margin:2px 0 0;background:transparent;box-shadow:none}.memory-edit summary{cursor:pointer;min-height:32px;display:inline-flex;align-items:center;font-size:.85rem;font-weight:400;color:var(--so-muted)}.tool-undo{border:0;padding:0;margin:8px 0;background:transparent;box-shadow:none}.tool-undo summary{cursor:pointer;min-height:36px;display:inline-flex;align-items:center;font-size:.9rem}.tool-undo-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:6px 0}.tool-undo-row select{width:auto}.teammate-overrides,.teammate-undo{padding-left:18px}.teammate-undo li{margin:6px 0}.teammate-undo form{display:inline}.teammate-week h3{font-size:1rem;margin:14px 0 4px}.teammate-week button{min-height:44px}.teammate-routines{list-style:none;padding:0;margin:8px 0}.teammate-routines li{display:grid;gap:4px;padding:10px 0;border-top:1px solid var(--so-line)}.teammate-routines li:first-child{border-top:0}.routine-buttons{display:flex;flex-wrap:wrap;gap:8px}.routine-buttons button,.routine-add button{min-height:44px}.routine-add{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;margin:8px 0}.routine-add label{display:grid;gap:4px;flex:1 1 200px}.routine-add label:first-of-type{flex:0 1 180px}.teammate-actions,.question-options{display:flex;flex-wrap:wrap;gap:8px;margin:8px 0}.teammate-actions form,.question-options form{margin:0}.memory-edit form{display:grid;gap:8px;margin-top:6px}.memory-buttons{display:flex;flex-wrap:wrap;gap:8px}@media(max-width:600px){.teammate-tools select,.teammate-tools input{font-size:16px}}`;
|
|
11
12
|
const USES = [["free", "Do it"], ["limit", "Do it, up to a limit"], ["ask", "Ask first"], ["never", "Never"]];
|
|
12
13
|
/** v94: which project tools it may use, and its rule for each action. */
|
|
13
|
-
function toolsSection(store, mate,
|
|
14
|
+
function toolsSection(store, mate, canManage) {
|
|
14
15
|
const name = nameOf(mate);
|
|
15
16
|
const tools = projectToolsOf(store, mate.repo);
|
|
16
17
|
const grants = store.teammateGrants(mate.id);
|
|
@@ -22,145 +23,115 @@ function toolsSection(store, mate, csrf, canManage) {
|
|
|
22
23
|
const use = rule.use === "free" && rule.limit !== undefined ? "limit" : rule.use;
|
|
23
24
|
const numbers = numberFields(action);
|
|
24
25
|
const words = rule.use === "never" ? "Never" : rule.use === "ask" ? "Ask first" : rule.limit === undefined ? "Do it" : `Do it up to ${rule.limit.field} ${rule.limit.over}, ask first above`;
|
|
26
|
+
const about = action.about !== "" && html `<span class="meta">${action.about}</span>`;
|
|
25
27
|
if (!canManage)
|
|
26
|
-
return `<li class="tool-rule"><span><strong>${
|
|
28
|
+
return html `<li class="tool-rule"><span><strong>${action.name}</strong> · ${words}</span>${about}</li>`;
|
|
27
29
|
const field = `${grant.tool}-${action.name}`.replace(/[^A-Za-z0-9_-]/g, "-");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}).join("");
|
|
33
|
-
const head = `<h3>${e(grant.tool)}</h3>${gone ? `<p class="problem">This project doesn't have ${e(grant.tool)} any more, so ${e(name)} can't use it.</p>` : ""}`;
|
|
30
|
+
const limit = numbers.length > 0 && html `<span class="tool-limit">Ask first when <select name="field.${action.name}" aria-label="The number the limit is on">${numbers.map(one => html `<option value="${one}"${rule.limit?.field === one && html ` selected`}>${one}</option>`)}</select> is over <input type="number" name="over.${action.name}" min="0" step="any" value="${rule.limit === undefined ? "" : rule.limit.over}" aria-label="The limit"></span>`;
|
|
31
|
+
return html `<div class="tool-rule" data-tool-action="${action.name}"><label for="use-${field}"><strong>${action.name}</strong>${about}</label><select id="use-${field}" name="use.${action.name}">${USES.filter(([value]) => value !== "limit" || numbers.length > 0).map(([value, label]) => html `<option value="${value}"${value === use && html ` selected`}>${label}</option>`)}</select>${limit}</div>`;
|
|
32
|
+
});
|
|
33
|
+
const head = html `<h3>${grant.tool}</h3>${gone && html `<p class="problem">This project doesn't have ${grant.tool} any more, so ${name} can't use it.</p>`}`;
|
|
34
34
|
// v97: which action undoes which, for the Undo button on its receipts. Optional, so it's folded away.
|
|
35
35
|
const undoing = grant.actions.filter(one => !one.readOnly).map(action => {
|
|
36
36
|
const chosen = grant.rules[action.name]?.undo ?? "";
|
|
37
|
-
return `<label class="tool-undo-row">${
|
|
38
|
-
})
|
|
39
|
-
const undoBox = undoing
|
|
37
|
+
return html `<label class="tool-undo-row">${action.name} is undone by<select name="undo.${action.name}"><option value="">nothing</option>${grant.actions.filter(other => other.name !== action.name).map(other => html `<option value="${other.name}"${other.name === chosen && html ` selected`}>${other.name}</option>`)}</select></label>`;
|
|
38
|
+
});
|
|
39
|
+
const undoBox = undoing.length > 0 && html `<details class="tool-undo"${grant.actions.some(one => grant.rules[one.name]?.undo !== undefined) && html ` open`}><summary>Undo</summary><p class="meta">When an action has an opposite, a person can press Undo on its receipts: the opposite is called with the same input.</p>${undoing}</details>`;
|
|
40
40
|
return canManage
|
|
41
|
-
? `<div class="tool-grant" data-tool-grant="${
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}).join("");
|
|
41
|
+
? html `<div class="tool-grant" data-tool-grant="${grant.tool}">${head}${postForm(`/teammates/${mate.id}/tools`, html `${rows}${undoBox}<div class="tool-buttons"><button name="op" value="rules">Save rules</button><button name="op" value="revoke" class="secondary">Stop using ${grant.tool}</button></div>`, { hidden: { tool: grant.tool } })}</div>`
|
|
42
|
+
: html `<div class="tool-grant" data-tool-grant="${grant.tool}">${head}<ul>${rows}</ul></div>`;
|
|
43
|
+
});
|
|
45
44
|
const open = tools.filter(one => !grants.some(grant => grant.tool === one.name));
|
|
46
|
-
const add = !canManage ?
|
|
47
|
-
: open.length === 0 ?
|
|
48
|
-
if (grants.length === 0 && add ===
|
|
45
|
+
const add = !canManage ? null : tools.length === 0 ? html `<p class="meta">This project has no tools yet. <a href="${toolsPage}">Add one</a>, then let ${name} use it here.</p>`
|
|
46
|
+
: open.length === 0 ? null : postForm(`/teammates/${mate.id}/tools`, html `<label>Let ${name} use<select name="tool">${open.map(one => html `<option value="${one.name}">${one.name}${one.spec.about === "" ? "" : ` — ${one.spec.about.slice(0, 80)}`}</option>`)}</select></label><button>Add tool</button>`, { attrs: { class: "tool-add" }, hidden: { op: "grant" } });
|
|
47
|
+
if (grants.length === 0 && add === null)
|
|
49
48
|
return "";
|
|
50
|
-
return `<section class="card teammate-tools" id="tools"><h2>Tools</h2>${grants.length === 0
|
|
49
|
+
return html `<section class="card teammate-tools" id="tools"><h2>Tools</h2>${grants.length === 0 && html `<p class="meta">${name} doesn't use any tools yet. Reading starts as “Do it”; everything else asks you first.</p>`}${granted}${add}</section>`;
|
|
51
50
|
}
|
|
52
|
-
const e = (text) => text.replace(/[&<>"']/g, one => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[one]);
|
|
53
51
|
const when = (at) => new Date(at).toLocaleString("en", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
|
54
|
-
const hidden = (csrf) => `<input type="hidden" name="csrf" value="${e(csrf)}">`;
|
|
55
52
|
const startOfDay = () => { const day = new Date(); day.setHours(0, 0, 0, 0); return day.toISOString(); };
|
|
53
|
+
/** Text whose line breaks show as breaks. */
|
|
54
|
+
const lines = (text) => joinHtml(text.split("\n"), html `<br>`);
|
|
56
55
|
/** A blank soul file: the sections a teammate reads, to fill in. */
|
|
57
56
|
export const BLANK_SOUL = `---\nname: \nrole: \n---\n\n## Who you are\n\n\n## How you write\n\n\n## What you know\n\n\n## Decide on your own\n- \n\n## Ask first\n- \n\n## Never\n- \n`;
|
|
58
|
-
export function teammatesListHtml(store, mates, projects, projectName,
|
|
57
|
+
export function teammatesListHtml(store, mates, projects, projectName, canCreate, notice) {
|
|
59
58
|
const rows = mates.map(mate => {
|
|
60
59
|
const today = store.teammateTurnsSince(mate.id, startOfDay());
|
|
61
60
|
const open = store.openTeammateQuestions([mate.id]).length;
|
|
62
|
-
return `<article class="card" data-teammate="${
|
|
63
|
-
})
|
|
61
|
+
return html `<article class="card" data-teammate="${mate.handle}"><div class="flow-row"><h2><a href="/teammates/${mate.id}">${labelOf(mate)}</a></h2><span class="flow-counts">${projectName(mate.repo)} · ${mate.state === "active" ? "Working" : "Paused"} · ${today} today${open > 0 ? ` · ${open} question${open === 1 ? "" : "s"} for you` : ""}</span></div></article>`;
|
|
62
|
+
});
|
|
64
63
|
const create = canCreate && projects.length > 0
|
|
65
|
-
? `<details class="card"${mates.length === 0
|
|
66
|
-
`<label>Start from<select name="template">${TEAMMATE_TEMPLATES.map(one => `<option value="${e(one.id)}">${e(one.label)}: ${e(one.about)}</option>`).join("")}<option value="blank">Blank: write its soul file yourself</option></select></label>` +
|
|
67
|
-
`<label>Name<input name="name" maxlength="40" placeholder="keep the template's name"></label>` +
|
|
68
|
-
(projects.length === 1 ? `<input type="hidden" name="repo" value="${e(projects[0])}">` : `<label>Project<select name="repo">${projects.map(repo => `<option value="${e(repo)}">${e(projectName(repo))}</option>`).join("")}</select></label>`) +
|
|
69
|
-
`<button>Add teammate</button></form></details>`
|
|
64
|
+
? html `<details class="card"${mates.length === 0 && html ` open`}><summary>New teammate</summary>${postForm("/teammates/new", html `<label>Start from<select name="template">${TEAMMATE_TEMPLATES.map(one => html `<option value="${one.id}">${one.label}: ${one.about}</option>`)}<option value="blank">Blank: write its soul file yourself</option></select></label><label>Name<input name="name" maxlength="40" placeholder="keep the template's name"></label>${projects.length === 1 ? html `<input type="hidden" name="repo" value="${projects[0]}">` : html `<label>Project<select name="repo">${projects.map(repo => html `<option value="${repo}">${projectName(repo)}</option>`)}</select></label>`}<button>Add teammate</button>`, { attrs: { "data-new-teammate": true } })}</details>`
|
|
70
65
|
: "";
|
|
71
|
-
const intro = `<p class="meta">AI teammates work your flows' cards within rules you write: they decide, reply and move cards on, and ask you when their rules say to. The quickest start is a <a href="/kits">starter kit</a>: a teammate and its flow, set up in one click.</p>`;
|
|
72
|
-
return `<section class="teammates">${notice.problem
|
|
66
|
+
const intro = html `<p class="meta">AI teammates work your flows' cards within rules you write: they decide, reply and move cards on, and ask you when their rules say to. The quickest start is a <a href="/kits">starter kit</a>: a teammate and its flow, set up in one click.</p>`;
|
|
67
|
+
return html `<section class="teammates">${notice.problem && html `<p class="problem" role="alert">${notice.problem}</p>`}${notice.said && html `<p class="said" role="status">${notice.said}</p>`}${intro}${rows.length > 0 ? rows : html `<p class="meta">No teammates yet.</p>`}${create}</section>`;
|
|
73
68
|
}
|
|
74
69
|
/** v97: its week: what it did and cost, what people overrode, and calls that can be undone. */
|
|
75
|
-
function weekSection(store, mate,
|
|
70
|
+
function weekSection(store, mate, canManage) {
|
|
76
71
|
const week = weekOf(store, mate, new Date());
|
|
77
|
-
const overrides = week.overrides.length
|
|
78
|
-
const undo = week.undoable.length
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return `<details class="card teammate-week" id="week" open><summary>This week</summary><p class="teammate-week-said">${e(weekWords(mate, week)).replace(/\n/g, "<br>")}</p>${overrides}${undo}${send}</details>`;
|
|
72
|
+
const overrides = week.overrides.length > 0 && html `<h3>What you overrode</h3><ul class="teammate-overrides">${week.overrides.map(one => html `<li>${one.said}${one.href !== null && html ` · <a href="${one.href}">open</a>`} <span class="meta">${when(one.at)}</span></li>`)}</ul>`;
|
|
73
|
+
const undo = week.undoable.length > 0 && html `<h3>Undo</h3><ul class="teammate-undo">${week.undoable.map(one => html `<li data-undo="${one.call}"><span>${one.words}</span> <span class="meta">${when(one.at)}${one.href !== null && html ` · <a href="${one.href}">open</a>`}</span>${canManage && postForm(`/teammates/${mate.id}/week`, html `<button name="op" value="undo" class="secondary">Undo with ${one.undo}</button>`, { hidden: { id: one.call } })}</li>`)}</ul>`;
|
|
74
|
+
const send = canManage && postForm(`/teammates/${mate.id}/week`, html `<button name="op" value="send" class="secondary">Send the week's report</button>`);
|
|
75
|
+
return html `<details class="card teammate-week" id="week" open><summary>This week</summary><p class="teammate-week-said">${lines(weekWords(mate, week))}</p>${overrides}${undo}${send}</details>`;
|
|
82
76
|
}
|
|
83
77
|
/** v96: its desk — where messages to it by name and its routines land — and its routines. */
|
|
84
|
-
function deskSection(store, mate,
|
|
78
|
+
function deskSection(store, mate, canManage) {
|
|
85
79
|
const name = nameOf(mate);
|
|
86
80
|
const desk = deskOf(store, mate);
|
|
87
81
|
const routines = routinesOf(store, mate);
|
|
88
|
-
const intro = `<p class="meta">Message ${
|
|
89
|
-
const list = routines.length
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const add = canManage ? `<form method="post" action="/teammates/${mate.id}/routines" class="routine-add">${hidden(csrf)}<input type="hidden" name="op" value="add">` +
|
|
93
|
-
`<label>When<input type="text" name="schedule" maxlength="80" placeholder="weekdays 09:00" required></label><label>What to do<input type="text" name="text" maxlength="200" placeholder="Look up yesterday's refunds and tell me the total." required></label><button>Add routine</button></form>` : "";
|
|
94
|
-
return `<section class="card teammate-desk" id="desk"><h2>Desk and routines</h2>${intro}${list}${add}${canManage ? `<p class="meta">A routine puts a card on its desk on that schedule (“weekdays 09:00”, “daily 17:00”, “monday 09:00”, “every 2 hours”; times are this computer's, ${e(localZone())}, unless you name one); the answer goes to ${e(mate.manager)}.</p>` : ""}</section>`;
|
|
82
|
+
const intro = html `<p class="meta">Message ${name} by name in your chat app, like “@${mate.handle} where's order 1042?”. It lands on ${desk === null ? "its desk" : html `<a href="/flows/${desk.id}">${name}'s desk</a>`}, and the answer comes back to you.</p>`;
|
|
83
|
+
const list = routines.length > 0 && html `<ul class="teammate-routines">${routines.map(one => html `<li data-routine="${one.id}"><span><strong>${scheduleWords(one.schedule)}</strong> · ${one.text}</span><span class="meta">${one.state === "paused" ? "Paused" : one.nextAt === null ? "" : `Next ${when(one.nextAt)}`}${one.lastAt !== null && ` · last ${when(one.lastAt)}${one.lastOutcome === null ? "" : `: ${one.lastOutcome}`}`}</span>${canManage && postForm(`/teammates/${mate.id}/routines`, html `<button name="op" value="run" class="secondary">Run now</button><button name="op" value="remove" class="secondary">Remove</button>`, { attrs: { class: "routine-buttons" }, hidden: { id: one.id } })}</li>`)}</ul>`;
|
|
84
|
+
const add = canManage && postForm(`/teammates/${mate.id}/routines`, html `<label>When<input type="text" name="schedule" maxlength="80" placeholder="weekdays 09:00" required></label><label>What to do<input type="text" name="text" maxlength="200" placeholder="Look up yesterday's refunds and tell me the total." required></label><button>Add routine</button>`, { attrs: { class: "routine-add" }, hidden: { op: "add" } });
|
|
85
|
+
return html `<section class="card teammate-desk" id="desk"><h2>Desk and routines</h2>${intro}${list}${add}${canManage && html `<p class="meta">A routine puts a card on its desk on that schedule (“weekdays 09:00”, “daily 17:00”, “monday 09:00”, “every 2 hours”; times are this computer's, ${localZone()}, unless you name one); the answer goes to ${mate.manager}.</p>`}</section>`;
|
|
95
86
|
}
|
|
96
87
|
/** A schedule's stored form ("weekdays:09:00@Europe/London") in words. */
|
|
97
88
|
const scheduleWords = (text) => { const parsed = parseSchedule(text); return parsed === null ? text : describeSchedule(parsed); };
|
|
98
89
|
/** v95: what it remembers — what its people told it, and what it kept from cards — to search, edit and forget. */
|
|
99
|
-
function memorySection(store, mate,
|
|
90
|
+
function memorySection(store, mate, canManage, query) {
|
|
100
91
|
const name = nameOf(mate);
|
|
101
92
|
const all = store.teammateMemories(mate.id, { limit: 500 });
|
|
102
93
|
const shown = query === null || query.trim() === "" ? all.slice(0, 30) : searchMemories(store, mate, query);
|
|
103
|
-
const tell = canManage
|
|
104
|
-
const search = all.length > 8 || (query ?? "") !== ""
|
|
94
|
+
const tell = canManage && postForm(`/teammates/${mate.id}/note`, html `<label class="sr-only" for="teammate-note">Tell ${name} something</label><textarea id="teammate-note" name="note" rows="2" maxlength="${MEMORY_CHARS}" placeholder="For example: this week, offer free shipping instead of a refund when you can."></textarea><button>Tell ${name}</button>`, { attrs: { class: "memory-tell" } });
|
|
95
|
+
const search = (all.length > 8 || (query ?? "") !== "") && html `<form method="get" action="/teammates/${mate.id}#memory" class="memory-search" role="search"><label class="sr-only" for="memory-q">Search what ${name} remembers</label><input id="memory-q" type="search" name="q" value="${query ?? ""}" placeholder="Search what ${name} remembers"><button class="secondary">Search</button></form>`;
|
|
105
96
|
const items = shown.map(one => {
|
|
106
97
|
const card = one.card === null ? null : store.getFlowCard(one.card);
|
|
107
|
-
const whence = one.source === "person" ? `${
|
|
108
|
-
const edit = canManage
|
|
109
|
-
return `<li data-memory="${one.id}" data-source="${one.source}"><span>${
|
|
110
|
-
})
|
|
111
|
-
const empty = (query ?? "") !== "" ? `<p class="meta">Nothing ${
|
|
112
|
-
return `<section class="card teammate-memory" id="memory"><h2>Memory</h2>${tell}${search}${shown.length === 0 ? empty : `<ul class="teammate-memories">${items}</ul>`}${query === null && all.length > shown.length
|
|
98
|
+
const whence = one.source === "person" ? `${one.createdBy} told it` : card === null ? "it kept this" : html `it kept this from <a href="/flows/${card.flow}?card=${card.id}">${card.title.slice(0, 60)}</a>`;
|
|
99
|
+
const edit = canManage && html `<details class="memory-edit"><summary>Edit</summary>${postForm(`/teammates/${mate.id}/memory`, html `<label class="sr-only" for="memory-${one.id}">Memory</label><textarea id="memory-${one.id}" name="text" rows="2" maxlength="${MEMORY_CHARS}">${one.text}</textarea><div class="memory-buttons"><button name="op" value="edit">Save</button><button name="op" value="forget" class="secondary">Forget</button></div>`, { hidden: { id: one.id } })}</details>`;
|
|
100
|
+
return html `<li data-memory="${one.id}" data-source="${one.source}"><span>${one.text}</span> <span class="meta">${whence} · ${when(one.updatedAt)}</span>${edit}</li>`;
|
|
101
|
+
});
|
|
102
|
+
const empty = (query ?? "") !== "" ? html `<p class="meta">Nothing ${name} remembers matches that.</p>` : html `<p class="meta">Nothing yet. What you tell ${name} is kept here, and it keeps short facts from the cards it works.</p>`;
|
|
103
|
+
return html `<section class="card teammate-memory" id="memory"><h2>Memory</h2>${tell}${search}${shown.length === 0 ? empty : html `<ul class="teammate-memories">${items}</ul>`}${query === null && all.length > shown.length && html `<p class="meta">${all.length - shown.length} older ${all.length - shown.length === 1 ? "memory" : "memories"}: search to find them.</p>`}</section>`;
|
|
113
104
|
}
|
|
114
|
-
export function teammatePageHtml(store, mate, viewer, projectName,
|
|
105
|
+
export function teammatePageHtml(store, mate, viewer, projectName, canManage, approvers, notice) {
|
|
115
106
|
const name = nameOf(mate);
|
|
116
107
|
const questions = store.openTeammateQuestions([mate.id]);
|
|
117
108
|
const asked = questions.map(question => {
|
|
118
109
|
const card = store.getFlowCard(question.card);
|
|
119
110
|
const flow = card === null ? null : store.getFlow(card.flow);
|
|
120
111
|
const mine = question.askedOf === viewer;
|
|
121
|
-
const cardLink = card === null || flow === null ? "a card" : `<a href="/flows/${flow.id}?card=${card.id}">${
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
`</article>`;
|
|
126
|
-
}).join("");
|
|
112
|
+
const cardLink = card === null || flow === null ? "a card" : html `<a href="/flows/${flow.id}?card=${card.id}">${card.title}</a>`;
|
|
113
|
+
const answer = mine && html `<div class="question-options">${question.options.map(one => postForm(`/teammates/questions/${question.id}/answer`, html `<button>${one.label}</button>`, { hidden: { choice: one.id } }))}</div>${postForm(`/teammates/questions/${question.id}/answer`, html `<label>Or answer in your words<textarea name="text" rows="2" maxlength="2000"></textarea></label><button>Answer</button>`, { attrs: { class: "question-reply" } })}`;
|
|
114
|
+
return html `<article class="card teammate-question" data-question="${question.id}"${question.suggestion !== null && html ` data-suggestion`}><p class="meta">${question.suggestion === null ? html `About ${cardLink}` : "Suggests a rule change"} · ${when(question.createdAt)}${!mine && ` · for ${question.askedOf}`}</p><p><strong>${question.question}</strong></p>${answer}</article>`;
|
|
115
|
+
});
|
|
127
116
|
const zones = zonesOf(store, mate);
|
|
128
117
|
const where = zones.length === 0
|
|
129
|
-
? `<p class="meta">Not on any zone yet. In a flow, choose ${
|
|
130
|
-
: `<ul>${zones.map(one => `<li><a href="/flows/${one.flow}">${
|
|
118
|
+
? html `<p class="meta">Not on any zone yet. In a flow, choose ${name} under “Who decides” on a “Person decides” zone, or add a “Teammate handles it” zone.</p>`
|
|
119
|
+
: html `<ul>${zones.map(one => html `<li><a href="/flows/${one.flow}">${one.flowName}</a> · ${one.kind === "decides" ? "decides" : "handles"} ${one.title}</li>`)}</ul>`;
|
|
131
120
|
// What it did and why, with every tool call it made or asked to make (v94), newest first.
|
|
132
121
|
const events = [
|
|
133
122
|
...store.teammateEvents(mate.id, 40).filter(one => one.kind !== "note").map(one => ({ kind: one.kind, said: one.said, card: one.card, at: one.at })),
|
|
134
123
|
...store.teammateCallsOf(mate.id, 40).map(one => ({ kind: "call", said: `Used ${callWords(one.tool, one.action, one.input, 200)} · ${receiptWords(store, one)}`, card: one.card, at: one.createdAt })),
|
|
135
124
|
].sort((a, b) => b.at.localeCompare(a.at)).slice(0, 40);
|
|
136
|
-
const activity = events.length === 0 ? `<p class="meta">Nothing yet.</p>` : `<ul class="teammate-activity">${events.map(one => {
|
|
125
|
+
const activity = events.length === 0 ? html `<p class="meta">Nothing yet.</p>` : html `<ul class="teammate-activity">${events.map(one => {
|
|
137
126
|
const card = one.card === null ? null : store.getFlowCard(one.card);
|
|
138
|
-
return `<li data-event="${one.kind}">${
|
|
139
|
-
})
|
|
127
|
+
return html `<li data-event="${one.kind}">${one.said}${card !== null && html ` · <a href="/flows/${card.flow}?card=${card.id}">open</a>`} <span class="meta">${when(one.at)}</span></li>`;
|
|
128
|
+
})}</ul>`;
|
|
140
129
|
const versions = store.teammateVersions(mate.id);
|
|
141
130
|
const latest = versions[0];
|
|
142
|
-
const manage = canManage
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
`<p class="meta">${e(projectName(mate.repo))} · ${mate.state === "active" ? "Working" : "Paused: its decisions go to people"} · reports to ${e(mate.manager)}</p>`,
|
|
149
|
-
manage,
|
|
150
|
-
questions.length === 0 ? "" : `<section><h2>Questions</h2>${asked}</section>`,
|
|
151
|
-
`<section class="card"><h2>Today</h2><p class="teammate-summary">${e(summaryOf(store, mate, startOfDay()).said).replace(/\n/g, "<br>")}</p></section>`,
|
|
152
|
-
weekSection(store, mate, csrf, canManage),
|
|
153
|
-
`<section class="card"><h2>Works on</h2>${where}</section>`,
|
|
154
|
-
toolsSection(store, mate, csrf, canManage),
|
|
155
|
-
deskSection(store, mate, csrf, canManage),
|
|
156
|
-
memorySection(store, mate, csrf, canManage, notice.query ?? null),
|
|
157
|
-
`<details class="card"${canManage ? " open" : ""}><summary>Soul file</summary><p class="meta">Who ${e(name)} is and its rules, read every turn. Version ${mate.version}${latest === undefined ? "" : ` · saved by ${e(latest.savedBy)}, ${when(latest.savedAt)}`} · <a href="/teammates/${mate.id}/soul.md" download>Download</a></p>` +
|
|
158
|
-
(canManage ? `<form method="post" action="/teammates/${mate.id}/soul" data-soul-form>${hidden(csrf)}<label class="sr-only" for="teammate-soul">Soul file</label><textarea id="teammate-soul" name="soul" rows="24" class="mono" maxlength="12000" spellcheck="false">${e(notice.soulDraft ?? mate.soul)}</textarea><button>Save</button></form>` : `<pre class="mono">${e(mate.soul)}</pre>`) + `</details>`,
|
|
159
|
-
`<details class="card"><summary>What ${e(name)} did</summary>${activity}</details>`,
|
|
160
|
-
canManage ? `<details class="card"><summary>Settings</summary><form method="post" action="/teammates/${mate.id}/settings">${hidden(csrf)}` +
|
|
161
|
-
`<label>Model<select name="model">${TEAMMATE_MODELS.map(one => `<option value="${one}"${(mate.model ?? "default") === one ? " selected" : ""}>${one === "default" ? "The lead chat's model" : one[0].toUpperCase() + one.slice(1)}</option>`).join("")}</select></label>` +
|
|
162
|
-
`<label>Turns a day, at most<input type="number" name="dailyTurns" min="1" max="2000" value="${mate.dailyTurns}"></label>` +
|
|
163
|
-
`<label>Reports to<select name="manager">${[...new Set([mate.manager, ...approvers])].map(one => `<option value="${e(one)}"${one === mate.manager ? " selected" : ""}>${e(one)}</option>`).join("")}</select></label><button>Save settings</button></form>` +
|
|
164
|
-
`<form method="post" action="/teammates/${mate.id}/state" class="danger-zone">${hidden(csrf)}<input type="hidden" name="state" value="removed"><button class="danger">Remove ${e(name)} from the team</button></form></details>` : "",
|
|
165
|
-
].join("");
|
|
131
|
+
const manage = canManage && html `<div class="teammate-actions">${postForm(`/teammates/${mate.id}/state`, html `<button${mate.state === "active" && html ` class="secondary"`}>${mate.state === "active" ? `Pause ${name}` : `Resume ${name}`}</button>`, { hidden: { state: mate.state === "active" ? "paused" : "active" } })}${postForm(`/teammates/${mate.id}/summary`, html `<button class="secondary">Send today's summary</button>`)}</div>`;
|
|
132
|
+
const soul = html `<details class="card"${canManage && html ` open`}><summary>Soul file</summary><p class="meta">Who ${name} is and its rules, read every turn. Version ${mate.version}${latest !== undefined && ` · saved by ${latest.savedBy}, ${when(latest.savedAt)}`} · <a href="/teammates/${mate.id}/soul.md" download>Download</a></p>${canManage
|
|
133
|
+
? postForm(`/teammates/${mate.id}/soul`, html `<label class="sr-only" for="teammate-soul">Soul file</label><textarea id="teammate-soul" name="soul" rows="24" class="mono" maxlength="12000" spellcheck="false">${notice.soulDraft ?? mate.soul}</textarea><button>Save</button>`, { attrs: { "data-soul-form": true } })
|
|
134
|
+
: html `<pre class="mono">${mate.soul}</pre>`}</details>`;
|
|
135
|
+
const settings = canManage && html `<details class="card"><summary>Settings</summary>${postForm(`/teammates/${mate.id}/settings`, html `<label>Model<select name="model">${TEAMMATE_MODELS.map(one => html `<option value="${one}"${(mate.model ?? "default") === one && html ` selected`}>${one === "default" ? "The lead chat's model" : one[0].toUpperCase() + one.slice(1)}</option>`)}</select></label><label>Turns a day, at most<input type="number" name="dailyTurns" min="1" max="2000" value="${mate.dailyTurns}"></label><label>Reports to<select name="manager">${[...new Set([mate.manager, ...approvers])].map(one => html `<option value="${one}"${one === mate.manager && html ` selected`}>${one}</option>`)}</select></label><button>Save settings</button>`)}${postForm(`/teammates/${mate.id}/state`, html `<button class="danger">Remove ${name} from the team</button>`, { attrs: { class: "danger-zone" }, hidden: { state: "removed" } })}</details>`;
|
|
136
|
+
return html `${notice.problem && html `<p class="problem" role="alert">${notice.problem}</p>`}${notice.said && html `<p class="said" role="status">${notice.said}</p>`}<p class="meta">${projectName(mate.repo)} · ${mate.state === "active" ? "Working" : "Paused: its decisions go to people"} · reports to ${mate.manager}</p>${manage}${questions.length > 0 && html `<section><h2>Questions</h2>${asked}</section>`}<section class="card"><h2>Today</h2><p class="teammate-summary">${lines(summaryOf(store, mate, startOfDay()).said)}</p></section>${weekSection(store, mate, canManage)}<section class="card"><h2>Works on</h2>${where}</section>${toolsSection(store, mate, canManage)}${deskSection(store, mate, canManage)}${memorySection(store, mate, canManage, notice.query ?? null)}${soul}<details class="card"><summary>What ${name} did</summary>${activity}</details>${settings}`;
|
|
166
137
|
}
|
package/dist/teams-settings.d.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* registration must point at is shown here; it has to be reachable from the
|
|
4
4
|
* internet, which a tailnet-only address is not. */
|
|
5
5
|
import type { Store } from "./store.js";
|
|
6
|
-
|
|
6
|
+
import { type Html } from "./html.js";
|
|
7
|
+
export declare function teamsSettingsHtml(store: Store, dir: string, options?: {
|
|
7
8
|
code?: string;
|
|
8
9
|
problem?: string;
|
|
9
10
|
now?: Date;
|
|
10
11
|
who?: string;
|
|
11
12
|
publicUrl?: string | null;
|
|
12
|
-
}):
|
|
13
|
+
}): Html;
|
package/dist/teams-settings.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { html, joinHtml, postForm } from "./html.js";
|
|
1
2
|
import { ChatState } from "./chat-delivery-state.js";
|
|
2
3
|
import { loadTeamsCredentials } from "./teams-api.js";
|
|
3
4
|
import { loadPrimary } from "./webhooks.js";
|
|
4
5
|
import { TEAMS_MESSAGES_PATH } from "./teams.js";
|
|
5
|
-
|
|
6
|
-
export function teamsSettingsHtml(store, dir, csrf, options = {}) {
|
|
6
|
+
export function teamsSettingsHtml(store, dir, options = {}) {
|
|
7
7
|
const credentials = loadTeamsCredentials(dir), state = new ChatState(store, "teams");
|
|
8
8
|
const bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [];
|
|
9
9
|
const binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null;
|
|
@@ -11,47 +11,36 @@ export function teamsSettingsHtml(store, dir, csrf, options = {}) {
|
|
|
11
11
|
const runtime = credentials ? state.runtime(credentials.installation) : null;
|
|
12
12
|
const now = options.now ?? new Date();
|
|
13
13
|
const live = runtime && runtime.leaseUntil !== null && runtime.leaseUntil > now.toISOString() && runtime.connected;
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const post = (action, content) => `<form method="post" action="/settings/teams/${action}" class="card">${hidden}${content}</form>`;
|
|
14
|
+
const password = html `<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>`;
|
|
15
|
+
const post = (action, content) => postForm(`/settings/teams/${action}`, content, { attrs: { class: "card" } });
|
|
17
16
|
const endpoint = options.publicUrl ? `${options.publicUrl.replace(/\/$/, "")}${TEAMS_MESSAGES_PATH}` : null;
|
|
18
|
-
|
|
17
|
+
const parts = [];
|
|
19
18
|
if (!credentials) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'<label>Directory (tenant) id<input style="min-height:44px" type="text" name="tenant" autocomplete="off" required></label>' +
|
|
27
|
-
'<label>Client secret<input style="min-height:44px" type="password" name="secret" autocomplete="off" required></label>' +
|
|
28
|
-
password + '<p class="meta">The secret stays on this installation. Next, pair your own Teams account.</p><button type="submit">Connect Teams</button>');
|
|
19
|
+
parts.push(html `<p>Manage projects and review results in a Teams chat, and let a channel follow a team conversation.</p>`, html `<details open><summary>Register the bot</summary><ol><li>In <a href="https://portal.azure.com" target="_blank" rel="noopener noreferrer">Azure</a>, create an <em>Azure Bot</em> with a new single-tenant app registration; note the app (client) id and the directory (tenant) id, and create a client secret.</li>`, html `<li>Set its messaging endpoint to <code>${endpoint === null ? "https://your-public-address" + TEAMS_MESSAGES_PATH : endpoint}</code>. Microsoft must reach it from the internet: a tailnet-only address needs Tailscale Funnel (or another public tunnel) on that path.</li>`, html `<li>Enable the Microsoft Teams channel on the bot, then add the app to Teams (personal scope; team scope for channels).</li></ol></details>`, post("connect", joinHtml([
|
|
20
|
+
html `<label>App (client) id<input style="min-height:44px" type="text" name="app-id" autocomplete="off" required></label>`,
|
|
21
|
+
html `<label>Directory (tenant) id<input style="min-height:44px" type="text" name="tenant" autocomplete="off" required></label>`,
|
|
22
|
+
html `<label>Client secret<input style="min-height:44px" type="password" name="secret" autocomplete="off" required></label>`,
|
|
23
|
+
html `${password}<p class="meta">The secret stays on this installation. Next, pair your own Teams account.</p><button type="submit">Connect Teams</button>`
|
|
24
|
+
])));
|
|
29
25
|
}
|
|
30
26
|
else {
|
|
31
|
-
|
|
27
|
+
parts.push(html `<p><strong>Microsoft Teams</strong> · ${live ? "Signed in" : "Waiting for sign-in"}</p>`);
|
|
32
28
|
if (endpoint !== null)
|
|
33
|
-
|
|
29
|
+
parts.push(html `<p class="meta">Messaging endpoint: <code>${endpoint}</code> — must be reachable from the internet.</p>`);
|
|
34
30
|
if (runtime?.problem)
|
|
35
|
-
|
|
31
|
+
parts.push(html `<p role="status">${String(runtime.problem)}</p>`);
|
|
36
32
|
if (!live && !runtime?.problem)
|
|
37
|
-
|
|
33
|
+
parts.push(html `<p>Keep the Toolroll worker running to answer Teams messages.</p>`);
|
|
38
34
|
if (options.code) {
|
|
39
|
-
|
|
40
|
-
`<label>Pairing message<input type="text" readonly value="pair ${escape(options.code)}" autocomplete="off" style="width:100%;max-width:100%;font-size:14px;min-height:44px"></label>` +
|
|
41
|
-
'<a class="button-link" style="min-height:44px;white-space:nowrap" href="/settings/teams">Check connection</a>';
|
|
35
|
+
parts.push(html `<h2>Pair your account</h2><p>Send this to the Toolroll bot in a personal Teams chat. It expires in 10 minutes.</p>`, html `<label>Pairing message<input type="text" readonly value="pair ${options.code}" autocomplete="off" style="width:100%;max-width:100%;font-size:14px;min-height:44px"></label>`, html `<a class="button-link" style="min-height:44px;white-space:nowrap" href="/settings/teams">Check connection</a>`);
|
|
42
36
|
}
|
|
43
37
|
else if (binding && state.live(binding)) {
|
|
44
|
-
|
|
45
|
-
"<p>Try “What needs my attention?” or send <code>status</code>. In a channel, mention the bot with <code>team</code> to follow a conversation.</p>" +
|
|
46
|
-
'<p><a href="/chat">Open saved chat</a></p>' +
|
|
47
|
-
(loadPrimary(process.env, dir) === "teams" ? "<p>Task updates are sent here.</p>" : post("alerts", '<button type="submit">Send task updates here</button>')) +
|
|
48
|
-
post("unpair", password + '<p class="meta">Unpairing ends every open button in your chat. Teammates are unaffected.</p><button type="submit">Unpair my account</button>');
|
|
38
|
+
parts.push(html `<p>Your Teams account is paired.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} paired too.` : ""}</p>`, html `<p>Try “What needs my attention?” or send <code>status</code>. In a channel, mention the bot with <code>team</code> to follow a conversation.</p>`, html `<p><a href="/chat">Open saved chat</a></p>`, loadPrimary(process.env, dir) === "teams" ? html `<p>Task updates are sent here.</p>` : post("alerts", html `<button type="submit">Send task updates here</button>`), post("unpair", html `${password}<p class="meta">Unpairing ends every open button in your chat. Teammates are unaffected.</p><button type="submit">Unpair my account</button>`));
|
|
49
39
|
}
|
|
50
40
|
else {
|
|
51
|
-
|
|
52
|
-
(others ? ` ${others} teammate${others === 1 ? " is" : "s are"} already paired.` : "") + "</p>" + password + '<button type="submit">Create pairing code</button>');
|
|
41
|
+
parts.push(post("pair", html `<h2>Pair your account</h2><p>Your paired Teams account can read your connected projects and confirm proposed changes. Password approvals still open in Toolroll.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} already paired.` : ""}</p>${password}<button type="submit">Create pairing code</button>`));
|
|
53
42
|
}
|
|
54
|
-
|
|
43
|
+
parts.push(html `<details><summary>Connection settings</summary>${post("disconnect", html `${password}<button type="submit">Disconnect Teams</button>`)}</details>`);
|
|
55
44
|
}
|
|
56
|
-
return `<section style="max-width:42rem;overflow-wrap:anywhere"><p><a href="/settings">Settings</a></p><h1>Teams</h1>${options.problem ? `<p class="problem" role="alert">${
|
|
45
|
+
return html `<section style="max-width:42rem;overflow-wrap:anywhere"><p><a href="/settings">Settings</a></p><h1>Teams</h1>${options.problem ? html `<p class="problem" role="alert">${options.problem}</p>` : ""}${parts}</section>`;
|
|
57
46
|
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* the settings page; this card only mints or revokes the signed-in person's
|
|
4
4
|
* pairing, never anyone else's. */
|
|
5
5
|
import type { Store } from "./store.js";
|
|
6
|
+
import { type Html } from "./html.js";
|
|
6
7
|
/** How long a written reply may wait to be sent before it shows here with a Retry. */
|
|
7
8
|
export declare const UNSENT_REPLY_MS: number;
|
|
8
|
-
export declare function telegramSettingsHtml(store: Store, botId: string | null, who: string,
|
|
9
|
+
export declare function telegramSettingsHtml(store: Store, botId: string | null, who: string, options?: {
|
|
9
10
|
code?: string;
|
|
10
11
|
problem?: string;
|
|
11
12
|
now?: Date;
|
|
12
|
-
}):
|
|
13
|
+
}): Html;
|
|
@@ -1,47 +1,36 @@
|
|
|
1
|
+
import { html, joinHtml, postForm } from "./html.js";
|
|
1
2
|
/** How long a written reply may wait to be sent before it shows here with a Retry. */
|
|
2
3
|
export const UNSENT_REPLY_MS = 2 * 60_000;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
.replaceAll('"', """)
|
|
8
|
-
.replaceAll("'", "'");
|
|
9
|
-
export function telegramSettingsHtml(store, botId, who, csrf, options = {}) {
|
|
10
|
-
const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">`;
|
|
11
|
-
const password = '<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>';
|
|
12
|
-
const post = (action, content) => `<form method="post" action="/settings/telegram/${action}" class="card">${hidden}${content}</form>`;
|
|
13
|
-
let content = "";
|
|
4
|
+
export function telegramSettingsHtml(store, botId, who, options = {}) {
|
|
5
|
+
const password = html `<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>`;
|
|
6
|
+
const post = (action, content) => postForm(`/settings/telegram/${action}`, content, { attrs: { class: "card" } });
|
|
7
|
+
let content;
|
|
14
8
|
if (botId === null) {
|
|
15
|
-
content =
|
|
9
|
+
content = html `<p>No Telegram bot is connected yet. An installation approver saves the bot token on the <a href="/settings">settings page</a> first.</p>`;
|
|
16
10
|
}
|
|
17
11
|
else {
|
|
18
12
|
const bindings = store.liveTelegramBindings(botId);
|
|
19
13
|
const mine = bindings.filter(binding => binding.approver === who);
|
|
20
14
|
const others = bindings.length - mine.length;
|
|
21
|
-
|
|
15
|
+
const parts = [html `<p><strong>${mine.length > 0 ? "Your phone is paired." : "Your phone is not paired."}</strong> ${others === 0 ? "No teammates are paired yet." : `${others} teammate${others === 1 ? " is" : "s are"} paired.`}</p>`];
|
|
22
16
|
// A reply the assistant wrote that has not reached this person's phone for two minutes.
|
|
23
17
|
const unsent = store.unsentTelegramReplies(botId, who, new Date((options.now ?? new Date()).getTime() - UNSENT_REPLY_MS));
|
|
24
18
|
if (unsent.length > 0) {
|
|
25
19
|
const oldest = unsent[0];
|
|
26
|
-
|
|
27
|
-
`<div class="card" role="status" data-unsent-replies><p><strong>${unsent.length === 1 ? "A reply hasn't reached your phone" : `${unsent.length} replies haven't reached your phone`}</strong></p>` +
|
|
28
|
-
`<p class="meta">Trying since ${escape(oldest.since.slice(11, 16))} UTC${oldest.error === null ? "" : `: ${escape(oldest.error)}`}</p>` +
|
|
29
|
-
`<form method="post" action="/settings/telegram/retry">${hidden}<button type="submit">Retry</button></form></div>`;
|
|
20
|
+
parts.push(html `<div class="card" role="status" data-unsent-replies><p><strong>${unsent.length === 1 ? html `A reply hasn't reached your phone` : html `${unsent.length} replies haven't reached your phone`}</strong></p><p class="meta">Trying since ${oldest.since.slice(11, 16)} UTC${oldest.error === null ? "" : `: ${oldest.error}`}</p>${postForm("/settings/telegram/retry", html `<button type="submit">Retry</button>`)}</div>`);
|
|
30
21
|
}
|
|
31
22
|
if (options.code !== undefined) {
|
|
32
|
-
|
|
33
|
-
"<h2>Pair your phone</h2><p>Send this to the bot in a private Telegram chat within 10 minutes:</p>" +
|
|
34
|
-
`<pre class="pairing-code">/pair ${escape(options.code)}</pre>` +
|
|
35
|
-
'<p class="meta">The code works once. That chat then answers as you, so treat it like your sign-in.</p>';
|
|
23
|
+
parts.push(html `<h2>Pair your phone</h2><p>Send this to the bot in a private Telegram chat within 10 minutes:</p><pre class="pairing-code">/pair ${options.code}</pre><p class="meta">The code works once. That chat then answers as you, so treat it like your sign-in.</p>`);
|
|
36
24
|
}
|
|
37
25
|
else if (mine.length === 0) {
|
|
38
|
-
|
|
26
|
+
parts.push(post("pair", html `${password}<button type="submit">Pair my phone</button>`));
|
|
39
27
|
}
|
|
40
28
|
else {
|
|
41
|
-
|
|
29
|
+
parts.push(post("unpair", html `${password}<p class="meta">This phone will lose access. Its existing Telegram buttons will stop working.</p><button type="submit" class="danger">Unpair my phone</button>`));
|
|
42
30
|
}
|
|
31
|
+
content = joinHtml(parts);
|
|
43
32
|
}
|
|
44
33
|
if (options.problem !== undefined)
|
|
45
|
-
content = `<p role="status">${
|
|
46
|
-
return `<h1>Telegram</h1>${content}<p class="meta">Each teammate pairs their own private chat. To connect a group, a conversation manager sends <code>/team</code> there and chooses a conversation. Everyone in the group can read its replies.</p>`;
|
|
34
|
+
content = html `<p role="status">${options.problem}</p>${content}`;
|
|
35
|
+
return html `<h1>Telegram</h1>${content}<p class="meta">Each teammate pairs their own private chat. To connect a group, a conversation manager sends <code>/team</code> there and chooses a conversation. Everyone in the group can read its replies.</p>`;
|
|
47
36
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* and a one-time "What's new" card afterwards. The work itself is the
|
|
5
5
|
* `toolroll update` command (toolroll-update.ts), started as its own job.
|
|
6
6
|
*/
|
|
7
|
+
import { type Html } from "./html.js";
|
|
7
8
|
import type { InstallMethod } from "./install-method.js";
|
|
8
9
|
import { type RuntimeUpdateJournal } from "./toolroll-update.js";
|
|
9
10
|
export declare const UPDATES_CSS: string;
|
|
@@ -29,11 +30,11 @@ export type UpdatesView = {
|
|
|
29
30
|
csrf: string;
|
|
30
31
|
};
|
|
31
32
|
/** The steps, each done, current, failed or waiting. Also the live region's fragment. */
|
|
32
|
-
export declare function updateStepsHtml(j: RuntimeUpdateJournal, running: boolean):
|
|
33
|
+
export declare function updateStepsHtml(j: RuntimeUpdateJournal, running: boolean): Html;
|
|
33
34
|
export declare function updatesHtml(view: UpdatesView, notice: {
|
|
34
35
|
said?: string | null;
|
|
35
36
|
problem?: string | null;
|
|
36
|
-
}):
|
|
37
|
+
}): Html;
|
|
37
38
|
export declare function newerThan(a: string, b: string): boolean;
|
|
38
39
|
/** Polls the steps every two seconds, keeps trying while the console
|
|
39
40
|
* restarts, and reloads once the update has finished. */
|