Why this matters
A local command is the path for slash commands that run local code directly and return a concrete result to the session.
The execution path is simple: slash command -> lazy load module -> run local code -> return a concrete result.
That matters because the command is doing real work in the runtime, not asking the model to keep driving the interaction.
/compact as the case study
/compact is the best case study because it shows argument handling, direct
side effects, and a LocalCommandResult value in one place.
import type { Command } from '../../commands.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
const compact = {
type: 'local',
name: 'compact',
description:
'Clear conversation history but keep a summary in context. Optional: /compact [instructions for summarization]',
isEnabled: () => !isEnvTruthy(process.env.DISABLE_COMPACT),
supportsNonInteractive: true,
argumentHint: '<optional custom summarization instructions>',
load: () => import('./compact.js'),
} satisfies Command
export default compact
This loader keeps the command definition small. The command advertises that it is local, that it can run outside the interactive UI, and that it accepts optional summarization instructions.
The implementation does the actual session work. It trims the arguments, tries session-memory compaction first, then checks a reactive-only branch before it falls back to the legacy compaction path if needed.
export const call: LocalCommandCall = async (args, context) => {
const { abortController } = context
let { messages } = context
// REPL keeps snipped messages for UI scrollback — project so the compact
// model doesn't summarize content that was intentionally removed.
messages = getMessagesAfterCompactBoundary(messages)
if (messages.length === 0) {
throw new Error('No messages to compact')
}
const customInstructions = args.trim()
try {
// Try session memory compaction first if no custom instructions
// (session memory compaction doesn't support custom instructions)
if (!customInstructions) {
const sessionMemoryResult = await trySessionMemoryCompaction(
messages,
context.agentId,
)
if (sessionMemoryResult) {
getUserContext.cache.clear?.()
runPostCompactCleanup()
// Reset cache read baseline so the post-compact drop isn't flagged
// as a break. compactConversation does this internally; SM-compact doesn't.
if (feature('PROMPT_CACHE_BREAK_DETECTION')) {
notifyCompaction(
context.options.querySource ?? 'compact',
context.agentId,
)
}
markPostCompaction()
// Suppress warning immediately after successful compaction
suppressCompactWarning()
return {
type: 'compact',
compactionResult: sessionMemoryResult,
displayText: buildDisplayText(context),
}
}
}
// Reactive-only mode: route /compact through the reactive path.
// Checked after session-memory (that path is cheap and orthogonal).
if (reactiveCompact?.isReactiveOnlyMode()) {
return await compactViaReactive(
messages,
context,
customInstructions,
reactiveCompact,
)
}
// Fall back to traditional compaction
// Run microcompact first to reduce tokens before summarization
const microcompactResult = await microcompactMessages(messages, context)
const messagesForCompact = microcompactResult.messages
const result = await compactConversation(
messagesForCompact,
context,
await getCacheSharingParams(context, messagesForCompact),
false,
customInstructions,
false,
)
// Reset lastSummarizedMessageId since legacy compaction replaces all messages
// and the old message UUID will no longer exist in the new messages array
setLastSummarizedMessageId(undefined)
// Suppress the "Context left until auto-compact" warning after successful compaction
suppressCompactWarning()
getUserContext.cache.clear?.()
runPostCompactCleanup()
return {
type: 'compact',
compactionResult: result,
displayText: buildDisplayText(context, result.userDisplayMessage),
}
} catch (error) {
if (abortController.signal.aborted) {
throw new Error('Compaction canceled.')
} else if (hasExactErrorMessage(error, ERROR_MESSAGE_NOT_ENOUGH_MESSAGES)) {
throw new Error(ERROR_MESSAGE_NOT_ENOUGH_MESSAGES)
} else if (hasExactErrorMessage(error, ERROR_MESSAGE_INCOMPLETE_RESPONSE)) {
throw new Error(ERROR_MESSAGE_INCOMPLETE_RESPONSE)
} else {
logError(error)
throw new Error(`Error during compaction: ${error}`)
}
}
}
The important part is the return value. A local command does not just emit text;
it returns a concrete LocalCommandResult that can carry both the compaction
result and the display text the session should show.
The smallest non-interactive wrapper
contextNonInteractive shows the smallest possible local command wrapper.
It keeps the command visible in non-interactive mode, hides it in the wrong
mode, and points at the lazy-loaded implementation.
export const contextNonInteractive: Command = {
type: 'local',
name: 'context',
supportsNonInteractive: true,
description: 'Show current context usage',
get isHidden() {
return !getIsNonInteractiveSession()
},
isEnabled() {
return getIsNonInteractiveSession()
},
load: () => import('./context-noninteractive.js'),
}
It can also work in non-interactive mode because it has a separate
supportsNonInteractive wrapper and does not need to open a screen.
Why this is not local-jsx
local commands are different from local-jsx commands: they do work directly
instead of opening a screen.
That is the whole split. A local-jsx command loads React UI and uses that UI
to hand control back later. A local command runs the work in the local
runtime, changes session state immediately, and returns a concrete result when
it is done.