Subagents and Compact on the server's /api/v2 facts (server 2.0.0-beta.7)
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s

- Models' `subagents {offered, named_only}` and /models' `subagents_cap`; a chat's
  `subagents {offered, named_only, cap}` decides the chip for an open chat (an agent chat
  gets none where the server says so); `named_only` shows the spec's `own` line in the
  popover and marks the chip. GET /chats/{chat}/team stays the fallback for older servers.
- Context: `auto_compact`, `until_compact` and `compactable` from the server (worked out from
  `threshold` with an older one); Compact greyed while not compactable.
- "Tasks: N" for a task's nested fold, the server's sentence; the subagent words aligned with
  the server's catalogues (Podagenti, Sottoagenti, Podagenci); hint says the same-model rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-09 17:15:38 +00:00
1 parent 6bad293b29
commit ede91887f7
8 files changed
+144 -116

No files matched your search

+17 -12
View File
@@ -475,20 +475,25 @@ function contextPart(c) {
}
/**
* How far it is to the automatic compaction -- the server compacts before a turn once the context
* would reach `threshold` % of the window; 0 is off -- and the button that compacts now.
* How far it is to the automatic compaction, and the button that compacts now. The server says it
* (2.0.0-beta.7: `auto_compact`, `until_compact` in tokens, `compactable` whether Compact can run
* now); an older one gives only `threshold` -- the server compacts before a turn once the context
* would reach that % of the window, 0 is off -- and it is worked out from that.
*/
function compactRow(c) {
const limit = Number(c.limit) || 0
const threshold = Number(c.threshold)
const auto = "auto_compact" in c ? !!c.auto_compact : threshold > 0
let until = c.until_compact
if (until === undefined && auto && limit && threshold < 100) until = Math.floor((limit * threshold) / 100) - (Number(c.used) || 0)
let words
if (!(threshold > 0)) words = t("Automatic compaction is off.")
else if (!limit || threshold >= 100) words = t("Compacts by itself once the window is known.")
else {
const until = Math.floor((limit * threshold) / 100) - (Number(c.used) || 0)
words = until > 0 ? t("%(tokens)s until auto-compact", { tokens: compact(until) }) : t("Compacts before the next reply.")
}
if (!auto) words = t("Automatic compaction is off.")
else if (until === null || until === undefined) words = t("Compacts by itself once the window is known.")
else words = until > 0 ? t("%(tokens)s until auto-compact", { tokens: compact(until) }) : t("Compacts before the next reply.")
const busy = compacting === chat?.id
// `compactable` false: nothing to summarise yet, a reply being written, compaction turned off --
// the button stays, greyed; pressing an older server's says why in its own words.
const can = !("compactable" in c) || !!c.compactable
return h(
"div",
{ class: "ctx__compact" },
@@ -497,7 +502,7 @@ function compactRow(c) {
type: "button",
class: "btn btn--sm",
text: busy ? t("Compacting…") : t("Compact"),
disabled: busy,
disabled: busy || !can,
title: t("Summarise the earlier turns now and send the summary in their place."),
onclick: compactNow,
}),
@@ -601,7 +606,7 @@ function roomPart() {
)
}
/** A folded list of tasks ("Show 3 more", "Finished 12", "2 tasks"), remembered across redraws. */
/** A folded list of tasks ("Show 3 more", "Finished 12", "Tasks: 2"), remembered across redraws. */
function fold(name, label, list, cls = "") {
const el = h(
"details",
@@ -613,11 +618,11 @@ function fold(name, label, list, cls = "") {
return el
}
/** A task and, folded under it, the tasks it leads or started ("1 task", "3 tasks"). */
/** A task and, folded under it, the tasks it leads or started ("Tasks: 3"). */
function agentRows(a) {
const kids = a.children || []
if (!kids.length) return agentRow(a)
const nested = fold(`under:${a.kind}:${a.id}`, kids.length === 1 ? t("1 task") : t("%(n)s tasks", { n: kids.length }), kids, "tasks__nested")
const nested = fold(`under:${a.kind}:${a.id}`, t("Tasks: %(n)s", { n: kids.length }), kids, "tasks__nested")
nested.style.setProperty("--depth", String(Math.max(0, Math.min(6, (Number(a.depth) || 0) + 1))))
return [agentRow(a), nested]
}
+15 -11
View File
@@ -13,7 +13,7 @@ import { markdown } from "./markdown.js"
import { openViewer, showKind } from "./viewer.js"
import { readLocal } from "./explorer.js"
import { menu, ask, showLine, popover, closePopover } from "./popup.js"
import { avatar, agentsControl, teamPossible, subagentsCap, SUBAGENTS_HINT, levelsOf, levelWord, picture } from "./models.js"
import { avatar, agentsControl, teamPossible, namedOnly, subagentsCap, SUBAGENTS_HINT, SUBAGENTS_OWN, levelsOf, levelWord, picture } from "./models.js"
import { readyIds, sending, sent, discard } from "./attach.js"
import { watchActivity, activityChanged, planList, activityButton } from "./activity.js"
import { agentBlock, agentReady, agentState, onAgentChange } from "./agent.js"
@@ -566,17 +566,20 @@ async function chatModels(chat) {
}
/**
* Whether this chat is offered subagents, and the instance's most: the chat's own word where the
* server gives one (`subagents` on the chat), else its team's (`GET /chats/{chat}/team`) -- only a
* chat on the server. null where it is not offered.
* Whether this chat is offered subagents: `{ cap, own }` (the instance's most; whether only a model
* the person names could serve them), or null. The chat's own `subagents` where the server gives
* it (2.0.0-beta.7), else its team's word (`GET /chats/{chat}/team`) -- only a chat on the server.
*/
async function teamOffered(chat) {
const said = chat.subagents
if (said !== undefined && said !== null) return (typeof said === "object" ? said.offered : said) ? { cap: subagentsCap(said, ctx.me()) } : null
if (said !== undefined && said !== null) {
const offered = typeof said === "object" ? said.offered : said
return offered ? { cap: subagentsCap(said), own: namedOnly(said) } : null
}
if (chat.kind !== "chat") return null
try {
const team = (await get(`/chats/${encodeURIComponent(chat.id)}/team`)).team
return team?.offered ? { cap: subagentsCap(team, ctx.me()) } : null
return team?.offered ? { cap: subagentsCap(), own: false } : null
} catch {
return null
}
@@ -652,13 +655,13 @@ function effortChip(model, value, onChange) {
* The subagents a chat may run: a chip saying the fewest and the most, opening the two fields.
* `value` { min, max } or empty; `cap` the instance's most, where the server says it.
*/
function agentsChip(value, onChange, cap) {
function agentsChip(value, onChange, { cap, own } = {}) {
const said = value?.min || value?.max ? `${value.min ?? "…"}–${value.max ?? "…"}` : t("default")
return chip({
ico: ICONS.users,
label: `${t("Subagents")} ${said}`,
title: SUBAGENTS_HINT(),
cls: "cchip--subagents",
title: own ? SUBAGENTS_OWN() : SUBAGENTS_HINT(),
cls: `cchip--subagents${own ? " is-warn" : ""}`,
onclick: (e) => {
const fields = agentsControl({ value, id: "agents-min", cap })
popover(
@@ -669,6 +672,7 @@ function agentsChip(value, onChange, cap) {
h("label", { class: "pop__title", for: "agents-min", text: t("Subagents") }),
h("div", { class: "agents__heads", "aria-hidden": "true" }, h("span", { text: t("Fewest") }), h("span"), h("span", { text: t("Most") })),
fields,
own ? h("p", { class: "hint is-warn", text: SUBAGENTS_OWN() }) : null,
h("p", { class: "hint", text: SUBAGENTS_HINT() }),
h("div", { class: "pop__actions" }, h("button", { type: "button", class: "btn btn--sm btn--primary", text: t("Save"), onclick: () => (closePopover(), onChange(fields.value)) })),
),
@@ -718,7 +722,7 @@ async function renderChips() {
renderChips()
}, mayPick))
if (model) parts.push(effortChip(model, draft.effort, (l) => ((draft.effort = l), renderChips())))
if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), subagentsCap(model?.subagents, ctx.me())))
if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), { cap: subagentsCap(), own: namedOnly(model?.subagents) }))
box.replaceChildren(...parts.filter(Boolean))
return
}
@@ -747,7 +751,7 @@ async function renderChips() {
box.replaceChildren(...parts.filter(Boolean))
const offer = await teamOffered(chat)
if (!offer || drawing !== chipsDrawn || open?.chat?.id !== chat.id) return
box.append(agentsChip(open.chat.agents || chat.agents, (a) => save({ agents: a }), offer.cap))
box.append(agentsChip(open.chat.agents || chat.agents, (a) => save({ agents: a }), offer))
}
/**
+4 -1
View File
@@ -19,6 +19,7 @@ import { initActivity, showPanel, togglePanel, panelOpen } from "./activity.js"
import { initAgent, link } from "./agent.js"
import { initScheduled, showScheduled, scheduledCommands } from "./scheduled.js"
import { closePopover } from "./popup.js"
import { noteSubagentsCap } from "./models.js"
let status = null
let view = "" // "setup" | "app"
@@ -118,7 +119,9 @@ function stopApp() {
async function loadModels() {
try {
models = (await get("/models")).models || []
const r = await get("/models")
models = r.models || []
noteSubagentsCap(r.subagents_cap)
} catch {
models = []
}
+23 -11
View File
@@ -193,13 +193,13 @@ export function effortControl({ model, value, onChange, id, compact }) {
}
/**
* Whether a new chat on `model` is offered subagents. The server says so where it can: a model's
* `subagents` in `/models` (a flag, or `{offered}`), true when subagents are on for the model and
* the instance and at least one model could serve as one -- the model itself with room for two
* requests at once, or a usable model outside its one-model-at-a-time server. A server that does
* not say it yet: a model that calls tools and serves two requests at once (`parallel` null is a
* hosted API with no stated limit). The server decides again; this only keeps the chip away where
* it could never apply. An open chat asks the server (`GET /chats/{chat}/team`).
* Whether a new chat on `model` is offered subagents: the model's `subagents.offered` in `/models`
* (server 2.0.0-beta.7: subagents on for the model and the instance, and at least one model could
* serve as one -- the model itself with room for two requests at once, or a usable model outside
* its one-model-at-a-time server). A server that does not say it: a model that calls tools and
* serves two requests at once (`parallel` null is a hosted API with no stated limit). The server
* decides again; this only keeps the chip away where it could never apply. An open chat has its
* own `subagents` (chat.js).
*/
export function teamPossible(model) {
const said = model?.subagents
@@ -207,16 +207,28 @@ export function teamPossible(model) {
return !!model?.tools && (model.parallel === null || model.parallel === undefined || model.parallel >= 2)
}
/** The most subagents the instance allows a chat (`cap`), where the server says it; else 32. */
/** Whether only a model the person names in the chat could serve its subagents (`named_only`). */
export function namedOnly(said) {
return !!(said && typeof said === "object" && said.named_only)
}
// The instance's most, as `/models` said it last (`subagents_cap`); 32 where it says nothing.
export const SUBAGENTS_CAP = 32
export const SUBAGENTS_HINT = () => t("The fewest and the most subagents this chat runs. They work on the same model unless you name another one in the chat.")
let knownCap = null
export function noteSubagentsCap(n) {
knownCap = Number(n) >= 1 ? Number(n) : null
}
/** The most subagents a chat may have: the first of `sources` that says it (`cap`), else the instance's. */
export function subagentsCap(...sources) {
for (const s of sources) {
const n = Number(s?.cap ?? s?.subagents_cap ?? s?.subagents?.cap)
const n = Number(s?.cap)
if (Number.isFinite(n) && n >= 1) return n
}
return SUBAGENTS_CAP
return knownCap || SUBAGENTS_CAP
}
export const SUBAGENTS_HINT = () => t("Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.")
// The spec's `subagents.setting.own`: beside the setting where the chat's model cannot serve them.
export const SUBAGENTS_OWN = () => t("This model serves one request at a time: subagents need another model, on another server, named in the chat.")
/**
* The subagents a chat may run: the fewest and the most, as two number fields on one shared