Compare commits

..

1 Commits

Author SHA1 Message Date
James Long 93c733c5f2 fix(tui): update tab titles immediately 2026-08-03 20:20:02 +00:00
7 changed files with 17 additions and 46 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ const layer = Layer.effect(
draft.default = id
},
update: (id, fn) => {
const current = draft.agents.get(id) ?? (Info.default(id) as Types.DeepMutable<Info>)
const current = draft.agents.get(id) ?? (Info.empty(id) as Types.DeepMutable<Info>)
if (!draft.agents.has(id)) draft.agents.set(id, current)
fn(current)
current.id = id
+1 -1
View File
@@ -120,7 +120,7 @@ describe("Agent", () => {
const id = Agent.ID.make("custom")
yield* agent.transform((editor) => editor.update(id, () => {}))
expect(yield* agent.get(id)).toEqual(Agent.Info.default(id))
expect(yield* agent.get(id)).toEqual(Agent.Info.empty(id))
yield* agent.transform((editor) => editor.remove(id))
expect(yield* agent.get(id)).toBeUndefined()
+1 -1
View File
@@ -12,7 +12,7 @@ import { readInitial, readUpdate } from "./lib/instructions"
const build = Agent.ID.make("build")
const selection = (permissions: Permission.Ruleset = []) => {
const info = Agent.Info.make({ ...Agent.Info.default(build), permissions })
const info = Agent.Info.make({ ...Agent.Info.empty(build), permissions })
return { id: info.id, info }
}
+1 -1
View File
@@ -166,7 +166,7 @@ test("Core reuses the canonical shared schemas", async () => {
]
for (const [core, shared] of schemas) expect(core).toBe(shared)
expect(Agent.Info.default(Agent.ID.make("test"))).toEqual(Agent.Info.default(Agent.ID.make("test")))
expect(Agent.Info.empty(Agent.ID.make("test"))).toEqual(Agent.Info.empty(Agent.ID.make("test")))
expect(coreModel.Info.default(coreProvider.ID.make("test"), coreModel.ID.make("model"))).toEqual(
Model.Info.default(Provider.ID.make("test"), Model.ID.make("model")),
)
@@ -47,7 +47,7 @@ const layer = (list: () => Skill.Info[]) =>
describe("SkillInstructions", () => {
it.effect("renders described agent skills and updates the complete available list", () => {
const agent = Agent.Info.make({
...Agent.Info.default(build),
...Agent.Info.empty(build),
permissions: [{ action: "skill", resource: "denied", effect: "deny" }],
})
let skills = [hidden, denied, manual, effect]
@@ -80,7 +80,7 @@ describe("SkillInstructions", () => {
})
it.effect("announces added and removed skills as deltas without restating the list", () => {
const agent = Agent.Info.make(Agent.Info.default(build))
const agent = Agent.Info.make(Agent.Info.empty(build))
const debugging = Skill.Info.make({
id: Skill.ID.make("debugging"),
name: Skill.Name.make("Debugging"),
@@ -117,7 +117,7 @@ describe("SkillInstructions", () => {
})
it.effect("restates the full skill list when a description changes", () => {
const agent = Agent.Info.make(Agent.Info.default(build))
const agent = Agent.Info.make(Agent.Info.empty(build))
let skills = [effect]
return Effect.gen(function* () {
const instructions = yield* SkillInstructions.Service
@@ -138,7 +138,7 @@ describe("SkillInstructions", () => {
it.effect("omits instructions when the selected agent denies all skills", () => {
const agent = Agent.Info.make({
...Agent.Info.default(build),
...Agent.Info.empty(build),
permissions: [{ action: "skill", resource: "*", effect: "deny" }],
})
return Effect.gen(function* () {
@@ -149,7 +149,7 @@ describe("SkillInstructions", () => {
it.effect("omits instructions when a resource-specific denial follows the global denial", () => {
const agent = Agent.Info.make({
...Agent.Info.default(build),
...Agent.Info.empty(build),
permissions: [
{ action: "skill", resource: "*", effect: "deny" },
{ action: "skill", resource: "hidden", effect: "deny" },
@@ -163,7 +163,7 @@ describe("SkillInstructions", () => {
it.effect("retains specifically allowed skills after a global denial", () => {
const agent = Agent.Info.make({
...Agent.Info.default(build),
...Agent.Info.empty(build),
permissions: [
{ action: "skill", resource: "*", effect: "deny" },
{ action: "skill", resource: "effect", effect: "allow" },
@@ -179,7 +179,7 @@ describe("SkillInstructions", () => {
it.effect("omits instructions when a specifically allowed skill is denied again", () => {
const agent = Agent.Info.make({
...Agent.Info.default(build),
...Agent.Info.empty(build),
permissions: [
{ action: "skill", resource: "*", effect: "deny" },
{ action: "skill", resource: "effect", effect: "allow" },
+1 -1
View File
@@ -36,7 +36,7 @@ export const Info = Schema.Struct({
.annotate({ identifier: "Agent.Info" })
.pipe(
statics(() => ({
default: (id: ID) =>
empty: (id: ID) =>
({
id,
name: Name.make(id),
+5 -34
View File
@@ -15,7 +15,7 @@ import {
type SessionTab,
type SessionTabUnread,
} from "../context/session-tabs-model"
import { createAnimatable, spring, tween } from "../ui/animation"
import { createAnimatable, spring } from "../ui/animation"
import { Locale } from "../util/locale"
import { stringWidth } from "../util/string-width"
import { TabPulse, unreadGlowIntensity } from "./tab-pulse"
@@ -554,21 +554,6 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
const glowColor = () => feedbackColor() ?? accent()
const glows = () => !selected() && (status().attention || (!status().busy && status().unread !== undefined))
const title = () => tab.title ?? "Untitled session"
const [outgoingTitle, setOutgoingTitle] = createSignal<string>()
const wipe = createAnimatable({ front: 1 }, { enabled: animations, transition: tween({ duration: 0.3 }) })
createEffect((previous: string) => {
const next = title()
if (next === previous) return next
if (previous === NEW_SESSION_TAB_TITLE) {
setOutgoingTitle(undefined)
wipe.jump({ front: 1 })
return next
}
setOutgoingTitle(previous)
wipe.jump({ front: 0 })
wipe.animate({ front: 1 })
return next
}, title())
const tabNumber = createMemo(() => items().findIndex((item) => item.sessionID === tab.sessionID) + 1)
// The number cell keeps one trailing space, even for double-digit tabs.
const numberWidth = () => String(tabNumber()).length + 1
@@ -577,20 +562,6 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
Math.max(1, width() - 1 - numberWidth() - (hovered() === tab.sessionID ? 2 : 0))
const visibleTitle = createMemo(() => Locale.takeWidth(title(), availableTitleWidth()))
const visibleTitleParts = createMemo(() => Locale.graphemes(visibleTitle()))
const outgoingTitleParts = createMemo(() => {
const outgoing = outgoingTitle()
if (outgoing === undefined) return undefined
return Locale.graphemes(Locale.takeWidth(outgoing, availableTitleWidth()))
})
// A new title wipes in from the left over the previous one.
const displayedParts = createMemo(() => {
const front = wipe.value().front
const parts = visibleTitleParts()
const previous = outgoingTitleParts()
if (previous === undefined || front >= 1) return parts
const cut = Math.round(front * Math.max(parts.length, previous.length))
return [...parts.slice(0, cut), ...previous.slice(cut)]
})
const titleFades = createMemo(
() => stringWidth(title()) >= availableTitleWidth() && availableTitleWidth() > FADE_WIDTH,
)
@@ -603,8 +574,8 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
const characterColor = (index: number) => {
const base = foreground()
const color = glows() ? glowTextColor(base, glowColor(), 1 + numberWidth() + index, width()) : base
if (!titleFades() || index < displayedParts().length - FADE_WIDTH) return color
const position = index - (displayedParts().length - FADE_WIDTH)
if (!titleFades() || index < visibleTitleParts().length - FADE_WIDTH) return color
const position = index - (visibleTitleParts().length - FADE_WIDTH)
return tint(color, background(), 0.2 + 0.72 * (position / Math.max(1, FADE_WIDTH - 1)))
}
// The running sweep's level under the number cell, reported by the pulse renderable.
@@ -677,8 +648,8 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
selectable={false}
attributes={bold()}
>
<Show when={glows() || titleFades()} fallback={displayedParts().join("")}>
<For each={displayedParts()}>
<Show when={glows() || titleFades()} fallback={visibleTitle()}>
<For each={visibleTitleParts()}>
{(character, index) => <span style={{ fg: characterColor(index()) }}>{character}</span>}
</For>
</Show>