toolroll 0.9.37 → 0.9.39
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 +37 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +3 -3
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +26 -26
- package/dist/browser-crew.js +5 -0
- package/dist/browser-workspace.d.ts +8 -0
- package/dist/chat-channel.js +20 -9
- package/dist/chat-decide.d.ts +4 -3
- package/dist/chat-decide.js +19 -5
- package/dist/desktop-update.d.ts +1 -1
- package/dist/mate-progress.d.ts +26 -0
- package/dist/mate-progress.js +24 -0
- package/dist/mate.js +11 -2
- package/dist/serve.js +95 -13
- package/dist/store.d.ts +5 -0
- package/dist/store.js +14 -0
- package/dist/team-contract.d.ts +11 -7
- package/dist/telegram-flow.d.ts +9 -0
- package/dist/telegram-flow.js +40 -5
- package/dist/telegram-mate.js +1 -1
- package/dist/telegram.js +42 -26
- package/package.json +17 -12
package/dist/browser-crew.js
CHANGED
|
@@ -38,6 +38,10 @@ export function browserCrewFromIndex(page, conversationId) {
|
|
|
38
38
|
const link = (href) => conversationId && href.startsWith('/chat?') ? href + '&conversation=' + encodeURIComponent(conversationId) : href;
|
|
39
39
|
const rows = page.items.map(summary => {
|
|
40
40
|
const status = summary.status;
|
|
41
|
+
// Keep the server's sentence intact; ignore only empty or repeated badge words.
|
|
42
|
+
const words = (text) => text.trim().replace(/\s+/g, ' ').replace(/[.!?]+$/, '').toLowerCase();
|
|
43
|
+
const detail = ['Waiting', 'Failed', 'Needs you'].includes(status.label)
|
|
44
|
+
&& words(status.detail) !== '' && words(status.detail) !== words(status.label) ? status.detail : undefined;
|
|
41
45
|
const resultHref = summary.resultRunId === null || summary.resultTaskId === null || (summary.resultOutcome !== 'built' && summary.resultOutcome !== 'no-change')
|
|
42
46
|
? null : chatResultHref(summary.resultTaskId, summary.resultRunId);
|
|
43
47
|
const href = chatControlHref('task', summary.rootId);
|
|
@@ -48,6 +52,7 @@ export function browserCrewFromIndex(page, conversationId) {
|
|
|
48
52
|
state: summary.assignmentState, label: status.label, tone: status.tone, href: link(href), resultHref: resultHref === null ? null : link(resultHref),
|
|
49
53
|
action: action === null || actionHref === null ? null : { label: action.label, href: link(actionHref) },
|
|
50
54
|
updatedAt: summary.updatedAt,
|
|
55
|
+
...(detail === undefined ? {} : { detail }),
|
|
51
56
|
...(summary.lead === undefined ? {} : { lead: summary.lead }),
|
|
52
57
|
};
|
|
53
58
|
return { item, rank: status.rank };
|
|
@@ -49,6 +49,8 @@ export type BrowserCrewItem = {
|
|
|
49
49
|
} | null;
|
|
50
50
|
/** When the task family last changed (the index's own sort time), shown as the row's age. */
|
|
51
51
|
updatedAt: string;
|
|
52
|
+
/** The server's status detail, when Waiting, Failed or Needs you adds to its badge. */
|
|
53
|
+
detail?: string;
|
|
52
54
|
/** "<name> is on it.": the person's own lead took it on, by the name they gave it. */
|
|
53
55
|
lead?: string;
|
|
54
56
|
};
|
|
@@ -98,6 +100,10 @@ export type BrowserConversation = {
|
|
|
98
100
|
resultRunId: number | null;
|
|
99
101
|
/** A project's own thread (v77); absent or null for the lead conversation or a task. */
|
|
100
102
|
project?: string | null;
|
|
103
|
+
/** The lead thread a change in reachable projects replaced (mate arc ruling 9): shown above a divider, read only. */
|
|
104
|
+
previous?: {
|
|
105
|
+
messages: BrowserMessage[];
|
|
106
|
+
} | null;
|
|
101
107
|
};
|
|
102
108
|
/** A page rebuilt as React components (shadcn/ui). The server still renders
|
|
103
109
|
* its HTML as the no-JavaScript fallback; forms post to the same routes. */
|
|
@@ -174,6 +180,8 @@ export type BrowserLimits = {
|
|
|
174
180
|
export type BrowserSettingsView = {
|
|
175
181
|
kind: 'settings';
|
|
176
182
|
said: string | null;
|
|
183
|
+
/** Using it from a phone, under Chat apps (approvers only). */
|
|
184
|
+
phone?: BrowserPhoneCard;
|
|
177
185
|
/** The settings destinations under short headings; a chat app's tile says whether it is connected and gets alerts. */
|
|
178
186
|
groups: BrowserSettingsGroup[];
|
|
179
187
|
theme: 'system' | 'light' | 'dark';
|
package/dist/chat-channel.js
CHANGED
|
@@ -273,16 +273,27 @@ export function proposalPreview(store, proposal, repos, channel = null) {
|
|
|
273
273
|
: undefined;
|
|
274
274
|
return path === undefined ? id : projectLabel(path);
|
|
275
275
|
})();
|
|
276
|
+
// Telegram's card leads with the action and what confirming does, then the exact terms; its buttons say the rest.
|
|
277
|
+
const phone = channel === "telegram";
|
|
276
278
|
const card = (headline, body = [], consequence = null) => ({
|
|
277
|
-
text:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
text: (phone
|
|
280
|
+
? [
|
|
281
|
+
headline,
|
|
282
|
+
...(consequence === null ? [] : [consequence]),
|
|
283
|
+
...(body.length === 0 ? [] : ["", ...body]),
|
|
284
|
+
"",
|
|
285
|
+
"Nothing changes until you confirm.",
|
|
286
|
+
]
|
|
287
|
+
: [
|
|
288
|
+
headline,
|
|
289
|
+
...body,
|
|
290
|
+
...(consequence === null ? [] : ["", consequence]),
|
|
291
|
+
"",
|
|
292
|
+
"Confirm or Dismiss below. Nothing changes until you confirm.",
|
|
293
|
+
]).join("\n"),
|
|
284
294
|
buttons: true,
|
|
285
295
|
});
|
|
296
|
+
const lasting = phone ? " ⚠ can't be undone" : " — IRREVERSIBLE";
|
|
286
297
|
// A handoff's text is origin-free on purpose: it is persisted before the
|
|
287
298
|
// send, and the button or the missing-setup line joins it at send time.
|
|
288
299
|
const handoff = (headline, body = []) => ({
|
|
@@ -361,11 +372,11 @@ export function proposalPreview(store, proposal, repos, channel = null) {
|
|
|
361
372
|
? ["(the decision is gone)"]
|
|
362
373
|
: [
|
|
363
374
|
`Q: ${phoneText(decision.question, 400)}`,
|
|
364
|
-
...decision.options.map((one) => `${one.id === pick ? "→" : " "} ${phoneText(one.label, 120)}${one.reversible ? "" :
|
|
375
|
+
...decision.options.map((one) => `${one.id === pick ? "→" : " "} ${phoneText(one.label, 120)}${one.reversible ? "" : lasting}${one.id === decision.recommendation ? " (the builder recommends this)" : ""}: ${phoneText(one.consequence, 300)}`),
|
|
365
376
|
...(t("rationale") === "" ? [] : [`Why: ${t("rationale", 300)}`]),
|
|
366
377
|
];
|
|
367
378
|
return card(`Answer decision #${String(id ?? "?")} on ${taskName} with "${t("optionLabel", 120) || pick}"`, body, irreversible
|
|
368
|
-
? "⚠ This choice is irreversible. Confirming asks you once more."
|
|
379
|
+
? phone ? "⚠ This choice can't be undone. Confirming asks you once more." : "⚠ This choice is irreversible. Confirming asks you once more."
|
|
369
380
|
: null);
|
|
370
381
|
}
|
|
371
382
|
case "task_action": {
|
package/dist/chat-decide.d.ts
CHANGED
|
@@ -108,11 +108,12 @@ export declare function mergeInChat(store: Store, taskId: string, run: number, a
|
|
|
108
108
|
ok: false;
|
|
109
109
|
why: string;
|
|
110
110
|
};
|
|
111
|
-
/** The plan card: title, goal in two lines, what changes, done when, and what the yes allows.
|
|
112
|
-
|
|
111
|
+
/** The plan card: title, goal in two lines, what changes, done when, and what the yes allows. On Telegram the yes's
|
|
112
|
+
* terms come second, under the title, and the steps are fewer: the full plan is behind Edit. */
|
|
113
|
+
export declare function planCardText(store: Store, taskId: string, scope: Scope, root?: string, channel?: DecideChannel): string;
|
|
113
114
|
/** The acts a card about this target can take in chat right now, or null when none can (the card keeps its own
|
|
114
115
|
* link). Reads only; nothing is minted. */
|
|
115
|
-
export declare function decideOffer(store: Store, target: DecideTarget, who: VerifiedApprover, now: Date, root?: string): DecideOffer | null;
|
|
116
|
+
export declare function decideOffer(store: Store, target: DecideTarget, who: VerifiedApprover, now: Date, root?: string, channel?: DecideChannel): DecideOffer | null;
|
|
116
117
|
/** One fingerprint of what the card offers, without its tokens: a repaint with the same offer keeps its buttons. */
|
|
117
118
|
export declare function offerFingerprint(offer: DecideOffer): string;
|
|
118
119
|
/** The card's buttons, minted now (message unknown until it is sent: `placeDecideTokens` puts them on it). */
|
package/dist/chat-decide.js
CHANGED
|
@@ -155,8 +155,9 @@ export function mergeInChat(store, taskId, run, approver, now) {
|
|
|
155
155
|
return mode;
|
|
156
156
|
return { ok: true, modeDigest: mode.digest, head: publication.headSha };
|
|
157
157
|
}
|
|
158
|
-
/** The plan card: title, goal in two lines, what changes, done when, and what the yes allows.
|
|
159
|
-
|
|
158
|
+
/** The plan card: title, goal in two lines, what changes, done when, and what the yes allows. On Telegram the yes's
|
|
159
|
+
* terms come second, under the title, and the steps are fewer: the full plan is behind Edit. */
|
|
160
|
+
export function planCardText(store, taskId, scope, root, channel) {
|
|
160
161
|
const ref = store.lookupRef(taskId);
|
|
161
162
|
const artifact = ref === null ? null : store.latestPlanArtifact(ref.id);
|
|
162
163
|
let milestones = [];
|
|
@@ -176,6 +177,19 @@ export function planCardText(store, taskId, scope, root) {
|
|
|
176
177
|
const goal = scope.goal.split("\n").filter(one => one.trim() !== "").slice(0, 2).map(one => phoneText(one, 160));
|
|
177
178
|
const allowing = [permissionPlainWords(scope.profile), scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`]
|
|
178
179
|
.filter((one) => one !== null);
|
|
180
|
+
if (channel === "telegram") {
|
|
181
|
+
return [
|
|
182
|
+
`Plan ready: ${title(store, taskId)}`,
|
|
183
|
+
`Starting allows: ${allowing.join(" · ")}`,
|
|
184
|
+
"",
|
|
185
|
+
...goal,
|
|
186
|
+
...(scope.touches.length > 0 ? [`Only in: ${scope.touches.slice(0, 6).map(one => phoneText(one, 80)).join(", ")}${scope.touches.length > 6 ? ", …" : ""}`] : ["Any file in the project."]),
|
|
187
|
+
"",
|
|
188
|
+
"Done when:",
|
|
189
|
+
...(scope.acceptance.length > 0 ? bullets(scope.acceptance.map(one => one.statement), 5) : ["You decide when you review the result."]),
|
|
190
|
+
...(milestones.length > 0 ? ["", "Steps:", ...bullets(milestones, 3)] : []),
|
|
191
|
+
].join("\n");
|
|
192
|
+
}
|
|
179
193
|
return [
|
|
180
194
|
`Plan ready: ${title(store, taskId)}`,
|
|
181
195
|
"",
|
|
@@ -194,7 +208,7 @@ export function planCardText(store, taskId, scope, root) {
|
|
|
194
208
|
// ---- what a card offers ---------------------------------------------------------------
|
|
195
209
|
/** The acts a card about this target can take in chat right now, or null when none can (the card keeps its own
|
|
196
210
|
* link). Reads only; nothing is minted. */
|
|
197
|
-
export function decideOffer(store, target, who, now, root) {
|
|
211
|
+
export function decideOffer(store, target, who, now, root, channel) {
|
|
198
212
|
const ref = store.lookupRef(target.taskId);
|
|
199
213
|
if (ref === null || ref.repo === null || !who.repos.includes(ref.repo))
|
|
200
214
|
return null;
|
|
@@ -222,7 +236,7 @@ export function decideOffer(store, target, who, now, root) {
|
|
|
222
236
|
const plan = planInChat(store, target.taskId, who.name, now, root);
|
|
223
237
|
if (!plan.ok)
|
|
224
238
|
return null;
|
|
225
|
-
return { text: planCardText(store, target.taskId, plan.scope, root),
|
|
239
|
+
return { text: planCardText(store, target.taskId, plan.scope, root, channel),
|
|
226
240
|
rows: [[{ act: "approve", run: null, digest: plan.digest }], [{ label: DECIDE_LABELS.edit, path: chatControlHref("approval", target.taskId) }, { act: "not-now", run: null, digest: plan.digest }]] };
|
|
227
241
|
}
|
|
228
242
|
case "merge": {
|
|
@@ -414,7 +428,7 @@ export function applyDecideTap(store, seat, input) {
|
|
|
414
428
|
}
|
|
415
429
|
if (row.phase === "cancel") {
|
|
416
430
|
spend();
|
|
417
|
-
const offer = decideOffer(store, target, who, now, input.root);
|
|
431
|
+
const offer = decideOffer(store, target, who, now, input.root, seat.channel);
|
|
418
432
|
if (offer === null)
|
|
419
433
|
return { ack: "Cancelled.", edit: { text: body, rows: linksFor(target), tokens: [] } };
|
|
420
434
|
const minted = mintDecideButtons(store, seat, target, offer, now, input.message);
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -125,13 +125,13 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
|
|
|
125
125
|
export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
|
|
126
126
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
127
127
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
128
|
-
error?: never;
|
|
129
128
|
active: boolean;
|
|
130
129
|
phase: string;
|
|
131
130
|
detail: string;
|
|
132
131
|
running: boolean;
|
|
133
132
|
canResume: boolean;
|
|
134
133
|
canCancel: boolean;
|
|
134
|
+
error?: never;
|
|
135
135
|
automaticRecovery?: never;
|
|
136
136
|
recoveryAttempts?: never;
|
|
137
137
|
recoveryRepairs?: never;
|
package/dist/mate-progress.d.ts
CHANGED
|
@@ -12,14 +12,40 @@ export type MateProgress = {
|
|
|
12
12
|
kind: "tool";
|
|
13
13
|
turn: number;
|
|
14
14
|
step: number;
|
|
15
|
+
id: string;
|
|
15
16
|
label: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "tool-result";
|
|
19
|
+
turn: number;
|
|
20
|
+
step: number;
|
|
21
|
+
id: string;
|
|
22
|
+
outcome: MateToolOutcome;
|
|
16
23
|
} | {
|
|
17
24
|
kind: "text";
|
|
18
25
|
turn: number;
|
|
19
26
|
step: number;
|
|
20
27
|
text: string;
|
|
21
28
|
};
|
|
29
|
+
export type MateToolOutcome = {
|
|
30
|
+
state: "succeeded";
|
|
31
|
+
} | {
|
|
32
|
+
state: "failed";
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
export type MateLiveTool = {
|
|
36
|
+
id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
} & ({
|
|
39
|
+
state: "running";
|
|
40
|
+
} | MateToolOutcome);
|
|
41
|
+
/** Keep tools as labels for older clients; toolCalls adds per-call outcomes. */
|
|
42
|
+
export type MateLiveStep = {
|
|
43
|
+
tools: string[];
|
|
44
|
+
toolCalls: MateLiveTool[];
|
|
45
|
+
text: string;
|
|
46
|
+
};
|
|
22
47
|
export declare function mateToolLabel(name: string): string;
|
|
48
|
+
export declare function mateToolFailureReason(name: string, message: string): string;
|
|
23
49
|
/** The `text` of a structured answer still being written, when the answer
|
|
24
50
|
* opens with it (`{"text": "…`); null until then. Escapes are decoded, and
|
|
25
51
|
* an escape cut off at the end is left for the next chunk. */
|
package/dist/mate-progress.js
CHANGED
|
@@ -43,6 +43,30 @@ export function mateToolLabel(name) {
|
|
|
43
43
|
return TOOL_LABELS[name];
|
|
44
44
|
return name.startsWith("propose_") ? "Preparing a card for you to confirm" : "Working";
|
|
45
45
|
}
|
|
46
|
+
/** Tool errors instruct the model how to retry. Only known, fixed copy belongs
|
|
47
|
+
* in the live UI; never echo arguments, redaction placeholders or exceptions. */
|
|
48
|
+
const TOOL_FAILURE_REASONS = new Map([
|
|
49
|
+
["Choose a project from list_repos.", "That project isn't available."],
|
|
50
|
+
["Choose an available project from list_repos.", "That project isn't available."],
|
|
51
|
+
["repo must be one of the ids from list_repos", "That project isn't available."],
|
|
52
|
+
["not-found: no such task in your projects", "That task isn't available in your projects."],
|
|
53
|
+
["That task is not in your projects.", "That task isn't available in your projects."],
|
|
54
|
+
["not-found: no such decision in your projects", "That decision isn't available in your projects."],
|
|
55
|
+
["that decision is no longer open", "That decision is already closed."],
|
|
56
|
+
["No such flow in your projects.", "No such flow in your projects."],
|
|
57
|
+
["That action is outside your access.", "You don't have access to that action."],
|
|
58
|
+
["Read the current task version before proposing this action.", "The task needs to be checked again before this action."],
|
|
59
|
+
["That file is not in these changes. Call get_diff without file for the list.", "That file isn't in the saved changes."],
|
|
60
|
+
["The saved changes could not be verified.", "The saved changes could not be verified."],
|
|
61
|
+
["The saved check log could not be verified.", "The saved check log could not be verified."],
|
|
62
|
+
["Saved results are unavailable here.", "Saved results are unavailable here."],
|
|
63
|
+
["There is no finished result for that version yet.", "There is no finished result for that version yet."],
|
|
64
|
+
["Task text cannot contain credentials.", "The task text contains a password or secret key."],
|
|
65
|
+
["that tool refused — the plane could not answer it right now", "Toolroll couldn't finish that step right now."],
|
|
66
|
+
]);
|
|
67
|
+
export function mateToolFailureReason(name, message) {
|
|
68
|
+
return TOOL_FAILURE_REASONS.get(message) ?? `That step didn't work (${mateToolLabel(name)}).`;
|
|
69
|
+
}
|
|
46
70
|
/** The `text` of a structured answer still being written, when the answer
|
|
47
71
|
* opens with it (`{"text": "…`); null until then. Escapes are decoded, and
|
|
48
72
|
* an escape cut off at the end is left for the next chunk. */
|
package/dist/mate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import { mateToolLabel } from "./mate-progress.js";
|
|
2
|
+
import { mateToolFailureReason, mateToolLabel } from "./mate-progress.js";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { mateTimeoutNotice } from "./store.js";
|
|
5
5
|
import { RESULT_IMAGES_PER_TURN_CAP } from "./chat-evidence.js";
|
|
@@ -499,8 +499,17 @@ export async function runMateTurn(input) {
|
|
|
499
499
|
if (changed !== null)
|
|
500
500
|
return changed;
|
|
501
501
|
}
|
|
502
|
-
|
|
502
|
+
// Position within this turn is unique even if a provider reuses call ids.
|
|
503
|
+
const progressId = `${turnId}:${steps}:${index}`;
|
|
504
|
+
progress({ kind: "tool", turn: turnId, step: steps, id: progressId, label: mateToolLabel(call.name) });
|
|
503
505
|
const outcome = executeMateTool({ store, who, now: clock(), draft, selectEvidence, step: steps, readDecisions, readResults, searchedMemory, checkedCapabilities, ask, ...(input.integrations === undefined ? {} : { integrations: input.integrations }), thread: thread.id, turn: turnId, ...(promiseChannelOfTurn(input.channel) === undefined ? {} : { channel: promiseChannelOfTurn(input.channel) }), ...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }), ...(input.mediaDelivery === undefined ? {} : { mediaDelivery: input.mediaDelivery }) }, call.name, call.args, view);
|
|
506
|
+
let displayOutcome = { state: "succeeded" };
|
|
507
|
+
if (!outcome.ok) {
|
|
508
|
+
const reason = mateToolFailureReason(call.name, outcome.message);
|
|
509
|
+
displayOutcome = { state: "failed", reason: scanForSecrets(reason).length === 0
|
|
510
|
+
? reason : `That step didn't work (${mateToolLabel(call.name)}).` };
|
|
511
|
+
}
|
|
512
|
+
progress({ kind: "tool-result", turn: turnId, step: steps, id: progressId, outcome: displayOutcome });
|
|
504
513
|
if (READ_TOOLS.has(call.name))
|
|
505
514
|
reads++;
|
|
506
515
|
if (call.name === "show_control" && outcome.ok)
|
package/dist/serve.js
CHANGED
|
@@ -744,6 +744,19 @@ export function createDecisionServer(options) {
|
|
|
744
744
|
provider: () => { const enabled = chatEnablement(); return enabled.ok ? { config: enabled.config, key: enabled.key } : null; },
|
|
745
745
|
...(options.subscriptionChatRunner ? { subscriptionRunner: options.subscriptionChatRunner } : {}),
|
|
746
746
|
});
|
|
747
|
+
// Only browser sessions receive HTML cards. The CLI keeps its summary contract.
|
|
748
|
+
const teamBrowserReply = (reply, csrf) => {
|
|
749
|
+
const snapshot = reply.snapshot;
|
|
750
|
+
if (!snapshot?.selected || !snapshot.proposals)
|
|
751
|
+
return reply;
|
|
752
|
+
return { ...reply, snapshot: { ...snapshot, proposals: snapshot.proposals.map(summary => {
|
|
753
|
+
const proposal = store.getMateProposal(summary.id);
|
|
754
|
+
if (!proposal || proposal.thread !== snapshot.selected.threadId)
|
|
755
|
+
return summary;
|
|
756
|
+
const decision = proposal.kind === 'answer' && typeof proposal.payload['decision'] === 'number' ? store.getDecision(proposal.payload['decision']) : null;
|
|
757
|
+
return { ...summary, card: teamProposalCardParts(proposal, snapshot, csrf, decision).card };
|
|
758
|
+
}) } };
|
|
759
|
+
};
|
|
747
760
|
const teamEndpoint = (request, response) => handleTeamHttp(request, response, {
|
|
748
761
|
authenticate: request => {
|
|
749
762
|
const who = identify(request, request.method === 'POST');
|
|
@@ -773,7 +786,11 @@ export function createDecisionServer(options) {
|
|
|
773
786
|
return false;
|
|
774
787
|
return request.headers['x-csrf-token'] === who.session.csrf;
|
|
775
788
|
},
|
|
776
|
-
execute:
|
|
789
|
+
execute: async (actor, input) => {
|
|
790
|
+
const reply = await team.execute(actor, input);
|
|
791
|
+
const who = identify(request, false);
|
|
792
|
+
return who?.via === 'cookie' ? teamBrowserReply(reply, who.session.csrf) : reply;
|
|
793
|
+
}, cursor: team.cursor, streams: teamStreams,
|
|
777
794
|
});
|
|
778
795
|
const sessionEndpoint = createSessionEndpoint({ store, workspace: coding, projects: codingProjects, projectAllowed: repo => rowVisible(liveCeiling(), repo) });
|
|
779
796
|
/** The enumerable admission list for roll-up SQL: repos-only ceilings
|
|
@@ -3585,11 +3602,11 @@ export function createDecisionServer(options) {
|
|
|
3585
3602
|
if (who.via !== "cookie")
|
|
3586
3603
|
return refuse(response, who, 403, "chat is a browser surface — it keeps your drafts in the session");
|
|
3587
3604
|
if (url.searchParams.get('private') !== '1' && !url.searchParams.has('task') && !url.searchParams.has('result') && !url.searchParams.has('project')) {
|
|
3588
|
-
const reply = await team.execute({ name: who.name, generation: who.session.generation }, {
|
|
3605
|
+
const reply = teamBrowserReply(await team.execute({ name: who.name, generation: who.session.generation }, {
|
|
3589
3606
|
operation: url.searchParams.has('conversation') ? 'show' : 'list',
|
|
3590
3607
|
args: { ...(url.searchParams.has('conversation') ? { conversationId: url.searchParams.get('conversation') } : {}),
|
|
3591
3608
|
...(url.searchParams.has('lead') ? { leadId: url.searchParams.get('lead') } : {}) },
|
|
3592
|
-
});
|
|
3609
|
+
}), who.session.csrf);
|
|
3593
3610
|
if (!reply.ok || !reply.snapshot)
|
|
3594
3611
|
return refuse(response, who, 403, reply.message, '/chat?private=1');
|
|
3595
3612
|
if (url.searchParams.has('proposal')) {
|
|
@@ -3599,11 +3616,7 @@ export function createDecisionServer(options) {
|
|
|
3599
3616
|
return refuse(response, who, 404, 'This proposal is unavailable.', '/chat');
|
|
3600
3617
|
const back = '/chat?conversation=' + encodeURIComponent(selected.id);
|
|
3601
3618
|
const decision = proposal.kind === 'answer' && typeof proposal.payload['decision'] === 'number' ? store.getDecision(proposal.payload['decision']) : null;
|
|
3602
|
-
const
|
|
3603
|
-
const canAct = reply.snapshot.canSend && reply.snapshot.chatAuthorization?.enabled === true;
|
|
3604
|
-
let card = mateProposalCard(proposal, who.session.csrf, !canAct || active, decision, back);
|
|
3605
|
-
if (!canAct)
|
|
3606
|
-
card = card.replace('Available when the current turn finishes.', reply.snapshot.canSend ? 'Enable chat in this conversation before acting on a proposal.' : 'An authorized contributor can act on this proposal.');
|
|
3619
|
+
const card = teamProposalCardParts(proposal, reply.snapshot, who.session.csrf, decision).html;
|
|
3607
3620
|
return sendScreen(response, 200, screen('Review action', `<p><a href="${escape(back)}">Back to conversation</a></p>` + card, { chrome: chromeFor(null, 'chat', undefined, 'all') }));
|
|
3608
3621
|
}
|
|
3609
3622
|
if (reply.snapshot.leads.length > 0 || url.searchParams.has('conversation') || url.searchParams.get('team') === '1') {
|
|
@@ -4290,7 +4303,7 @@ export function createDecisionServer(options) {
|
|
|
4290
4303
|
})(), (() => {
|
|
4291
4304
|
const chosen = store.notificationPreference(who.name);
|
|
4292
4305
|
return { mode: chosen.mode, digestAt: chosen.digestAt, screenshots: chosen.screenshots, projects: notificationProjects(store, who.name) };
|
|
4293
|
-
})(), telegramTrouble(store, loadBotToken(process.env, options.telegramTokenFile))));
|
|
4306
|
+
})(), telegramTrouble(store, loadBotToken(process.env, options.telegramTokenFile)), who.via === "cookie" ? phoneSetup(who) ?? null : null));
|
|
4294
4307
|
}
|
|
4295
4308
|
if (url.pathname === "/push/key") {
|
|
4296
4309
|
// Session-gated: the key is not secret, but strangers get nothing.
|
|
@@ -4464,12 +4477,19 @@ export function createDecisionServer(options) {
|
|
|
4464
4477
|
return first !== undefined && String(first["id"]) === rootId;
|
|
4465
4478
|
}
|
|
4466
4479
|
/** After the first Ready result, once (onboarding): the phone, by a chat app or the console over Tailscale. */
|
|
4480
|
+
/** Chat's one-line pointer to the phone setup: after the first Ready result, until put away or Telegram is paired. */
|
|
4467
4481
|
function phoneCard(who, now) {
|
|
4468
4482
|
if (who.role !== "approver" || store.isDemo() || store.firstSuccessAt(now) === null || store.installationFact(PHONE_CARD_FACT) !== null)
|
|
4469
4483
|
return undefined;
|
|
4470
4484
|
const botId = options.telegramTokenFile === undefined ? null : loadBotToken(process.env, options.telegramTokenFile)?.botId ?? null;
|
|
4471
4485
|
if (botId !== null && store.liveTelegramBindings(botId).length > 0)
|
|
4472
4486
|
return undefined;
|
|
4487
|
+
return phoneSetup(who);
|
|
4488
|
+
}
|
|
4489
|
+
/** The phone setup itself (Settings → Chat apps): a chat app, or this console over Tailscale. */
|
|
4490
|
+
function phoneSetup(who) {
|
|
4491
|
+
if (who.role !== "approver" || store.isDemo())
|
|
4492
|
+
return undefined;
|
|
4473
4493
|
const port = servedPort() ?? 4180;
|
|
4474
4494
|
const address = server.address();
|
|
4475
4495
|
const bound = typeof address === "object" && address !== null ? address.address : "127.0.0.1";
|
|
@@ -4509,7 +4529,11 @@ export function createDecisionServer(options) {
|
|
|
4509
4529
|
const ask = last?.role === "assistant" && last.turn !== null && store.getMateTurn(last.turn)?.approver === who.name ? store.mateAskOpen(last.turn, now) : null;
|
|
4510
4530
|
// Answered questions stay readable above the answer; only their buttons go.
|
|
4511
4531
|
const asks = new Map(messages.flatMap(one => { const asked = one.role === "assistant" && one.turn !== null ? store.mateAsk(one.turn) : null; return asked === null ? [] : [[asked.turn, asked]]; }));
|
|
4532
|
+
// The thread a ceiling change replaced (ruling 9): read after the open, display only.
|
|
4533
|
+
const replaced = store.replacedLeadThread(opened.thread);
|
|
4534
|
+
const previousProposals = replaced === null ? [] : store.listMateProposals(replaced.id);
|
|
4512
4535
|
return {
|
|
4536
|
+
previous: replaced === null ? null : { messages: store.listMateMessages(replaced.id, 40), proposals: previousProposals, decisions: decisionsFor(store, previousProposals) },
|
|
4513
4537
|
messages,
|
|
4514
4538
|
ask,
|
|
4515
4539
|
asks,
|
|
@@ -4540,14 +4564,25 @@ export function createDecisionServer(options) {
|
|
|
4540
4564
|
clearTimeout(previous.expiry);
|
|
4541
4565
|
const live = { steps: [], done: false, ok: false, listeners: previous?.listeners ?? new Set() };
|
|
4542
4566
|
liveTurns.set(thread, live);
|
|
4543
|
-
const current = () => live.steps.at(-1) ?? (live.steps.push({ tools: [], text: "" }), live.steps.at(-1));
|
|
4567
|
+
const current = () => live.steps.at(-1) ?? (live.steps.push({ tools: [], toolCalls: [], text: "" }), live.steps.at(-1));
|
|
4544
4568
|
return event => {
|
|
4545
4569
|
if (live.done)
|
|
4546
4570
|
return;
|
|
4547
4571
|
if (event.kind === "step")
|
|
4548
|
-
live.steps.push({ tools: [], text: "" });
|
|
4549
|
-
else if (event.kind === "tool")
|
|
4572
|
+
live.steps.push({ tools: [], toolCalls: [], text: "" });
|
|
4573
|
+
else if (event.kind === "tool") {
|
|
4550
4574
|
current().tools.push(event.label);
|
|
4575
|
+
current().toolCalls.push({ id: event.id, label: event.label, state: "running" });
|
|
4576
|
+
}
|
|
4577
|
+
else if (event.kind === "tool-result") {
|
|
4578
|
+
for (const step of live.steps) {
|
|
4579
|
+
const index = step.toolCalls.findIndex(tool => tool.id === event.id);
|
|
4580
|
+
if (index !== -1) {
|
|
4581
|
+
step.toolCalls[index] = { ...step.toolCalls[index], ...event.outcome };
|
|
4582
|
+
break;
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4551
4586
|
else if (event.kind === "text")
|
|
4552
4587
|
current().text = event.text;
|
|
4553
4588
|
for (const listener of live.listeners)
|
|
@@ -15395,6 +15430,8 @@ ${THEME_DARK}
|
|
|
15395
15430
|
min-height: min(32rem, 48vh); padding: .25rem;
|
|
15396
15431
|
}
|
|
15397
15432
|
.thread .msg { max-width: 48rem; line-height: 1.65; overflow-wrap: anywhere; }
|
|
15433
|
+
.chat-previous-divider { display: flex; align-items: center; gap: .6rem; margin: 1rem 0; color: var(--so-muted); font-size: .8rem; }
|
|
15434
|
+
.chat-previous-divider::before, .chat-previous-divider::after { content: ""; flex: 1; border-top: 1px solid var(--so-line); }
|
|
15398
15435
|
.thread .msg p { margin: .3rem 0; }
|
|
15399
15436
|
.thread .msg.op {
|
|
15400
15437
|
align-self: flex-end; max-width: min(82%, 40rem); padding: .75rem 1rem;
|
|
@@ -18300,6 +18337,19 @@ function mateProposalCard(proposal, csrf, inert, decision, returnTo = null) {
|
|
|
18300
18337
|
function mateProposalCardParts(proposal, csrf, inert, decision, returnTo = null) {
|
|
18301
18338
|
return proposalCardParts({ id: proposal.id, kind: proposal.kind, payload: proposal.payload, state: proposal.state, outcome: proposal.outcome, by: { mate: true }, actionBase: "/chat/proposal" }, csrf, inert, decision, returnTo);
|
|
18302
18339
|
}
|
|
18340
|
+
/** Review and inline team cards share the same server-decided controls and reasons. */
|
|
18341
|
+
function teamProposalCardParts(proposal, snapshot, csrf, decision) {
|
|
18342
|
+
const canAct = snapshot.canSend && snapshot.chatAuthorization?.enabled === true;
|
|
18343
|
+
const active = snapshot.messages.some(message => message.status === 'running');
|
|
18344
|
+
const back = '/chat?conversation=' + encodeURIComponent(snapshot.selected.id);
|
|
18345
|
+
const parts = mateProposalCardParts(proposal, csrf, !canAct || active, decision, back);
|
|
18346
|
+
if (!canAct && proposal.state === 'pending') {
|
|
18347
|
+
const reason = snapshot.canSend ? 'Enable chat in this conversation before acting on a proposal.' : 'An authorized contributor can act on this proposal.';
|
|
18348
|
+
parts.html = parts.html.replace('Available when the current turn finishes.', reason);
|
|
18349
|
+
parts.card = { ...parts.card, note: reason };
|
|
18350
|
+
}
|
|
18351
|
+
return parts;
|
|
18352
|
+
}
|
|
18303
18353
|
function coordinatorProposalCard(proposal, csrf, now, decision, returnTo = null) {
|
|
18304
18354
|
return proposalCard({ id: proposal.id, kind: proposal.kind, payload: proposal.payload, state: proposal.state, outcome: proposal.outcome, by: { mate: false, name: proposal.name, ago: relativeAge(proposal.createdAt, now) }, actionBase: "/proposals" }, csrf, false, decision, returnTo);
|
|
18305
18355
|
}
|
|
@@ -18319,6 +18369,24 @@ function coordinatorProposalsSection(proposals, decisions, csrf, now, heading =
|
|
|
18319
18369
|
proposals.map(one => coordinatorProposalCard(one, csrf, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo)).join("") +
|
|
18320
18370
|
`</div>`);
|
|
18321
18371
|
}
|
|
18372
|
+
const REPLACED_THREAD_DIVIDER = "New conversation — the projects I can reach changed";
|
|
18373
|
+
/** The replaced thread as the React conversation renders it: words and card outcomes, no buttons. */
|
|
18374
|
+
function replacedBrowserMessages(previous, csrf) {
|
|
18375
|
+
return mateBrowserMessages({ ...previous, pending: null, ask: null, asks: new Map() }, csrf, null).map(message => ({
|
|
18376
|
+
...message, cardsHtml: "",
|
|
18377
|
+
cards: (message.cards ?? []).map(card => ({ ...card, primary: null, dismissable: false })),
|
|
18378
|
+
}));
|
|
18379
|
+
}
|
|
18380
|
+
/** The replaced thread for the server-rendered page: read only, then the divider. */
|
|
18381
|
+
function replacedThreadHtml(previous) {
|
|
18382
|
+
if (previous == null)
|
|
18383
|
+
return "";
|
|
18384
|
+
return `<section class="thread chat-previous" aria-label="Earlier conversation" data-previous-thread>` +
|
|
18385
|
+
previous.messages.map(one => one.role === "operator"
|
|
18386
|
+
? `<div class="msg op" data-previous-message="${one.id}"><p style="white-space:pre-wrap">${escape(one.text)}</p></div>`
|
|
18387
|
+
: `<div class="msg mate" data-previous-message="${one.id}">${renderChatText(one.text)}</div>`).join("") +
|
|
18388
|
+
`<p class="chat-previous-divider" role="separator" data-thread-divider>${escape(REPLACED_THREAD_DIVIDER)}</p></section>`;
|
|
18389
|
+
}
|
|
18322
18390
|
/** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
|
|
18323
18391
|
* "Something else" moves to the composer. Nothing shows while a reply is running. */
|
|
18324
18392
|
function mateAskHtml(ask, csrf, target, composer, open = true) {
|
|
@@ -18367,6 +18435,7 @@ function mateChatVersion(rows) {
|
|
|
18367
18435
|
const focus = rows.focusTask;
|
|
18368
18436
|
const facts = [
|
|
18369
18437
|
rows.messages.map(one => [one.id, one.role, one.turn]),
|
|
18438
|
+
rows.previous == null ? null : rows.previous.messages.map(one => one.id),
|
|
18370
18439
|
rows.proposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
|
|
18371
18440
|
rows.coordinatorProposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
|
|
18372
18441
|
[...rows.decisions.values()].map(one => [one.id, one.state, one.choice ?? null]),
|
|
@@ -18493,6 +18562,7 @@ function matePage(chrome, data) {
|
|
|
18493
18562
|
}
|
|
18494
18563
|
const lastMessage = data.messages.at(-1);
|
|
18495
18564
|
const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
|
|
18565
|
+
conversation.push(replacedThreadHtml(data.previous));
|
|
18496
18566
|
conversation.push(mateThreadHtml({ ...data, problem: null, chatProject }));
|
|
18497
18567
|
conversation.push(
|
|
18498
18568
|
// The New update action (package 2): hidden until a live update lands
|
|
@@ -18513,6 +18583,7 @@ function matePage(chrome, data) {
|
|
|
18513
18583
|
messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
|
|
18514
18584
|
pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: MATE_MESSAGE_MAX_CHARS,
|
|
18515
18585
|
taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
|
|
18586
|
+
...(data.previous == null ? {} : { previous: { messages: replacedBrowserMessages(data.previous, data.csrf) } }),
|
|
18516
18587
|
},
|
|
18517
18588
|
focus: data.focusTask === null ? null : { id: data.focusTask.id, title: data.focusTask.title,
|
|
18518
18589
|
html: taskChatLiveRegion(data.focusTask, data.csrf, requestContext.getStore()?.workspaceRead === true, data.pending !== null) },
|
|
@@ -24046,7 +24117,7 @@ function notificationProjects(store, account) {
|
|
|
24046
24117
|
.map(one => ({ repo: one.path, name: one.name, muted: muted.has(one.path) }))
|
|
24047
24118
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
24048
24119
|
}
|
|
24049
|
-
function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null, push = null, providerKeys = null, digest = null, permissionDefault = null, qualityDefault = null, email = null, telegramDelivery = null, workers = null, updates = null, firstResult = null, chatNotices = null, telegramFailing = false) {
|
|
24120
|
+
function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null, push = null, providerKeys = null, digest = null, permissionDefault = null, qualityDefault = null, email = null, telegramDelivery = null, workers = null, updates = null, firstResult = null, chatNotices = null, telegramFailing = false, phone = null) {
|
|
24050
24121
|
const permissionCard = permissionDefault === null
|
|
24051
24122
|
? ""
|
|
24052
24123
|
: [
|
|
@@ -24272,10 +24343,12 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
24272
24343
|
workers,
|
|
24273
24344
|
updates,
|
|
24274
24345
|
firstResult,
|
|
24346
|
+
...(phone === null ? {} : { phone }),
|
|
24275
24347
|
};
|
|
24276
24348
|
return screen("Settings", [
|
|
24277
24349
|
"<h1>Settings</h1>",
|
|
24278
24350
|
settingsTiles(view.groups),
|
|
24351
|
+
phone === null ? "" : phoneSetupHtml(phone),
|
|
24279
24352
|
appearanceCard(csrf),
|
|
24280
24353
|
permissionCard,
|
|
24281
24354
|
qualityCard,
|
|
@@ -24301,6 +24374,15 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
24301
24374
|
`</details>`,
|
|
24302
24375
|
].join("\n"), { chrome, workspace: { view }, functional: { script: SETTINGS_AUTOSAVE_SCRIPT + (pushScript ?? ""), ...(pushScript === null ? {} : { fetches: true }) } });
|
|
24303
24376
|
}
|
|
24377
|
+
/** The phone setup for the server-rendered Settings page: the same choices as the React card. */
|
|
24378
|
+
function phoneSetupHtml(phone) {
|
|
24379
|
+
const [first, ...others] = phone.chatApps;
|
|
24380
|
+
return `<section class="card" id="phone" aria-labelledby="phone-card-title" data-phone-card><h2 id="phone-card-title">Use it from your phone</h2>` +
|
|
24381
|
+
`<p>${first === undefined ? "" : `<a href="${escape(first.href)}">Pair ${escape(first.label)}</a>`}${others.length === 0 ? "" : ` · or ${others.map(one => `<a href="${escape(one.href)}">${escape(one.label)}</a>`).join(", ")}`}</p>` +
|
|
24382
|
+
`<p class="meta" data-phone-tailnet>${phone.tailnet === null ? `Or install <a href="https://tailscale.com/download">Tailscale</a> on this computer and your phone, then reload this page for the address.`
|
|
24383
|
+
: phone.tailnet.restart === null ? `Or on your phone open <code>${escape(phone.tailnet.address)}</code> and sign in.`
|
|
24384
|
+
: `Or start Toolroll with <code>${escape(phone.tailnet.restart)}</code>, then on your phone open <code>${escape(phone.tailnet.address)}</code> and sign in.`}</p></section>`;
|
|
24385
|
+
}
|
|
24304
24386
|
/** Every worker that is not retired: its capacity, and the tasks it holds now. */
|
|
24305
24387
|
function settingsWorkers(store, now) {
|
|
24306
24388
|
const claims = store.liveClaims(null, now);
|
package/dist/store.d.ts
CHANGED
|
@@ -6323,6 +6323,11 @@ export declare class Store {
|
|
|
6323
6323
|
};
|
|
6324
6324
|
/** The approver's live thread for one scope, read only — null when none is open. */
|
|
6325
6325
|
liveMateThreadFor(approver: string, scope?: MateThreadScope): MateThread | null;
|
|
6326
|
+
/** The lead thread a ceiling change closed just before `current` (ruling 9),
|
|
6327
|
+
* read only and for display only: it never continues and never reaches the
|
|
6328
|
+
* model. Null when there is none, when it was ended or revoked (its text is
|
|
6329
|
+
* gone), or when it closed under the same ceiling. */
|
|
6330
|
+
replacedLeadThread(current: MateThread): MateThread | null;
|
|
6326
6331
|
/** The task a paired chat is talking about (v78), or null for the lead conversation. */
|
|
6327
6332
|
chatFocus(surface: string, binding: number): string | null;
|
|
6328
6333
|
setChatFocus(surface: string, binding: number, task: string | null, now: Date): void;
|
package/dist/store.js
CHANGED
|
@@ -19275,6 +19275,20 @@ export class Store {
|
|
|
19275
19275
|
const row = this.db.prepare("SELECT * FROM mate_thread WHERE approver = ? AND scope_kind = ? AND scope_key IS ? AND closed_at IS NULL AND NOT EXISTS (SELECT 1 FROM team_conversation tc WHERE tc.thread=mate_thread.id) ORDER BY id DESC LIMIT 1").get(approver, scope.kind, scope.kind === "lead" ? null : scope.key);
|
|
19276
19276
|
return row === undefined ? null : readMateThread(row);
|
|
19277
19277
|
}
|
|
19278
|
+
/** The lead thread a ceiling change closed just before `current` (ruling 9),
|
|
19279
|
+
* read only and for display only: it never continues and never reaches the
|
|
19280
|
+
* model. Null when there is none, when it was ended or revoked (its text is
|
|
19281
|
+
* gone), or when it closed under the same ceiling. */
|
|
19282
|
+
replacedLeadThread(current) {
|
|
19283
|
+
if (current.scope.kind !== "lead")
|
|
19284
|
+
return null;
|
|
19285
|
+
const row = this.db.prepare(`SELECT * FROM mate_thread WHERE approver = ? AND scope_kind = 'lead' AND scope_key IS NULL AND id < ? AND closed_at IS NOT NULL
|
|
19286
|
+
AND NOT EXISTS (SELECT 1 FROM team_conversation tc WHERE tc.thread=mate_thread.id) ORDER BY id DESC LIMIT 1`).get(current.approver, current.id);
|
|
19287
|
+
if (row === undefined || String(row["ceiling_digest"]) === current.ceilingDigest)
|
|
19288
|
+
return null;
|
|
19289
|
+
const previous = readMateThread(row);
|
|
19290
|
+
return this.db.prepare("SELECT 1 FROM mate_message WHERE thread = ? LIMIT 1").get(previous.id) === undefined ? null : previous;
|
|
19291
|
+
}
|
|
19278
19292
|
/** The task a paired chat is talking about (v78), or null for the lead conversation. */
|
|
19279
19293
|
chatFocus(surface, binding) {
|
|
19280
19294
|
const row = this.db.prepare("SELECT task FROM chat_focus WHERE surface = ? AND binding = ?").get(surface, binding);
|
package/dist/team-contract.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WorkIndexPage } from './work-index.js';
|
|
2
|
+
import type { BrowserActionCard } from './browser-workspace.js';
|
|
2
3
|
/** Shared central-team transport contract. Requests never establish actor identity. */
|
|
3
4
|
export type TeamActor = {
|
|
4
5
|
name: string;
|
|
@@ -52,6 +53,15 @@ export type TeamChatAuthorization = {
|
|
|
52
53
|
termsDigest: string;
|
|
53
54
|
waitingReason?: string;
|
|
54
55
|
};
|
|
56
|
+
/** Browser cards extend the existing CLI summary; both refer to the same saved proposal. */
|
|
57
|
+
export type TeamProposal = {
|
|
58
|
+
id: number;
|
|
59
|
+
turnId: number;
|
|
60
|
+
title: string;
|
|
61
|
+
state: string;
|
|
62
|
+
href: string;
|
|
63
|
+
card?: BrowserActionCard;
|
|
64
|
+
};
|
|
55
65
|
export type TeamSnapshot = {
|
|
56
66
|
leads: TeamLead[];
|
|
57
67
|
conversations: TeamConversation[];
|
|
@@ -67,13 +77,7 @@ export type TeamSnapshot = {
|
|
|
67
77
|
accounts: string[];
|
|
68
78
|
chatAuthorization?: TeamChatAuthorization;
|
|
69
79
|
tasks?: WorkIndexPage;
|
|
70
|
-
proposals?:
|
|
71
|
-
id: number;
|
|
72
|
-
turnId: number;
|
|
73
|
-
title: string;
|
|
74
|
-
state: string;
|
|
75
|
-
href: string;
|
|
76
|
-
}[];
|
|
80
|
+
proposals?: TeamProposal[];
|
|
77
81
|
};
|
|
78
82
|
export declare const TEAM_OPERATIONS: readonly ['list', 'show', 'create-lead', 'update-lead', 'create-conversation', 'member', 'send', 'edit', 'withdraw', 'read', 'authorize', 'follow', 'stop', 'transfer'];
|
|
79
83
|
export type TeamOperation = typeof TEAM_OPERATIONS[number];
|
package/dist/telegram-flow.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ type Waiting = {
|
|
|
11
11
|
};
|
|
12
12
|
/** The card, if it is still at that visit, waiting at a decision. */
|
|
13
13
|
export declare function flowDecisionAt(store: Store, cardId: number, entry: number): Waiting | null;
|
|
14
|
+
/**
|
|
15
|
+
* A flow decision as Telegram shows it: the action and where each button takes the card first, then the draft (as
|
|
16
|
+
* the card holds it now, so what is approved is what is read) and why a teammate handed it over, then the flow and
|
|
17
|
+
* zone. `said` is the saved notice's body: the words before its instruction are the teammate's handoff.
|
|
18
|
+
*/
|
|
19
|
+
export declare function flowDecisionText(waiting: Waiting, said: string | null, head?: string): string;
|
|
14
20
|
/** Buttons for one visit, minted before the send; `place` stamps the message they landed on. */
|
|
15
21
|
export declare function flowButtons(store: Store, binding: TelegramBinding, waiting: Waiting, now: Date): {
|
|
16
22
|
keyboard: InlineButton[][];
|
|
@@ -20,6 +26,9 @@ export declare function flowButtons(store: Store, binding: TelegramBinding, wait
|
|
|
20
26
|
export declare function flowSentContent(store: Store, card: number, entry: number): FlowSendContent | null;
|
|
21
27
|
/** A sent visit's links as one row of buttons: Toolroll's pages under the trusted origin, and its pull request. */
|
|
22
28
|
export declare function flowSendKeyboardRow(origin: string | null, content: FlowSendContent): InlineButton[];
|
|
29
|
+
/** A choice as Telegram shows it, first: what to choose, then where each option (and a reply) takes the card. The
|
|
30
|
+
* options are the ones its buttons carry: offered when sent, and still offered. */
|
|
31
|
+
export declare function flowChoiceHead(visit: FlowChoiceVisit, content: FlowSendContent): string[];
|
|
23
32
|
/** A choice's buttons for one visit, one option to a row, minted before the send; `place` stamps the message they landed on. */
|
|
24
33
|
export declare function flowChoiceButtons(store: Store, binding: TelegramBinding, visit: FlowChoiceVisit, content: FlowSendContent, now: Date): {
|
|
25
34
|
keyboard: InlineButton[][];
|