toolroll 0.9.53 → 0.9.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +5 -4
- package/dist/agent-onboard.js +4 -1
- package/dist/approval-rules-ui.d.ts +3 -2
- package/dist/approval-rules-ui.js +15 -20
- package/dist/assignment-ui.d.ts +3 -2
- package/dist/assignment-ui.js +4 -11
- package/dist/backup-ui.d.ts +2 -1
- package/dist/backup-ui.js +16 -27
- package/dist/brand-mark.d.ts +3 -2
- package/dist/brand-mark.js +4 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-shell.d.ts +2 -1
- package/dist/browser-shell.js +9 -9
- package/dist/browser-workspace.d.ts +2 -3
- package/dist/browser-workspace.js +0 -3
- package/dist/chat-ask.js +8 -8
- package/dist/chat-core.d.ts +171 -0
- package/dist/chat-core.js +288 -0
- package/dist/chat-decide.d.ts +10 -8
- package/dist/chat-decide.js +26 -21
- package/dist/chat-delivery-state.d.ts +51 -75
- package/dist/chat-delivery-state.js +140 -170
- package/dist/chat-delivery.d.ts +21 -2
- package/dist/chat-delivery.js +313 -194
- package/dist/chat-flow.d.ts +29 -1
- package/dist/chat-flow.js +24 -50
- package/dist/chat-messages.d.ts +319 -0
- package/dist/chat-messages.js +841 -0
- package/dist/chat-migration.d.ts +30 -0
- package/dist/chat-migration.js +802 -0
- package/dist/chat-polish.d.ts +5 -4
- package/dist/chat-polish.js +9 -10
- package/dist/chat-quiet.d.ts +3 -0
- package/dist/chat-quiet.js +5 -0
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +26 -53
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +9 -3
- package/dist/coding-shipping-ui.d.ts +2 -1
- package/dist/coding-shipping-ui.js +6 -6
- package/dist/coding-ui.d.ts +3 -1
- package/dist/coding-ui.js +30 -29
- package/dist/contracts/chat-content.d.ts +71 -2
- package/dist/contracts/chat-content.js +22 -3
- package/dist/contracts/chat-tables.d.ts +11 -4
- package/dist/contracts/chat-tables.js +165 -87
- package/dist/contracts/cli.d.ts +1 -3
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +6 -5
- package/dist/contracts/discord-callback.js +2 -1
- package/dist/contracts/health.d.ts +1 -3
- package/dist/contracts/slack-callback.js +3 -2
- package/dist/contracts/store-json.d.ts +1 -5
- package/dist/contracts/store-json.js +1 -4
- package/dist/contracts/teams-callback.js +2 -1
- package/dist/control-ui.d.ts +5 -4
- package/dist/control-ui.js +9 -34
- package/dist/credentials-ui.d.ts +5 -4
- package/dist/credentials-ui.js +23 -24
- package/dist/demo-chat.d.ts +3 -3
- package/dist/demo-chat.js +29 -76
- package/dist/discord-chat.js +19 -38
- package/dist/discord-settings.d.ts +3 -2
- package/dist/discord-settings.js +21 -55
- package/dist/discord.js +4 -14
- package/dist/envelope.js +10 -0
- package/dist/evidence-pack.d.ts +2 -1
- package/dist/evidence-pack.js +45 -49
- package/dist/export-ui.d.ts +7 -2
- package/dist/export-ui.js +4 -10
- package/dist/export.js +4 -4
- package/dist/flow-gallery-ui.d.ts +4 -3
- package/dist/flow-gallery-ui.js +28 -43
- package/dist/flow-starters-ui.d.ts +2 -1
- package/dist/flow-starters-ui.js +12 -15
- package/dist/flow-triggers.d.ts +1 -0
- package/dist/flow-triggers.js +16 -14
- package/dist/flows-ui.d.ts +4 -3
- package/dist/flows-ui.js +19 -30
- package/dist/flows.js +2 -1
- package/dist/guides.js +108 -3
- package/dist/html.d.ts +76 -0
- package/dist/html.js +173 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +19 -23
- package/dist/kits-ui.d.ts +5 -4
- package/dist/kits-ui.js +18 -20
- package/dist/knowledge-ui.d.ts +6 -5
- package/dist/knowledge-ui.js +26 -25
- package/dist/lead-context.d.ts +11 -1
- package/dist/lead-context.js +6 -8
- package/dist/ledger-view.d.ts +2 -1
- package/dist/ledger-view.js +27 -29
- package/dist/limits-ui.d.ts +2 -1
- package/dist/limits-ui.js +7 -7
- package/dist/live-bus.d.ts +50 -8
- package/dist/live-bus.js +133 -50
- package/dist/mate-tools.js +3 -2
- package/dist/mcp-oauth.js +35 -36
- package/dist/models-ui.d.ts +2 -1
- package/dist/models-ui.js +25 -35
- package/dist/modes.d.ts +13 -1
- package/dist/modes.js +17 -1
- package/dist/monitoring-ui.d.ts +4 -3
- package/dist/monitoring-ui.js +13 -26
- package/dist/notification-rows.d.ts +30 -0
- package/dist/notification-rows.js +102 -0
- package/dist/openrouter-models.d.ts +2 -1
- package/dist/openrouter-models.js +5 -9
- package/dist/operate.js +16 -9
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +13 -13
- package/dist/plane-review.js +5 -11
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +17 -23
- package/dist/project-delete-ui.d.ts +4 -3
- package/dist/project-delete-ui.js +15 -27
- package/dist/project-delete.js +21 -26
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +17 -32
- package/dist/recipe-ui.d.ts +8 -7
- package/dist/recipe-ui.js +39 -32
- package/dist/release-adapters.d.ts +60 -0
- package/dist/release-adapters.js +435 -0
- package/dist/release-cli.d.ts +24 -0
- package/dist/release-cli.js +181 -0
- package/dist/release-gate.d.ts +80 -0
- package/dist/release-gate.js +150 -0
- package/dist/release-links.d.ts +29 -0
- package/dist/release-links.js +102 -0
- package/dist/release.d.ts +331 -0
- package/dist/release.js +550 -0
- package/dist/reply-shape.d.ts +2 -1
- package/dist/reply-shape.js +5 -5
- package/dist/repository-context-ui.d.ts +2 -1
- package/dist/repository-context-ui.js +2 -7
- package/dist/request-budget-ui.d.ts +2 -1
- package/dist/request-budget-ui.js +16 -16
- package/dist/result-review.d.ts +2 -3
- package/dist/result-review.js +3 -9
- package/dist/result-shots.js +2 -2
- package/dist/retention-ui.d.ts +7 -1
- package/dist/retention-ui.js +7 -11
- package/dist/retention.js +1 -1
- package/dist/serve.d.ts +11 -2
- package/dist/serve.js +69 -58
- package/dist/server/chat.d.ts +3 -6
- package/dist/server/chat.js +969 -1006
- package/dist/server/chrome.d.ts +291 -0
- package/dist/server/chrome.js +747 -0
- package/dist/server/console.css +2819 -0
- package/dist/server/flows.d.ts +2 -6
- package/dist/server/flows.js +840 -766
- package/dist/server/guards.d.ts +1 -1
- package/dist/server/guards.js +5 -2
- package/dist/server/handler-context.d.ts +1 -1
- package/dist/server/handler-context.js +1 -1
- package/dist/server/handler-registry.d.ts +8 -6
- package/dist/server/handler-registry.js +14 -2
- package/dist/server/http.d.ts +274 -0
- package/dist/server/http.js +239 -0
- package/dist/server/live.d.ts +34 -0
- package/dist/server/live.js +309 -0
- package/dist/server/pages.d.ts +2 -6
- package/dist/server/pages.js +808 -789
- package/dist/server/people-tokens.d.ts +1 -6
- package/dist/server/people-tokens.js +580 -572
- package/dist/server/remote.d.ts +4 -7
- package/dist/server/remote.js +37 -70
- package/dist/server/render-chat.d.ts +402 -0
- package/dist/server/render-chat.js +927 -0
- package/dist/server/render-pages.d.ts +535 -0
- package/dist/server/render-pages.js +1277 -0
- package/dist/server/render-people.d.ts +32 -0
- package/dist/server/render-people.js +121 -0
- package/dist/server/render-results.d.ts +744 -0
- package/dist/server/render-results.js +2237 -0
- package/dist/server/render-settings.d.ts +147 -0
- package/dist/server/render-settings.js +577 -0
- package/dist/server/render-tasks.d.ts +632 -0
- package/dist/server/render-tasks.js +2345 -0
- package/dist/server/request-context.d.ts +46 -0
- package/dist/server/request-context.js +17 -0
- package/dist/server/route-table.d.ts +25 -3
- package/dist/server/route-table.js +163 -75
- package/dist/server/runtime.d.ts +15 -7
- package/dist/server/runtime.js +7 -1
- package/dist/server/session.d.ts +138 -0
- package/dist/server/session.js +94 -0
- package/dist/server/settings.d.ts +1 -5
- package/dist/server/settings.js +2182 -2117
- package/dist/server/task-views.d.ts +11 -4
- package/dist/server/task-views.js +26 -22
- package/dist/server/tasks.d.ts +9 -9
- package/dist/server/tasks.js +1486 -1418
- package/dist/server-telemetry.d.ts +4 -3
- package/dist/server-telemetry.js +4 -8
- package/dist/skills-ui.d.ts +4 -3
- package/dist/skills-ui.js +14 -17
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +2 -1
- package/dist/slack-api.d.ts +5 -1
- package/dist/slack-api.js +2 -2
- package/dist/slack-chat.d.ts +9 -4
- package/dist/slack-chat.js +29 -52
- package/dist/slack-settings.d.ts +3 -2
- package/dist/slack-settings.js +23 -57
- package/dist/slack.js +6 -16
- package/dist/spend-ui.d.ts +2 -1
- package/dist/spend-ui.js +9 -32
- package/dist/sso-ui.d.ts +3 -2
- package/dist/sso-ui.js +26 -27
- package/dist/storage-ui.d.ts +2 -1
- package/dist/storage-ui.js +14 -33
- package/dist/store.d.ts +97 -364
- package/dist/store.js +207 -1531
- package/dist/surface.js +14 -3
- package/dist/task-composer.d.ts +2 -1
- package/dist/task-composer.js +7 -7
- package/dist/task-status.d.ts +4 -3
- package/dist/task-status.js +15 -16
- package/dist/team-http.d.ts +5 -9
- package/dist/team-http.js +5 -63
- package/dist/team-ui.d.ts +2 -1
- package/dist/team-ui.js +2 -5
- package/dist/teammate-question.js +12 -12
- package/dist/teammates-ui.d.ts +5 -4
- package/dist/teammates-ui.js +61 -90
- package/dist/teams-chat.js +12 -9
- package/dist/teams-settings.d.ts +3 -2
- package/dist/teams-settings.js +22 -33
- package/dist/teams.js +4 -5
- package/dist/telegram-flow.js +5 -5
- package/dist/telegram-settings.d.ts +3 -2
- package/dist/telegram-settings.js +14 -25
- package/dist/telegram.js +4 -4
- package/dist/toolroll-update-ui.d.ts +3 -2
- package/dist/toolroll-update-ui.js +35 -39
- package/dist/toolroll-update.d.ts +10 -0
- package/dist/toolroll-update.js +50 -3
- package/dist/tools-ui.d.ts +5 -2
- package/dist/tools-ui.js +31 -31
- package/dist/when-html.d.ts +11 -3
- package/dist/when-html.js +5 -6
- package/dist/workspace-revision.js +3 -1
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +10 -10
- package/package.json +1 -1
- package/dist/server/shared.d.ts +0 -3151
- package/dist/server/shared.js +0 -12001
- package/dist/slack-state.d.ts +0 -13
- package/dist/slack-state.js +0 -10
package/dist/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-chat.js
CHANGED
|
@@ -167,12 +167,12 @@ export async function deliverTeamsPart(options) {
|
|
|
167
167
|
const shared = teamsDelivery(options), state = shared.state, { store, identity } = options, now = options.clock?.() ?? new Date();
|
|
168
168
|
if (!options.current() || !state.owns(identity.installation, options.owner, now))
|
|
169
169
|
return false;
|
|
170
|
-
const row = state.
|
|
170
|
+
const row = state.nextPart(identity.installation, options.canNotify?.() !== false, now, "created");
|
|
171
171
|
if (!row)
|
|
172
172
|
return false;
|
|
173
173
|
const event = state.event(row.event), binding = event.binding === null ? null : state.bindingById(event.binding);
|
|
174
174
|
if (!binding || new Date(row.created).getTime() + 86_400_000 < now.getTime()) {
|
|
175
|
-
state.
|
|
175
|
+
state.dropPart(row.id, "Delivery expired or access changed; open the saved chat");
|
|
176
176
|
return true;
|
|
177
177
|
}
|
|
178
178
|
try {
|
|
@@ -189,7 +189,7 @@ export async function deliverTeamsPart(options) {
|
|
|
189
189
|
throw new TeamsError("Connected projects changed");
|
|
190
190
|
let text = content.text, actions = [];
|
|
191
191
|
if (content.image && content.shot && resultShotsPruned(options.evidenceRoot, content.image.run)) {
|
|
192
|
-
state.
|
|
192
|
+
state.dropPart(row.id, "Removed by retention");
|
|
193
193
|
return true;
|
|
194
194
|
}
|
|
195
195
|
if (content.image) {
|
|
@@ -206,7 +206,7 @@ export async function deliverTeamsPart(options) {
|
|
|
206
206
|
const preview = proposalPreview(store, proposal, repos, "teams");
|
|
207
207
|
text = content.phase === "armed" ? armedCardText(proposal, preview.text) : preview.text.replace("\n\nConfirm or Dismiss below. Nothing changes until you confirm.", "");
|
|
208
208
|
if (preview.buttons && preview.text.length <= PLATFORM_LIMITS.teams) {
|
|
209
|
-
actions = state.
|
|
209
|
+
actions = state.partTokens(row.id, now)
|
|
210
210
|
.map(action => ({ type: "Action.Submit", title: action.phase === "yes" ? armedYesLabel(proposal) : action.phase === "cancel" ? "Cancel" : action.phase === "dismiss" ? "Dismiss" : "Confirm", data: { so: String(action.token) } }));
|
|
211
211
|
if (!actions.length)
|
|
212
212
|
text = "This confirmation expired. Ask for a fresh proposal.";
|
|
@@ -224,7 +224,11 @@ export async function deliverTeamsPart(options) {
|
|
|
224
224
|
const asked = content.question ? chatQuestionButtons(state, row.id, now).map(one => ({ type: "Action.Submit", title: one.label.slice(0, 80), data: { so: one.token } })) : [];
|
|
225
225
|
// The lead's question to its owner: its options, then "Something else".
|
|
226
226
|
const owner = content.ask ? chatAskButtons(state, row.id, now).map(one => ({ type: "Action.Submit", title: one.label.slice(0, 80), data: { so: one.token } })) : [];
|
|
227
|
-
|
|
227
|
+
// A result's, plan's, failure's or pull request's own buttons (chat-decide.ts): its acts, then its links.
|
|
228
|
+
const decided = (content.decide?.rows.flat() ?? []).flatMap((one, index) => "token" in one
|
|
229
|
+
? [{ type: "Action.Submit", title: one.label.slice(0, 80), data: { so: one.token }, ...(index === 0 ? { style: "positive" } : {}) }]
|
|
230
|
+
: openUrlAction(options.origin(), one.link));
|
|
231
|
+
actions = [...flow, ...asked, ...owner, ...decided, ...openUrlAction(options.origin(), content.link), ...(content.also ?? []).flatMap(one => openUrlAction(options.origin(), one))];
|
|
228
232
|
}
|
|
229
233
|
const target = content.edit ?? row.message;
|
|
230
234
|
// The lead's own reply goes out in Teams' Markdown (bold anchors, labelled links); everything else stays plain.
|
|
@@ -238,17 +242,16 @@ export async function deliverTeamsPart(options) {
|
|
|
238
242
|
const messageId = target ?? (typeof answer.id === "string" ? answer.id : null);
|
|
239
243
|
if (messageId === null)
|
|
240
244
|
throw new TeamsError("Teams did not confirm the message", 15_000, true);
|
|
241
|
-
state.
|
|
245
|
+
state.partSent(row.id, messageId, now);
|
|
242
246
|
return true;
|
|
243
247
|
}
|
|
244
248
|
catch (error) {
|
|
245
249
|
if (error instanceof ChatDeliveryError && error.permanent) {
|
|
246
|
-
state.
|
|
250
|
+
state.dropPart(row.id, error.message);
|
|
247
251
|
return true;
|
|
248
252
|
}
|
|
249
253
|
const problem = error instanceof TeamsError ? error : new TeamsError("Teams delivery failed", 15_000, true);
|
|
250
|
-
state.
|
|
251
|
-
.run(problem.uncertain ? 1 : 0, new Date(now.getTime() + problem.retryMs).toISOString(), problem.message, row.id);
|
|
254
|
+
state.partFailed(row.id, { problem: problem.message, until: new Date(now.getTime() + problem.retryMs).toISOString(), uncertain: problem.uncertain }, 20);
|
|
252
255
|
return true;
|
|
253
256
|
}
|
|
254
257
|
}
|
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,57 +1,46 @@
|
|
|
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;
|
|
10
10
|
const others = bindings.length - (binding === null ? 0 : 1);
|
|
11
|
-
const runtime = credentials ? state.
|
|
11
|
+
const runtime = credentials ? state.runtime(credentials.installation) : null;
|
|
12
12
|
const now = options.now ?? new Date();
|
|
13
|
-
const live = runtime &&
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const post = (action, content) => `<form method="post" action="/settings/teams/${action}" class="card">${hidden}${content}</form>`;
|
|
13
|
+
const live = runtime && runtime.leaseUntil !== null && runtime.leaseUntil > now.toISOString() && runtime.connected;
|
|
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
|
}
|
package/dist/teams.js
CHANGED
|
@@ -93,7 +93,7 @@ export async function followTeams(options) {
|
|
|
93
93
|
const current = loadTeamsCredentials(options.dir);
|
|
94
94
|
return alive && !options.signal.aborted && current?.installation === credentials.installation && current.secret === credentials.secret && state.owns(credentials.installation, owner);
|
|
95
95
|
};
|
|
96
|
-
const problem = (message) => state.
|
|
96
|
+
const problem = (message) => state.setProblem(credentials.installation, owner, message);
|
|
97
97
|
const stop = () => { alive = false; };
|
|
98
98
|
options.signal.addEventListener("abort", stop, { once: true });
|
|
99
99
|
const heartbeat = setInterval(() => { if (!same() || !state.lease(credentials.installation, owner, new Date()))
|
|
@@ -103,7 +103,7 @@ export async function followTeams(options) {
|
|
|
103
103
|
// Connected means the app can sign in; inbound reachability is the operator's endpoint to verify.
|
|
104
104
|
try {
|
|
105
105
|
await teamsAccessToken(credentials, options.fetcher);
|
|
106
|
-
state.
|
|
106
|
+
state.setConnected(credentials.installation, owner, new Date());
|
|
107
107
|
}
|
|
108
108
|
catch (error) {
|
|
109
109
|
problem(error instanceof TeamsError ? error.message : "Microsoft sign-in failed. Check the Teams app credentials.");
|
|
@@ -111,8 +111,7 @@ export async function followTeams(options) {
|
|
|
111
111
|
continue;
|
|
112
112
|
}
|
|
113
113
|
while (same()) {
|
|
114
|
-
|
|
115
|
-
if (typeof retry === "string" && retry > new Date().toISOString()) {
|
|
114
|
+
if (state.retryAt(credentials.installation) > new Date().toISOString()) {
|
|
116
115
|
await pause(1000);
|
|
117
116
|
continue;
|
|
118
117
|
}
|
|
@@ -134,7 +133,7 @@ export async function followTeams(options) {
|
|
|
134
133
|
clearInterval(heartbeat);
|
|
135
134
|
alive = false;
|
|
136
135
|
options.signal.removeEventListener("abort", stop);
|
|
137
|
-
state.
|
|
136
|
+
state.stop(credentials.installation, owner);
|
|
138
137
|
}
|
|
139
138
|
await pause(5000);
|
|
140
139
|
}
|
package/dist/telegram-flow.js
CHANGED
|
@@ -180,7 +180,7 @@ export function applyFlowTap(store, binding, action, message, repos, now) {
|
|
|
180
180
|
store.retireTelegramFlowVisit(action.card, action.entry, now);
|
|
181
181
|
const mint = (phase) => {
|
|
182
182
|
const token = randomBytes(16).toString("hex");
|
|
183
|
-
store.
|
|
183
|
+
store.telegramChat().prepare("INSERT INTO chat_flow_action (provider, token, binding, chat, message, card, entry, action, created, expires) VALUES (:provider,?,?,?,?,?,?,?,?,?)")
|
|
184
184
|
.run(token, binding.id, binding.chatId, message.messageId, action.card, action.entry, phase, now.toISOString(), new Date(now.getTime() + CONFIRM_TTL_MS).toISOString());
|
|
185
185
|
return token;
|
|
186
186
|
};
|
|
@@ -201,17 +201,17 @@ export function applyFlowTap(store, binding, action, message, repos, now) {
|
|
|
201
201
|
export function flowConfirmOf(store, token) {
|
|
202
202
|
if (!/^[0-9a-f]{32}$/.test(token))
|
|
203
203
|
return null;
|
|
204
|
-
const row = store.
|
|
204
|
+
const row = store.telegramChat().prepare("SELECT * FROM chat_flow_action WHERE provider = :provider AND token = ? AND action IN ('yes','cancel')").get(token);
|
|
205
205
|
if (row === undefined)
|
|
206
206
|
return null;
|
|
207
|
-
return { token: String(row["token"]), binding: Number(row["binding"]), chatId: String(row["
|
|
208
|
-
phase: String(row["
|
|
207
|
+
return { token: String(row["token"]), binding: Number(row["binding"]), chatId: String(row["chat"]), messageId: String(row["message"]), card: Number(row["card"]), entry: Number(row["entry"]),
|
|
208
|
+
phase: String(row["action"]) === "cancel" ? "cancel" : "yes", expiresAt: String(row["expires"]), consumedAt: row["consumed"] == null ? null : String(row["consumed"]) };
|
|
209
209
|
}
|
|
210
210
|
/** The second tap, inside the update's transaction: Yes decides the card through the console's door, once; Cancel puts
|
|
211
211
|
* the card's own buttons back. The caller has proved the binding, chat and message are the ones it was armed on. */
|
|
212
212
|
export function applyFlowConfirm(store, binding, confirm, message, repos, now) {
|
|
213
213
|
const body = message.text.replace(FLOW_QUESTION, "");
|
|
214
|
-
const spend = () => store.
|
|
214
|
+
const spend = () => store.telegramChat().prepare("UPDATE chat_flow_action SET consumed = ? WHERE provider = :provider AND action IN ('yes','cancel') AND card = ? AND entry = ? AND message = ? AND consumed IS NULL").run(now.toISOString(), confirm.card, confirm.entry, confirm.messageId);
|
|
215
215
|
if (confirm.consumedAt !== null || confirm.expiresAt <= now.toISOString()) {
|
|
216
216
|
spend();
|
|
217
217
|
const now_ = flowWhereNow(store, confirm.card, repos);
|
|
@@ -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;
|