omni-notify-mcp 1.3.287 → 1.3.289
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/dist/gateway/chatPage.js +10 -430
- package/dist/gateway/conversations.js +52 -19
- package/dist/gateway/mcpDoor.js +12 -12
- package/dist/gateway/restDoor.js +5 -6
- package/dist/gateway/sessionDriver.js +7 -15
- package/dist/gateway/terminalDoor.js +6 -6
- package/dist/gateway/terminalPage.js +78 -56
- package/dist/googlehome/foyer-client.js +210 -0
- package/dist/googlehome/foyer-codec.js +318 -0
- package/dist/googlehome/gpsoauth.js +119 -0
- package/dist/googlehome/index.js +267 -0
- package/package.json +2 -1
package/dist/gateway/chatPage.js
CHANGED
|
@@ -42,199 +42,6 @@ export function servingLine() {
|
|
|
42
42
|
const line = `BN ${me.build} · pid ${me.pid} · started ${storedStamp(me.startedAt)}`;
|
|
43
43
|
return `<div id="serving" class="idbar" data-be-tip="The process that answered this page — the build its own code was loaded from, its process id, and when that process started — and when this page's sign-in expires"><span class="srv">${line}</span><small id="expires" hidden></small></div>`;
|
|
44
44
|
}
|
|
45
|
-
/** A reply body drawn as Markdown, node by node: every character of the source reaches the page through
|
|
46
|
-
* `textContent`, so raw HTML in a reply stays literal text and nothing it names can run, load or navigate. A link is
|
|
47
|
-
* an anchor only for http, https and mailto; any other scheme stays the literal source. A table is a real table
|
|
48
|
-
* inside its own sideways scroller, so a wide one never widens the bubble that holds it. Plain text is unchanged:
|
|
49
|
-
* the body keeps `white-space: pre-wrap`, so a reply with no Markdown reads exactly as it did. */
|
|
50
|
-
export function markdownBody(doc, source) {
|
|
51
|
-
const body = doc.createElement("div");
|
|
52
|
-
body.className = "body";
|
|
53
|
-
const text = (parent, value) => { parent.append(doc.createTextNode(value)); };
|
|
54
|
-
const inline = (parent, source) => {
|
|
55
|
-
let at = 0;
|
|
56
|
-
while (at < source.length) {
|
|
57
|
-
const rest = source.slice(at);
|
|
58
|
-
let m = /^`([^`\n]+)`/.exec(rest);
|
|
59
|
-
if (m !== null) {
|
|
60
|
-
const code = doc.createElement("code");
|
|
61
|
-
text(code, m[1]);
|
|
62
|
-
parent.append(code);
|
|
63
|
-
at += m[0].length;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
m = /^!\[([^\]\n]*)\]\([^)\s]*\)/.exec(rest);
|
|
67
|
-
if (m !== null) {
|
|
68
|
-
text(parent, m[1]);
|
|
69
|
-
at += m[0].length;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
m = /^\[([^\]\n]+)\]\(([^)\s]+)\)/.exec(rest);
|
|
73
|
-
if (m !== null) {
|
|
74
|
-
const target = m[2];
|
|
75
|
-
if (/^(https?:|mailto:)/i.test(target)) {
|
|
76
|
-
const link = doc.createElement("a");
|
|
77
|
-
link.setAttribute("href", target);
|
|
78
|
-
link.setAttribute("target", "_blank");
|
|
79
|
-
link.setAttribute("rel", "noopener noreferrer");
|
|
80
|
-
inline(link, m[1]);
|
|
81
|
-
parent.append(link);
|
|
82
|
-
}
|
|
83
|
-
else
|
|
84
|
-
text(parent, m[0]);
|
|
85
|
-
at += m[0].length;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest);
|
|
89
|
-
if (m !== null) {
|
|
90
|
-
const strong = doc.createElement("strong");
|
|
91
|
-
inline(strong, m[2]);
|
|
92
|
-
parent.append(strong);
|
|
93
|
-
at += m[0].length;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
m = /^(\*|_)(?=\S)([\s\S]*?\S)\1/.exec(rest);
|
|
97
|
-
if (m !== null) {
|
|
98
|
-
const em = doc.createElement("em");
|
|
99
|
-
inline(em, m[2]);
|
|
100
|
-
parent.append(em);
|
|
101
|
-
at += m[0].length;
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
const stop = rest.slice(1).search(/[`*_!\[]/);
|
|
105
|
-
const take = stop === -1 ? rest.length : stop + 1;
|
|
106
|
-
text(parent, rest.slice(0, take));
|
|
107
|
-
at += take;
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
const cells = (line) => {
|
|
111
|
-
let row = line.trim();
|
|
112
|
-
if (row.startsWith("|"))
|
|
113
|
-
row = row.slice(1);
|
|
114
|
-
if (row.endsWith("|"))
|
|
115
|
-
row = row.slice(0, -1);
|
|
116
|
-
return row.split("|").map((cell) => cell.trim());
|
|
117
|
-
};
|
|
118
|
-
const isRow = (line) => line.trim() !== "" && line.indexOf("|") !== -1;
|
|
119
|
-
const isRule = (line) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(line);
|
|
120
|
-
const isBlock = (line) => /^\s*(#{1,6}\s|```|>|\||([-*+]|\d+[.)])\s)/.test(line);
|
|
121
|
-
const lines = String(source ?? "").split(/\r?\n/);
|
|
122
|
-
let i = 0;
|
|
123
|
-
while (i < lines.length) {
|
|
124
|
-
const line = lines[i];
|
|
125
|
-
if (line.trim() === "") {
|
|
126
|
-
i++;
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (/^\s*```/.test(line)) {
|
|
130
|
-
const fenced = [];
|
|
131
|
-
i++;
|
|
132
|
-
while (i < lines.length && !/^\s*```/.test(lines[i])) {
|
|
133
|
-
fenced.push(lines[i]);
|
|
134
|
-
i++;
|
|
135
|
-
}
|
|
136
|
-
i++;
|
|
137
|
-
const pre = doc.createElement("pre");
|
|
138
|
-
const code = doc.createElement("code");
|
|
139
|
-
text(code, fenced.join("\n"));
|
|
140
|
-
pre.append(code);
|
|
141
|
-
body.append(pre);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (isRow(line) && i + 1 < lines.length && isRule(lines[i + 1])) {
|
|
145
|
-
const head = doc.createElement("thead");
|
|
146
|
-
const headRow = doc.createElement("tr");
|
|
147
|
-
cells(line).forEach((cell) => { const th = doc.createElement("th"); inline(th, cell); headRow.append(th); });
|
|
148
|
-
head.append(headRow);
|
|
149
|
-
const rows = doc.createElement("tbody");
|
|
150
|
-
i += 2;
|
|
151
|
-
while (i < lines.length && isRow(lines[i])) {
|
|
152
|
-
const tr = doc.createElement("tr");
|
|
153
|
-
cells(lines[i]).forEach((cell) => { const td = doc.createElement("td"); inline(td, cell); tr.append(td); });
|
|
154
|
-
rows.append(tr);
|
|
155
|
-
i++;
|
|
156
|
-
}
|
|
157
|
-
const table = doc.createElement("table");
|
|
158
|
-
table.append(head, rows);
|
|
159
|
-
const box = doc.createElement("div");
|
|
160
|
-
box.className = "tbl";
|
|
161
|
-
box.append(table);
|
|
162
|
-
body.append(box);
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
const heading = /^\s*(#{1,6})\s+(.*)$/.exec(line);
|
|
166
|
-
if (heading !== null) {
|
|
167
|
-
const h = doc.createElement("h" + Math.min(heading[1].length + 2, 6));
|
|
168
|
-
inline(h, heading[2].replace(/\s+#+\s*$/, ""));
|
|
169
|
-
body.append(h);
|
|
170
|
-
i++;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (/^\s*([-*_])\s*(\1\s*){2,}$/.test(line)) {
|
|
174
|
-
body.append(doc.createElement("hr"));
|
|
175
|
-
i++;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (/^\s*>/.test(line)) {
|
|
179
|
-
const quoted = [];
|
|
180
|
-
while (i < lines.length && /^\s*>/.test(lines[i])) {
|
|
181
|
-
quoted.push(lines[i].replace(/^\s*>\s?/, ""));
|
|
182
|
-
i++;
|
|
183
|
-
}
|
|
184
|
-
const quote = doc.createElement("blockquote");
|
|
185
|
-
markdownBodyInto(doc, quote, quoted.join("\n"));
|
|
186
|
-
body.append(quote);
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
if (/^\s*([-*+]|\d+[.)])\s+/.test(line)) {
|
|
190
|
-
const items = [];
|
|
191
|
-
while (i < lines.length && (/^\s*([-*+]|\d+[.)])\s+/.test(lines[i]) || (items.length > 0 && /^\s{2,}\S/.test(lines[i]) && lines[i].trim() !== ""))) {
|
|
192
|
-
const item = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(lines[i]);
|
|
193
|
-
if (item === null) {
|
|
194
|
-
items[items.length - 1].source += " " + lines[i].trim();
|
|
195
|
-
i++;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
items.push({ depth: Math.floor(item[1].length / 2), ordered: /\d/.test(item[2]), source: item[3] });
|
|
199
|
-
i++;
|
|
200
|
-
}
|
|
201
|
-
const lists = [];
|
|
202
|
-
const containerFor = (depth, ordered) => {
|
|
203
|
-
while (lists.length > 0 && lists[lists.length - 1].depth > depth)
|
|
204
|
-
lists.pop();
|
|
205
|
-
if (lists.length === 0 || lists[lists.length - 1].depth < depth) {
|
|
206
|
-
const made = doc.createElement(ordered ? "ol" : "ul");
|
|
207
|
-
if (lists.length === 0)
|
|
208
|
-
body.append(made);
|
|
209
|
-
else
|
|
210
|
-
lists[lists.length - 1].node.lastElementChild.appendChild(made);
|
|
211
|
-
lists.push({ depth, node: made });
|
|
212
|
-
}
|
|
213
|
-
return lists[lists.length - 1].node;
|
|
214
|
-
};
|
|
215
|
-
items.forEach((item) => {
|
|
216
|
-
const list = containerFor(item.depth, item.ordered);
|
|
217
|
-
const li = doc.createElement("li");
|
|
218
|
-
inline(li, item.source);
|
|
219
|
-
list.append(li);
|
|
220
|
-
});
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
const paragraph = [];
|
|
224
|
-
while (i < lines.length && lines[i].trim() !== "" && (paragraph.length === 0 || !isBlock(lines[i]))) {
|
|
225
|
-
paragraph.push(lines[i]);
|
|
226
|
-
i++;
|
|
227
|
-
}
|
|
228
|
-
const para = doc.createElement("p");
|
|
229
|
-
inline(para, paragraph.join("\n"));
|
|
230
|
-
body.append(para);
|
|
231
|
-
}
|
|
232
|
-
return body;
|
|
233
|
-
}
|
|
234
|
-
/** The same drawing for a nested block (a blockquote's own paragraphs and lists), appending into `into`. */
|
|
235
|
-
export function markdownBodyInto(doc, into, source) {
|
|
236
|
-
markdownBody(doc, source).childNodes.forEach((child) => into.append(child));
|
|
237
|
-
}
|
|
238
45
|
/** The look every page of the door shares (#2873): BN's page and each service page the public gateway serves. */
|
|
239
46
|
export const PAGE_STYLE = `:root{--bg:#fff;--fg:#111;--mut:#667;--line:#d8dce2;--me:#0b5fff;--agent:#eef1f5;--bad:#a40000}
|
|
240
47
|
@media(prefers-color-scheme:dark){:root{--bg:#14171c;--fg:#e8eaed;--mut:#9aa3b0;--line:#2b313b;--agent:#222831;--me:#2f6df0;--bad:#ff8a80}}
|
|
@@ -382,13 +189,13 @@ async function resumeSignIn() {
|
|
|
382
189
|
}
|
|
383
190
|
function startSignIn() { if (token) resumeSignIn().catch(function (err) { signIn("Could not check sign-in: " + err.message); }); else signIn(""); }`;
|
|
384
191
|
/**
|
|
385
|
-
* The conversations half of BN's one page: the list he manages
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* `
|
|
192
|
+
* The conversations half of BN's one page: the list he manages — the table, its per-row actions and the project picker.
|
|
193
|
+
* It is carried from the deleted Conversations page (#3401) into the native CLI page because that capability lives
|
|
194
|
+
* nowhere else. The transcript view is NOT carried and is gone (#3640): every conversation now opens on the provider's
|
|
195
|
+
* own full interactive CLI, so what was the record's reading view is the terminal itself. Its ONE creation control (the
|
|
196
|
+
* New form and `POST /v1/conversations`) is NOT carried either: the native CLI page's New Agent is the one way to
|
|
197
|
+
* create an agent. The functions here rely on the page's shared `el`, `api`, `base`, `main`, `show` and `storedStamp`,
|
|
198
|
+
* provided by the page's own script, and on the header buttons it builds (`btnBack`, `btnShelf`).
|
|
392
199
|
*/
|
|
393
200
|
export const CONVERSATIONS_VIEWS = `
|
|
394
201
|
/** The one way this page puts text on the clipboard, calling back with what happened. */
|
|
@@ -685,7 +492,7 @@ async function listView(shelf) {
|
|
|
685
492
|
var actrow = el("div", "actrow");
|
|
686
493
|
var acts = el("div", "acts");
|
|
687
494
|
var open = el("button", "", "📂 Open");
|
|
688
|
-
open.onclick = function (e) { e.stopPropagation(); go("
|
|
495
|
+
open.onclick = function (e) { e.stopPropagation(); go("term", node.c.id); };
|
|
689
496
|
acts.append(open);
|
|
690
497
|
// A turn conversation is STOPPED — its agent runs no further turn and a later message resumes it. A console
|
|
691
498
|
// session's own CLI is CLOSED — that process ends and the record stays as history. Never the same label.
|
|
@@ -713,8 +520,8 @@ async function listView(shelf) {
|
|
|
713
520
|
tr.append(cell);
|
|
714
521
|
node.cells.push({ td: cell, col: col });
|
|
715
522
|
});
|
|
716
|
-
tr.onclick = function (e) { if (e.shiftKey) { e.preventDefault(); extendRange(node); return; } go("
|
|
717
|
-
tr.onkeydown = function (e) { if (e.target === tr && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); go("
|
|
523
|
+
tr.onclick = function (e) { if (e.shiftKey) { e.preventDefault(); extendRange(node); return; } go("term", node.c.id); };
|
|
524
|
+
tr.onkeydown = function (e) { if (e.target === tr && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); go("term", node.c.id); } };
|
|
718
525
|
return node;
|
|
719
526
|
};
|
|
720
527
|
var updateNode = function (node, c) {
|
|
@@ -919,233 +726,6 @@ async function listView(shelf) {
|
|
|
919
726
|
window.addEventListener("resize", listResize);
|
|
920
727
|
run();
|
|
921
728
|
}
|
|
922
|
-
|
|
923
|
-
function chatView(meta, why) {
|
|
924
|
-
var mine = show("chat", meta.project ? base(meta.project) : "No project");
|
|
925
|
-
var shelved = meta.shelf === "archived";
|
|
926
|
-
var badge = el("span", "badge", "");
|
|
927
|
-
var banner = el("div", "rowmeta", "");
|
|
928
|
-
var facts = el("div", "convmeta");
|
|
929
|
-
facts.append(idLine(meta.id), timeLine(meta), el("div", "rowmeta", agentLine(meta)), el("span", "origin", originOf(meta)), badge, banner);
|
|
930
|
-
main.append(facts);
|
|
931
|
-
var after = 0;
|
|
932
|
-
text.value = ""; text.style.height = "";
|
|
933
|
-
var closed = meta.state === "closed";
|
|
934
|
-
var following = false;
|
|
935
|
-
// ONE console: the stored record and the turn still arriving are the same surface, so the only thing that marks a
|
|
936
|
-
// streamed line is that it is provisional — it is taken back the moment the turn's own line lands in the record.
|
|
937
|
-
var clearLive = function () { Array.prototype.forEach.call(main.querySelectorAll(".line.tail"), function (row) { row.remove(); }); };
|
|
938
|
-
var liveTail = function (kind, who, text) {
|
|
939
|
-
var row = el("div", "line tail " + kind);
|
|
940
|
-
row.append(el("span", "stamp", ""), el("span", "who", who), el("span", "say", ""));
|
|
941
|
-
row.lastElementChild.textContent = text;
|
|
942
|
-
main.append(row);
|
|
943
|
-
main.scrollTop = main.scrollHeight;
|
|
944
|
-
return row;
|
|
945
|
-
};
|
|
946
|
-
// The agent's own words and its own reasoning, the two told apart, exactly as the stream names them (#3611), each
|
|
947
|
-
// written into the one console as it arrives (#3613) — deltas of the same kind run on into the line already open.
|
|
948
|
-
var liveText = function (kind, text) {
|
|
949
|
-
if (shelved || closed) return;
|
|
950
|
-
|
|
951
|
-
var last = main.lastElementChild;
|
|
952
|
-
var open = last !== null && last.className === "line tail " + kind ? last.lastElementChild : null;
|
|
953
|
-
if (open === null) open = liveTail(kind, kind === "thinking" ? "…" : "agent", "").lastElementChild;
|
|
954
|
-
open.textContent += text;
|
|
955
|
-
if (kind === "thinking") open.parentNode.className = "line tail thinking";
|
|
956
|
-
main.scrollTop = main.scrollHeight;
|
|
957
|
-
};
|
|
958
|
-
var liveDoing = function (text) {
|
|
959
|
-
if (shelved || closed) return;
|
|
960
|
-
|
|
961
|
-
var last = main.lastElementChild;
|
|
962
|
-
if (last !== null && last.className === "line tail doing" && last.lastElementChild.textContent === text) return;
|
|
963
|
-
liveTail("doing", "⚙", text);
|
|
964
|
-
};
|
|
965
|
-
var badged = function (state) {
|
|
966
|
-
var running = !shelved && state !== "closed";
|
|
967
|
-
badge.className = "badge " + (running ? "live" : "history");
|
|
968
|
-
badge.textContent = running ? "🟢 LIVE" : "⏸ HISTORY";
|
|
969
|
-
badge.title = running ? "A CLI on this machine answers messages sent here" : "Nothing is running for this conversation — this is its stored record";
|
|
970
|
-
banner.textContent = running
|
|
971
|
-
? "A CLI on this machine is behind this conversation. What it is thinking, what it is writing and the tool it is on are written into this console as they arrive; each turn then stays here as its own stored line. This is the conversation's console, not the CLI's own terminal screen."
|
|
972
|
-
: "Read-only replay of this conversation's stored record: what was sent, what the agent answered and what BN reported. No CLI is running for it and its terminal screen was not kept, so nothing here is live — a message sent from the box below resumes it.";
|
|
973
|
-
// Why this conversation is on its record rather than on a CLI of its own: the refusal is said here, ON this
|
|
974
|
-
// conversation, in the CLI's own absence — never a pane that looks the same whether the CLI is there or not (#3620).
|
|
975
|
-
if (why !== undefined && why !== "") banner.textContent = why + " " + banner.textContent;
|
|
976
|
-
if (!running) clearLive();
|
|
977
|
-
};
|
|
978
|
-
var sync = function (state) { closed = state === "closed"; btnStop.disabled = closed; text.placeholder = !closed ? "Message" : shelved ? "Archived - send a message to resume it" : "Stopped - send a message to resume"; badged(state); if (!following && !shelved && !closed) { following = true; follow(); } };
|
|
979
|
-
var stick = function () { main.scrollTop = main.scrollHeight; };
|
|
980
|
-
var notice = el("div", "err");
|
|
981
|
-
var asked = {};
|
|
982
|
-
var ENDS = { answer: 1, failed: 1, ask: 1 };
|
|
983
|
-
// The turn is over — its own line is in the record, so the lines the stream had only provisionally written are given back.
|
|
984
|
-
var ENDED = { answer: 1, failed: 1, stopped: 1 };
|
|
985
|
-
var took = function (ms) {
|
|
986
|
-
var s = Math.max(0, Math.round(ms / 1000)), m = Math.floor(s / 60), h = Math.floor(m / 60);
|
|
987
|
-
return h > 0 ? h + "h " + (m % 60) + "m " + (s % 60) + "s" : m > 0 ? m + "m " + (s % 60) + "s" : s + "s";
|
|
988
|
-
};
|
|
989
|
-
var queuedText = function (t, show, why) {
|
|
990
|
-
return t.queuedMs !== undefined ? show(t.queuedMs) + " queued" + (why || "") : t.queued === false ? "not queued" : "queue not measured";
|
|
991
|
-
};
|
|
992
|
-
var activeText = function (t, show, why) {
|
|
993
|
-
return t.agentMs !== undefined ? show(t.agentMs) + " active" + (why || "") : "agent time not measured";
|
|
994
|
-
};
|
|
995
|
-
var add = function (event, got) {
|
|
996
|
-
var role = event.role === "user" ? "user" : event.role === "agent" ? "agent" : "system";
|
|
997
|
-
var who = role === "user" ? "›" : role === "agent" ? "⏺" : "·";
|
|
998
|
-
var when = storedStamp(event.ts);
|
|
999
|
-
if (role === "user") asked[event.n] = event.ts;
|
|
1000
|
-
else if (ENDS[event.kind] && asked[event.turn] !== undefined) {
|
|
1001
|
-
var t = event.timing, wall = took(Date.parse(event.ts) - Date.parse(asked[event.turn]));
|
|
1002
|
-
var endToEnd = t === undefined ? wall + " end to end (no layer timing stored for this one: queue and agent time are not measured)"
|
|
1003
|
-
: t.bnMs !== undefined ? took(t.bnMs) + " end to end"
|
|
1004
|
-
: wall + " end to end (the door's arrival was not measured)";
|
|
1005
|
-
when += " - " + endToEnd + (t === undefined ? "" : " · " + queuedText(t, took) + " · " + activeText(t, took));
|
|
1006
|
-
}
|
|
1007
|
-
var row = el("div", "line " + role);
|
|
1008
|
-
row.append(el("span", "stamp", when), el("span", "who", who), el("span", "say", event.text));
|
|
1009
|
-
main.append(row);
|
|
1010
|
-
if (event.timing || (role === "agent" && ENDS[event.kind])) timed(row, event, got);
|
|
1011
|
-
after = Math.max(after, event.n);
|
|
1012
|
-
};
|
|
1013
|
-
var ms = function (n) { return Math.round(n).toLocaleString() + " ms"; };
|
|
1014
|
-
var timed = function (box, event, got) {
|
|
1015
|
-
var t = event.timing || {}, s = sent[meta.id + ":" + event.turn];
|
|
1016
|
-
var tm = el("details", "tm"), head = el("summary"), wrap = el("div", "box"), table = el("table");
|
|
1017
|
-
var line = function (layer, value, cls) {
|
|
1018
|
-
var tr = el("tr", cls || "");
|
|
1019
|
-
tr.append(el("td", "", layer), el("td", "ms", value === undefined ? "unmeasured" : ms(value)));
|
|
1020
|
-
table.append(tr);
|
|
1021
|
-
};
|
|
1022
|
-
var traced = event.trace ? " · trace " + event.trace : "";
|
|
1023
|
-
if (typeof t.totalMs !== "number") {
|
|
1024
|
-
head.textContent = "⏱ no measured timing for this response";
|
|
1025
|
-
tm.append(head);
|
|
1026
|
-
box.append(tm);
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
var bn = t.bnMs;
|
|
1030
|
-
var hold = got.servedAt - Date.parse(event.ts);
|
|
1031
|
-
var summary = (bn === undefined
|
|
1032
|
-
? "⏱ end to end not measured for this turn: its door measured no arrival to count from"
|
|
1033
|
-
: "⏱ " + ms(bn) + " end to end (request arrived → answer stored)")
|
|
1034
|
-
+ " · " + queuedText(t, ms, " (message stored → turn started)")
|
|
1035
|
-
+ " · " + activeText(t, ms, " (the agent CLI's own share)")
|
|
1036
|
-
+ traced;
|
|
1037
|
-
if (bn !== undefined) line("BN: request arrived → answer stored (bnMs)", bn, "sum");
|
|
1038
|
-
t.rows.forEach(function (r) { line(r.layer, r.ms, r.part ? "part" : ""); });
|
|
1039
|
-
if (s) line("BN: answer stored → served to this page's read", hold);
|
|
1040
|
-
line("BN's own total, message stored → answer stored (inside the end to end above, never the agent's time)", t.totalMs, "sum");
|
|
1041
|
-
var copyNote = el("span", "copynote");
|
|
1042
|
-
var copy = el("button", "copy", "Copy");
|
|
1043
|
-
copy.onclick = function (e) {
|
|
1044
|
-
e.preventDefault(); e.stopPropagation();
|
|
1045
|
-
var lines = [head.textContent || ""];
|
|
1046
|
-
table.querySelectorAll("tr").forEach(function (tr) {
|
|
1047
|
-
var cells = tr.querySelectorAll("td");
|
|
1048
|
-
lines.push(cells[0].textContent + "\\t" + cells[1].textContent);
|
|
1049
|
-
});
|
|
1050
|
-
copyText(lines.join("\\n"),
|
|
1051
|
-
function () { copyNote.textContent = "Copied"; setTimeout(function () { copyNote.textContent = ""; }, 1500); },
|
|
1052
|
-
function () { copyNote.textContent = "Copy failed — the rows are above to select by hand"; });
|
|
1053
|
-
};
|
|
1054
|
-
var bar = el("div", "tmbar"); bar.append(copyNote, copy);
|
|
1055
|
-
tm.append(head, wrap); wrap.append(bar, table); box.append(tm);
|
|
1056
|
-
head.textContent = summary;
|
|
1057
|
-
if (!s) {
|
|
1058
|
-
line("page and network: not measured — this message was not sent from this page view", undefined);
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
requestAnimationFrame(function () {
|
|
1062
|
-
var shown = performance.now(), render = shown - got.at, total = shown - s.clickAt;
|
|
1063
|
-
line("page: answer received → shown", render);
|
|
1064
|
-
line("↳ outside BN: this page → BN → this page — UNMEASURED (measured by neither end, and never worked out by subtracting BN's figures from this page's total)", undefined, "part");
|
|
1065
|
-
line("total, Send pressed → answer shown — this page's own clock, BN's rows above inside it", total, "sum");
|
|
1066
|
-
head.textContent = summary + " · this page " + ms(total) + " from Send to shown";
|
|
1067
|
-
});
|
|
1068
|
-
};
|
|
1069
|
-
var poll = async function () {
|
|
1070
|
-
while (mine === view) {
|
|
1071
|
-
try {
|
|
1072
|
-
var data = await api("GET", "/v1/conversations/" + meta.id + "/events?after=" + after + "&wait=25");
|
|
1073
|
-
if (mine !== view) return;
|
|
1074
|
-
var got = { at: performance.now(), servedAt: data.servedAt };
|
|
1075
|
-
notice.remove();
|
|
1076
|
-
data.events.forEach(function (event) { add(event, got); });
|
|
1077
|
-
if (data.events.length > 0) stick();
|
|
1078
|
-
sync(data.state);
|
|
1079
|
-
} catch (err) {
|
|
1080
|
-
if (err.message === "signed out") return;
|
|
1081
|
-
if (mine === view) { notice.textContent = err.message; main.append(notice); stick(); }
|
|
1082
|
-
await new Promise(function (r) { setTimeout(r, 4000); });
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
};
|
|
1086
|
-
/**
|
|
1087
|
-
* The live stream of this conversation (#3611): the agent's own reasoning, its own words and the tool it is on, as they
|
|
1088
|
-
* arrive, read from the conversation's own stream on the same door. A turn frame that ends or pauses the turn means its
|
|
1089
|
-
* record line is written, so the live box empties and the record takes over; a turn's own start and its "still working"
|
|
1090
|
-
* progress do not, or the reasoning already on screen would be wiped mid-turn. A drop mid-stream re-attaches from the
|
|
1091
|
-
* last frame read, so nothing is shown twice and nothing is missed, and a first read that failed is retried by the next
|
|
1092
|
-
* pass of the events poll rather than giving the view up.
|
|
1093
|
-
*/
|
|
1094
|
-
var follow = async function () {
|
|
1095
|
-
var at = null;
|
|
1096
|
-
for (;;) {
|
|
1097
|
-
if (mine !== view) return;
|
|
1098
|
-
var control = new AbortController();
|
|
1099
|
-
stopStream = function () { control.abort(); };
|
|
1100
|
-
try {
|
|
1101
|
-
var response = await fetch("/v1/conversations/" + meta.id + "/stream" + (at === null ? "" : "?after=" + at), { headers: { authorization: "Bearer " + token }, signal: control.signal });
|
|
1102
|
-
if (response.status === 401) { signIn("Sign in again to reach your CLI."); return; }
|
|
1103
|
-
if (!response.ok || response.body === null) throw Error("the gateway answered " + response.status);
|
|
1104
|
-
notice.remove();
|
|
1105
|
-
var reader = response.body.getReader(), decoder = new TextDecoder(), buffer = "";
|
|
1106
|
-
for (;;) {
|
|
1107
|
-
var chunk = await reader.read();
|
|
1108
|
-
if (chunk.done) { await new Promise(function (r) { setTimeout(r, 1000); }); break; }
|
|
1109
|
-
if (mine !== view) { reader.cancel(); return; }
|
|
1110
|
-
buffer += decoder.decode(chunk.value, { stream: true });
|
|
1111
|
-
var lines = buffer.split("\\n");
|
|
1112
|
-
buffer = lines.pop();
|
|
1113
|
-
lines.forEach(function (line) {
|
|
1114
|
-
if (line.indexOf("data: ") !== 0) return;
|
|
1115
|
-
var frame = JSON.parse(line.slice(6));
|
|
1116
|
-
at = frame.seq;
|
|
1117
|
-
if (frame.kind === "turn") { if (ENDED[frame.event]) clearLive(); }
|
|
1118
|
-
else if (frame.kind === "doing") liveDoing(frame.text);
|
|
1119
|
-
else liveText(frame.kind, frame.text);
|
|
1120
|
-
});
|
|
1121
|
-
}
|
|
1122
|
-
} catch (err) {
|
|
1123
|
-
if (mine !== view || control.signal.aborted) return;
|
|
1124
|
-
// OWNER: this view's own feed of the agent's stream — a failed read is emptied and restarted here, never left to reach the record or any other view.
|
|
1125
|
-
if (!(err instanceof Error)) throw err;
|
|
1126
|
-
clearLive();
|
|
1127
|
-
if (at === null) {
|
|
1128
|
-
notice.textContent = "This conversation's live stream could not be read — " + err.message + ". Its record below is unaffected and the stream is tried again.";
|
|
1129
|
-
if (!notice.isConnected) main.append(notice);
|
|
1130
|
-
following = false;
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
await new Promise(function (r) { setTimeout(r, 3000); });
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
};
|
|
1137
|
-
sync(meta.state);
|
|
1138
|
-
btnStop.onclick = async function () { btnStop.disabled = true; try { var stopped = await api("DELETE", "/v1/conversations/" + meta.id); if (mine === view) sync(stopped.state); } catch (err) { btnStop.disabled = false; main.append(el("div", "err", err.message)); } };
|
|
1139
|
-
btnSend.onclick = async function () {
|
|
1140
|
-
var body = text.value;
|
|
1141
|
-
if (!body.trim()) return;
|
|
1142
|
-
btnSend.disabled = true;
|
|
1143
|
-
try { var clickAt = performance.now(); var posted = await api("POST", "/v1/conversations/" + meta.id + "/messages", { text: body }); sent[meta.id + ":" + posted.n] = { clickAt: clickAt }; text.value = ""; text.style.height = ""; shelved = false; if (closed && mine === view) sync("idle"); }
|
|
1144
|
-
catch (err) { main.append(el("div", "err", err.message)); }
|
|
1145
|
-
btnSend.disabled = false;
|
|
1146
|
-
};
|
|
1147
|
-
poll();
|
|
1148
|
-
}
|
|
1149
729
|
`;
|
|
1150
730
|
/** What a service page adds to the shared look: one card per VS Code window, with its screens. */
|
|
1151
731
|
const SERVICE_STYLE = `.win{border:1px solid var(--line);border-radius:10px;margin:0 0 8px;padding:10px}
|
|
@@ -460,6 +460,32 @@ export function resolveProject(project, text = "") {
|
|
|
460
460
|
throw new GatewayError(400, `the request names no project and the home project ${home} under ${workspaceRoot()} holds no BACKLOG.md; name a project (${(names.length > 0 ? names : allowedProjects()).join(", ")}) or set NOTIFY_AGENT_HOME_PROJECT`);
|
|
461
461
|
return join(workspaceRoot(), chosen);
|
|
462
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* The workspace a session is CREATED in, from the name its creation supplied (#3640): exactly one of the allowed project
|
|
465
|
+
* names, resolved to its folder. A creation that names none is refused here by name with what to send — nothing is ever
|
|
466
|
+
* minted on a folder the caller did not choose, and no request's own text stands in for it.
|
|
467
|
+
*/
|
|
468
|
+
export function namedProject(project) {
|
|
469
|
+
if (typeof project !== "string" || project.trim() === "")
|
|
470
|
+
throw new GatewayError(400, `a new session needs the workspace it runs in: send project as exactly one of ${allowedProjects().join(", ")}`);
|
|
471
|
+
return projectFolder(project);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* The folder a session is CREATED in, proved to be one of the allowed projects' folders: a creation that supplies no
|
|
475
|
+
* folder at all is refused by name, exactly as one that supplies a wrong one is (#3640) — this is the record's own
|
|
476
|
+
* guard, so no door can mint a record on a folder nobody chose.
|
|
477
|
+
*/
|
|
478
|
+
export function createdFolder(project) {
|
|
479
|
+
if (typeof project !== "string" || project.trim() === "")
|
|
480
|
+
throw new GatewayError(400, `a new session needs the workspace it runs in: send project as exactly one of ${allowedProjects().join(", ")}`);
|
|
481
|
+
return allowedFolder(project);
|
|
482
|
+
}
|
|
483
|
+
/** The agent CLI a session is CREATED on, from the name its creation supplied (#3640): nothing is minted on a provider the caller never named. */
|
|
484
|
+
export function namedProvider(provider) {
|
|
485
|
+
if (typeof provider !== "string" || providerFor(provider) === undefined)
|
|
486
|
+
throw new GatewayError(400, `a new session needs the agent CLI it runs: send provider as exactly one of ${PROVIDER_NAMES.join(", ")}`);
|
|
487
|
+
return provider;
|
|
488
|
+
}
|
|
463
489
|
/** Every project folder under the workspace root, by name, with whether it is a Git repository. */
|
|
464
490
|
export function listProjects() {
|
|
465
491
|
const root = workspaceRoot();
|
|
@@ -472,7 +498,7 @@ export function listProjects() {
|
|
|
472
498
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
473
499
|
return { root, projects };
|
|
474
500
|
}
|
|
475
|
-
/** The record every new conversation starts from: one id, one unstarted session of its own, no turn run and nothing spent.
|
|
501
|
+
/** The record every new conversation starts from: one id, one unstarted session of its own, no turn run and nothing spent. Its workspace and its agent CLI are required and are bound HERE, in the one write that mints it (#3640): a record cannot exist without either, so no door can leave one to be filled in later. A console session carries its mode, its owner and its notice here too, so the record is never on disk describing itself as a turn conversation the session driver would read its events for. */
|
|
476
502
|
function newConversation(input) {
|
|
477
503
|
mkdirSync(agentDir(), { recursive: true });
|
|
478
504
|
const now = new Date().toISOString();
|
|
@@ -508,30 +534,35 @@ export function openInteractiveConversation(project, provider, model, owner) {
|
|
|
508
534
|
/** Every door measures its own wait: `receivedMs` is the first half of the turn's `bnMs`, so a door that had none could not name BN's whole time. */
|
|
509
535
|
export function startConversation(input) {
|
|
510
536
|
requireConversationEnabled();
|
|
511
|
-
const project =
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const meta = newConversation({ platform: input.platform, technical: input.technical, chat: input.chat, slack: input.slack, email: input.email, project, provider: input.provider, model: input.model, state: "queued" });
|
|
537
|
+
const project = createdFolder(input.project);
|
|
538
|
+
const provider = namedProvider(input.provider);
|
|
539
|
+
const meta = newConversation({ platform: input.platform, technical: input.technical, chat: input.chat, slack: input.slack, email: input.email, project, provider, model: input.model, state: "queued" });
|
|
515
540
|
appendEvent(meta.id, { role: "user", text: input.text, receivedMs: input.receivedMs, ...input.arrival });
|
|
516
541
|
return meta;
|
|
517
542
|
}
|
|
518
543
|
/**
|
|
519
|
-
* Opens a conversation with nothing in it: no message
|
|
520
|
-
*
|
|
521
|
-
*
|
|
544
|
+
* Opens a conversation with nothing in it: no message and no agent work (#2723). Nothing is asked, so no user event is
|
|
545
|
+
* stored and no process is started, and opening one costs nothing. Its workspace — the folder, resolved from the name
|
|
546
|
+
* the call supplied — and its agent CLI are named BY THE CALL that opens it (#3640), so it is never an empty
|
|
547
|
+
* conversation waiting for either; it keeps its id and its whole transcript across a later switch of the folder.
|
|
522
548
|
*/
|
|
523
549
|
export function openConversation(input) {
|
|
524
550
|
requireConversationEnabled();
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
return newConversation({ platform: input.platform, technical: input.technical, chat: input.chat, project, provider: input.provider, model: input.model, state: "idle" });
|
|
551
|
+
const project = createdFolder(input.project);
|
|
552
|
+
const provider = namedProvider(input.provider);
|
|
553
|
+
return newConversation({ platform: input.platform, technical: input.technical, chat: input.chat, project, provider, model: input.model, state: "idle" });
|
|
529
554
|
}
|
|
530
555
|
/**
|
|
531
|
-
* Sets or switches the folder the conversation's agent works in (#2723)
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
556
|
+
* Sets or switches the folder the conversation's agent works in (#2723), reached by every door that offers it: the Web
|
|
557
|
+
* UI's per-row picker and the agent's own `agent_project`. THE AGENT IS NOT REPLACED. A session is resumed by its id from
|
|
558
|
+
* any folder — measured: a session written in one folder resumed from another recalled its earlier turn and worked in the
|
|
559
|
+
* new folder — so the record keeps its session id and the next turn carries the same agent on in the new folder with its
|
|
560
|
+
* whole history. The provider process is not moved either: its folder is fixed at its start, so the driver ends it and
|
|
561
|
+
* opens a new one where the record now points, exactly as it does for a resume after an idle spell.
|
|
562
|
+
*
|
|
563
|
+
* A change is refused at two safe boundaries, both because a running process is never moved under itself: while a turn is
|
|
564
|
+
* in flight, and while a CLI of the browser's own holds the conversation — that CLI runs in a folder of its own and no
|
|
565
|
+
* door can move it.
|
|
535
566
|
*/
|
|
536
567
|
export function setConversationProject(id, project) {
|
|
537
568
|
requireConversationEnabled();
|
|
@@ -543,13 +574,15 @@ export function setConversationProject(id, project) {
|
|
|
543
574
|
// NULL-OK: field turn is OPTIONAL and therefore it is ok to check for NULL
|
|
544
575
|
if (turn !== undefined)
|
|
545
576
|
throw new GatewayError(409, `turn ${turn.turn} is ${turn.started ? "running" : "waiting to start"} since ${clockOf(turn.startedAt)}, so the project cannot change until it ends — a running process is never moved under itself`);
|
|
577
|
+
if (cliOwns(meta))
|
|
578
|
+
throw new GatewayError(409, "a CLI is running this conversation's own session, and a running CLI cannot be moved to another folder — close the CLI, change the project, then open it again");
|
|
546
579
|
const folder = projectFolder(project);
|
|
547
580
|
// NULL-OK: field project is OPTIONAL and therefore it is ok to check for NULL
|
|
548
581
|
if (meta.project !== undefined && sameFolder(meta.project, folder))
|
|
549
582
|
return meta;
|
|
550
|
-
// The record is written first: the folder the next turn runs in comes from it, so a transcript line may never claim a switch the record does not hold.
|
|
551
|
-
const switched = updateConversation(id, { project: folder,
|
|
552
|
-
appendEvent(id, { role: "system", kind: "status", text: `📁 Project set to ${basename(folder)} — the next turn
|
|
583
|
+
// The record is written first: the folder the next turn runs in comes from it, so a transcript line may never claim a switch the record does not hold. `providerArchived` is cleared because a later finish must confirm the provider's archive again rather than trust a handshake from before this work.
|
|
584
|
+
const switched = updateConversation(id, { project: folder, providerArchived: undefined });
|
|
585
|
+
appendEvent(id, { role: "system", kind: "status", text: `📁 Project set to ${basename(folder)} — the next turn carries this same agent on in that folder.` });
|
|
553
586
|
return switched;
|
|
554
587
|
}
|
|
555
588
|
/** Appends the message; a stopped conversation is resumed first, so its next turn picks the agent's own session up where it stopped. A message stored while a turn runs gets its place in line at once, so nobody waits behind it in silence. `receivedMs` is the door's own measured wait, as in `startConversation`. */
|