feat(chat): /summary slash command for non-destructive session summaries

Typing /summary (optionally with a topic hint) produces a structured
summary as a normal assistant message — without compacting or mutating
session history. Reuses the existing "Start new session from this answer"
button for hand-off.

Prompts are customizable in Settings → Magic Prompts under a new Session
group. Autocomplete shows an 'openchamber' badge to distinguish our
commands from OpenCode-built-in ones.
This commit is contained in:
Bohdan Triapitsyn
2026-04-22 21:56:35 +03:00
parent f24e6de21b
commit 863be8073b
5 changed files with 108 additions and 3 deletions
@@ -28,6 +28,7 @@ import * as sessionActions from '@/sync/session-actions';
import { useUserMessageHistory } from '@/sync/sync-context';
import { useInlineCommentDraftStore, type InlineCommentDraft } from '@/stores/useInlineCommentDraftStore';
import { appendInlineComments } from '@/lib/messages/inlineComments';
import { renderMagicPrompt } from '@/lib/magicPrompts';
import { AttachedFilesList } from './FileAttachment';
import { QueuedMessageChips } from './QueuedMessageChips';
import { FileMentionAutocomplete, type FileMentionHandle } from './FileMentionAutocomplete';
@@ -1533,6 +1534,35 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
}
return;
}
else if (commandName === 'summary' && currentSessionId) {
try {
await sessionActions.waitForConnectionOrThrow();
// Everything after `/summary ` is an optional topic hint
// the user wants the summary focused on.
const topic = normalizedCommand.replace(/^\/summary\b/i, '').trim();
const topicLine = topic ? ` focused on: ${topic}` : '';
const topicBlock = topic
? `The user asked you to focus this summary on: ${topic}. Prioritize that topic; mention unrelated threads only in passing.`
: '';
const visibleText = await renderMagicPrompt('session.summary.visible', { topic_line: topicLine });
const instructionsText = await renderMagicPrompt('session.summary.instructions', { topic_block: topicBlock });
await sendMessage(
visibleText,
currentProviderId,
currentModelId,
currentAgentName,
[],
agentMentionName,
[{ text: instructionsText, synthetic: true }],
currentVariant,
inputMode,
);
scrollToBottom?.({ instant: true, force: true });
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to generate summary');
}
return;
}
}
// Collect all attachments for error recovery
@@ -13,6 +13,7 @@ interface CommandInfo {
agent?: string;
model?: string;
isBuiltIn?: boolean;
isOpenChamber?: boolean;
isSkill?: boolean;
scope?: string;
}
@@ -111,6 +112,10 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
: []
),
{ name: 'compact', description: 'Compress session history using AI to reduce context size', isBuiltIn: true },
...(hasSession
? [{ name: 'summary', description: 'Non-destructive session summary. Optional topic hint after the command.', isOpenChamber: true }]
: []
),
];
const commandMap = new Map<string, CommandInfo>();
@@ -154,6 +159,10 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
: []
),
{ name: 'compact', description: 'Compress session history using AI to reduce context size', isBuiltIn: true },
...(hasSession
? [{ name: 'summary', description: 'Non-destructive session summary. Optional topic hint after the command.', isOpenChamber: true }]
: []
),
];
const filtered = (searchQuery
@@ -293,6 +302,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
<div>
{commands.map((command, index) => {
const isSystem = command.isBuiltIn;
const isOpenChamberBadge = command.isOpenChamber;
const isProject = command.scope === 'project';
return (
@@ -359,7 +369,18 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
skill
</span>
) : null}
{isSystem ? (
{isOpenChamberBadge ? (
<span
className="text-[10px] leading-none uppercase font-bold tracking-tight px-1.5 py-1 rounded border flex-shrink-0"
style={{
backgroundColor: 'color-mix(in srgb, var(--primary-base) 14%, transparent)',
color: 'var(--primary-base)',
borderColor: 'color-mix(in srgb, var(--primary-base) 28%, transparent)',
}}
>
openchamber
</span>
) : isSystem ? (
<span className="text-[10px] leading-none uppercase font-bold tracking-tight bg-[var(--status-warning-background)] text-[var(--status-warning)] border-[var(--status-warning-border)] px-1.5 py-1 rounded border flex-shrink-0">
system
</span>
@@ -123,6 +123,14 @@ const PROMPT_PAGE_MAP: Record<string, PromptPageConfig> = {
{ id: 'plan.implement.instructions', title: 'Instructions' },
],
},
'session.summary': {
title: 'Session Summary',
description: 'Prompts used by the /summary slash command: visible user message + hidden instructions. Non-destructive — does not compact session history.',
blocks: [
{ id: 'session.summary.visible', title: 'Visible Prompt' },
{ id: 'session.summary.instructions', title: 'Instructions' },
],
},
};
const hasOwn = (input: Record<string, string>, key: string) => Object.prototype.hasOwnProperty.call(input, key);
@@ -40,6 +40,12 @@ export const MagicPromptsSidebar: React.FC<MagicPromptsSidebarProps> = ({ onItem
{ id: 'plan.implement', title: 'Implement Plan' },
],
},
{
group: 'Session',
items: [
{ id: 'session.summary', title: 'Session Summary' },
],
},
] as const;
}, []);
+42 -2
View File
@@ -22,13 +22,15 @@ export type MagicPromptId =
| 'plan.improve.visible'
| 'plan.improve.instructions'
| 'plan.implement.visible'
| 'plan.implement.instructions';
| 'plan.implement.instructions'
| 'session.summary.visible'
| 'session.summary.instructions';
export interface MagicPromptDefinition {
id: MagicPromptId;
title: string;
description: string;
group: 'Git' | 'GitHub' | 'Planning';
group: 'Git' | 'GitHub' | 'Planning' | 'Session';
template: string;
placeholders?: Array<{ key: string; description: string }>;
}
@@ -466,6 +468,44 @@ Do the implementation work continuously. When a plan step is ambiguous, do not s
Do not expand scope beyond the plan. If during implementation you find the plan itself is wrong or genuinely blocks completion (not merely ambiguous), stop, state exactly what is broken and why, and propose a plan adjustment to save back into this same file ({{plan_path}}) before continuing.`,
},
{
id: 'session.summary.visible',
title: 'Session Summary Visible Prompt',
group: 'Session',
description: 'Visible user message sent by the /summary command.',
placeholders: [
{ key: 'topic_line', description: 'Pre-formatted topic clause (e.g. " focused on: <topic>") or empty string.' },
],
template: 'Summarize this session{{topic_line}}.',
},
{
id: 'session.summary.instructions',
title: 'Session Summary Instructions',
group: 'Session',
description: 'Hidden instructions attached to the /summary command. Produces a non-destructive summary usable for handing off to a new session.',
placeholders: [
{ key: 'topic_block', description: 'Pre-formatted topic focus paragraph, or empty string when no topic hint was given.' },
],
template: `Produce a non-destructive summary of this conversation. Do NOT compact or mutate session history — your output is an additional assistant message the user will read and may use to hand off to a new session.
Cover the information useful for continuing this work:
- What was done (completed work, in order)
- What is currently in progress
- Files modified brief what and why per file
- Open questions and next steps
- User requests, constraints, or preferences to carry forward
- Important technical decisions and why they were made
{{topic_block}}
Formatting:
- Concise markdown with short sections and bullet lists
- No preamble like "Here is a summary" jump straight to content
- Do not answer questions found in the conversation only summarize
- Keep length proportional to session length; do not pad
Respond in the same language the user used most in the conversation.`,
},
] as const;
const MAGIC_PROMPT_DEFINITION_BY_ID = new Map<MagicPromptId, MagicPromptDefinition>(