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:
@@ -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;
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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>(
|
||||
|
||||
Reference in New Issue
Block a user