Subagents by name; the activity panel as the reference: header button, compact, tasks
ci / check (push) Failing after 3s
ci / start (push) Failing after 4s

- "Subagents" everywhere a person reads it (chip, popover with Fewest/Most, scheduled task,
  panel); offered per the server's `subagents` on a model or chat where it says it, else as
  before; the fields' most is the server's cap, else 32. No subagent-model setting.
- Panel: always reachable from the header's button; the line above the composer only while
  something runs; Environment for an agent chat; Context with "N until auto-compact" and a
  Compact button (POST /chats/{chat}/compact; a server without it is said so); Background
  tasks under Running with elapsed time and a stop square each, unfolding to kind, step,
  live text and conversation, nested tasks folded under their parent, Finished N folded;
  Room and Plan as sections.
- i18n: 24 new sentences x 8 languages, 9 retired; CHANGELOG [Unreleased].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-09 16:55:09 +00:00
1 parent 4058b4b018
commit 6bad293b29
10 files changed
+773 -236

No files matched your search

+32 -18
View File
@@ -13,9 +13,9 @@ 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, levelsOf, levelWord, picture } from "./models.js"
import { avatar, agentsControl, teamPossible, subagentsCap, SUBAGENTS_HINT, levelsOf, levelWord, picture } from "./models.js"
import { readyIds, sending, sent, discard } from "./attach.js"
import { watchActivity, activityChanged, planList } from "./activity.js"
import { watchActivity, activityChanged, planList, activityButton } from "./activity.js"
import { agentBlock, agentReady, agentState, onAgentChange } from "./agent.js"
import { folderName } from "./list.js"
@@ -30,7 +30,7 @@ export function openChatId() {
// --- Reading -----------------------------------------------------------------------------------
function onThisComputer(chat) {
export function onThisComputer(chat) {
return !!chat?.device && ctx.thisDevices().includes(chat.device.token_id)
}
@@ -552,6 +552,7 @@ function renderHead(chat) {
)
}
}
tools.append(activityButton())
tools.append(h("button", { type: "button", class: "tool", "aria-label": t("More"), title: t("More"), onclick: (e) => chatMenu(e.currentTarget, open?.chat || chat) }, icon(ICONS.more)))
}
@@ -564,13 +565,20 @@ async function chatModels(chat) {
}
}
/** Whether this chat may have a team (`GET /chats/{chat}/team`): only a chat on the server. */
/**
* 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.
*/
async function teamOffered(chat) {
if (chat.kind !== "chat") return false
const said = chat.subagents
if (said !== undefined && said !== null) return (typeof said === "object" ? said.offered : said) ? { cap: subagentsCap(said, ctx.me()) } : null
if (chat.kind !== "chat") return null
try {
return !!(await get(`/chats/${encodeURIComponent(chat.id)}/team`)).team?.offered
const team = (await get(`/chats/${encodeURIComponent(chat.id)}/team`)).team
return team?.offered ? { cap: subagentsCap(team, ctx.me()) } : null
} catch {
return false
return null
}
}
@@ -640,26 +648,31 @@ function effortChip(model, value, onChange) {
})
}
/** The team's size: a chip saying it, opening the two fields. `value` { min, max } or empty. */
function agentsChip(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) {
const said = value?.min || value?.max ? `${value.min ?? "…"}–${value.max ?? "…"}` : t("default")
return chip({
ico: ICONS.users,
label: `${t("Agents")} ${said}`,
title: t("Its own agent included. Above one, it can add members that do the work while it leads them."),
label: `${t("Subagents")} ${said}`,
title: SUBAGENTS_HINT(),
cls: "cchip--subagents",
onclick: (e) => {
const fields = agentsControl({ value, id: "agents-min" })
const fields = agentsControl({ value, id: "agents-min", cap })
popover(
e.currentTarget,
h(
"div",
{ class: "pop__body" },
h("label", { class: "pop__title", for: "agents-min", text: t("Agents") }),
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,
h("p", { class: "hint", text: t("Its own agent included. Above one, it can add members that do the work while it leads them.") }),
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)) })),
),
{ up: true, label: t("Agents") },
{ up: true, label: t("Subagents") },
)
},
})
@@ -705,7 +718,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())))
if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), subagentsCap(model?.subagents, ctx.me())))
box.replaceChildren(...parts.filter(Boolean))
return
}
@@ -732,8 +745,9 @@ async function renderChips() {
if (chat.model) parts.push(modelChip(models, chat.model, (id) => save({ model: id }), mayPick))
parts.push(effortChip(current, chat.effort || levelsOf(current).start, (l) => save({ effort: l })))
box.replaceChildren(...parts.filter(Boolean))
if (!(await teamOffered(chat)) || drawing !== chipsDrawn || open?.chat?.id !== chat.id) return
box.append(agentsChip(open.chat.agents || chat.agents, (a) => save({ agents: a })))
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))
}
/**