Files
archy/packages/app/src/composables/useCodeContext.ts
T
DorianandClaude Opus 4.6 e8fc54cade feat(content): add places, code mode, mobile context tab, and detail views
- Add Places/Restaurants content type with PlaceCard, PlaceDetail, PlaceGrid
- Add WebsiteDetail and MagazineSectionDetail views for Context panel
- Enhance MagazineGrid hero with background image and 3x taller header
- Add mobile 3-tab layout (Chat, Content, Context) with detail navigation
- Add /code command system: useCodeContext composable, ProjectGrid, FileTreeNode,
  CodeDetail for IDE-style code viewing across all three panels
- Fix /code bubble and prompt index clicks to re-activate code mode
- Fix updatePanelFromText overwriting code tab by skipping command messages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:31:18 +00:00

207 lines
7.9 KiB
TypeScript

import { ref, computed, shallowRef } from 'vue'
export interface ProjectInfo {
name: string
path: string
isGit: boolean
language?: string
}
export interface FileEntry {
name: string
path: string
isDirectory: boolean
children?: FileEntry[]
}
// Module-level singleton state
const codeMode = ref(false)
const activeProject = ref<ProjectInfo | null>(null)
const projectList = shallowRef<ProjectInfo[]>([])
const fileTree = shallowRef<FileEntry[]>([])
const activeFile = ref<string | null>(null)
const activeFileContent = ref<string>('')
const activeFileLanguage = ref<string>('plaintext')
// Demo projects path
const PROJECTS_ROOT = '/Users/dorian/Projects'
function detectLanguage(filename: string): string {
const ext = filename.split('.').pop()?.toLowerCase() ?? ''
const map: Record<string, string> = {
ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript',
vue: 'vue', svelte: 'svelte', py: 'python', rs: 'rust', go: 'go',
java: 'java', kt: 'kotlin', swift: 'swift', rb: 'ruby', php: 'php',
css: 'css', scss: 'scss', html: 'html', json: 'json', yaml: 'yaml',
yml: 'yaml', md: 'markdown', toml: 'toml', sh: 'shell', bash: 'shell',
sql: 'sql', graphql: 'graphql', dockerfile: 'dockerfile',
c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
}
return map[ext] ?? 'plaintext'
}
function detectProjectLanguage(files: string[]): string {
if (files.includes('package.json')) return 'TypeScript/JavaScript'
if (files.includes('Cargo.toml')) return 'Rust'
if (files.includes('go.mod')) return 'Go'
if (files.includes('requirements.txt') || files.includes('setup.py') || files.includes('pyproject.toml')) return 'Python'
if (files.includes('pom.xml') || files.includes('build.gradle')) return 'Java'
if (files.includes('Package.swift')) return 'Swift'
if (files.includes('Gemfile')) return 'Ruby'
if (files.includes('composer.json')) return 'PHP'
if (files.some(f => f.endsWith('.csproj') || f.endsWith('.sln'))) return 'C#'
return 'Unknown'
}
export function useCodeContext() {
const isCodeMode = computed(() => codeMode.value)
const hasActiveProject = computed(() => activeProject.value !== null)
async function loadProjects(): Promise<void> {
// In dev/demo mode, scan the Projects folder
// This would be replaced by Archy integration later
try {
const response = await fetch(`/api/fs/list?path=${encodeURIComponent(PROJECTS_ROOT)}`)
if (response.ok) {
const data = await response.json()
projectList.value = data.projects ?? []
}
} catch {
// Fallback: use hardcoded list from build time
// In real app, this would come from local filesystem or Archy nodes
projectList.value = getDemoProjects()
}
}
function getDemoProjects(): ProjectInfo[] {
// Hardcoded demo list matching actual ~/Projects folder
return [
{ name: 'AIUI', path: `${PROJECTS_ROOT}/AIUI`, isGit: true, language: 'TypeScript/JavaScript' },
{ name: 'archy', path: `${PROJECTS_ROOT}/archy`, isGit: true, language: 'TypeScript/JavaScript' },
{ name: 'angor', path: `${PROJECTS_ROOT}/angor`, isGit: true, language: 'C#' },
{ name: 'angor-prototype', path: `${PROJECTS_ROOT}/angor-prototype`, isGit: true, language: 'TypeScript/JavaScript' },
{ name: 'archipelago', path: `${PROJECTS_ROOT}/archipelago`, isGit: true, language: 'Unknown' },
{ name: 'archipelago-foundation', path: `${PROJECTS_ROOT}/archipelago-foundation`, isGit: true, language: 'Unknown' },
{ name: 'blossom', path: `${PROJECTS_ROOT}/blossom`, isGit: true, language: 'TypeScript/JavaScript' },
{ name: 'fedimint', path: `${PROJECTS_ROOT}/fedimint`, isGit: true, language: 'Rust' },
{ name: 'Syntopy', path: `${PROJECTS_ROOT}/Syntopy`, isGit: true, language: 'Unknown' },
{ name: 'Syntropy-Institute', path: `${PROJECTS_ROOT}/Syntropy-Institute`, isGit: true, language: 'Unknown' },
{ name: 'LoRaBell', path: `${PROJECTS_ROOT}/LoRaBell`, isGit: true, language: 'Unknown' },
{ name: 'satoshi-services', path: `${PROJECTS_ROOT}/satoshi-services`, isGit: true, language: 'Unknown' },
{ name: 'Proux', path: `${PROJECTS_ROOT}/Proux`, isGit: true, language: 'Unknown' },
{ name: 'KYC', path: `${PROJECTS_ROOT}/KYC`, isGit: true, language: 'Unknown' },
{ name: 'k484', path: `${PROJECTS_ROOT}/k484`, isGit: true, language: 'Unknown' },
{ name: 'tbf', path: `${PROJECTS_ROOT}/tbf`, isGit: true, language: 'Unknown' },
{ name: 'Icon', path: `${PROJECTS_ROOT}/Icon`, isGit: false, language: 'Unknown' },
{ name: 'indeehub-frontend', path: `${PROJECTS_ROOT}/indeehub-frontend`, isGit: true, language: 'TypeScript/JavaScript' },
{ name: 'Indeedhub Prototype', path: `${PROJECTS_ROOT}/Indeedhub Prototype`, isGit: true, language: 'Unknown' },
{ name: '21', path: `${PROJECTS_ROOT}/21`, isGit: true, language: 'Unknown' },
]
}
function enterCodeMode(): void {
codeMode.value = true
loadProjects()
}
function exitCodeMode(): void {
codeMode.value = false
activeProject.value = null
activeFile.value = null
activeFileContent.value = ''
fileTree.value = []
}
function selectProject(project: ProjectInfo): void {
activeProject.value = project
loadFileTree(project.path)
}
async function loadFileTree(projectPath: string): Promise<void> {
try {
const response = await fetch(`/api/fs/tree?path=${encodeURIComponent(projectPath)}`)
if (response.ok) {
const data = await response.json()
fileTree.value = data.files ?? []
}
} catch {
// Demo fallback: generate a simple tree
fileTree.value = getDemoFileTree()
}
}
function getDemoFileTree(): FileEntry[] {
// Generic project structure for demo
return [
{ name: 'src', path: 'src', isDirectory: true, children: [
{ name: 'index.ts', path: 'src/index.ts', isDirectory: false },
{ name: 'app.ts', path: 'src/app.ts', isDirectory: false },
{ name: 'utils.ts', path: 'src/utils.ts', isDirectory: false },
]},
{ name: 'package.json', path: 'package.json', isDirectory: false },
{ name: 'tsconfig.json', path: 'tsconfig.json', isDirectory: false },
{ name: 'README.md', path: 'README.md', isDirectory: false },
]
}
async function openFile(filePath: string): Promise<void> {
activeFile.value = filePath
activeFileLanguage.value = detectLanguage(filePath)
try {
const fullPath = activeProject.value
? `${activeProject.value.path}/${filePath}`
: filePath
const response = await fetch(`/api/fs/read?path=${encodeURIComponent(fullPath)}`)
if (response.ok) {
const data = await response.json()
activeFileContent.value = data.content ?? ''
}
} catch {
// Demo fallback
activeFileContent.value = getDemoFileContent(filePath)
}
}
function getDemoFileContent(filePath: string): string {
const name = filePath.split('/').pop() ?? filePath
if (name === 'package.json') {
return JSON.stringify({
name: activeProject.value?.name?.toLowerCase() ?? 'project',
version: '1.0.0',
type: 'module',
scripts: { dev: 'vite', build: 'vite build', test: 'vitest' },
dependencies: {},
}, null, 2)
}
if (name === 'README.md') {
return `# ${activeProject.value?.name ?? 'Project'}\n\nA project in the AIUI ecosystem.\n`
}
if (name.endsWith('.ts') || name.endsWith('.js')) {
return `// ${name}\n// ${activeProject.value?.name ?? 'Project'}\n\nexport function main() {\n console.log('Hello from ${name}')\n}\n`
}
return `// ${name}\n`
}
return {
// State
codeMode,
isCodeMode,
activeProject,
hasActiveProject,
projectList,
fileTree,
activeFile,
activeFileContent,
activeFileLanguage,
// Actions
enterCodeMode,
exitCodeMode,
selectProject,
openFile,
loadProjects,
detectLanguage,
}
}