toolroll 0.9.20 → 0.9.21
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 +14 -0
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +22 -21
- package/dist/browser-workspace.d.ts +41 -1
- package/dist/chat-actions.d.ts +5 -0
- package/dist/chat-actions.js +57 -6
- package/dist/chat-delivery-state.d.ts +15 -0
- package/dist/chat-delivery-state.js +27 -2
- package/dist/chat-delivery.js +24 -4
- package/dist/chat-flow.d.ts +26 -3
- package/dist/chat-flow.js +217 -5
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +2 -2
- package/dist/evidence.d.ts +13 -1
- package/dist/evidence.js +10 -1
- package/dist/flow-engine.d.ts +7 -0
- package/dist/flow-engine.js +71 -27
- package/dist/flow-gallery-ui.d.ts +1 -1
- package/dist/flow-gallery-ui.js +8 -7
- package/dist/flow-gallery.d.ts +8 -0
- package/dist/flow-gallery.js +37 -3
- package/dist/flow-send.d.ts +80 -0
- package/dist/flow-send.js +249 -0
- package/dist/flow-share.js +7 -3
- package/dist/flow-steps.js +1 -1
- package/dist/flows-cli.js +11 -3
- package/dist/flows-ui.js +20 -4
- package/dist/flows.d.ts +27 -1
- package/dist/flows.js +104 -18
- package/dist/mate-tools.js +33 -10
- package/dist/operate.js +41 -0
- package/dist/project-delete.js +4 -0
- package/dist/project-demo.d.ts +12 -0
- package/dist/project-demo.js +71 -0
- package/dist/provider.d.ts +6 -0
- package/dist/provider.js +4 -1
- package/dist/report-summary.d.ts +9 -0
- package/dist/report-summary.js +9 -0
- package/dist/result-shots.d.ts +2 -0
- package/dist/result-shots.js +24 -5
- package/dist/scout-net.d.ts +22 -0
- package/dist/scout-net.js +214 -0
- package/dist/scout-report.d.ts +67 -0
- package/dist/scout-report.js +108 -1
- package/dist/scout.d.ts +42 -0
- package/dist/scout.js +225 -16
- package/dist/serve.js +43 -5
- package/dist/slack-chat.js +3 -3
- package/dist/store.d.ts +53 -0
- package/dist/store.js +95 -4
- package/dist/teams-chat.js +1 -1
- package/dist/telegram-flow.d.ts +16 -2
- package/dist/telegram-flow.js +58 -0
- package/dist/telegram.js +46 -10
- package/package.json +1 -1
|
@@ -725,7 +725,7 @@ export type BrowserRunChecks = {
|
|
|
725
725
|
export type BrowserFlowStage = {
|
|
726
726
|
id: string;
|
|
727
727
|
title: string;
|
|
728
|
-
kind: "inbox" | "task" | "report" | "approval" | "check" | "pull-request" | "update" | "notify" | "sort" | "draft" | "request" | "email" | "tool" | "wait" | "teammate" | "done";
|
|
728
|
+
kind: "inbox" | "task" | "report" | "approval" | "check" | "pull-request" | "update" | "notify" | "sort" | "draft" | "request" | "email" | "tool" | "wait" | "teammate" | "send" | "choose" | "done";
|
|
729
729
|
zone: {
|
|
730
730
|
x: number;
|
|
731
731
|
y: number;
|
|
@@ -799,6 +799,13 @@ export type BrowserFlowStage = {
|
|
|
799
799
|
teammate?: string | undefined;
|
|
800
800
|
/** v96: a teammate zone sends what the teammate writes back to whoever asked. */
|
|
801
801
|
reply?: boolean | undefined;
|
|
802
|
+
/** A "Person chooses" zone's buttons: each one's words and the zone it leads to ("end" ignores the card). */
|
|
803
|
+
options?: {
|
|
804
|
+
label: string;
|
|
805
|
+
to: string;
|
|
806
|
+
}[] | undefined;
|
|
807
|
+
/** A Build zone in another project: its path. */
|
|
808
|
+
repo?: string | undefined;
|
|
802
809
|
next: string | null;
|
|
803
810
|
onFail: string | null;
|
|
804
811
|
};
|
|
@@ -884,6 +891,34 @@ export type BrowserFlowCard = {
|
|
|
884
891
|
from: string;
|
|
885
892
|
note: string;
|
|
886
893
|
} | null;
|
|
894
|
+
/** Waiting at a "Person chooses" zone: what was sent, its options and whether the viewer is the one who chooses. */
|
|
895
|
+
choose?: {
|
|
896
|
+
entry: number;
|
|
897
|
+
title: string;
|
|
898
|
+
summary: string;
|
|
899
|
+
links: {
|
|
900
|
+
label: string;
|
|
901
|
+
href: string;
|
|
902
|
+
}[];
|
|
903
|
+
person: string;
|
|
904
|
+
mine: boolean;
|
|
905
|
+
options: {
|
|
906
|
+
choice: number;
|
|
907
|
+
label: string;
|
|
908
|
+
}[];
|
|
909
|
+
reply: boolean;
|
|
910
|
+
} | null;
|
|
911
|
+
/** What a "Send to me" (or an earlier choice) last sent the card's person. */
|
|
912
|
+
sent?: {
|
|
913
|
+
title: string;
|
|
914
|
+
summary: string;
|
|
915
|
+
links: {
|
|
916
|
+
label: string;
|
|
917
|
+
href: string;
|
|
918
|
+
}[];
|
|
919
|
+
person: string;
|
|
920
|
+
at: string;
|
|
921
|
+
} | null;
|
|
887
922
|
/** v94: every tool call teammates made or asked to make on this card, oldest first: the receipts. */
|
|
888
923
|
calls?: {
|
|
889
924
|
id: number;
|
|
@@ -1015,6 +1050,11 @@ export type BrowserFlowView = {
|
|
|
1015
1050
|
live: string | null;
|
|
1016
1051
|
canEdit: boolean;
|
|
1017
1052
|
approvers: string[];
|
|
1053
|
+
/** The projects a Build zone may work in: the flow's own first, then others the viewer can reach. */
|
|
1054
|
+
projects?: {
|
|
1055
|
+
path: string;
|
|
1056
|
+
name: string;
|
|
1057
|
+
}[];
|
|
1018
1058
|
kinds: {
|
|
1019
1059
|
kind: BrowserFlowStage["kind"];
|
|
1020
1060
|
label: string;
|
package/dist/chat-actions.d.ts
CHANGED
|
@@ -91,6 +91,11 @@ export declare const CHAT_ACTIONS: {
|
|
|
91
91
|
readonly protected: false;
|
|
92
92
|
readonly password: false;
|
|
93
93
|
};
|
|
94
|
+
readonly flow_card_choose: {
|
|
95
|
+
readonly label: "Choose";
|
|
96
|
+
readonly protected: false;
|
|
97
|
+
readonly password: false;
|
|
98
|
+
};
|
|
94
99
|
readonly flow_card_cancel: {
|
|
95
100
|
readonly label: "Cancel card";
|
|
96
101
|
readonly protected: false;
|
package/dist/chat-actions.js
CHANGED
|
@@ -16,9 +16,10 @@ import { acceptAndCompleteAsOperator, assignmentOf } from "./assignment.js";
|
|
|
16
16
|
import { getDecision, recordDecision, retireDecision } from "./project-memory.js";
|
|
17
17
|
import { resumeTaskStop, taskControlOf } from "./task-control.js";
|
|
18
18
|
import { addToolTo, catalogTool, projectToolsOf, removeToolFrom, toolCommandLine, validateToolSpec } from "./project-tools.js";
|
|
19
|
-
import { deciderOf, FLOW_KIND_WORDS, flowDigest, flowTerms, validateFlowDefinition } from "./flows.js";
|
|
19
|
+
import { deciderOf, FLOW_KIND_WORDS, flowDigest, flowTerms, replyTarget, validateFlowDefinition } from "./flows.js";
|
|
20
20
|
import { saveScript, scriptDigest, validateScript } from "./flow-scripts.js";
|
|
21
|
-
import { addCardToFlow, advanceFlows, cancelFlowCard, decideFlowCard, flowCardHref, flowCardText, flowDefinitionOf, moveCardInFlow } from "./flow-engine.js";
|
|
21
|
+
import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, flowCardHref, flowCardText, flowDefinitionOf, moveCardInFlow } from "./flow-engine.js";
|
|
22
|
+
import { chooseFlowCard, flowPersonOf } from "./flow-send.js";
|
|
22
23
|
import { assignFlowCard, commentOnFlowCard, flowPeople, mentionsIn, watchFlowCard } from "./flow-people.js";
|
|
23
24
|
import { addFlowTriggerTo, describeTrigger, readLinearKey, removeFlowTrigger, scheduleFromWords, takesDeliveries, triggerConfigOf, validateTriggerConfig } from "./flow-triggers.js";
|
|
24
25
|
import { describeSchedule, parseSchedule } from "./routine.js";
|
|
@@ -87,6 +88,7 @@ export const CHAT_ACTIONS = {
|
|
|
87
88
|
flow_card_move: { label: "Move card", protected: false, password: false },
|
|
88
89
|
flow_card_approve: { label: "Approve", protected: false, password: false },
|
|
89
90
|
flow_card_send_back: { label: "Send back", protected: false, password: false },
|
|
91
|
+
flow_card_choose: { label: "Choose", protected: false, password: false },
|
|
90
92
|
flow_card_cancel: { label: "Cancel card", protected: false, password: false },
|
|
91
93
|
flow_card_comment: { label: "Comment", protected: false, password: false },
|
|
92
94
|
flow_card_assign: { label: "Set owner", protected: false, password: false },
|
|
@@ -144,6 +146,7 @@ export const CHAT_ACTION_FIELDS = {
|
|
|
144
146
|
flow_card_move: ["card", "zone"],
|
|
145
147
|
flow_card_approve: ["card", "note"],
|
|
146
148
|
flow_card_send_back: ["card", "note"],
|
|
149
|
+
flow_card_choose: ["card", "choice", "note"],
|
|
147
150
|
flow_card_cancel: ["card"],
|
|
148
151
|
flow_card_comment: ["card", "note"],
|
|
149
152
|
flow_card_assign: ["card", "owner"],
|
|
@@ -580,6 +583,9 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
580
583
|
if (operation === "flow_create") {
|
|
581
584
|
const name = text(input, "name", 80).trim();
|
|
582
585
|
const definition = flowDrawingOf(store, input, repo);
|
|
586
|
+
const elsewhere = crossProjectProblem(store, definition, { repo, owner: who.name });
|
|
587
|
+
if (elsewhere !== null)
|
|
588
|
+
throw Error(elsewhere);
|
|
583
589
|
request["name"] = name;
|
|
584
590
|
request["definition"] = definition;
|
|
585
591
|
state = {};
|
|
@@ -613,6 +619,9 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
613
619
|
const name = input["name"] === undefined ? flow.name : text(input, "name", 80).trim();
|
|
614
620
|
const definition = input["definition"] === undefined ? before : flowDrawingOf(store, input, repo);
|
|
615
621
|
const redrawn = flowDigest(definition) !== flowDigest(before);
|
|
622
|
+
const elsewhere = crossProjectProblem(store, definition, flow, who.name);
|
|
623
|
+
if (elsewhere !== null)
|
|
624
|
+
throw Error(elsewhere);
|
|
616
625
|
if (!redrawn && name === flow.name)
|
|
617
626
|
throw Error("That's the flow as it is now.");
|
|
618
627
|
Object.assign(request, { name, definition });
|
|
@@ -728,6 +737,38 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
728
737
|
title = `Take ${quoted(card.title)} out of the flow`;
|
|
729
738
|
terms.push("The card leaves the flow. Any tasks it filed stay as they are.");
|
|
730
739
|
}
|
|
740
|
+
else if (operation === "flow_card_choose") {
|
|
741
|
+
// "Person chooses" (flow-send.ts): one of the zone's own options, or a reply that becomes the note.
|
|
742
|
+
if (at?.kind !== "choose")
|
|
743
|
+
throw Error("That card isn't waiting for a choice.");
|
|
744
|
+
const person = flowPersonOf(card, flowTarget.flow);
|
|
745
|
+
if (person !== who.name)
|
|
746
|
+
throw Error(`Only ${person} chooses here.`);
|
|
747
|
+
const titleOf = (id) => definition.stages.find(one => one.id === id)?.title ?? id;
|
|
748
|
+
if (input["choice"] !== undefined && input["choice"] !== null) {
|
|
749
|
+
const index = Number(input["choice"]) - 1;
|
|
750
|
+
const option = Number.isSafeInteger(input["choice"]) ? (at.options ?? [])[index] : undefined;
|
|
751
|
+
if (option === undefined)
|
|
752
|
+
throw Error(`Choose one of its ${(at.options ?? []).length} options, by number.`);
|
|
753
|
+
delete request["note"];
|
|
754
|
+
// Bound to the option as shown: if the flow's options change before it is confirmed, it is refused.
|
|
755
|
+
Object.assign(state, { label: option.label, to: option.to });
|
|
756
|
+
title = `Choose “${option.label}” for ${quoted(card.title)}`;
|
|
757
|
+
terms.push(option.to === "end" ? `${at.title}: “${option.label}”. The card is closed as Ignored.` : `${at.title}: “${option.label}”. It moves to ${titleOf(option.to)}.`);
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
const note = input["note"] === undefined ? "" : text(input, "note", 2000).trim();
|
|
761
|
+
const target = replyTarget(at);
|
|
762
|
+
if (target === null)
|
|
763
|
+
throw Error("This step takes one of its options, not a reply.");
|
|
764
|
+
if (note === "")
|
|
765
|
+
throw Error("Say what you'd change, or choose an option by number.");
|
|
766
|
+
request["note"] = note;
|
|
767
|
+
state["to"] = target;
|
|
768
|
+
title = `Reply on ${quoted(card.title)}`;
|
|
769
|
+
terms.push(`${at.title}: your reply goes to ${titleOf(target)} as its note:\n${note}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
731
772
|
else {
|
|
732
773
|
if (at?.kind !== "approval")
|
|
733
774
|
throw Error("That card isn't waiting for a decision.");
|
|
@@ -1342,10 +1383,20 @@ function runFlowAction(store, payload, actor, repos, root, now) {
|
|
|
1342
1383
|
}
|
|
1343
1384
|
const acted = payload.operation === "flow_card_move" ? moveCardInFlow(store, card, String(req["zone"]), actor, now)
|
|
1344
1385
|
: payload.operation === "flow_card_cancel" ? cancelFlowCard(store, card, actor, now)
|
|
1345
|
-
: (() => {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1386
|
+
: payload.operation === "flow_card_choose" ? (() => {
|
|
1387
|
+
// The option (its words and where it leads) or reply target exactly as proposed; a proposal from before carries neither and is refused.
|
|
1388
|
+
const bound = payload.state;
|
|
1389
|
+
if (typeof bound["to"] !== "string" || (Number.isSafeInteger(req["choice"]) && typeof bound["label"] !== "string"))
|
|
1390
|
+
return { ok: false, message: "Those options changed since; nothing was changed. Ask for a fresh proposal." };
|
|
1391
|
+
const chosen = chooseFlowCard(store, { card: card.id, entry: Number(bound["entry"]), choice: Number.isSafeInteger(req["choice"]) ? Number(req["choice"]) - 1 : null,
|
|
1392
|
+
...(typeof bound["label"] === "string" ? { label: bound["label"] } : {}), to: bound["to"],
|
|
1393
|
+
note: typeof req["note"] === "string" ? req["note"] : null, actor, where: "chat", repos, ...(root === undefined ? {} : { evidenceRoot: root }) }, now);
|
|
1394
|
+
return chosen.ok ? { ok: true, said: chosen.said, card: card.id } : { ok: false, message: chosen.message };
|
|
1395
|
+
})()
|
|
1396
|
+
: (() => {
|
|
1397
|
+
const decided = decideFlowCard(store, { card: card.id, decision: payload.operation === "flow_card_approve" ? "approve" : "send-back", note: typeof req["note"] === "string" ? req["note"] : null, actor, repos, ...(root === undefined ? {} : { evidenceRoot: root }) }, now);
|
|
1398
|
+
return decided.ok ? { ok: true, said: decided.said, card: card.id } : { ok: false, message: decided.message };
|
|
1399
|
+
})();
|
|
1349
1400
|
if (!acted.ok)
|
|
1350
1401
|
throw Error(acted.message);
|
|
1351
1402
|
return settle(card.flow, acted.said, card.id);
|
|
@@ -92,6 +92,21 @@ export type ChatContent = {
|
|
|
92
92
|
entry: number;
|
|
93
93
|
actions: Array<"approve" | "edit" | "send-back">;
|
|
94
94
|
};
|
|
95
|
+
/** A flow's "Person chooses" buttons ride this part: minted when it is planned. */
|
|
96
|
+
choose?: {
|
|
97
|
+
card: number;
|
|
98
|
+
entry: number;
|
|
99
|
+
options: Array<{
|
|
100
|
+
choice: number;
|
|
101
|
+
label: string;
|
|
102
|
+
}>;
|
|
103
|
+
};
|
|
104
|
+
/** "Use this as your note?" Yes / No about the person's message `held` (an event id), for one choice's visit. */
|
|
105
|
+
note?: {
|
|
106
|
+
card: number;
|
|
107
|
+
entry: number;
|
|
108
|
+
held: string;
|
|
109
|
+
};
|
|
95
110
|
/** A teammate's question's buttons ride this part (v93): each option, then one to answer in words (choice null). */
|
|
96
111
|
question?: {
|
|
97
112
|
id: number;
|
|
@@ -78,6 +78,25 @@ CREATE TABLE IF NOT EXISTS chat_flow_prompt (
|
|
|
78
78
|
);
|
|
79
79
|
CREATE INDEX IF NOT EXISTS chat_flow_prompt_open ON chat_flow_prompt(binding, consumed);
|
|
80
80
|
`;
|
|
81
|
+
/** A flow's "Person chooses" zone in the chat app (flow-send.ts): one button per option (choice is its index, label its words
|
|
82
|
+
* when sent) for one visit of one card, on the part it rides. A reply in that notice's thread is the note; chat_flow_note
|
|
83
|
+
* is "Use this as your note?" Yes / No about one other message (held: its event; words: what it said, kept only until
|
|
84
|
+
* answered, as an event's own payload is cleared once it is handled), asked once per visit.
|
|
85
|
+
* Never in CHAT_TABLES: an older database has no such table until this schema creates it. */
|
|
86
|
+
const CHAT_FLOW_CHOICE_SCHEMA = `
|
|
87
|
+
CREATE TABLE IF NOT EXISTS chat_flow_choice (
|
|
88
|
+
token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id),
|
|
89
|
+
card INTEGER NOT NULL REFERENCES flow_card(id), entry INTEGER NOT NULL,
|
|
90
|
+
choice INTEGER NOT NULL, label TEXT NOT NULL, expires TEXT NOT NULL, consumed TEXT
|
|
91
|
+
);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS chat_flow_choice_visit ON chat_flow_choice(card, entry);
|
|
93
|
+
CREATE TABLE IF NOT EXISTS chat_flow_note (
|
|
94
|
+
token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id),
|
|
95
|
+
card INTEGER NOT NULL REFERENCES flow_card(id), entry INTEGER NOT NULL,
|
|
96
|
+
held TEXT NOT NULL, words TEXT, answer TEXT NOT NULL CHECK(answer IN ('yes','no')), expires TEXT NOT NULL, consumed TEXT
|
|
97
|
+
);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS chat_flow_note_visit ON chat_flow_note(card, entry);
|
|
99
|
+
`;
|
|
81
100
|
/** v93: a teammate's question in the chat app. One button per option (choice)
|
|
82
101
|
* and one to answer in words (choice NULL); that one opens a prompt the
|
|
83
102
|
* person's next message in their DM answers. Never in CHAT_TABLES: an older
|
|
@@ -121,7 +140,7 @@ export class ChatState {
|
|
|
121
140
|
this.channel = channel;
|
|
122
141
|
}
|
|
123
142
|
prepare(sql) {
|
|
124
|
-
return this.db.prepare(sql.replace(/\bchat_(binding|pair|event|part|action|progress|runtime|room|meta|flow_action|flow_prompt|question_action|question_prompt|ask_action)\b/g, `${this.channel}_$1`));
|
|
143
|
+
return this.db.prepare(sql.replace(/\bchat_(binding|pair|event|part|action|progress|runtime|room|meta|flow_action|flow_prompt|flow_choice|flow_note|question_action|question_prompt|ask_action)\b/g, `${this.channel}_$1`));
|
|
125
144
|
}
|
|
126
145
|
/** Small transport facts a channel must remember per chat (a Teams service URL). Never credentials. */
|
|
127
146
|
meta(installation, key) {
|
|
@@ -286,6 +305,12 @@ export class ChatState {
|
|
|
286
305
|
if (Number(inserted.changes) && part.ask)
|
|
287
306
|
for (const choice of [...part.ask.options.map((_, index) => index), null])
|
|
288
307
|
this.prepare("INSERT INTO chat_ask_action(token,part,turn,choice,expires) VALUES(?,?,?,?,?)").run(randomBytes(16).toString("hex"), Number(inserted.lastInsertRowid), part.ask.turn, choice, new Date(now.getTime() + MATE_ASK_TTL_MS).toISOString());
|
|
308
|
+
if (Number(inserted.changes) && part.choose)
|
|
309
|
+
for (const one of part.choose.options)
|
|
310
|
+
this.prepare("INSERT INTO chat_flow_choice(token,part,card,entry,choice,label,expires) VALUES(?,?,?,?,?,?,?)").run(randomBytes(16).toString("hex"), Number(inserted.lastInsertRowid), part.choose.card, part.choose.entry, one.choice, one.label, new Date(now.getTime() + 7 * 86_400_000).toISOString());
|
|
311
|
+
if (Number(inserted.changes) && part.note)
|
|
312
|
+
for (const answer of ["yes", "no"])
|
|
313
|
+
this.prepare("INSERT INTO chat_flow_note(token,part,card,entry,held,answer,expires) VALUES(?,?,?,?,?,?,?)").run(randomBytes(16).toString("hex"), Number(inserted.lastInsertRowid), part.note.card, part.note.entry, part.note.held, answer, new Date(now.getTime() + 86_400_000).toISOString());
|
|
289
314
|
if (Number(inserted.changes) && part.flow)
|
|
290
315
|
for (const action of part.flow.actions)
|
|
291
316
|
this.prepare("INSERT INTO chat_flow_action(token,part,card,entry,action,expires) VALUES(?,?,?,?,?,?)").run(randomBytes(16).toString("hex"), Number(inserted.lastInsertRowid), part.flow.card, part.flow.entry, action, new Date(now.getTime() + 7 * 86_400_000).toISOString());
|
|
@@ -307,7 +332,7 @@ export class ChatState {
|
|
|
307
332
|
return !!this.prepare("SELECT 1 FROM chat_runtime WHERE installation=? AND owner=? AND lease_until>?").get(installation, owner, now.toISOString());
|
|
308
333
|
}
|
|
309
334
|
}
|
|
310
|
-
export const chatSchema = (channel) => (CHAT_SCHEMA + CHAT_ROOM_SCHEMA + CHAT_FLOW_SCHEMA + CHAT_QUESTION_SCHEMA + CHAT_ASK_SCHEMA).replaceAll("chat_", `${channel}_`);
|
|
335
|
+
export const chatSchema = (channel) => (CHAT_SCHEMA + CHAT_ROOM_SCHEMA + CHAT_FLOW_SCHEMA + CHAT_FLOW_CHOICE_SCHEMA + CHAT_QUESTION_SCHEMA + CHAT_ASK_SCHEMA).replaceAll("chat_", `${channel}_`);
|
|
311
336
|
export const chatTables = (channel) => CHAT_TABLES.map((name) => name.replace("chat_", `${channel}_`));
|
|
312
337
|
export class ChatDeliveryError extends Error {
|
|
313
338
|
code;
|
package/dist/chat-delivery.js
CHANGED
|
@@ -8,7 +8,7 @@ import { warmTurn } from "./chat-warmth.js";
|
|
|
8
8
|
import { confirmMateProposal, dismissMateProposal, } from "./mate-doors.js";
|
|
9
9
|
import { ceilingDigestOf, verifyApproverStanding } from "./principal.js";
|
|
10
10
|
import { ChatState, ChatDeliveryError, chatHash, } from "./chat-delivery-state.js";
|
|
11
|
-
import { answerChatFlowPrompt, applyChatFlowTap, flowDecisionParts } from "./chat-flow.js";
|
|
11
|
+
import { answerChatFlowPrompt, answerChoiceMessage, applyChatFlowTap, flowDecisionParts, flowSendParts } from "./chat-flow.js";
|
|
12
12
|
import { connectChannel, FLOW_WORDS, takeChannelMessage, watchedChannel } from "./chat-inbox.js";
|
|
13
13
|
import { triggerConfigOf } from "./flow-triggers.js";
|
|
14
14
|
import { telegramProgressCard } from "./telegram-progress.js";
|
|
@@ -19,8 +19,8 @@ import { leadChannelOf } from "./lead-context.js";
|
|
|
19
19
|
import { promiseChannelOf } from "./lead-commitments.js";
|
|
20
20
|
import { phoneText, PHONE_HELP, phoneCommand, phoneStatus, phoneTaskView, phoneTaskChoices, phoneTaskListText, resolvePhoneTask, phoneFocusText, PHONE_NO_MATCH, PHONE_BACK_TO_LEAD } from "./telegram-status.js";
|
|
21
21
|
import { applyRoomInbound, conversationRow, roomCardApprover, roomCommand, roomGrantAllowed, roomMessagesAfter, roomMessageText, teamDomain } from "./chat-rooms.js";
|
|
22
|
-
import { isTelegramProgressNotification, proposalTaskOf
|
|
23
|
-
import { resultShotsFor } from "./result-shots.js";
|
|
22
|
+
import { isTelegramProgressNotification, proposalTaskOf } from "./store.js";
|
|
23
|
+
import { isShotsKind, resultShotsFor } from "./result-shots.js";
|
|
24
24
|
import { messageTeammate } from "./teammate-desk.js";
|
|
25
25
|
import { CHAT_APP_NAMES } from "./flow-triggers.js";
|
|
26
26
|
export const chatObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
|
|
@@ -124,6 +124,9 @@ export async function processChatEvent(options) {
|
|
|
124
124
|
// A flow decision asked for this person's next message (Edit, Send back): it is the draft or the note.
|
|
125
125
|
if (answerChatFlowPrompt({ store, state, label: options.label }, event, binding, { text, ...(typeof input.originalLength === "number" ? { originalLength: input.originalLength } : {}) }, repos, nowOf(options)))
|
|
126
126
|
return true;
|
|
127
|
+
// A "Person chooses" notice's thread reply is the note; the first other message after it is asked about (chat-flow.ts).
|
|
128
|
+
if (answerChoiceMessage({ store, state, label: options.label }, event, binding, { text, ...(typeof input.originalLength === "number" ? { originalLength: input.originalLength } : {}), lead: input.lead === true }, nowOf(options), repos))
|
|
129
|
+
return true;
|
|
127
130
|
// A message to a teammate by name (v96), in someone's own chat with Toolroll: a card on its desk.
|
|
128
131
|
if (event.channel === binding.channel) {
|
|
129
132
|
const handed = messageTeammate(store, { who: binding.approver, repos, via: CHAT_APP_NAMES[state.channel] ?? "Chat" }, text, nowOf(options));
|
|
@@ -680,6 +683,17 @@ function planResultShots(options, binding, notification, repos, id, now) {
|
|
|
680
683
|
// The result's message here: its finished-work batch (quiet), its progress card (every step), or the result's own
|
|
681
684
|
// notice. The transport threads the screenshots under it once it is posted, and holds them until then.
|
|
682
685
|
const follows = (() => {
|
|
686
|
+
// A flow's screenshots follow the "Send to me" or "Person chooses" notice they go with.
|
|
687
|
+
const visit = /^flow-shots:([1-9][0-9]*):([1-9][0-9]*)$/.exec(notification.dedupeKey);
|
|
688
|
+
if (visit !== null) {
|
|
689
|
+
for (const key of [`flow-send:${visit[1]}:${visit[2]}`, `flow-choose:${visit[1]}:${visit[2]}`]) {
|
|
690
|
+
const notice = store.handle.prepare("SELECT id FROM notification WHERE dedupe_key=?").get(key);
|
|
691
|
+
const part = notice === undefined ? undefined : state.prepare("SELECT id FROM chat_part WHERE event=? ORDER BY id DESC LIMIT 1").get(chatHash(`${state.channel}:notice:${binding.id}:${Number(notice["id"])}`));
|
|
692
|
+
if (part !== undefined)
|
|
693
|
+
return Number(part.id);
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
683
697
|
const batch = notification.taskRef === null ? null : store.chatBatchMessageFor(`${state.channel}:${binding.id}`, notification.taskRef, notification.run);
|
|
684
698
|
if (batch !== null)
|
|
685
699
|
return Number(batch);
|
|
@@ -745,7 +759,7 @@ export async function planChatNotifications(options) {
|
|
|
745
759
|
// Only when I'm needed (the default): the task's one card is edited in place, and a new message
|
|
746
760
|
// follows only when this person is needed. Every step keeps the branches below unchanged.
|
|
747
761
|
const quiet = !personal && isTaskFact(notification) && store.notificationPreference(binding.approver).mode === "quiet";
|
|
748
|
-
if (notification.kind
|
|
762
|
+
if (isShotsKind(notification.kind)) {
|
|
749
763
|
planResultShots(options, binding, notification, repos, id, now);
|
|
750
764
|
return;
|
|
751
765
|
}
|
|
@@ -833,6 +847,12 @@ export async function planChatNotifications(options) {
|
|
|
833
847
|
state.plan(id, parts, now);
|
|
834
848
|
}
|
|
835
849
|
}
|
|
850
|
+
else if (personal && flowSendParts(store, notification) !== null) {
|
|
851
|
+
// A flow's "Send to me" (flow-send.ts): what was done, with its links as buttons.
|
|
852
|
+
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
853
|
+
ts: "", thread: "", payload: "{}", created: now.toISOString() });
|
|
854
|
+
state.plan(id, flowSendParts(store, notification), now);
|
|
855
|
+
}
|
|
836
856
|
else if (notification.kind === "flow-card" && questionParts(store, notification, binding) !== null) {
|
|
837
857
|
// A teammate's question (v93): its options and "Answer in words" on the notice, for the person it asks.
|
|
838
858
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
package/dist/chat-flow.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ChatBinding, type ChatContent, type ChatEvent, type ChatState } from "./chat-delivery-state.js";
|
|
2
2
|
import type { Store } from "./store.js";
|
|
3
3
|
export type ChatFlowAction = "approve" | "edit" | "send-back";
|
|
4
4
|
export declare const CHAT_FLOW_LABELS: Record<ChatFlowAction, string>;
|
|
5
5
|
/** Words kept as written, line breaks and all; only control characters go. */
|
|
6
6
|
export declare function chatFlowText(text: string): string;
|
|
7
|
+
/** A "Send to me" notice as chat parts: what was done, then its links; null when it isn't one. */
|
|
8
|
+
export declare function flowSendParts(store: Store, notification: {
|
|
9
|
+
dedupeKey: string;
|
|
10
|
+
subject: string;
|
|
11
|
+
body: string;
|
|
12
|
+
}): ChatContent[] | null;
|
|
7
13
|
/**
|
|
8
14
|
* A "flow-decision" notice as chat parts, or null when it isn't one (or the
|
|
9
15
|
* card has already moved on, and there is nothing left to decide).
|
|
@@ -14,11 +20,13 @@ export declare function flowDecisionParts(store: Store, notification: {
|
|
|
14
20
|
body: string;
|
|
15
21
|
link: string | null;
|
|
16
22
|
}): ChatContent[] | null;
|
|
17
|
-
/** The live buttons on one part, in the order they were minted
|
|
23
|
+
/** The live buttons on one part, in the order they were minted: a decision's, a choice's options in their order, or
|
|
24
|
+
* "Use this as your note?" Yes / No. `key` is unique on the part (Slack names each button by it). */
|
|
18
25
|
export declare function chatFlowButtons(state: ChatState, part: number, now: Date): Array<{
|
|
19
26
|
token: string;
|
|
20
|
-
action: ChatFlowAction;
|
|
27
|
+
action: ChatFlowAction | "choose" | "note";
|
|
21
28
|
label: string;
|
|
29
|
+
key: string;
|
|
22
30
|
}>;
|
|
23
31
|
/**
|
|
24
32
|
* A tapped flow button, inside the action's transaction. false when the
|
|
@@ -30,6 +38,21 @@ export declare function applyChatFlowTap(options: {
|
|
|
30
38
|
state: ChatState;
|
|
31
39
|
label: string;
|
|
32
40
|
}, event: ChatEvent, binding: ChatBinding, token: string, repos: readonly string[], now: Date): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* A message in the person's own chat while one of their choices waits. Only a reply in the Choose notice's thread (Slack,
|
|
43
|
+
* Discord) is the note. Any other message there is the lead's, except the first one after a choice that takes replies:
|
|
44
|
+
* that one is asked about, once — "Use this as your note?" — and only Yes makes it the note (No passes it on to the lead).
|
|
45
|
+
* false when the message isn't about a choice at all.
|
|
46
|
+
*/
|
|
47
|
+
export declare function answerChoiceMessage(options: {
|
|
48
|
+
store: Store;
|
|
49
|
+
state: ChatState;
|
|
50
|
+
label: string;
|
|
51
|
+
}, event: ChatEvent, binding: ChatBinding, input: {
|
|
52
|
+
text: string;
|
|
53
|
+
originalLength?: number;
|
|
54
|
+
lead?: boolean;
|
|
55
|
+
}, now: Date, repos: readonly string[]): boolean;
|
|
33
56
|
/**
|
|
34
57
|
* A message from the person while a prompt is open: the new draft or the
|
|
35
58
|
* note. false when no prompt is open (the message goes on as usual).
|