Building a Space
This guide walks you through creating your first custom space from start to finish. We'll build a "Book Review" space that helps users write and manage book reviews.
Prerequisites
Before starting, ensure you have:
- Construct running (
bun run dev) - Basic Vue 3 knowledge
- Understanding of Spaces and Agents and Tools
- Familiarity with the project structure
Step 1: Create the Space Directory
Create a new space directory in frontend/src/spaces/:
bash
mkdir -p frontend/src/spaces/book-review
cd frontend/src/spaces/book-reviewStep 2: Create the Space Manifest (space.json)
Create space.json to declare your space to the shell:
json
{
"id": "book-review",
"name": "Book Review",
"description": "Write and manage book reviews with AI assistance",
"version": "1.0.0",
"author": "Your Name",
"icon": "assets/icon.svg",
"color": "#F59E0B",
"pages": [
{
"id": "index",
"path": "/",
"title": "Reviews",
"component": "pages/index.vue"
},
{
"id": "editor",
"path": "/editor",
"title": "New Review",
"component": "pages/editor.vue"
},
{
"id": "view",
"path": "/view/:id",
"title": "Review",
"component": "pages/view.vue"
}
],
"agent": {
"enabled": true,
"configPath": "agent/config.md"
},
"scope": ["project"],
"dependencies": {
"@construct-space/ui": "^1.0.0",
"@construct-space/sdk": "^1.0.0"
},
"marketplace": {
"publishable": false,
"category": "productivity"
}
}Step 3: Create the Directory Structure
Set up the space file structure:
bash
mkdir -p pages components composables stores lib agent/tools agent/hooks assets stylesStep 4: Create Pages
Main Reviews Page (pages/index.vue)
vue
<template>
<div class="p-6">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">Book Reviews</h1>
<RouterLink to="/editor" class="btn btn-primary">
+ New Review
</RouterLink>
</div>
<div v-if="reviews.length === 0" class="text-center py-12">
<p class="text-gray-500">No reviews yet. Create your first one!</p>
</div>
<div v-else class="grid gap-4">
<ReviewCard
v-for="review in reviews"
:key="review.id"
:review="review"
@click="navigateTo(`/view/${review.id}`)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import ReviewCard from '../components/ReviewCard.vue'
import { useReviews } from '../composables/useReviews'
const router = useRouter()
const { reviews, loadReviews } = useReviews()
const navigateTo = (path: string) => {
router.push(path)
}
onMounted(() => {
loadReviews()
})
</script>Review Editor Page (pages/editor.vue)
vue
<template>
<div class="p-6 max-w-2xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Write a Review</h1>
<form @submit.prevent="submitReview" class="space-y-6">
<div>
<label class="block text-sm font-medium mb-2">Book Title</label>
<input
v-model="form.title"
type="text"
class="input w-full"
placeholder="Enter book title"
required
/>
</div>
<div>
<label class="block text-sm font-medium mb-2">Author</label>
<input
v-model="form.author"
type="text"
class="input w-full"
placeholder="Enter author name"
required
/>
</div>
<div>
<label class="block text-sm font-medium mb-2">Rating</label>
<select v-model="form.rating" class="input w-full" required>
<option value="">Select rating</option>
<option value="5">5 stars</option>
<option value="4">4 stars</option>
<option value="3">3 stars</option>
<option value="2">2 stars</option>
<option value="1">1 star</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-2">Review</label>
<textarea
v-model="form.content"
class="input w-full h-48"
placeholder="Write your review here..."
required
/>
</div>
<div class="flex gap-4">
<button type="submit" class="btn btn-primary">
Save Review
</button>
<button type="button" @click="askAI" class="btn btn-secondary">
Improve with AI
</button>
</div>
</form>
<div v-if="aiSuggestion" class="mt-8 p-4 bg-blue-50 rounded">
<h3 class="font-semibold mb-2">AI Suggestion</h3>
<p>{{ aiSuggestion }}</p>
<button @click="applyAISuggestion" class="btn btn-sm mt-3">
Apply
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useReviews } from '../composables/useReviews'
import { useAgent } from '@construct-space/sdk'
const router = useRouter()
const { addReview } = useReviews()
const { execute: executeAgent } = useAgent()
const form = ref({
title: '',
author: '',
rating: '',
content: ''
})
const aiSuggestion = ref('')
const submitReview = async () => {
await addReview({
title: form.value.title,
author: form.value.author,
rating: parseInt(form.value.rating),
content: form.value.content,
createdAt: new Date()
})
router.push('/')
}
const askAI = async () => {
aiSuggestion.value = 'Improving your review...'
const suggestion = await executeAgent(
`Improve this book review for clarity and impact:\n${form.value.content}`
)
aiSuggestion.value = suggestion
}
const applyAISuggestion = () => {
form.value.content = aiSuggestion.value
aiSuggestion.value = ''
}
</script>Step 5: Create Components
ReviewCard Component (components/ReviewCard.vue)
vue
<template>
<div
class="p-4 border rounded-lg hover:shadow-lg transition cursor-pointer"
@click="$emit('click')"
>
<div class="flex justify-between items-start mb-2">
<div>
<h3 class="text-lg font-semibold">{{ review.title }}</h3>
<p class="text-sm text-gray-600">by {{ review.author }}</p>
</div>
<div class="text-2xl">{{ "⭐".repeat(review.rating) }}</div>
</div>
<p class="text-gray-700 line-clamp-3">{{ review.content }}</p>
<p class="text-xs text-gray-500 mt-3">
{{ formatDate(review.createdAt) }}
</p>
</div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
interface Review {
id: string
title: string
author: string
rating: number
content: string
createdAt: Date
}
defineProps<{
review: Review
}>()
defineEmits(['click'])
const formatDate = (date: Date) => {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
</script>Step 6: Create Composables
useReviews Composable (composables/useReviews.ts)
typescript
import { ref, reactive } from 'vue'
import { useStorage } from '@construct-space/sdk'
interface Review {
id: string
title: string
author: string
rating: number
content: string
createdAt: Date
}
const reviews = ref<Review[]>([])
const { getItem, setItem } = useStorage('book-review')
export function useReviews() {
const loadReviews = async () => {
const stored = await getItem('reviews')
if (stored) {
reviews.value = JSON.parse(stored)
}
}
const addReview = async (review: Omit<Review, 'id'>) => {
const newReview: Review = {
...review,
id: Math.random().toString(36).substr(2, 9)
}
reviews.value.push(newReview)
await setItem('reviews', JSON.stringify(reviews.value))
}
const getReview = (id: string) => {
return reviews.value.find(r => r.id === id)
}
const updateReview = async (id: string, updates: Partial<Review>) => {
const index = reviews.value.findIndex(r => r.id === id)
if (index !== -1) {
reviews.value[index] = { ...reviews.value[index], ...updates }
await setItem('reviews', JSON.stringify(reviews.value))
}
}
const deleteReview = async (id: string) => {
reviews.value = reviews.value.filter(r => r.id !== id)
await setItem('reviews', JSON.stringify(reviews.value))
}
return {
reviews,
loadReviews,
addReview,
getReview,
updateReview,
deleteReview
}
}Step 7: Create the Agent Configuration
Agent Config (agent/config.md)
markdown
---
model: claude-3-5-sonnet-20241022
temperature: 0.7
maxIterations: 15
provider: anthropic
canSpawn: false
tools:
- id: read
enabled: true
- id: write
enabled: true
---
# Book Review Assistant
You are an expert book reviewer and literary critic. Your role is to help users write compelling, thoughtful book reviews.
Your expertise includes:
- Analyzing books for themes, structure, and impact
- Identifying strengths and weaknesses in writing
- Suggesting improvements for clarity and persuasiveness
- Providing constructive feedback
When helping with reviews:
1. Ask clarifying questions about the book and reader's perspective
2. Identify key themes and elements worth discussing
3. Suggest structure and flow improvements
4. Help refine language for impact and clarity
5. Ensure the review is balanced and well-reasoned
## Available Tools
- **read:** Read book notes or draft reviews
- **write:** Create or refine review text
## Important Guidelines
- Be encouraging and constructive
- Focus on clarity and impact
- Respect the reviewer's opinion
- Never rewrite without approvalCustom Tool: improveContent (agent/tools/improveContent.md)
markdown
# improveContent
Improve writing clarity, style, and impact.
## Parameters
- **content** (string, required): The text to improve
- **focusArea** (string, optional): Focus on (clarity, style, tone, grammar)
- **audience** (string, optional): Target audience (general, academic, casual)
## Description
This tool analyzes writing and suggests improvements for readability and impact.
## ExampleimproveContent(content="This book was good.", focusArea="clarity", audience="general")
## Returns
Improved version of the text with explanations of changes.Step 8: Create Styles
Theme Overrides (styles/theme.css)
css
:root {
--color-space-primary: #F59E0B;
--color-space-secondary: #DBEAFE;
}
/* Space-specific button styling */
.btn-primary {
background-color: #F59E0B;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
transition: background-color 0.2s;
}
.btn-primary:hover {
background-color: #D97706;
}
.btn-secondary {
background-color: #E5E7EB;
color: #1F2937;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
transition: background-color 0.2s;
}
.btn-secondary:hover {
background-color: #D1D5DB;
}Step 9: Create the Icon
Create a simple SVG icon for the sidebar (assets/icon.svg):
svg
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 3H20C21.1046 3 22 3.89543 22 5V19C22 20.1046 21.1046 21 20 21H4C2.89543 21 2 20.1046 2 19V5C2 3.89543 2.89543 3 4 3Z" fill="currentColor" opacity="0.1"/>
<path d="M7 7H17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M7 12H17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M7 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>Step 10: Register the Space
Add your space to the frontend space loader. In frontend/src/lib/spaceHost.ts, add:
typescript
import bookReviewSpace from '../spaces/book-review/space.json'
export const BUILTIN_SPACES = [
// ... existing spaces
bookReviewSpace
]Step 11: Test Your Space
- Stop Construct if running:
Ctrl+C - Restart with
bun run dev - Look for the "Book Review" icon in the sidebar
- Click it to open your new space
- Test creating and viewing reviews
Step 12: Publish (Optional)
For Marketplace Distribution
- Create a
README.mdwith usage instructions:
markdown
# Book Review Space
Write and manage book reviews with AI assistance.
## Features
- Create and store book reviews
- Rate books with 5-star system
- Get AI suggestions for improvement
- View review history
## Installation
Install from the Construct marketplace.
## Usage
1. Click the Book Review icon in the sidebar
2. Click "+ New Review" to create a review
3. Fill in book details and your thoughts
4. Click "Improve with AI" for suggestions- Add screenshots showing the space in action
- Update
space.jsonwith"publishable": true - Submit to the marketplace (process TBD)
For Private Installation
Share the space directory for manual installation:
bash
# User copies the space to:
~/Library/Application Support/Construct/spaces/book-review/
# Then restarts ConstructCommon Patterns
Using the Host API
typescript
import { useHost } from '@construct-space/sdk'
const { sendMessage, onMessage } = useHost()
// Send notification to shell
sendMessage({
type: 'notification',
message: 'Review saved successfully!',
level: 'success'
})Broadcasting Between Spaces
typescript
import { useBroadcast } from '@construct-space/sdk'
const { broadcast, onBroadcast } = useBroadcast()
// Broadcast when review is saved
broadcast({
type: 'review:created',
id: review.id,
title: review.title
})
// Listen for broadcasts from other spaces
onBroadcast((message) => {
if (message.type === 'project:changed') {
loadReviews() // Reload reviews for new project
}
})Using Project Context
typescript
import { useSpaceContext } from '@construct-space/sdk'
const { projectId, projectPath, scope } = useSpaceContext()
// Data is project-scoped
const dataKey = `reviews:${projectId}`Troubleshooting
Space doesn't appear in sidebar
- Check that
space.jsonhas correctidandname - Verify space directory is in
frontend/src/spaces/ - Check console for JSON parsing errors
- Restart Construct
Agent tools not working
- Verify tool paths in
agent/config.mdare correct - Check that tool markdown files exist
- Restart Construct to reload agent config
Pages don't load
- Check route paths in
space.jsonmatchpages/directory - Verify Vue component imports are correct
- Check browser console for errors
Next Steps
- Explore other built-in spaces for patterns and ideas
- Read Spaces for deep dive on space architecture
- Check @construct-space/sdk for all available APIs
- Share your space with the community!