toolroll 0.9.96 → 0.9.97
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 +8 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/chat-delivery.d.ts +5 -1
- package/dist/chat-delivery.js +74 -25
- package/dist/chat-follow.d.ts +6 -3
- package/dist/chat-follow.js +23 -7
- package/dist/chat-live-schema.d.ts +17 -0
- package/dist/chat-live-schema.js +32 -0
- package/dist/chat-live.d.ts +161 -0
- package/dist/chat-live.js +491 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-api.d.ts +3 -1
- package/dist/discord-api.js +9 -3
- package/dist/discord-chat.d.ts +13 -3
- package/dist/discord-chat.js +77 -21
- package/dist/discord.js +8 -1
- package/dist/lead-answer-eval.d.ts +86 -0
- package/dist/lead-answer-eval.js +255 -0
- package/dist/lead-cli-live.d.ts +56 -0
- package/dist/lead-cli-live.js +200 -0
- package/dist/lead-cli.d.ts +3 -0
- package/dist/lead-cli.js +34 -4
- package/dist/lead-latency-schema.d.ts +24 -0
- package/dist/lead-latency-schema.js +36 -0
- package/dist/lead-progress.d.ts +10 -1
- package/dist/lead-progress.js +1 -1
- package/dist/lead-queue-runtime.d.ts +1 -1
- package/dist/lead-queue-runtime.js +1 -1
- package/dist/lead-route.d.ts +56 -0
- package/dist/lead-route.js +104 -0
- package/dist/lead.d.ts +4 -2
- package/dist/lead.js +79 -26
- package/dist/live-reply.d.ts +83 -0
- package/dist/live-reply.js +302 -0
- package/dist/operate.js +39 -1
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/reply-shape.d.ts +9 -0
- package/dist/reply-shape.js +13 -0
- package/dist/serve.js +11 -4
- package/dist/server/chat.d.ts +2 -1
- package/dist/server/chat.js +18 -6
- package/dist/server/live.js +18 -21
- package/dist/server/render-chat.d.ts +5 -0
- package/dist/server/render-chat.js +4 -2
- package/dist/server/render-settings.d.ts +8 -0
- package/dist/server/render-settings.js +13 -2
- package/dist/server/route-table.js +2 -0
- package/dist/server/settings.js +14 -1
- package/dist/slack-api.js +11 -0
- package/dist/slack-chat.d.ts +11 -1
- package/dist/slack-chat.js +50 -5
- package/dist/slack-settings.js +8 -1
- package/dist/slack.js +10 -3
- package/dist/store.d.ts +31 -2
- package/dist/store.js +59 -6
- package/dist/subscription-chat.d.ts +2 -0
- package/dist/subscription-chat.js +3 -1
- package/dist/teams-chat.d.ts +9 -2
- package/dist/teams-chat.js +50 -2
- package/dist/teams.js +5 -2
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +63 -9
- package/dist/telegram-live.d.ts +116 -0
- package/dist/telegram-live.js +224 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +50 -2
- package/dist/toolroll-update.js +4 -1
- package/package.json +1 -1
package/dist/operate.js
CHANGED
|
@@ -9,6 +9,7 @@ import { adapterPolicy } from "./server/route-policy.js";
|
|
|
9
9
|
import { UNSENT_REPLY_MS } from "./telegram-settings.js";
|
|
10
10
|
import { checkPublicCommand } from "./public-check.js";
|
|
11
11
|
import { leadNameOf } from "./lead-identity.js";
|
|
12
|
+
import { modelName, quickLeadModel } from "./lead-route.js";
|
|
12
13
|
import { maybeTriggerRepair } from "./dispose.js";
|
|
13
14
|
import { CHECK_LEVEL_HINTS, CHECK_LEVEL_WORDS, isCheckLevel, liveQuickCommand, projectCheckLevel, quickVerifyKey, setProjectCheckLevel, setTaskCheckLevel, suggestQuickCommand } from "./check-levels.js";
|
|
14
15
|
import { fileAddTestsTask, requestFollowUpChecks, runFollowUpCheck, runWaitingChecks } from "./result-follow-ups.js";
|
|
@@ -122,7 +123,7 @@ import { homedir, hostname } from "node:os";
|
|
|
122
123
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
123
124
|
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
124
125
|
import { dur } from "./design/kit.js";
|
|
125
|
-
import { checkoutIdle, setCheckoutIdle, openStore, openStoreNoMigrate, initLocalMode, enrollOwnerPassword, installationCreatedBy, laptopPortOf, recordLaptopPort, localModeOf, localOwnerBinding, archiveTaskFamily, restoreTaskFamily, shipChecksForTask, elapsedSeconds, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
|
|
126
|
+
import { checkoutIdle, setCheckoutIdle, leadQuickAnswersOn, setLeadQuickAnswers, openStore, openStoreNoMigrate, initLocalMode, enrollOwnerPassword, installationCreatedBy, laptopPortOf, recordLaptopPort, localModeOf, localOwnerBinding, archiveTaskFamily, restoreTaskFamily, shipChecksForTask, elapsedSeconds, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
|
|
126
127
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
127
128
|
import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
|
|
128
129
|
import { backgroundOff, backgroundOn, backgroundShow, laptopStatus, laptopStop, restartOwnBuilder } from "./laptop-background.js";
|
|
@@ -208,6 +209,7 @@ import { scanRepo } from "./capscan.js";
|
|
|
208
209
|
import { computeGaps, describeCapability } from "./gaps.js";
|
|
209
210
|
import { ask as promptAsk, askHidden as promptHidden, confirm as promptConfirm, interactive as promptInteractive, underAgent } from "./prompt.js";
|
|
210
211
|
import { runLeadCli, answerContextLines } from "./lead-cli.js";
|
|
212
|
+
import { liveTerminalWanted, processTerminal } from "./lead-cli-live.js";
|
|
211
213
|
import { confirmCoordinatorProposal, dismissCoordinatorProposal } from "./lead-doors.js";
|
|
212
214
|
import { verifyApproverByPassword as approverByPassword } from "./principal.js";
|
|
213
215
|
import { activeRemote, REMOTE_MESSAGES, REMOTE_REFUSED_FLAGS, REMOTE_SCOPES, RemoteRefusal, remoteAllows, remoteLensOf, remoteProjectOf, remoteRowOf, remoteSecret, reproveRemote, withRemote } from "./operate-remote.js";
|
|
@@ -598,6 +600,9 @@ Agents — which provider and model each phase runs on
|
|
|
598
600
|
[--model <m>] [--weekly-usd <n>] [--daily-turns <n>] --as <you> --token <t>
|
|
599
601
|
membership providers reuse a logged-in local harness with no dollar
|
|
600
602
|
maximum; direct API providers require a key and weekly dollar ceiling
|
|
603
|
+
toolroll config set|clear|show quick-answers --as <you> --token <t>
|
|
604
|
+
quick answers on (set) or off (clear): plain status questions go to a
|
|
605
|
+
faster model on the Claude membership; changes stay on the main model
|
|
601
606
|
toolroll chat --as <you> [--repo <path>…] [--say "…"] [--end]
|
|
602
607
|
[--ceiling-usd <n>] [--json] (password at the prompt;
|
|
603
608
|
--token <t> only for scripts — it lands in shell history)
|
|
@@ -5101,6 +5106,9 @@ async function configCommand(positional, flags, context) {
|
|
|
5101
5106
|
return fail(write, json, "config", "usage", `unknown \`config ${action}\` — try ${CONFIG_ACTIONS.join(", ")}`, EXIT.usage);
|
|
5102
5107
|
const repoGiven = text(flags, "repo");
|
|
5103
5108
|
const scope = repoGiven === undefined ? INSTALLATION_SCOPE : canonicalProject(repoGiven) ?? resolve(repoGiven);
|
|
5109
|
+
// Settings → Lead's "Quick answers use a faster model", the same switch (responsive-chat.md §8).
|
|
5110
|
+
if (phase === "quick-answers")
|
|
5111
|
+
return quickAnswersConfig(action ?? "show", repoGiven !== undefined, flags, context);
|
|
5104
5112
|
if (action === undefined || action === "show") {
|
|
5105
5113
|
const installation = store.listPhaseConfig(INSTALLATION_SCOPE);
|
|
5106
5114
|
const project = scope === INSTALLATION_SCOPE ? [] : store.listPhaseConfig(scope);
|
|
@@ -10671,6 +10679,8 @@ async function chatCommand(flags, context) {
|
|
|
10671
10679
|
...(context.evidenceRoot === undefined ? {} : { evidenceRoot: context.evidenceRoot }),
|
|
10672
10680
|
// The console's own address (and its public one): links there read as "the task", "the result", "Settings → Lead".
|
|
10673
10681
|
appOrigin: [loadConsoleUrl(process.env, dirname(context.databaseFile)), phoneOrigin(process.env, dirname(context.databaseFile))],
|
|
10682
|
+
// A live status line and the answer as it is written, on a real terminal only (tests drive runLeadCli directly).
|
|
10683
|
+
...(context.leadSeams === undefined && liveTerminalWanted(json, process.env, process.stdout.isTTY === true, () => underAgent(process.env)) ? { liveTerminal: processTerminal() } : {}),
|
|
10674
10684
|
});
|
|
10675
10685
|
return result.code;
|
|
10676
10686
|
}
|
|
@@ -14087,3 +14097,31 @@ async function projectFallbackCommand(positional, flags, context) {
|
|
|
14087
14097
|
return fail(context.write, context.json, command, "refused", done.problem, EXIT.refused);
|
|
14088
14098
|
return succeed(context.write, context.json, command, shown(), () => [...words(), "Tasks scoped from now on use this; tasks already approved keep the chain they were approved with."]);
|
|
14089
14099
|
}
|
|
14100
|
+
/** `toolroll config set|clear|show quick-answers`: Settings → Lead's quick-answer switch (responsive-chat.md §8). On
|
|
14101
|
+
* or off is installation-wide, so changing it takes an instance operator's password, as the web page's route does. */
|
|
14102
|
+
async function quickAnswersConfig(action, repoGiven, flags, context) {
|
|
14103
|
+
const { store, write, json, clock } = context;
|
|
14104
|
+
const command = `config ${action}`;
|
|
14105
|
+
if (repoGiven)
|
|
14106
|
+
return fail(write, json, command, "usage", "quick answers are installation-wide: no --repo", EXIT.usage);
|
|
14107
|
+
const said = () => {
|
|
14108
|
+
const on = leadQuickAnswersOn(store);
|
|
14109
|
+
const model = quickLeadModel(store);
|
|
14110
|
+
return { payload: { quickAnswers: { on, model } }, lines: () => [on
|
|
14111
|
+
? `Quick answers are on: plain status questions are answered by ${modelName(model)}; anything that asks for a change stays on the lead's main model.`
|
|
14112
|
+
: "Quick answers are off: the lead's main model answers everything."] };
|
|
14113
|
+
};
|
|
14114
|
+
if (action === "show") {
|
|
14115
|
+
const shown = said();
|
|
14116
|
+
return succeed(write, json, command, shown.payload, shown.lines);
|
|
14117
|
+
}
|
|
14118
|
+
const acting = await askCredentials(flags, context);
|
|
14119
|
+
if (acting === null)
|
|
14120
|
+
return fail(write, json, command, "usage", "changing quick answers takes `--as <you> --token <t>`", EXIT.usage);
|
|
14121
|
+
const verified = authenticateApprover(store, acting.name, acting.token);
|
|
14122
|
+
if (!verified.ok || !store.isInstanceOperator(acting.name))
|
|
14123
|
+
return fail(write, json, command, "unauthenticated", "an instance operator changes quick answers, with their password", EXIT.refused);
|
|
14124
|
+
setLeadQuickAnswers(store, action === "set", acting.name, clock());
|
|
14125
|
+
const shown = said();
|
|
14126
|
+
return succeed(write, json, command, shown.payload, shown.lines);
|
|
14127
|
+
}
|
|
@@ -22,7 +22,6 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
|
|
|
22
22
|
export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
|
|
23
23
|
/** Chat reads only an admitted project. Keep the full source in the project UI. */
|
|
24
24
|
export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
|
|
25
|
-
references?: never;
|
|
26
25
|
revision: number;
|
|
27
26
|
stale: boolean;
|
|
28
27
|
notice: string;
|
|
@@ -30,10 +29,10 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
30
29
|
content?: never;
|
|
31
30
|
history?: never;
|
|
32
31
|
instructions?: never;
|
|
32
|
+
references?: never;
|
|
33
33
|
decisions?: never;
|
|
34
34
|
waiting?: never;
|
|
35
35
|
} | {
|
|
36
|
-
references?: never;
|
|
37
36
|
stale?: never;
|
|
38
37
|
decision: {
|
|
39
38
|
repo: string;
|
|
@@ -56,10 +55,10 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
56
55
|
revision?: never;
|
|
57
56
|
history?: never;
|
|
58
57
|
instructions?: never;
|
|
58
|
+
references?: never;
|
|
59
59
|
decisions?: never;
|
|
60
60
|
waiting?: never;
|
|
61
61
|
} | {
|
|
62
|
-
references?: never;
|
|
63
62
|
stale?: never;
|
|
64
63
|
revision: number;
|
|
65
64
|
title: string;
|
|
@@ -67,6 +66,7 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
67
66
|
notice: string;
|
|
68
67
|
history?: never;
|
|
69
68
|
instructions?: never;
|
|
69
|
+
references?: never;
|
|
70
70
|
decisions?: never;
|
|
71
71
|
waiting?: never;
|
|
72
72
|
} | {
|
package/dist/reply-shape.d.ts
CHANGED
|
@@ -35,6 +35,15 @@ export declare function linkLabel(url: string, appOrigin?: ShapeOptions["appOrig
|
|
|
35
35
|
* changes nothing.
|
|
36
36
|
*/
|
|
37
37
|
export declare function shapeReply(text: string, options?: ShapeOptions): string;
|
|
38
|
+
/**
|
|
39
|
+
* A reply still being written, shaped with the final reply's presentation policy (docs/plans/responsive-chat.md §2.2),
|
|
40
|
+
* except that ids are dropped unless the owner asked for them: whether a reply names several of a kind isn't known
|
|
41
|
+
* until it ends, so a prefix never keeps them on that ground. Presentation, not redaction.
|
|
42
|
+
*/
|
|
43
|
+
export declare function shapeReplyPrefix(text: string, options?: ShapeOptions): string;
|
|
44
|
+
/** The first window of a shaped text of at most `size` characters, cut where a part would be (never inside code, a link,
|
|
45
|
+
* a bold anchor or an emoji's two halves). A single link longer than the window stays whole, so the caller measures. */
|
|
46
|
+
export declare function shapedWindow(shaped: string, size: number): string;
|
|
38
47
|
/**
|
|
39
48
|
* A long reply split into parts of at most `size` characters, then each part shaped: the cut is made on the reply as
|
|
40
49
|
* written, at a paragraph or line where possible and never inside code, a link or a bold anchor, so no part carries
|
package/dist/reply-shape.js
CHANGED
|
@@ -199,6 +199,14 @@ function shapeWith(text, options, state) {
|
|
|
199
199
|
export function shapeReply(text, options = {}) {
|
|
200
200
|
return shapeWith(text, options, stateFor(text, options));
|
|
201
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* A reply still being written, shaped with the final reply's presentation policy (docs/plans/responsive-chat.md §2.2),
|
|
204
|
+
* except that ids are dropped unless the owner asked for them: whether a reply names several of a kind isn't known
|
|
205
|
+
* until it ends, so a prefix never keeps them on that ground. Presentation, not redaction.
|
|
206
|
+
*/
|
|
207
|
+
export function shapeReplyPrefix(text, options = {}) {
|
|
208
|
+
return shapeWith(text, options, { boldLeft: BOLD_ANCHORS, keepIds: askedForIds(options.asked) ? "all" : new Set() });
|
|
209
|
+
}
|
|
202
210
|
/** Where a long reply may be cut: never inside code, a link, a URL or a bold anchor; best at a paragraph, then a line,
|
|
203
211
|
* then a space. Always past `from`, and never between the two halves of an emoji. */
|
|
204
212
|
function cutAt(text, from, size) {
|
|
@@ -231,6 +239,11 @@ function cutAt(text, from, size) {
|
|
|
231
239
|
end = end - 1 > from ? end - 1 : end + 1;
|
|
232
240
|
return end;
|
|
233
241
|
}
|
|
242
|
+
/** The first window of a shaped text of at most `size` characters, cut where a part would be (never inside code, a link,
|
|
243
|
+
* a bold anchor or an emoji's two halves). A single link longer than the window stays whole, so the caller measures. */
|
|
244
|
+
export function shapedWindow(shaped, size) {
|
|
245
|
+
return shaped.slice(0, cutAt(shaped, 0, size));
|
|
246
|
+
}
|
|
234
247
|
/**
|
|
235
248
|
* A long reply split into parts of at most `size` characters, then each part shaped: the cut is made on the reply as
|
|
236
249
|
* written, at a paragraph or line where possible and never inside code, a link or a bold anchor, so no part carries
|
package/dist/serve.js
CHANGED
|
@@ -121,7 +121,7 @@ import { findFirstTasks, firstRunSteps, signInCommandFor } from "./first-run.js"
|
|
|
121
121
|
import { installMethod } from "./install-method.js";
|
|
122
122
|
import { createIntegrationMonitor } from "./integrations.js";
|
|
123
123
|
import { logEvent } from "./log.js";
|
|
124
|
-
import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
|
|
124
|
+
import { LEAD_MESSAGE_MAX_CHARS, leadFailureText } from "./lead.js";
|
|
125
125
|
import {} from "./model-catalog.js";
|
|
126
126
|
import { modeTermsFromJson } from "./modes.js";
|
|
127
127
|
import { envValue } from "./names.js";
|
|
@@ -171,7 +171,7 @@ import { createBrowserKeys, KEY_FILL_SCRIPT, pagePathOf } from './server/mutatio
|
|
|
171
171
|
import { DEFAULT_GUARD_POLICY, passwordGuardOf, provenPasswordAccount, SourceBudget, withPasswordSource } from "./sign-in-guard.js";
|
|
172
172
|
import { sourceKey } from "./source-key.js";
|
|
173
173
|
import { readSsoSettings } from "./sso-settings.js";
|
|
174
|
-
import { LEAD_THREAD, localOwnerBinding } from "./store.js";
|
|
174
|
+
import { LEAD_THREAD, latestLeadTurnOfThread, localOwnerBinding } from "./store.js";
|
|
175
175
|
import { waitingUpdate } from "./toolroll-update.js";
|
|
176
176
|
import { PACKAGE_VERSION } from "./version.js";
|
|
177
177
|
import { loadConsoleUrl } from "./webhooks.js";
|
|
@@ -629,7 +629,8 @@ export function createDecisionServer(options) {
|
|
|
629
629
|
return run !== null && family.versions.some(one => one.refId === run.taskRef);
|
|
630
630
|
}),
|
|
631
631
|
names: { project: projectName, modes: TASK_COMPOSER_MODES },
|
|
632
|
-
|
|
632
|
+
// A queued message's partials are shaped as its final reply will be: for its words, with this console's own addresses.
|
|
633
|
+
live: { begin: (thread, asked) => chatHandlers.beginLiveTurn(thread, { asked, appOrigin: [publicOrigin?.origin ?? null, options.configDir === undefined ? null : loadConsoleUrl(process.env, options.configDir)] }), end: (thread, ok, progress) => chatHandlers.endLiveTurn(thread, ok, progress) },
|
|
633
634
|
...(options.chatFetcher ? { fetcher: options.chatFetcher } : {}),
|
|
634
635
|
...(options.subscriptionChatRunner ? { subscriptionRunner: options.subscriptionChatRunner } : {}),
|
|
635
636
|
});
|
|
@@ -1326,7 +1327,13 @@ export function createDecisionServer(options) {
|
|
|
1326
1327
|
const replaced = thread !== null ? store.replacedLeadThread(thread)
|
|
1327
1328
|
: found !== null && found.scope.kind === "lead" && store.listLeadMessages(found.id, 1).length > 0 ? found : null;
|
|
1328
1329
|
const previousProposals = replaced === null ? [] : store.listLeadProposals(replaced.id);
|
|
1330
|
+
// How this thread's last reply ended, read from its saved turn (never process memory, which a restart loses): a
|
|
1331
|
+
// failed turn that saved no message of its own says why in the thread, until the next reply starts.
|
|
1332
|
+
const lastTurn = thread === null ? null : latestLeadTurnOfThread(store, thread.id);
|
|
1333
|
+
const failure = lastTurn !== null && lastTurn.state === "failed" && !messages.some(one => one.role === "assistant" && one.turn === lastTurn.id)
|
|
1334
|
+
? leadFailureText(lastTurn.failureReason, store.leadTurnCostUnconfirmed(lastTurn.id), lastTurn.failureDetail ?? null) : null;
|
|
1329
1335
|
return {
|
|
1336
|
+
failure,
|
|
1330
1337
|
previous: replaced === null ? null : { messages: store.listLeadMessages(replaced.id, 40), proposals: previousProposals, decisions: decisionsFor(store, previousProposals) },
|
|
1331
1338
|
messages,
|
|
1332
1339
|
ask,
|
|
@@ -1369,7 +1376,7 @@ export function createDecisionServer(options) {
|
|
|
1369
1376
|
sessionId: session?.id ?? NOT_STARTED_SESSION, user: session?.approver ?? who.name, version: leadChatVersion({ ...rows, focusTask }),
|
|
1370
1377
|
messages: leadBrowserMessages(rows, back, { task: focusTask?.id ?? null, project: chatProject }),
|
|
1371
1378
|
pendingTurnId: rows.pending?.id ?? null, requestId: randomBytes(16).toString("hex"), maxChars: LEAD_MESSAGE_MAX_CHARS,
|
|
1372
|
-
taskId: focusTask?.id ?? null, resultRunId, project: focusTask === null ? chatProject : null,
|
|
1379
|
+
taskId: focusTask?.id ?? null, resultRunId, project: focusTask === null ? chatProject : null, failure: rows.pending === null ? rows.failure : null,
|
|
1373
1380
|
};
|
|
1374
1381
|
}
|
|
1375
1382
|
/** The session-layer principal (ruling 3): cookie, csrf, and role were
|
package/dist/server/chat.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type DemoResultView } from "../demo-chat.js";
|
|
|
3
3
|
import type { DemoExchange } from "../demo.js";
|
|
4
4
|
import { type FirstTaskSuggestion } from "../first-run.js";
|
|
5
5
|
import type { LeadProgress } from "../lead-progress.js";
|
|
6
|
+
import type { ShapeOptions } from "../reply-shape.js";
|
|
6
7
|
import type { DirectChatProviderId } from "../store.js";
|
|
7
8
|
import type { ServerRuntime } from './runtime.js';
|
|
8
9
|
import { type ChatEnablement } from "./render-chat.js";
|
|
@@ -14,7 +15,7 @@ export declare function createChatHandlers(runtime: ServerRuntime): {
|
|
|
14
15
|
via: "cookie";
|
|
15
16
|
}, now: Date) => BrowserPhoneCard | undefined;
|
|
16
17
|
chatProjectOf: (values: string[]) => string | null | undefined;
|
|
17
|
-
beginLiveTurn: (thread: number) => (event: LeadProgress) => void;
|
|
18
|
+
beginLiveTurn: (thread: number, shape?: ShapeOptions) => (event: LeadProgress) => void;
|
|
18
19
|
endLiveTurn: (thread: number, ok: boolean, progress?: (event: LeadProgress) => void) => void;
|
|
19
20
|
noteLead: (csrf: string, turn: number | null, message: string) => void;
|
|
20
21
|
takeLeadNote: (csrf: string, liveSession: number | null) => string | null;
|
package/dist/server/chat.js
CHANGED
|
@@ -23,6 +23,7 @@ import { configureLeadFollow, leadFollowStatus } from '../lead-follow.js';
|
|
|
23
23
|
import { leadActivity } from "../lead-voice.js";
|
|
24
24
|
import { limitsView } from "../limits-ui.js";
|
|
25
25
|
import { confirmCoordinatorProposal, confirmLeadProposal, dismissCoordinatorProposal, dismissLeadProposal } from "../lead-doors.js";
|
|
26
|
+
import { createLiveReply } from "../live-reply.js";
|
|
26
27
|
import { LEAD_MESSAGE_MAX_CHARS, runLeadTurn } from "../lead.js";
|
|
27
28
|
import { livePin, modelOptions } from "../model-catalog.js";
|
|
28
29
|
import { verifyApproverByPassword, verifyApproverStanding } from "../principal.js";
|
|
@@ -991,20 +992,24 @@ export function createChatHandlers(runtime) {
|
|
|
991
992
|
// The live display starts once this message holds a turn: a refusal (a secret, a turn still running) never blanks
|
|
992
993
|
// the display of a reply that is still running in the same conversation.
|
|
993
994
|
let onProgress;
|
|
995
|
+
// Partials are shaped as the final reply is: for this message, with this console's own addresses.
|
|
996
|
+
const liveShape = { asked: message, appOrigin: requestContext.getStore()?.appOrigins ?? null };
|
|
994
997
|
const ended = (ok) => { if (onProgress !== undefined)
|
|
995
998
|
endLiveTurn(opened.thread.id, ok, onProgress); };
|
|
996
999
|
// Detached work never carries this request's browser session (Passwordless reads it): it runs with no session facts.
|
|
997
1000
|
void freshFacts.run(null, () => runLeadTurn({ store, who: principal, session: leadSession, thread: opened.thread, config: enabled.config, key: enabled.key, message,
|
|
998
|
-
onAdmitted: () => { onProgress = beginLiveTurn(opened.thread.id); }, onProgress: event => onProgress?.(event), channel: "console", requestId, ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot }))
|
|
1001
|
+
onAdmitted: () => { onProgress = beginLiveTurn(opened.thread.id, liveShape); }, onProgress: event => onProgress?.(event), channel: "console", requestId, ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot }))
|
|
999
1002
|
.then(outcome => {
|
|
1000
1003
|
// Lost a race to another turn of this person's between deciding and admission: it waits instead of being refused.
|
|
1001
1004
|
if (!outcome.ok && "refused" in outcome && (outcome.refused === "concurrent" || outcome.refused === "backlog") && queueIt().kind === "queued") {
|
|
1002
1005
|
ended(false);
|
|
1003
1006
|
return;
|
|
1004
1007
|
}
|
|
1005
|
-
// A turn that saved its outcome in the thread (one stopped at its deadline) is said there, once.
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
+
// A turn that saved its outcome in the thread (one stopped at its deadline) is said there, once. Any other turn
|
|
1009
|
+
// that ended failed is said from its saved row (leadConversationRows `failure`), which a restart keeps; only a
|
|
1010
|
+
// refusal before any turn existed is said from memory.
|
|
1011
|
+
if (!outcome.ok && !("saved" in outcome && outcome.saved === true) && !("turn" in outcome))
|
|
1012
|
+
noteLead(who.session.csrf, null, outcome.message);
|
|
1008
1013
|
ended(outcome.ok);
|
|
1009
1014
|
})
|
|
1010
1015
|
.catch(() => { noteLead(who.session.csrf, null, "the turn failed unexpectedly"); ended(false); })
|
|
@@ -1208,11 +1213,11 @@ export function createChatHandlers(runtime) {
|
|
|
1208
1213
|
/** Which live display each progress handle started: ending a turn ends only its own display (a turn stopped from
|
|
1209
1214
|
* another window that settles late can never end the next turn's). */
|
|
1210
1215
|
const progressOf = new WeakMap();
|
|
1211
|
-
function beginLiveTurn(thread) {
|
|
1216
|
+
function beginLiveTurn(thread, shape = {}) {
|
|
1212
1217
|
const previous = liveTurns.get(thread);
|
|
1213
1218
|
if (previous?.expiry)
|
|
1214
1219
|
clearTimeout(previous.expiry);
|
|
1215
|
-
const live = { steps: [], done: false, ok: false, listeners: previous?.listeners ?? new Set(), startedAt: clock().getTime(), stepAt: [] };
|
|
1220
|
+
const live = { steps: [], done: false, ok: false, listeners: previous?.listeners ?? new Set(), startedAt: clock().getTime(), stepAt: [], reply: createLiveReply(shape) };
|
|
1216
1221
|
liveTurns.set(thread, live);
|
|
1217
1222
|
// A page that has this conversation open follows the reply (GET /live, its chat room).
|
|
1218
1223
|
runtime.liveTurnStarted(thread);
|
|
@@ -1222,6 +1227,7 @@ export function createChatHandlers(runtime) {
|
|
|
1222
1227
|
if (live.done)
|
|
1223
1228
|
return;
|
|
1224
1229
|
live.turn ??= event.turn;
|
|
1230
|
+
live.reply?.push(event);
|
|
1225
1231
|
if (event.kind === "step")
|
|
1226
1232
|
newStep();
|
|
1227
1233
|
else if (event.kind === "tool") {
|
|
@@ -1241,6 +1247,11 @@ export function createChatHandlers(runtime) {
|
|
|
1241
1247
|
current().text = event.text;
|
|
1242
1248
|
(live.confirmed ??= [])[live.steps.length - 1] = event.final === true;
|
|
1243
1249
|
}
|
|
1250
|
+
else if (event.kind === "blocked") {
|
|
1251
|
+
// Held back as a possible password or key: no step keeps its words (the Live grid's tile reads these too).
|
|
1252
|
+
for (const step of live.steps)
|
|
1253
|
+
step.text = "";
|
|
1254
|
+
}
|
|
1244
1255
|
for (const listener of live.listeners)
|
|
1245
1256
|
listener();
|
|
1246
1257
|
};
|
|
@@ -1255,6 +1266,7 @@ export function createChatHandlers(runtime) {
|
|
|
1255
1266
|
return;
|
|
1256
1267
|
live.done = true;
|
|
1257
1268
|
live.ok = ok;
|
|
1269
|
+
live.reply?.close(ok ? "answered" : "failed");
|
|
1258
1270
|
live.endedAt = clock().getTime();
|
|
1259
1271
|
for (const listener of live.listeners)
|
|
1260
1272
|
listener();
|
package/dist/server/live.js
CHANGED
|
@@ -4,6 +4,7 @@ import { agentRunFingerprint } from "../agents-board.js";
|
|
|
4
4
|
import { createLiveRooms, LiveConnection } from "../live-bus.js";
|
|
5
5
|
import { reproveRemote } from "../operate-remote.js";
|
|
6
6
|
import { rowVisible } from "../project.js";
|
|
7
|
+
import { createLiveSender, LIVE_STATUS } from "../live-reply.js";
|
|
7
8
|
import { TEAM_PASSWORD_REVERIFY_MS } from "../team-http.js";
|
|
8
9
|
import { handlersOf } from "./handler-registry.js";
|
|
9
10
|
import { respond } from "./http.js";
|
|
@@ -18,6 +19,11 @@ const ROOM_NAME = /^(workspace|task|flow|chat|team|agent|summary|agents-live)(?:
|
|
|
18
19
|
const WORKSPACE_SETTLE_MS = 1_000;
|
|
19
20
|
/** A running reply's preview: at most one snapshot every 80 ms; the last one always goes out. */
|
|
20
21
|
const TURN_SETTLE_MS = 80;
|
|
22
|
+
function chatSnapshot(turn) {
|
|
23
|
+
const view = turn.reply?.view() ?? { phase: "working", status: LIVE_STATUS.thinking };
|
|
24
|
+
const steps = (turn.reply?.tools() ?? []).map(calls => ({ tools: calls.map(call => call.label), toolCalls: calls }));
|
|
25
|
+
return { steps, phase: view.phase, status: turn.done ? "" : turn.reply?.status() ?? LIVE_STATUS.thinking, text: view.phase === "writing" ? view.text : "", done: turn.done, ok: turn.ok };
|
|
26
|
+
}
|
|
21
27
|
/** A room's name, read strictly: anything else is not a room. */
|
|
22
28
|
export function parseLiveRoom(name) {
|
|
23
29
|
const match = ROOM_NAME.exec(name);
|
|
@@ -114,39 +120,30 @@ export function createLiveHandlers(runtime, live) {
|
|
|
114
120
|
}
|
|
115
121
|
return valid;
|
|
116
122
|
}
|
|
117
|
-
/** A running reply for one viewer's thread: snapshots while it runs, then the last one.
|
|
123
|
+
/** A running reply for one viewer's thread: snapshots while it runs, then the last one. Every snapshot goes through
|
|
124
|
+
* one serialized sender (live-reply.ts): one in flight, the newest waiting, standing re-proved before each, and the
|
|
125
|
+
* last one only after the sender has closed, so no preview lands after it. */
|
|
118
126
|
function follow(viewer, thread) {
|
|
119
127
|
if (!admitted(viewer, thread))
|
|
120
128
|
return;
|
|
121
129
|
const turn = liveTurns.get(thread);
|
|
122
130
|
if (turn === undefined || turn.done || viewer.detach !== null)
|
|
123
131
|
return;
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
if (admitted(viewer, thread))
|
|
127
|
-
viewer.connection.send(viewer.room, "turn", { steps: turn.steps, done: turn.done, ok: turn.ok });
|
|
128
|
-
};
|
|
129
|
-
const flush = () => {
|
|
130
|
-
queued = null;
|
|
131
|
-
snapshot();
|
|
132
|
-
if (turn.done)
|
|
133
|
-
stop();
|
|
134
|
-
};
|
|
132
|
+
const send = (payload) => { viewer.connection.send(viewer.room, "turn", payload); };
|
|
133
|
+
const sender = createLiveSender({ send, standing: () => admitted(viewer, thread), spacingMs: TURN_SETTLE_MS });
|
|
135
134
|
const listener = () => {
|
|
136
|
-
if (turn.done) {
|
|
137
|
-
|
|
138
|
-
clearTimeout(queued);
|
|
139
|
-
flush();
|
|
135
|
+
if (!turn.done) {
|
|
136
|
+
sender.offer(chatSnapshot(turn));
|
|
140
137
|
return;
|
|
141
138
|
}
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
stop();
|
|
140
|
+
void sender.close().then(() => { if (admitted(viewer, thread))
|
|
141
|
+
send(chatSnapshot(turn)); });
|
|
144
142
|
};
|
|
145
|
-
const stop = () => {
|
|
146
|
-
clearTimeout(queued); turn.listeners.delete(listener); viewer.detach = null; };
|
|
143
|
+
const stop = () => { turn.listeners.delete(listener); viewer.detach = null; void sender.close(); };
|
|
147
144
|
turn.listeners.add(listener);
|
|
148
145
|
viewer.detach = stop;
|
|
149
|
-
|
|
146
|
+
sender.offer(chatSnapshot(turn));
|
|
150
147
|
}
|
|
151
148
|
/** A reply starts on a thread: whoever has that conversation open follows it. */
|
|
152
149
|
function turnStarted(thread) {
|
|
@@ -300,6 +300,8 @@ export declare function replacedBrowserMessages(previous: ReplacedThread): impor
|
|
|
300
300
|
/** The replaced thread for the server-rendered page: read only, then the divider. */
|
|
301
301
|
export declare function replacedThreadHtml(previous: ReplacedThread | null | undefined): Html;
|
|
302
302
|
export type LeadThreadRows = {
|
|
303
|
+
/** How the thread's last reply failed, from its saved turn (no message of its own); null otherwise. */
|
|
304
|
+
failure?: string | null;
|
|
303
305
|
/** The lead thread a change in reachable projects replaced; shown only. */
|
|
304
306
|
previous?: ReplacedThread | null;
|
|
305
307
|
messages: LeadMessage[];
|
|
@@ -404,6 +406,9 @@ export type LiveTurn = {
|
|
|
404
406
|
stepAt?: number[];
|
|
405
407
|
/** Per step: its text is the provider's finished answer, secret-checked whole (lead.ts `final`). */
|
|
406
408
|
confirmed?: boolean[];
|
|
409
|
+
/** The shared live-reply core (live-reply.ts): the only source of what the chat room shows — one status line, the
|
|
410
|
+
* tools behind a disclosure, and shaped, settled partial text. `steps[].text` above is raw and never sent. */
|
|
411
|
+
reply?: import("../live-reply.js").LiveReply;
|
|
407
412
|
};
|
|
408
413
|
export type ChatEnablement = {
|
|
409
414
|
ok: true;
|
|
@@ -825,7 +825,7 @@ export function replacedThreadHtml(previous) {
|
|
|
825
825
|
* A div, not a p: the Remove form can't sit inside a paragraph (the parser would close it first). */
|
|
826
826
|
function queuedStateHtml(row, back) {
|
|
827
827
|
if (row.status === "queued") {
|
|
828
|
-
const waiting = row.error?.startsWith("Waiting: ") === true ? row.error.slice("Waiting: ".length) : "
|
|
828
|
+
const waiting = row.error?.startsWith("Waiting: ") === true ? row.error.slice("Waiting: ".length) : "Waiting for your earlier messages…";
|
|
829
829
|
return html `<div class="so-queued" data-queued="${row.message}" data-queue-state="queued"><span class="so-queued-state">Queued</span> · ${waiting}${postForm(`/chat/queued/${row.message}/remove`, html `<button type="submit" class="so-queued-remove quiet">Remove</button>`, { attrs: { class: "inline" }, ...(back === null ? {} : { returnTo: back }) })}</div>`;
|
|
830
830
|
}
|
|
831
831
|
if (row.status === "running" && row.turnId === null)
|
|
@@ -949,6 +949,8 @@ export function leadThreadHtml(data) {
|
|
|
949
949
|
const ask = message.turn === null ? undefined : data.asks?.get(message.turn);
|
|
950
950
|
thread.push(html `<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? html ` id="latest"` : ""}>${renderChatText(message.text, asked)}${ask === undefined ? "" : leadAskHtml(ask, { task: data.focusTask?.id ?? null, project: data.chatProject ?? null }, "chat-message", data.pending === null && data.ask?.turn === ask.turn)}${cards.map(one => leadProposalCard(one, inert, data.decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, data.focusTask === null ? null : returnTo))}<div class="chat-message-foot">${chatActivity(message.activity)}<time datetime="${message.createdAt}">${relativeAge(message.createdAt, data.now)}</time></div></div>`);
|
|
951
951
|
}
|
|
952
|
+
if (data.pending === null && data.failure)
|
|
953
|
+
thread.push(html `<div class="problem" data-key="failed" data-lead-failed role="alert">${data.failure}</div>`);
|
|
952
954
|
parts.push(html `<div class="thread" data-key="thread" data-chat-list>\n${joinHtml(thread, "\n")}\n</div>`);
|
|
953
955
|
if (data.pending !== null) {
|
|
954
956
|
const subscription = data.pending.reservedMicrousd === 0;
|
|
@@ -1033,7 +1035,7 @@ export function leadPage(chrome, data) {
|
|
|
1033
1035
|
sessionId, user: approver, version: leadChatVersion(data),
|
|
1034
1036
|
messages: leadBrowserMessages(data, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
|
|
1035
1037
|
pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: LEAD_MESSAGE_MAX_CHARS,
|
|
1036
|
-
taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
|
|
1038
|
+
taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject, failure: data.pending === null ? data.failure ?? null : null,
|
|
1037
1039
|
...(data.previous == null ? {} : { previous: { messages: replacedBrowserMessages(data.previous) } }),
|
|
1038
1040
|
},
|
|
1039
1041
|
focus: data.focusTask === null ? null : { id: data.focusTask.id, title: data.focusTask.title,
|
|
@@ -41,6 +41,14 @@ export declare function leadSettingsHtml(data: {
|
|
|
41
41
|
when: string;
|
|
42
42
|
until: string;
|
|
43
43
|
}[];
|
|
44
|
+
/** Quick answers (responsive-chat.md §8): whether they're on, the faster model's name, whether this person may change
|
|
45
|
+
* them (an instance operator), and whether the last save was this one. Absent: not shown. */
|
|
46
|
+
quick?: {
|
|
47
|
+
on: boolean;
|
|
48
|
+
model: string;
|
|
49
|
+
canChange: boolean;
|
|
50
|
+
saved: boolean;
|
|
51
|
+
};
|
|
44
52
|
/** D5: the lead's subagents in the projects this person can use: name and role, project, and whether it's paused. */
|
|
45
53
|
subagents?: {
|
|
46
54
|
id: number;
|
|
@@ -11,7 +11,7 @@ import {} from "../telegram.js";
|
|
|
11
11
|
import { chatMoney, screen, when } from "./chrome.js";
|
|
12
12
|
import { taskHref } from "./http.js";
|
|
13
13
|
import { html, joinHtml, postForm } from "../html.js";
|
|
14
|
-
import { acts, btn, chip, codeLine, disclose, facts, formRow, input, note, notice, select, settingsLink, settingsRecord, settingsSection, textarea } from "./kit.js";
|
|
14
|
+
import { acts, btn, chip, codeLine, disclose, facts, formRow, input, note, notice, select, settingsLink, settingsRecord, settingsSection, switchInput, textarea } from "./kit.js";
|
|
15
15
|
import { kitSectionForm, SECRET_OPT_OUTS } from "../settings-kit.js";
|
|
16
16
|
import { settingsSubPage } from "../settings-groups-ui.js";
|
|
17
17
|
/** Settings → Lead: one line for what runs the lead and one action; the full form, turning it off and stored keys under Advanced. */
|
|
@@ -43,6 +43,17 @@ export function leadSettingsHtml(data) {
|
|
|
43
43
|
attrs: { maxlength: ABOUT_YOU_MAX_LINES * (ABOUT_YOU_LINE_MAX + 1), placeholder: "Keep copy terse.\nI test changes myself.", "aria-label": "What your lead knows about you" } }), "top"),
|
|
44
44
|
html `<div class="k-ssave">${savedLine(data.aboutSaved === true)}${btn("Save", { sm: true, type: "submit" })}</div>`,
|
|
45
45
|
]), { attrs: { "data-lead-about": true } }) });
|
|
46
|
+
// Quick answers: plain status questions go to a faster model; anything that asks for a change stays on the main one.
|
|
47
|
+
const quick = data.quick === undefined || config === null ? "" : settingsSection({ id: "lead-quick", title: "Quick answers",
|
|
48
|
+
lead: config.provider === "claude-subscription"
|
|
49
|
+
? `Short questions about what's going on, like "what's running?", are answered by ${data.quick.model}, a faster model. Anything that asks for a change still goes to the lead's main model.`
|
|
50
|
+
: "Short questions about what's going on can go to a faster model. This works only when the lead uses the Anthropic membership.",
|
|
51
|
+
body: data.quick.canChange
|
|
52
|
+
? postForm("/settings/lead/quick", joinHtml([
|
|
53
|
+
switchInput({ id: "lead-quick", name: "enabled", label: "Quick answers use a faster model", on: data.quick.on }),
|
|
54
|
+
html `<div class="k-ssave">${savedLine(data.quick.saved)}${btn("Save", { sm: true, type: "submit" })}</div>`,
|
|
55
|
+
]), { attrs: { "data-lead-quick": true } })
|
|
56
|
+
: note(data.quick.on ? "On." : "Off.") });
|
|
46
57
|
// What the lead promised to follow up on; it reports each once, here in chat, and drops it after 7 days.
|
|
47
58
|
const promises = (data.promises ?? []).length === 0 ? "" : settingsSection({ id: "lead-promises", title: "Promises", lead: "What the lead said it would follow up on. Each is reported once, then dropped after 7 days.",
|
|
48
59
|
body: joinHtml(data.promises.map(one => settingsRecord({
|
|
@@ -69,7 +80,7 @@ export function leadSettingsHtml(data) {
|
|
|
69
80
|
save: { label: "Turn the lead off", variant: "danger" }, confirm: "Turn the lead off", body: note("The lead stops answering until you turn it on again.") }) });
|
|
70
81
|
return settingsSubPage("agents", "Lead", joinHtml([
|
|
71
82
|
data.problem ? notice(data.problem.charAt(0).toUpperCase() + data.problem.slice(1)) : data.said === null ? "" : html `<p class="k-notice info" role="status">${data.said}</p>`,
|
|
72
|
-
runs, identity, about, promises, subagents,
|
|
83
|
+
runs, identity, about, quick, promises, subagents,
|
|
73
84
|
disclose("Advanced", joinHtml([leadConfigForm(config, facts), keys, off])),
|
|
74
85
|
], "\n"));
|
|
75
86
|
}
|
|
@@ -619,6 +619,8 @@ post("chat", "onboarding.phone-dismiss", "/onboarding/phone/dismiss", approver("
|
|
|
619
619
|
const LEAD_WORDS = { identity: "An approver names their lead.", about: "An approver edits what their lead knows about them.", "promise/cancel": "An approver manages the lead's promises.", on: "An approver turns the lead on." };
|
|
620
620
|
for (const one of ["identity", "about", "promise/cancel", "on"])
|
|
621
621
|
post("settings", `settings.lead-${one.replaceAll("/", "-")}`, `/settings/lead/${one}`, approver(LEAD_WORDS[one], "/settings/lead"));
|
|
622
|
+
// Quick answers is installation-wide: which model answers everyone's plain questions (responsive-chat.md §8).
|
|
623
|
+
post("settings", "settings.lead-quick", "/settings/lead/quick", { callers: ["cookie"], ...operator("An instance operator chooses how the lead answers.", "/settings/lead") });
|
|
622
624
|
post("chat", "chat.config", "/chat/config", { scope: "step-up" });
|
|
623
625
|
for (const one of ["mint", "follow", "end", "stop"])
|
|
624
626
|
post("chat", `chat.mate-${one}`, `/chat/mate/${one}`, { callers: ["cookie"], ...(one === "mint" ? { scope: "step-up" } : {}) });
|
package/dist/server/settings.js
CHANGED
|
@@ -76,6 +76,7 @@ import { cleanUpHtml, decisionsHtml, framedSection, knowledgeHtml, MEMORY_INTRO,
|
|
|
76
76
|
import { aboutYouOf, checkAboutYou, saveAboutYou } from "../lead-about.js";
|
|
77
77
|
import { cancelCommitment, conditionWords, openCommitments } from '../lead-commitments.js';
|
|
78
78
|
import { checkLeadIdentity, leadIdentityOf } from "../lead-identity.js";
|
|
79
|
+
import { modelName as leadModelName, quickLeadModel } from "../lead-route.js";
|
|
79
80
|
import { mailboxAccess, readThroughImap } from "../mailbox.js";
|
|
80
81
|
import { startConnect } from "../mcp-connect.js";
|
|
81
82
|
import { decideProposal, defaultTidyAnalyzer, listProposals, memoryStatus } from "../memory-pass.js";
|
|
@@ -122,6 +123,7 @@ import { addProjectInstructions, ASSISTANTS, detectPreparation, modelChoices, pr
|
|
|
122
123
|
import { skillsHtml, skillsScript } from "../skills-ui.js";
|
|
123
124
|
import { checkSlackCredentials, clearSlackCredentials, loadSlackCredentials, saveSlackCredentials, SLACK_MANIFEST, SlackError } from "../slack-api.js";
|
|
124
125
|
import { slackSettingsHtml } from "../slack-settings.js";
|
|
126
|
+
import { ACK_REFUSED_META } from "../chat-live.js";
|
|
125
127
|
import { spendHtml, spendScript } from "../spend-ui.js";
|
|
126
128
|
import { approvalFactsNow } from "./kit.js";
|
|
127
129
|
import { spendReport, spendReportCsv, spendRows } from "../spend-report.js";
|
|
@@ -135,7 +137,7 @@ import { sweepSchedule } from "../storage-sweep-schedule.js";
|
|
|
135
137
|
import { storageHtml } from "../storage-ui.js";
|
|
136
138
|
import { landedSummary } from "../landed-cleanup.js";
|
|
137
139
|
import { bytesWords, parseCleanup, parseIdle } from "../storage.js";
|
|
138
|
-
import { checkoutIdle, isDigestTime, liveBackupState, setCheckoutIdle, localModeOf, RESULT_SCREENSHOTS, setLiveBackupSettings, setLocalModeOff } from "../store.js";
|
|
140
|
+
import { checkoutIdle, isDigestTime, leadQuickAnswersOn, liveBackupState, setCheckoutIdle, localModeOf, RESULT_SCREENSHOTS, setLeadQuickAnswers, setLiveBackupSettings, setLocalModeOff } from "../store.js";
|
|
139
141
|
import { defaultLiveFolder, liveFolderOf, liveFolderProblem } from "../live-backup.js";
|
|
140
142
|
import { LIVE_EVERY } from "../backup-ui.js";
|
|
141
143
|
import { checkTeamsCredentials, clearTeamsCredentials, loadTeamsCredentials, saveTeamsCredentials, TeamsError } from "../teams-api.js";
|
|
@@ -673,6 +675,7 @@ ${restoreHtml(chosen, retired, undoableMerges(store, chosen), disabledLessons(st
|
|
|
673
675
|
return sendScreen(response, 200, screen("Lead", leadSettingsHtml({ config, facts, words: leadWords(), signedIn, command: agentSignInCommand(),
|
|
674
676
|
said: url.searchParams.get("said"), problem: url.searchParams.get("problem"), saved: url.searchParams.get("saved") === "1", identity: leadIdentityOf(store, who.name), promises,
|
|
675
677
|
about: aboutYouOf(store, who.name), aboutSaved: url.searchParams.get("saved") === "about",
|
|
678
|
+
quick: { on: leadQuickAnswersOn(store), model: leadModelName(quickLeadModel(store)), canChange: who.role === "approver" && store.isInstanceOperator(who.name), saved: url.searchParams.get("saved") === "quick" },
|
|
676
679
|
subagents: store.subagents(consoleProjects().filter(repo => store.accountCanAccess(who.name, repo))).map(one => ({ id: one.id, label: labelOf(one), project: projectName(one.repo), paused: one.state === "paused" })) }), { chrome: chromeFor(project, "settings") }));
|
|
677
680
|
}
|
|
678
681
|
async function telegramPage(ctx) {
|
|
@@ -2005,6 +2008,8 @@ ${restoreHtml(chosen, retired, undoableMerges(store, chosen), disabledLessons(st
|
|
|
2005
2008
|
if (credentials || loadSlackCredentials(dir))
|
|
2006
2009
|
return show("Disconnect the current Slack app before connecting another.", 409);
|
|
2007
2010
|
saveSlackCredentials(dir, checked);
|
|
2011
|
+
// A fresh connection asks for the 👀 again at once (an app reinstalled with reactions:write).
|
|
2012
|
+
state.deleteMeta(checked.installation, ACK_REFUSED_META);
|
|
2008
2013
|
}
|
|
2009
2014
|
catch (error) {
|
|
2010
2015
|
return show(error instanceof SlackError ? error.message : "Slack could not be connected. Check the tokens and try again.");
|
|
@@ -2906,6 +2911,7 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2906
2911
|
"mode.revoke": modeRevoke,
|
|
2907
2912
|
"settings.lead-identity": leadIdentity,
|
|
2908
2913
|
"settings.lead-about": leadAbout,
|
|
2914
|
+
"settings.lead-quick": leadQuick,
|
|
2909
2915
|
"settings.lead-promise-cancel": leadPromiseCancel,
|
|
2910
2916
|
"settings.lead-on": leadOn,
|
|
2911
2917
|
"settings.code-map": codeMapAct("switch"),
|
|
@@ -3223,5 +3229,12 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
3223
3229
|
});
|
|
3224
3230
|
return done.ok ? back("said", "Saved. Tasks scoped from now on use it; tasks already approved keep the chain they were approved with.") : back("problem", `Nothing changed: ${done.problem}`);
|
|
3225
3231
|
}
|
|
3232
|
+
// Settings → Lead: quick answers on or off (installation-wide; the route admits an instance operator only).
|
|
3233
|
+
async function leadQuick(ctx) {
|
|
3234
|
+
const { who, response, now, posted } = ctx;
|
|
3235
|
+
const body = readForm(posted, CONSOLE_FORMS.leadQuick);
|
|
3236
|
+
setLeadQuickAnswers(store, body.get("enabled") === "on", who.name, now);
|
|
3237
|
+
return redirect(response, "/settings/lead?saved=quick");
|
|
3238
|
+
}
|
|
3226
3239
|
return { registrations, modelsView };
|
|
3227
3240
|
}
|
package/dist/slack-api.js
CHANGED
|
@@ -19,6 +19,10 @@ export class SlackError extends ChatDeliveryError {
|
|
|
19
19
|
channel_not_found: "The Slack conversation is unavailable. Pair your account again.",
|
|
20
20
|
message_not_found: "The original Slack message was removed. Open the saved chat to recover it.",
|
|
21
21
|
already_complete: "Waiting to confirm the uploaded file’s receipt.",
|
|
22
|
+
cant_update_message: "Slack wouldn't let that message be changed.",
|
|
23
|
+
edit_window_closed: "Slack wouldn't let that message be changed any more.",
|
|
24
|
+
already_reacted: "The reaction was already there.",
|
|
25
|
+
no_reaction: "The reaction was already gone.",
|
|
22
26
|
}[code] ?? code;
|
|
23
27
|
}
|
|
24
28
|
}
|
|
@@ -36,6 +40,11 @@ const errors = new Set([
|
|
|
36
40
|
"file_not_found",
|
|
37
41
|
"already_complete",
|
|
38
42
|
"not_in_channel",
|
|
43
|
+
// The 👀 and edits of the live message: named, so they are told apart from a real failure.
|
|
44
|
+
"already_reacted",
|
|
45
|
+
"no_reaction",
|
|
46
|
+
"cant_update_message",
|
|
47
|
+
"edit_window_closed",
|
|
39
48
|
]);
|
|
40
49
|
export function slackApi(token, fetcher = fetch) {
|
|
41
50
|
return async (method, args = {}) => {
|
|
@@ -208,6 +217,8 @@ export const SLACK_MANIFEST = {
|
|
|
208
217
|
"users:read",
|
|
209
218
|
"files:read",
|
|
210
219
|
"files:write",
|
|
220
|
+
// The 👀 on a message when it arrives, and taking it off once the answer is delivered.
|
|
221
|
+
"reactions:write",
|
|
211
222
|
],
|
|
212
223
|
},
|
|
213
224
|
},
|