toolroll 0.9.38 → 0.9.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +19 -19
- package/dist/browser-workspace.d.ts +6 -0
- package/dist/serve.js +81 -10
- package/dist/store.d.ts +5 -0
- package/dist/store.js +14 -0
- package/dist/team-contract.d.ts +11 -7
- package/package.json +1 -1
|
@@ -100,6 +100,10 @@ export type BrowserConversation = {
|
|
|
100
100
|
resultRunId: number | null;
|
|
101
101
|
/** A project's own thread (v77); absent or null for the lead conversation or a task. */
|
|
102
102
|
project?: string | null;
|
|
103
|
+
/** The lead thread a change in reachable projects replaced (mate arc ruling 9): shown above a divider, read only. */
|
|
104
|
+
previous?: {
|
|
105
|
+
messages: BrowserMessage[];
|
|
106
|
+
} | null;
|
|
103
107
|
};
|
|
104
108
|
/** A page rebuilt as React components (shadcn/ui). The server still renders
|
|
105
109
|
* its HTML as the no-JavaScript fallback; forms post to the same routes. */
|
|
@@ -176,6 +180,8 @@ export type BrowserLimits = {
|
|
|
176
180
|
export type BrowserSettingsView = {
|
|
177
181
|
kind: 'settings';
|
|
178
182
|
said: string | null;
|
|
183
|
+
/** Using it from a phone, under Chat apps (approvers only). */
|
|
184
|
+
phone?: BrowserPhoneCard;
|
|
179
185
|
/** The settings destinations under short headings; a chat app's tile says whether it is connected and gets alerts. */
|
|
180
186
|
groups: BrowserSettingsGroup[];
|
|
181
187
|
theme: 'system' | 'light' | 'dark';
|
package/dist/serve.js
CHANGED
|
@@ -744,6 +744,19 @@ export function createDecisionServer(options) {
|
|
|
744
744
|
provider: () => { const enabled = chatEnablement(); return enabled.ok ? { config: enabled.config, key: enabled.key } : null; },
|
|
745
745
|
...(options.subscriptionChatRunner ? { subscriptionRunner: options.subscriptionChatRunner } : {}),
|
|
746
746
|
});
|
|
747
|
+
// Only browser sessions receive HTML cards. The CLI keeps its summary contract.
|
|
748
|
+
const teamBrowserReply = (reply, csrf) => {
|
|
749
|
+
const snapshot = reply.snapshot;
|
|
750
|
+
if (!snapshot?.selected || !snapshot.proposals)
|
|
751
|
+
return reply;
|
|
752
|
+
return { ...reply, snapshot: { ...snapshot, proposals: snapshot.proposals.map(summary => {
|
|
753
|
+
const proposal = store.getMateProposal(summary.id);
|
|
754
|
+
if (!proposal || proposal.thread !== snapshot.selected.threadId)
|
|
755
|
+
return summary;
|
|
756
|
+
const decision = proposal.kind === 'answer' && typeof proposal.payload['decision'] === 'number' ? store.getDecision(proposal.payload['decision']) : null;
|
|
757
|
+
return { ...summary, card: teamProposalCardParts(proposal, snapshot, csrf, decision).card };
|
|
758
|
+
}) } };
|
|
759
|
+
};
|
|
747
760
|
const teamEndpoint = (request, response) => handleTeamHttp(request, response, {
|
|
748
761
|
authenticate: request => {
|
|
749
762
|
const who = identify(request, request.method === 'POST');
|
|
@@ -773,7 +786,11 @@ export function createDecisionServer(options) {
|
|
|
773
786
|
return false;
|
|
774
787
|
return request.headers['x-csrf-token'] === who.session.csrf;
|
|
775
788
|
},
|
|
776
|
-
execute:
|
|
789
|
+
execute: async (actor, input) => {
|
|
790
|
+
const reply = await team.execute(actor, input);
|
|
791
|
+
const who = identify(request, false);
|
|
792
|
+
return who?.via === 'cookie' ? teamBrowserReply(reply, who.session.csrf) : reply;
|
|
793
|
+
}, cursor: team.cursor, streams: teamStreams,
|
|
777
794
|
});
|
|
778
795
|
const sessionEndpoint = createSessionEndpoint({ store, workspace: coding, projects: codingProjects, projectAllowed: repo => rowVisible(liveCeiling(), repo) });
|
|
779
796
|
/** The enumerable admission list for roll-up SQL: repos-only ceilings
|
|
@@ -3585,11 +3602,11 @@ export function createDecisionServer(options) {
|
|
|
3585
3602
|
if (who.via !== "cookie")
|
|
3586
3603
|
return refuse(response, who, 403, "chat is a browser surface — it keeps your drafts in the session");
|
|
3587
3604
|
if (url.searchParams.get('private') !== '1' && !url.searchParams.has('task') && !url.searchParams.has('result') && !url.searchParams.has('project')) {
|
|
3588
|
-
const reply = await team.execute({ name: who.name, generation: who.session.generation }, {
|
|
3605
|
+
const reply = teamBrowserReply(await team.execute({ name: who.name, generation: who.session.generation }, {
|
|
3589
3606
|
operation: url.searchParams.has('conversation') ? 'show' : 'list',
|
|
3590
3607
|
args: { ...(url.searchParams.has('conversation') ? { conversationId: url.searchParams.get('conversation') } : {}),
|
|
3591
3608
|
...(url.searchParams.has('lead') ? { leadId: url.searchParams.get('lead') } : {}) },
|
|
3592
|
-
});
|
|
3609
|
+
}), who.session.csrf);
|
|
3593
3610
|
if (!reply.ok || !reply.snapshot)
|
|
3594
3611
|
return refuse(response, who, 403, reply.message, '/chat?private=1');
|
|
3595
3612
|
if (url.searchParams.has('proposal')) {
|
|
@@ -3599,11 +3616,7 @@ export function createDecisionServer(options) {
|
|
|
3599
3616
|
return refuse(response, who, 404, 'This proposal is unavailable.', '/chat');
|
|
3600
3617
|
const back = '/chat?conversation=' + encodeURIComponent(selected.id);
|
|
3601
3618
|
const decision = proposal.kind === 'answer' && typeof proposal.payload['decision'] === 'number' ? store.getDecision(proposal.payload['decision']) : null;
|
|
3602
|
-
const
|
|
3603
|
-
const canAct = reply.snapshot.canSend && reply.snapshot.chatAuthorization?.enabled === true;
|
|
3604
|
-
let card = mateProposalCard(proposal, who.session.csrf, !canAct || active, decision, back);
|
|
3605
|
-
if (!canAct)
|
|
3606
|
-
card = card.replace('Available when the current turn finishes.', reply.snapshot.canSend ? 'Enable chat in this conversation before acting on a proposal.' : 'An authorized contributor can act on this proposal.');
|
|
3619
|
+
const card = teamProposalCardParts(proposal, reply.snapshot, who.session.csrf, decision).html;
|
|
3607
3620
|
return sendScreen(response, 200, screen('Review action', `<p><a href="${escape(back)}">Back to conversation</a></p>` + card, { chrome: chromeFor(null, 'chat', undefined, 'all') }));
|
|
3608
3621
|
}
|
|
3609
3622
|
if (reply.snapshot.leads.length > 0 || url.searchParams.has('conversation') || url.searchParams.get('team') === '1') {
|
|
@@ -4290,7 +4303,7 @@ export function createDecisionServer(options) {
|
|
|
4290
4303
|
})(), (() => {
|
|
4291
4304
|
const chosen = store.notificationPreference(who.name);
|
|
4292
4305
|
return { mode: chosen.mode, digestAt: chosen.digestAt, screenshots: chosen.screenshots, projects: notificationProjects(store, who.name) };
|
|
4293
|
-
})(), telegramTrouble(store, loadBotToken(process.env, options.telegramTokenFile))));
|
|
4306
|
+
})(), telegramTrouble(store, loadBotToken(process.env, options.telegramTokenFile)), who.via === "cookie" ? phoneSetup(who) ?? null : null));
|
|
4294
4307
|
}
|
|
4295
4308
|
if (url.pathname === "/push/key") {
|
|
4296
4309
|
// Session-gated: the key is not secret, but strangers get nothing.
|
|
@@ -4464,12 +4477,19 @@ export function createDecisionServer(options) {
|
|
|
4464
4477
|
return first !== undefined && String(first["id"]) === rootId;
|
|
4465
4478
|
}
|
|
4466
4479
|
/** After the first Ready result, once (onboarding): the phone, by a chat app or the console over Tailscale. */
|
|
4480
|
+
/** Chat's one-line pointer to the phone setup: after the first Ready result, until put away or Telegram is paired. */
|
|
4467
4481
|
function phoneCard(who, now) {
|
|
4468
4482
|
if (who.role !== "approver" || store.isDemo() || store.firstSuccessAt(now) === null || store.installationFact(PHONE_CARD_FACT) !== null)
|
|
4469
4483
|
return undefined;
|
|
4470
4484
|
const botId = options.telegramTokenFile === undefined ? null : loadBotToken(process.env, options.telegramTokenFile)?.botId ?? null;
|
|
4471
4485
|
if (botId !== null && store.liveTelegramBindings(botId).length > 0)
|
|
4472
4486
|
return undefined;
|
|
4487
|
+
return phoneSetup(who);
|
|
4488
|
+
}
|
|
4489
|
+
/** The phone setup itself (Settings → Chat apps): a chat app, or this console over Tailscale. */
|
|
4490
|
+
function phoneSetup(who) {
|
|
4491
|
+
if (who.role !== "approver" || store.isDemo())
|
|
4492
|
+
return undefined;
|
|
4473
4493
|
const port = servedPort() ?? 4180;
|
|
4474
4494
|
const address = server.address();
|
|
4475
4495
|
const bound = typeof address === "object" && address !== null ? address.address : "127.0.0.1";
|
|
@@ -4509,7 +4529,11 @@ export function createDecisionServer(options) {
|
|
|
4509
4529
|
const ask = last?.role === "assistant" && last.turn !== null && store.getMateTurn(last.turn)?.approver === who.name ? store.mateAskOpen(last.turn, now) : null;
|
|
4510
4530
|
// Answered questions stay readable above the answer; only their buttons go.
|
|
4511
4531
|
const asks = new Map(messages.flatMap(one => { const asked = one.role === "assistant" && one.turn !== null ? store.mateAsk(one.turn) : null; return asked === null ? [] : [[asked.turn, asked]]; }));
|
|
4532
|
+
// The thread a ceiling change replaced (ruling 9): read after the open, display only.
|
|
4533
|
+
const replaced = store.replacedLeadThread(opened.thread);
|
|
4534
|
+
const previousProposals = replaced === null ? [] : store.listMateProposals(replaced.id);
|
|
4512
4535
|
return {
|
|
4536
|
+
previous: replaced === null ? null : { messages: store.listMateMessages(replaced.id, 40), proposals: previousProposals, decisions: decisionsFor(store, previousProposals) },
|
|
4513
4537
|
messages,
|
|
4514
4538
|
ask,
|
|
4515
4539
|
asks,
|
|
@@ -15406,6 +15430,8 @@ ${THEME_DARK}
|
|
|
15406
15430
|
min-height: min(32rem, 48vh); padding: .25rem;
|
|
15407
15431
|
}
|
|
15408
15432
|
.thread .msg { max-width: 48rem; line-height: 1.65; overflow-wrap: anywhere; }
|
|
15433
|
+
.chat-previous-divider { display: flex; align-items: center; gap: .6rem; margin: 1rem 0; color: var(--so-muted); font-size: .8rem; }
|
|
15434
|
+
.chat-previous-divider::before, .chat-previous-divider::after { content: ""; flex: 1; border-top: 1px solid var(--so-line); }
|
|
15409
15435
|
.thread .msg p { margin: .3rem 0; }
|
|
15410
15436
|
.thread .msg.op {
|
|
15411
15437
|
align-self: flex-end; max-width: min(82%, 40rem); padding: .75rem 1rem;
|
|
@@ -18311,6 +18337,19 @@ function mateProposalCard(proposal, csrf, inert, decision, returnTo = null) {
|
|
|
18311
18337
|
function mateProposalCardParts(proposal, csrf, inert, decision, returnTo = null) {
|
|
18312
18338
|
return proposalCardParts({ id: proposal.id, kind: proposal.kind, payload: proposal.payload, state: proposal.state, outcome: proposal.outcome, by: { mate: true }, actionBase: "/chat/proposal" }, csrf, inert, decision, returnTo);
|
|
18313
18339
|
}
|
|
18340
|
+
/** Review and inline team cards share the same server-decided controls and reasons. */
|
|
18341
|
+
function teamProposalCardParts(proposal, snapshot, csrf, decision) {
|
|
18342
|
+
const canAct = snapshot.canSend && snapshot.chatAuthorization?.enabled === true;
|
|
18343
|
+
const active = snapshot.messages.some(message => message.status === 'running');
|
|
18344
|
+
const back = '/chat?conversation=' + encodeURIComponent(snapshot.selected.id);
|
|
18345
|
+
const parts = mateProposalCardParts(proposal, csrf, !canAct || active, decision, back);
|
|
18346
|
+
if (!canAct && proposal.state === 'pending') {
|
|
18347
|
+
const reason = snapshot.canSend ? 'Enable chat in this conversation before acting on a proposal.' : 'An authorized contributor can act on this proposal.';
|
|
18348
|
+
parts.html = parts.html.replace('Available when the current turn finishes.', reason);
|
|
18349
|
+
parts.card = { ...parts.card, note: reason };
|
|
18350
|
+
}
|
|
18351
|
+
return parts;
|
|
18352
|
+
}
|
|
18314
18353
|
function coordinatorProposalCard(proposal, csrf, now, decision, returnTo = null) {
|
|
18315
18354
|
return proposalCard({ id: proposal.id, kind: proposal.kind, payload: proposal.payload, state: proposal.state, outcome: proposal.outcome, by: { mate: false, name: proposal.name, ago: relativeAge(proposal.createdAt, now) }, actionBase: "/proposals" }, csrf, false, decision, returnTo);
|
|
18316
18355
|
}
|
|
@@ -18330,6 +18369,24 @@ function coordinatorProposalsSection(proposals, decisions, csrf, now, heading =
|
|
|
18330
18369
|
proposals.map(one => coordinatorProposalCard(one, csrf, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo)).join("") +
|
|
18331
18370
|
`</div>`);
|
|
18332
18371
|
}
|
|
18372
|
+
const REPLACED_THREAD_DIVIDER = "New conversation — the projects I can reach changed";
|
|
18373
|
+
/** The replaced thread as the React conversation renders it: words and card outcomes, no buttons. */
|
|
18374
|
+
function replacedBrowserMessages(previous, csrf) {
|
|
18375
|
+
return mateBrowserMessages({ ...previous, pending: null, ask: null, asks: new Map() }, csrf, null).map(message => ({
|
|
18376
|
+
...message, cardsHtml: "",
|
|
18377
|
+
cards: (message.cards ?? []).map(card => ({ ...card, primary: null, dismissable: false })),
|
|
18378
|
+
}));
|
|
18379
|
+
}
|
|
18380
|
+
/** The replaced thread for the server-rendered page: read only, then the divider. */
|
|
18381
|
+
function replacedThreadHtml(previous) {
|
|
18382
|
+
if (previous == null)
|
|
18383
|
+
return "";
|
|
18384
|
+
return `<section class="thread chat-previous" aria-label="Earlier conversation" data-previous-thread>` +
|
|
18385
|
+
previous.messages.map(one => one.role === "operator"
|
|
18386
|
+
? `<div class="msg op" data-previous-message="${one.id}"><p style="white-space:pre-wrap">${escape(one.text)}</p></div>`
|
|
18387
|
+
: `<div class="msg mate" data-previous-message="${one.id}">${renderChatText(one.text)}</div>`).join("") +
|
|
18388
|
+
`<p class="chat-previous-divider" role="separator" data-thread-divider>${escape(REPLACED_THREAD_DIVIDER)}</p></section>`;
|
|
18389
|
+
}
|
|
18333
18390
|
/** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
|
|
18334
18391
|
* "Something else" moves to the composer. Nothing shows while a reply is running. */
|
|
18335
18392
|
function mateAskHtml(ask, csrf, target, composer, open = true) {
|
|
@@ -18378,6 +18435,7 @@ function mateChatVersion(rows) {
|
|
|
18378
18435
|
const focus = rows.focusTask;
|
|
18379
18436
|
const facts = [
|
|
18380
18437
|
rows.messages.map(one => [one.id, one.role, one.turn]),
|
|
18438
|
+
rows.previous == null ? null : rows.previous.messages.map(one => one.id),
|
|
18381
18439
|
rows.proposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
|
|
18382
18440
|
rows.coordinatorProposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
|
|
18383
18441
|
[...rows.decisions.values()].map(one => [one.id, one.state, one.choice ?? null]),
|
|
@@ -18504,6 +18562,7 @@ function matePage(chrome, data) {
|
|
|
18504
18562
|
}
|
|
18505
18563
|
const lastMessage = data.messages.at(-1);
|
|
18506
18564
|
const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
|
|
18565
|
+
conversation.push(replacedThreadHtml(data.previous));
|
|
18507
18566
|
conversation.push(mateThreadHtml({ ...data, problem: null, chatProject }));
|
|
18508
18567
|
conversation.push(
|
|
18509
18568
|
// The New update action (package 2): hidden until a live update lands
|
|
@@ -18524,6 +18583,7 @@ function matePage(chrome, data) {
|
|
|
18524
18583
|
messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
|
|
18525
18584
|
pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: MATE_MESSAGE_MAX_CHARS,
|
|
18526
18585
|
taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
|
|
18586
|
+
...(data.previous == null ? {} : { previous: { messages: replacedBrowserMessages(data.previous, data.csrf) } }),
|
|
18527
18587
|
},
|
|
18528
18588
|
focus: data.focusTask === null ? null : { id: data.focusTask.id, title: data.focusTask.title,
|
|
18529
18589
|
html: taskChatLiveRegion(data.focusTask, data.csrf, requestContext.getStore()?.workspaceRead === true, data.pending !== null) },
|
|
@@ -24057,7 +24117,7 @@ function notificationProjects(store, account) {
|
|
|
24057
24117
|
.map(one => ({ repo: one.path, name: one.name, muted: muted.has(one.path) }))
|
|
24058
24118
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
24059
24119
|
}
|
|
24060
|
-
function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null, push = null, providerKeys = null, digest = null, permissionDefault = null, qualityDefault = null, email = null, telegramDelivery = null, workers = null, updates = null, firstResult = null, chatNotices = null, telegramFailing = false) {
|
|
24120
|
+
function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null, push = null, providerKeys = null, digest = null, permissionDefault = null, qualityDefault = null, email = null, telegramDelivery = null, workers = null, updates = null, firstResult = null, chatNotices = null, telegramFailing = false, phone = null) {
|
|
24061
24121
|
const permissionCard = permissionDefault === null
|
|
24062
24122
|
? ""
|
|
24063
24123
|
: [
|
|
@@ -24283,10 +24343,12 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
24283
24343
|
workers,
|
|
24284
24344
|
updates,
|
|
24285
24345
|
firstResult,
|
|
24346
|
+
...(phone === null ? {} : { phone }),
|
|
24286
24347
|
};
|
|
24287
24348
|
return screen("Settings", [
|
|
24288
24349
|
"<h1>Settings</h1>",
|
|
24289
24350
|
settingsTiles(view.groups),
|
|
24351
|
+
phone === null ? "" : phoneSetupHtml(phone),
|
|
24290
24352
|
appearanceCard(csrf),
|
|
24291
24353
|
permissionCard,
|
|
24292
24354
|
qualityCard,
|
|
@@ -24312,6 +24374,15 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
24312
24374
|
`</details>`,
|
|
24313
24375
|
].join("\n"), { chrome, workspace: { view }, functional: { script: SETTINGS_AUTOSAVE_SCRIPT + (pushScript ?? ""), ...(pushScript === null ? {} : { fetches: true }) } });
|
|
24314
24376
|
}
|
|
24377
|
+
/** The phone setup for the server-rendered Settings page: the same choices as the React card. */
|
|
24378
|
+
function phoneSetupHtml(phone) {
|
|
24379
|
+
const [first, ...others] = phone.chatApps;
|
|
24380
|
+
return `<section class="card" id="phone" aria-labelledby="phone-card-title" data-phone-card><h2 id="phone-card-title">Use it from your phone</h2>` +
|
|
24381
|
+
`<p>${first === undefined ? "" : `<a href="${escape(first.href)}">Pair ${escape(first.label)}</a>`}${others.length === 0 ? "" : ` · or ${others.map(one => `<a href="${escape(one.href)}">${escape(one.label)}</a>`).join(", ")}`}</p>` +
|
|
24382
|
+
`<p class="meta" data-phone-tailnet>${phone.tailnet === null ? `Or install <a href="https://tailscale.com/download">Tailscale</a> on this computer and your phone, then reload this page for the address.`
|
|
24383
|
+
: phone.tailnet.restart === null ? `Or on your phone open <code>${escape(phone.tailnet.address)}</code> and sign in.`
|
|
24384
|
+
: `Or start Toolroll with <code>${escape(phone.tailnet.restart)}</code>, then on your phone open <code>${escape(phone.tailnet.address)}</code> and sign in.`}</p></section>`;
|
|
24385
|
+
}
|
|
24315
24386
|
/** Every worker that is not retired: its capacity, and the tasks it holds now. */
|
|
24316
24387
|
function settingsWorkers(store, now) {
|
|
24317
24388
|
const claims = store.liveClaims(null, now);
|
package/dist/store.d.ts
CHANGED
|
@@ -6323,6 +6323,11 @@ export declare class Store {
|
|
|
6323
6323
|
};
|
|
6324
6324
|
/** The approver's live thread for one scope, read only — null when none is open. */
|
|
6325
6325
|
liveMateThreadFor(approver: string, scope?: MateThreadScope): MateThread | null;
|
|
6326
|
+
/** The lead thread a ceiling change closed just before `current` (ruling 9),
|
|
6327
|
+
* read only and for display only: it never continues and never reaches the
|
|
6328
|
+
* model. Null when there is none, when it was ended or revoked (its text is
|
|
6329
|
+
* gone), or when it closed under the same ceiling. */
|
|
6330
|
+
replacedLeadThread(current: MateThread): MateThread | null;
|
|
6326
6331
|
/** The task a paired chat is talking about (v78), or null for the lead conversation. */
|
|
6327
6332
|
chatFocus(surface: string, binding: number): string | null;
|
|
6328
6333
|
setChatFocus(surface: string, binding: number, task: string | null, now: Date): void;
|
package/dist/store.js
CHANGED
|
@@ -19275,6 +19275,20 @@ export class Store {
|
|
|
19275
19275
|
const row = this.db.prepare("SELECT * FROM mate_thread WHERE approver = ? AND scope_kind = ? AND scope_key IS ? AND closed_at IS NULL AND NOT EXISTS (SELECT 1 FROM team_conversation tc WHERE tc.thread=mate_thread.id) ORDER BY id DESC LIMIT 1").get(approver, scope.kind, scope.kind === "lead" ? null : scope.key);
|
|
19276
19276
|
return row === undefined ? null : readMateThread(row);
|
|
19277
19277
|
}
|
|
19278
|
+
/** The lead thread a ceiling change closed just before `current` (ruling 9),
|
|
19279
|
+
* read only and for display only: it never continues and never reaches the
|
|
19280
|
+
* model. Null when there is none, when it was ended or revoked (its text is
|
|
19281
|
+
* gone), or when it closed under the same ceiling. */
|
|
19282
|
+
replacedLeadThread(current) {
|
|
19283
|
+
if (current.scope.kind !== "lead")
|
|
19284
|
+
return null;
|
|
19285
|
+
const row = this.db.prepare(`SELECT * FROM mate_thread WHERE approver = ? AND scope_kind = 'lead' AND scope_key IS NULL AND id < ? AND closed_at IS NOT NULL
|
|
19286
|
+
AND NOT EXISTS (SELECT 1 FROM team_conversation tc WHERE tc.thread=mate_thread.id) ORDER BY id DESC LIMIT 1`).get(current.approver, current.id);
|
|
19287
|
+
if (row === undefined || String(row["ceiling_digest"]) === current.ceilingDigest)
|
|
19288
|
+
return null;
|
|
19289
|
+
const previous = readMateThread(row);
|
|
19290
|
+
return this.db.prepare("SELECT 1 FROM mate_message WHERE thread = ? LIMIT 1").get(previous.id) === undefined ? null : previous;
|
|
19291
|
+
}
|
|
19278
19292
|
/** The task a paired chat is talking about (v78), or null for the lead conversation. */
|
|
19279
19293
|
chatFocus(surface, binding) {
|
|
19280
19294
|
const row = this.db.prepare("SELECT task FROM chat_focus WHERE surface = ? AND binding = ?").get(surface, binding);
|
package/dist/team-contract.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WorkIndexPage } from './work-index.js';
|
|
2
|
+
import type { BrowserActionCard } from './browser-workspace.js';
|
|
2
3
|
/** Shared central-team transport contract. Requests never establish actor identity. */
|
|
3
4
|
export type TeamActor = {
|
|
4
5
|
name: string;
|
|
@@ -52,6 +53,15 @@ export type TeamChatAuthorization = {
|
|
|
52
53
|
termsDigest: string;
|
|
53
54
|
waitingReason?: string;
|
|
54
55
|
};
|
|
56
|
+
/** Browser cards extend the existing CLI summary; both refer to the same saved proposal. */
|
|
57
|
+
export type TeamProposal = {
|
|
58
|
+
id: number;
|
|
59
|
+
turnId: number;
|
|
60
|
+
title: string;
|
|
61
|
+
state: string;
|
|
62
|
+
href: string;
|
|
63
|
+
card?: BrowserActionCard;
|
|
64
|
+
};
|
|
55
65
|
export type TeamSnapshot = {
|
|
56
66
|
leads: TeamLead[];
|
|
57
67
|
conversations: TeamConversation[];
|
|
@@ -67,13 +77,7 @@ export type TeamSnapshot = {
|
|
|
67
77
|
accounts: string[];
|
|
68
78
|
chatAuthorization?: TeamChatAuthorization;
|
|
69
79
|
tasks?: WorkIndexPage;
|
|
70
|
-
proposals?:
|
|
71
|
-
id: number;
|
|
72
|
-
turnId: number;
|
|
73
|
-
title: string;
|
|
74
|
-
state: string;
|
|
75
|
-
href: string;
|
|
76
|
-
}[];
|
|
80
|
+
proposals?: TeamProposal[];
|
|
77
81
|
};
|
|
78
82
|
export declare const TEAM_OPERATIONS: readonly ['list', 'show', 'create-lead', 'update-lead', 'create-conversation', 'member', 'send', 'edit', 'withdraw', 'read', 'authorize', 'follow', 'stop', 'transfer'];
|
|
79
83
|
export type TeamOperation = typeof TEAM_OPERATIONS[number];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.39",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|