feat(code): add Esc to exit code mode, project indicator pill, design token context

- Escape key exits code mode on desktop (T14)
- Show active project name pill near chat input when in code mode (T15)
- Inject selected design tokens, files, and open file content into AI
  system prompt when in code mode (T16)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-04 12:24:58 +00:00
co-authored by Claude Opus 4.6
parent decee9163b
commit ba0320fa8d
3 changed files with 76 additions and 11 deletions
@@ -103,6 +103,24 @@
class="flex-1 min-h-0"
/>
<!-- Code mode project indicator -->
<div
v-if="codeContext.isCodeMode.value && codeContext.activeProject.value"
class="px-3 pb-1 flex items-center gap-1.5"
>
<span class="text-[10px] px-2 py-0.5 rounded-md bg-accent/15 text-accent font-medium truncate max-w-[200px]">
{{ codeContext.activeProject.value.name }}
</span>
<button
class="text-white/30 hover:text-white/60 transition-colors p-0.5"
@click="codeContext.exitCodeMode()"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<ChatInput
:disabled="isStreaming || comparison.isAnyStreaming.value"
:streaming="isStreaming || comparison.isAnyStreaming.value"
+23
View File
@@ -6,6 +6,7 @@ import type { ImageAttachment } from '@aiui/core/types/message'
import { usePersonaStore } from '@/stores/personas'
import { useMemoryStore } from '@/stores/memory'
import { useArchy } from '@/composables/useArchy'
import { useCodeContext } from '@/composables/useCodeContext'
type Provider = 'claude' | 'openrouter' | 'mock'
@@ -350,6 +351,28 @@ function buildSystemPrompt(chatStore: ReturnType<typeof useChatStore>): string {
const archy = useArchy()
prompt += archy.buildArchyContext()
// Append code context when in code mode
const code = useCodeContext()
if (code.isCodeMode.value) {
const sections: string[] = []
if (code.activeProject.value) {
sections.push(`**Active project:** ${code.activeProject.value.name} (${code.activeProject.value.language ?? 'Unknown'})`)
}
if (code.selectedDesignTokens.value.length > 0) {
sections.push(`**Selected design tokens:** ${code.selectedDesignTokens.value.join(', ')}`)
}
if (code.selectedFiles.value.length > 0) {
sections.push(`**Selected files:** ${code.selectedFiles.value.join(', ')}`)
}
if (code.activeFileContent.value && code.activeFile.value) {
const content = code.activeFileContent.value.slice(0, 2000)
sections.push(`**Open file (${code.activeFile.value}):**\n\`\`\`${code.activeFileLanguage.value}\n${content}\n\`\`\``)
}
if (sections.length > 0) {
prompt += `\n\n**Code Context:**\n${sections.join('\n')}`
}
}
return prompt
}
+35 -11
View File
@@ -4,24 +4,27 @@
:class="[
hasTrack && 'pb-[72px]'
]"
:style="isDark
? { background: '#000 url(/assets/img/bg-intro-3.jpg) center center / cover no-repeat fixed' }
: { backgroundColor: '#f5f4f1' }"
:style="isEmbedded
? { background: 'transparent' }
: isDark
? { background: '#000 url(' + bgImageUrl + ') center center / cover no-repeat fixed' }
: { backgroundColor: '#f5f4f1' }"
>
<div v-if="isDark" class="absolute inset-0 pointer-events-none bg-black/20" />
<div v-if="isDark && !isEmbedded" class="absolute inset-0 pointer-events-none bg-black/20" />
<!-- Desktop layout -->
<div class="flex-1 flex h-full p-3 md:p-4 gap-3 md:gap-4" :class="isMobile ? 'hidden' : ''">
<!-- Content surface (main area) -->
<main
class="flex-1 min-w-0 path-glass-card overflow-hidden flex relative"
class="flex-1 min-w-0 path-glass-card overflow-hidden flex relative panel-slide-in"
:class="[
isWideDesktop
? (panelSide === 'left' ? 'order-2' : 'order-1')
: (panelSide === 'left' ? 'order-last' : 'order-first'),
!isWideDesktop && hasDetailOpen && !hasGridContent && 'detail-active'
]"
style="animation-delay: 0.1s"
>
<!-- Grid/list panel (on wide desktop: always show grid; on regular: show when no detail) -->
<div
@@ -115,7 +118,8 @@
<!-- Detail panel (wide desktop only persistent, no close/back) -->
<div
v-if="isWideDesktop"
class="flex-1 min-w-0 path-glass-card overflow-hidden flex flex-col order-3 detail-persistent"
class="flex-1 min-w-0 path-glass-card overflow-hidden flex flex-col order-3 detail-persistent panel-slide-in"
style="animation-delay: 0.2s"
>
<template v-if="hasDetailOpen">
<ErrorBoundary title="Detail view error">
@@ -143,7 +147,8 @@
<!-- Chat panel -->
<aside
class="relative z-[100] w-80 xl:w-96 shrink-0 flex flex-col path-glass-card overflow-visible"
class="relative z-[100] w-80 xl:w-96 shrink-0 flex flex-col path-glass-card overflow-visible panel-slide-in"
style="animation-delay: 0s"
:class="isWideDesktop
? (panelSide === 'left' ? 'order-1' : 'order-2')
: (panelSide === 'left' ? 'order-first' : 'order-last')"
@@ -354,7 +359,7 @@
</div>
</div>
<PlayerBar />
<PlayerBar v-if="!isEmbedded" />
</div>
</template>
@@ -375,10 +380,16 @@ import { usePlayer } from '@/composables/usePlayer'
import { useCodeContext } from '@/composables/useCodeContext'
const chatStore = useChatStore()
const { activeFile: codeActiveFile, isCodeMode, clearActiveFile: clearCodeFile } = useCodeContext()
const { activeFile: codeActiveFile, isCodeMode, exitCodeMode, clearActiveFile: clearCodeFile } = useCodeContext()
const { hasTrack } = usePlayer()
const { isDark } = useTheme()
// Detect if running inside Archy's iframe — transparent bg, hide player bar
const isEmbedded = !!(window as unknown as Record<string, unknown>).__AIUI_EMBEDDED__
// Background image URL — use BASE_URL for correct path in all deployments
const bgImageUrl = `${import.meta.env.BASE_URL}assets/img/bg-intro-3.jpg`
useAI()
const {
@@ -441,8 +452,21 @@ const isWideDesktop = computed(() => windowWidth.value >= 1440)
const mobileTab = ref<'chat' | 'content' | 'context'>('chat')
function onResize() { windowWidth.value = window.innerWidth }
onMounted(() => window.addEventListener('resize', onResize))
onUnmounted(() => window.removeEventListener('resize', onResize))
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape' && isCodeMode.value && !isMobile.value) {
exitCodeMode()
}
}
onMounted(() => {
window.addEventListener('resize', onResize)
window.addEventListener('keydown', onKeydown)
})
onUnmounted(() => {
window.removeEventListener('resize', onResize)
window.removeEventListener('keydown', onKeydown)
})
// Auto-switch to content tab on mobile when panel opens or content changes
watch(panelOpen, (open) => {