Skip to content

Commit 784f515

Browse files
committed
feat: slash commands + cross-session history + mode shortcuts
Slash Commands: - Added /fix, /test, /review (coding), /ask, /agent, /plan (mode switch), /clear, /compact, /model (session). Total: 30 commands with categories. - /ask, /agent, /plan switch modes locally (no gateway round-trip) - /clear resets chat + activities Cross-Session History: - lib/chat-history.ts: localStorage-backed input history (50 entries max) - ↑/↓ arrow keys recall previous inputs from any session - HistoryNavigator class preserves current draft while browsing - History saved on every send, deduped, most-recent-first Mode Cycling: - ⇧Tab hint shown next to mode selector - Shift+Tab keyboard shortcut cycles Ask → Agent → Plan
1 parent 50db321 commit 784f515

3 files changed

Lines changed: 145 additions & 14 deletions

File tree

components/agent-panel.tsx

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
getEffectiveSystemPrompt,
5252
getAgentConfig,
5353
} from '@/lib/agent-session'
54+
import { addChatHistory, HistoryNavigator } from '@/lib/chat-history'
5455
import {
5556
SKILL_FIRST_OVERRIDE_TOKEN,
5657
buildSkillFirstBlockMessage,
@@ -396,6 +397,7 @@ export function AgentPanel() {
396397
} | null>(null)
397398

398399
const inputRef = useRef<HTMLTextAreaElement>(null)
400+
const historyNav = useRef(new HistoryNavigator())
399401
const sessionInitRef = useRef(false)
400402
const sentKeysRef = useRef(new Set<string>())
401403
const handledKeysRef = useRef(new Set<string>())
@@ -554,6 +556,9 @@ export function AgentPanel() {
554556
}
555557
}, [isStreaming, sending, turnStartTime])
556558

559+
// ─── Load chat input history ─────────────────────────────────
560+
useEffect(() => { historyNav.current.load() }, [])
561+
557562
// ─── Listen for chat events (streaming replies) ───────────────
558563
useEffect(() => {
559564
const callbacks = {
@@ -1213,6 +1218,21 @@ export function AgentPanel() {
12131218
const text = (overrideText ?? input).trim()
12141219
if (!text || sending) return
12151220

1221+
// Handle slash commands locally
1222+
if (text === '/ask') { setAgentMode('ask'); setInput(''); return }
1223+
if (text === '/agent') { setAgentMode('agent'); setInput(''); return }
1224+
if (text === '/plan') { setAgentMode('plan'); setInput(''); return }
1225+
if (text === '/clear') {
1226+
setMessages([])
1227+
setAgentActivities([])
1228+
setInput('')
1229+
return
1230+
}
1231+
1232+
// Save to cross-session history
1233+
addChatHistory(text)
1234+
historyNav.current.reset()
1235+
12161236
logChatDebug('send attempt', {
12171237
textLength: text.length,
12181238
mode: agentMode,
@@ -1881,33 +1901,39 @@ export function AgentPanel() {
18811901
const suggestions = useMemo(() => {
18821902
if (!input.startsWith('/')) return []
18831903
const cmds = [
1904+
// Coding
18841905
{ cmd: '/edit', desc: 'Edit current file', icon: 'lucide:pencil' },
18851906
{ cmd: '/explain', desc: 'Explain code', icon: 'lucide:book-open' },
18861907
{ cmd: '/refactor', desc: 'Refactor code', icon: 'lucide:refresh-cw' },
18871908
{ cmd: '/generate', desc: 'Generate new code', icon: 'lucide:plus' },
18881909
{ cmd: '/search', desc: 'Search across repo', icon: 'lucide:search' },
1889-
{
1890-
cmd: '/commit',
1891-
desc: 'Commit changes (AI if empty)',
1892-
icon: 'lucide:git-commit-horizontal',
1893-
},
1910+
{ cmd: '/fix', desc: 'Fix errors in code', icon: 'lucide:wrench' },
1911+
{ cmd: '/test', desc: 'Write tests for code', icon: 'lucide:flask-conical' },
1912+
{ cmd: '/review', desc: 'Code review current changes', icon: 'lucide:scan-eye' },
1913+
// Git
1914+
{ cmd: '/commit', desc: 'Commit changes (AI message)', icon: 'lucide:git-commit-horizontal' },
18941915
{ cmd: '/diff', desc: 'Show changes', icon: 'lucide:git-compare' },
1895-
{ cmd: '/skill', desc: 'Open skill commands', icon: 'lucide:sparkles' },
1896-
{ cmd: '/skill find', desc: 'Search for more skills', icon: 'lucide:search' },
1897-
{ cmd: '/skill use', desc: 'Apply a bundled skill', icon: 'lucide:play' },
18981916
{ cmd: '/changes', desc: 'Pre-commit review', icon: 'lucide:eye' },
18991917
{ cmd: '/unstage', desc: 'Unstage all staged files', icon: 'lucide:minus-circle' },
19001918
{ cmd: '/undo', desc: 'Undo last commit', icon: 'lucide:undo-2' },
19011919
{ cmd: '/pull', desc: 'Pull latest changes', icon: 'lucide:arrow-down-circle' },
19021920
{ cmd: '/push', desc: 'Push to origin', icon: 'lucide:arrow-up-circle' },
19031921
{ cmd: '/sync', desc: 'Pull and push current branch', icon: 'lucide:refresh-cw' },
19041922
{ cmd: '/pr', desc: 'View pull requests', icon: 'lucide:git-pull-request' },
1905-
{
1906-
cmd: '/pr create',
1907-
desc: 'Create pull request',
1908-
icon: 'lucide:git-pull-request-create-arrow',
1909-
},
1923+
{ cmd: '/pr create', desc: 'Create pull request', icon: 'lucide:git-pull-request-create-arrow' },
19101924
{ cmd: '/merge', desc: 'Merge pull request', icon: 'lucide:git-merge' },
1925+
// Modes
1926+
{ cmd: '/ask', desc: 'Switch to Ask mode', icon: 'lucide:message-circle' },
1927+
{ cmd: '/agent', desc: 'Switch to Agent mode', icon: 'lucide:bot' },
1928+
{ cmd: '/plan', desc: 'Switch to Plan mode', icon: 'lucide:list-checks' },
1929+
// Session
1930+
{ cmd: '/clear', desc: 'Clear chat history', icon: 'lucide:trash-2' },
1931+
{ cmd: '/compact', desc: 'Compact session context', icon: 'lucide:minimize-2' },
1932+
{ cmd: '/model', desc: 'Show or set model', icon: 'lucide:cpu' },
1933+
// Skills
1934+
{ cmd: '/skill', desc: 'Open skill commands', icon: 'lucide:sparkles' },
1935+
{ cmd: '/skill find', desc: 'Search for more skills', icon: 'lucide:search' },
1936+
{ cmd: '/skill use', desc: 'Apply a bundled skill', icon: 'lucide:play' },
19111937
]
19121938
const term = input.toLowerCase()
19131939
return cmds.filter((c) => c.cmd.startsWith(term))
@@ -1941,6 +1967,30 @@ export function AgentPanel() {
19411967
return
19421968
}
19431969
}
1970+
// ↑/↓: navigate input history (only when input is empty or at history position)
1971+
if (e.key === 'ArrowUp' && !e.shiftKey) {
1972+
const textarea = inputRef.current
1973+
if (textarea && textarea.selectionStart === 0 && textarea.selectionEnd === 0) {
1974+
historyNav.current.setDraft(input)
1975+
const prev = historyNav.current.up()
1976+
if (prev !== null) {
1977+
e.preventDefault()
1978+
setInput(prev)
1979+
return
1980+
}
1981+
}
1982+
}
1983+
if (e.key === 'ArrowDown' && !e.shiftKey) {
1984+
const textarea = inputRef.current
1985+
if (textarea && textarea.selectionStart === textarea.value.length) {
1986+
const next = historyNav.current.down()
1987+
if (next !== null) {
1988+
e.preventDefault()
1989+
setInput(next)
1990+
return
1991+
}
1992+
}
1993+
}
19441994
// Shift+Tab: cycle agent mode (Ask → Agent → Plan → Ask)
19451995
if (e.key === 'Tab' && e.shiftKey) {
19461996
e.preventDefault()

components/chat/chat-input-bar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,10 @@ export function ChatInputBar({
596596

597597
{/* Bottom bar — mode + model (model hidden on mobile) */}
598598
<div className="flex items-center justify-between mt-1">
599-
<ModeSelector mode={agentMode} onChange={setAgentMode} />
599+
<div className="flex items-center gap-1.5">
600+
<ModeSelector mode={agentMode} onChange={setAgentMode} />
601+
<span className="hidden sm:inline text-[9px] text-[var(--text-disabled)]">⇧Tab</span>
602+
</div>
600603
<div className="hidden sm:flex items-center gap-2">
601604
{modelInfo.current && (
602605
<div className="relative">

lib/chat-history.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Cross-session chat input history.
3+
* Persists to localStorage for recall across app restarts.
4+
*/
5+
6+
const STORAGE_KEY = 'code-editor:chat-history'
7+
const MAX_ENTRIES = 50
8+
9+
export function getChatHistory(): string[] {
10+
try {
11+
const raw = localStorage.getItem(STORAGE_KEY)
12+
return raw ? JSON.parse(raw) : []
13+
} catch {
14+
return []
15+
}
16+
}
17+
18+
export function addChatHistory(text: string): void {
19+
const trimmed = text.trim()
20+
if (!trimmed) return
21+
try {
22+
const history = getChatHistory()
23+
// Remove duplicate if exists
24+
const filtered = history.filter(h => h !== trimmed)
25+
// Add to front (most recent first)
26+
filtered.unshift(trimmed)
27+
// Trim to max
28+
if (filtered.length > MAX_ENTRIES) filtered.length = MAX_ENTRIES
29+
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered))
30+
} catch {}
31+
}
32+
33+
export function clearChatHistory(): void {
34+
try { localStorage.removeItem(STORAGE_KEY) } catch {}
35+
}
36+
37+
/**
38+
* Hook-friendly history navigator.
39+
* Returns [current, goUp, goDown, reset].
40+
*/
41+
export class HistoryNavigator {
42+
private history: string[] = []
43+
private index = -1
44+
private draft = ''
45+
46+
load() {
47+
this.history = getChatHistory()
48+
this.index = -1
49+
this.draft = ''
50+
}
51+
52+
setDraft(text: string) {
53+
if (this.index === -1) this.draft = text
54+
}
55+
56+
up(): string | null {
57+
if (this.history.length === 0) return null
58+
if (this.index < this.history.length - 1) {
59+
this.index++
60+
return this.history[this.index]
61+
}
62+
return null
63+
}
64+
65+
down(): string | null {
66+
if (this.index <= 0) {
67+
this.index = -1
68+
return this.draft
69+
}
70+
this.index--
71+
return this.history[this.index]
72+
}
73+
74+
reset() {
75+
this.index = -1
76+
this.draft = ''
77+
}
78+
}

0 commit comments

Comments
 (0)