Skip to content
This repository was archived by the owner on Aug 17, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 76 additions & 10 deletions plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ function trackInboundActivity(roster) {
/** Last good cron list, same idea as the roster snapshot. */
const $lastJobs = atom([])

/** User pref: hide canonical "Bot Chat" sessions from the global Sessions
* sidebar (they always remain in the Bots roster). Persisted via ctx.storage.
* Default ON — Bot Chats are plugin-owned forever-chats, not scratch sessions,
* so keeping them out of the shared recents list is the expected behavior.
* Backed by the core generic `hidden` session flag (session.create hidden:true
* / session.set_hidden); older gateways ignore it and Bot Chats stay visible. */
const $hideBotChats = atom(true)

/** Bot the Routines tile is scoped to. Follows the live gateway profile
* (the bot you're actually chatting with) and roster clicks. */
const $selectedBot = atom('default')
Expand Down Expand Up @@ -167,6 +175,31 @@ function saveBotMeta(name, patch) {
}
}

/** Flip the "hide Bot Chats from the sidebar" pref, persist it, and reconcile
* every known canonical chat via the core session.set_hidden RPC so the change
* applies to already-created Bot Chats (not just future ones). Feature-detected:
* older gateways lack session.set_hidden and simply keep the chats visible. */
async function setHideBotChats(hidden) {
$hideBotChats.set(hidden)

try {
Promise.resolve(pluginCtx?.storage?.set?.('hide-bot-chats', hidden)).catch(() => undefined)
} catch {
/* storage unavailable — pref holds for this window only */
}

const meta = $botMeta.get()
const ids = Object.values(meta)
.map(m => m && m.chat)
.filter(Boolean)

await Promise.all(
ids.map(sid =>
Promise.resolve(host.request('session.set_hidden', { session_id: sid, hidden })).catch(() => undefined)
)
)
}

/** Fetch server-side avatars for roster rows flagged has_avatar when the
* local cache doesn't already have an image for them. Fire-and-forget. */
const avatarFetchInflight = new Set()
Expand Down Expand Up @@ -1532,7 +1565,11 @@ function createCanonicalChat(name) {
const run = (async () => {
const res = await host.request('session.create', {
profile: name,
title: 'Bot Chat'
title: 'Bot Chat',
// Born hidden from the global sidebar when the pref is on. Core applies
// this via the generic `hidden` flag (deferred as pending_hidden until the
// row exists); older gateways ignore the unknown param and it stays visible.
...($hideBotChats.get() ? { hidden: true } : {})
})
const sid = res?.stored_session_id
const runtime = res?.session_id
Expand Down Expand Up @@ -4257,6 +4294,7 @@ function BotsPane() {
const [editing, setEditing] = useState(null)
const [deleting, setDeleting] = useState(null)
const [query, setQuery] = useState('')
const hideBotChats = useValue($hideBotChats)

// The socket opening (boot, SSH reconnect, sleep/wake) is the signal to
// retry immediately instead of waiting out the poll interval.
Expand Down Expand Up @@ -4319,15 +4357,30 @@ function BotsPane() {
className: 'text-[0.6875rem] font-semibold uppercase tracking-wider text-(--ui-text-quaternary)',
children: 'Bots'
}),
jsx(Tip, {
label: 'New Agent',
children: jsx('button', {
type: 'button',
className:
'flex size-6 items-center justify-center rounded-md text-(--ui-text-tertiary) transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground',
onClick: () => setCreateOpen(true),
children: jsx(Codicon, { name: 'add' })
})
jsxs('div', {
className: 'flex items-center gap-0.5',
children: [
jsx(Tip, {
label: hideBotChats ? 'Bot Chats hidden from Sessions — click to show' : 'Bot Chats shown in Sessions — click to hide',
children: jsx('button', {
type: 'button',
className:
'flex size-6 items-center justify-center rounded-md text-(--ui-text-tertiary) transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground',
onClick: () => void setHideBotChats(!hideBotChats),
children: jsx(Codicon, { name: hideBotChats ? 'eye-closed' : 'eye' })
})
}),
jsx(Tip, {
label: 'New Agent',
children: jsx('button', {
type: 'button',
className:
'flex size-6 items-center justify-center rounded-md text-(--ui-text-tertiary) transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground',
onClick: () => setCreateOpen(true),
children: jsx(Codicon, { name: 'add' })
})
})
]
})
]
}),
Expand Down Expand Up @@ -4487,6 +4540,19 @@ export default {
/* no storage on this shell — defaults stay */
}

// Hydrate the "hide Bot Chats from the sidebar" pref (default ON).
try {
Promise.resolve(ctx.storage?.get?.('hide-bot-chats'))
.then(value => {
if (typeof value === 'boolean') {
$hideBotChats.set(value)
}
})
.catch(() => undefined)
} catch {
/* no storage — default (hide) stays */
}

// Routines follow the chat you're in: track the live gateway profile.
host.state.profile.listen(profile => {
if (profile && typeof profile === 'string') {
Expand Down
1 change: 1 addition & 0 deletions tests/canonical-chat-creation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function loadCanonicalCreation({ openSession, request }) {
const context = {
host: { openSession, request },
saveBotMeta: (name, patch) => saved.push({ name, patch }),
$hideBotChats: { get: () => false },
window: { setTimeout: callback => callback() }
}
const section = source
Expand Down
1 change: 1 addition & 0 deletions tests/canonical-chat-empty-recovery.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function loadCanonicalRecovery({ openSession, request }) {
const context = {
host: { openSession, request },
saveBotMeta: (name, patch) => saved.push({ name, patch }),
$hideBotChats: { get: () => false },
window: { setTimeout: callback => callback() }
}
const section = source
Expand Down
59 changes: 59 additions & 0 deletions tests/hide-bot-chats.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import vm from 'node:vm'

// #46: canonical "Bot Chat" sessions can be hidden from the global Sessions
// sidebar via the core generic `hidden` session flag, while staying in the Bots
// roster. The plugin passes hidden:true on session.create when the pref is on,
// and setHideBotChats reconciles existing chats via session.set_hidden.

const source = readFileSync(new URL('../plugin.js', import.meta.url), 'utf8')

function loadCreate(hidePref) {
const start = source.indexOf('const canonicalCreations = new Map()')
const end = source.indexOf('function displayName(', start)
const created = []
const context = {
host: {
openSession: async () => {},
request: async (method, params) => {
if (method === 'session.create') {
created.push(params)
return { stored_session_id: 'sid-1', session_id: 'rt-1' }
}
return {}
}
},
saveBotMeta: () => {},
$hideBotChats: { get: () => hidePref },
window: { setTimeout: cb => cb() }
}
const section = source.slice(start, end).concat('\nglobalThis.__c = { createCanonicalChat };\n')
vm.runInNewContext(section, context, { filename: 'c.js' })
return { create: context.__c.createCanonicalChat, created }
}

test('createCanonicalChat passes hidden:true when the pref is on', async () => {
const { create, created } = loadCreate(true)
await create('alpha')
assert.equal(created.length, 1)
assert.equal(created[0].hidden, true)
assert.equal(created[0].title, 'Bot Chat')
})

test('createCanonicalChat omits hidden when the pref is off', async () => {
const { create, created } = loadCreate(false)
await create('beta')
assert.equal(created.length, 1)
assert.ok(!('hidden' in created[0]), 'hidden should be omitted, not false')
})

test('setHideBotChats persists the pref and reconciles known chats via session.set_hidden', () => {
// Source-level: the toggle calls session.set_hidden for each known canonical
// chat id and persists the pref to storage.
const fn = source.slice(source.indexOf('async function setHideBotChats('), source.indexOf('const avatarFetchInflight'))
assert.match(fn, /storage\?\.set\?\.\('hide-bot-chats', hidden\)/)
assert.match(fn, /session\.set_hidden.*session_id: sid, hidden/)
assert.match(fn, /\.map\(m => m && m\.chat\)/)
})