Plugins: use var for reserved commands to avoid bundler TDZ; scope expand button to assistant; wire cron run navigate

This commit is contained in:
Val Alexander
2026-03-17 23:52:29 -05:00
parent 0bd4c7cd43
commit 5116dbeb60
2 changed files with 54 additions and 53 deletions

View File

@@ -35,56 +35,15 @@ let registryLocked = false;
const MAX_ARGS_LENGTH = 4096;
/**
* Reserved command names that plugins cannot override.
* These are built-in commands from commands-registry.data.ts.
* Reserved command names that plugins cannot override (built-in commands).
*
* Lazily initialized to avoid TDZ errors when the bundler places this
* module's body after call sites within the same output chunk.
* Constructed lazily inside validateCommandName to avoid TDZ errors: the
* bundler can place this module's body after call sites within the same
* output chunk, so any module-level const/let would be uninitialized when
* first accessed during plugin registration.
*/
let _reservedCommands: Set<string> | undefined;
function getReservedCommands(): Set<string> {
return (_reservedCommands ??= new Set([
// Core commands
"help",
"commands",
"status",
"whoami",
"context",
"btw",
// Session management
"stop",
"restart",
"reset",
"new",
"compact",
// Configuration
"config",
"debug",
"allowlist",
"activation",
// Agent control
"skill",
"subagents",
"kill",
"steer",
"tell",
"model",
"models",
"queue",
// Messaging
"send",
// Execution
"bash",
"exec",
// Mode toggles
"think",
"verbose",
"reasoning",
"elevated",
// Billing
"usage",
]));
}
// eslint-disable-next-line no-var -- var avoids TDZ when bundler reorders module bodies in a chunk
var reservedCommands: Set<string> | undefined;
/**
* Validate a command name.
@@ -103,8 +62,41 @@ export function validateCommandName(name: string): string | null {
return "Command name must start with a letter and contain only letters, numbers, hyphens, and underscores";
}
// Check reserved commands
if (getReservedCommands().has(trimmed)) {
reservedCommands ??= new Set([
"help",
"commands",
"status",
"whoami",
"context",
"btw",
"stop",
"restart",
"reset",
"new",
"compact",
"config",
"debug",
"allowlist",
"activation",
"skill",
"subagents",
"kill",
"steer",
"tell",
"model",
"models",
"queue",
"send",
"bash",
"exec",
"think",
"verbose",
"reasoning",
"elevated",
"usage",
]);
if (reservedCommands.has(trimmed)) {
return `Command name "${trimmed}" is reserved by a built-in command`;
}

View File

@@ -675,7 +675,7 @@ export function renderCron(props: CronProps) {
`
: html`
<div class="list" style="margin-top: 12px;">
${runs.map((entry) => renderRun(entry, props.basePath))}
${runs.map((entry) => renderRun(entry, props.basePath, props.onNavigateToChat))}
</div>
`
}
@@ -1710,7 +1710,11 @@ function runDeliveryLabel(value: string): string {
}
}
function renderRun(entry: CronRunLogEntry, basePath: string) {
function renderRun(
entry: CronRunLogEntry,
basePath: string,
onNavigateToChat?: (sessionKey: string) => void,
) {
const chatUrl =
typeof entry.sessionKey === "string" && entry.sessionKey.trim().length > 0
? `${pathForTab("chat", basePath)}?session=${encodeURIComponent(entry.sessionKey)}`
@@ -1750,7 +1754,12 @@ function renderRun(entry: CronRunLogEntry, basePath: string) {
}
${
chatUrl
? html`<div><a class="session-link" href=${chatUrl}>${t("cron.runEntry.openRunChat")}</a></div>`
? html`<div><a class="session-link" href=${chatUrl} @click=${(e: Event) => {
if (onNavigateToChat && entry.sessionKey) {
e.preventDefault();
onNavigateToChat(entry.sessionKey);
}
}}>${t("cron.runEntry.openRunChat")}</a></div>`
: nothing
}
${entry.error ? html`<div class="muted">${entry.error}</div>` : nothing}