# Discuss Contract Feature - Implementation Plan
## Overview
Add a "Discuss Contract" feature to the listen page that enables users to have a natural conversation with Makima (in character) to flesh out and ultimately create a contract specification. This feature bridges the gap between raw transcript/requirements and a well-defined contract through interactive dialogue.
**Goal:** Enable users to talk through their project idea with Makima, refine requirements conversationally, and then have the LLM create a properly-structured contract when the conversation reaches a natural conclusion.
## Key Design Principles
1. **Ephemeral conversation** - Does not require an existing contract (creates one at the end)
2. **Character-aware** - Makima responds in character, aware of the makima.jp platform
3. **Contextual** - Can incorporate transcript context from the current session
4. **Audible** - Each response can be spoken using the TTS functionality
5. **Natural conclusion** - The LLM decides when to create the contract via tool use
6. **Reuse existing infrastructure** - Leverage chatWithContract API patterns and useSpeakWebSocket
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Listen Page │
├─────────────────────────────────────────────────────────────────┤
│ ControlPanel │
│ ├── Contract Dropdown │
│ │ ├── Ephemeral │
│ │ ├── [existing contracts...] │
│ │ └── ** "Discuss Contract" ** <-- NEW OPTION │
│ └── ... │
├─────────────────────────────────────────────────────────────────┤
│ DiscussContractModal (NEW) │
│ ├── Conversation history (chat bubbles) │
│ ├── Text input │
│ ├── Speak button per response │
│ └── Contract created notification │
└─────────────────────────────────────────────────────────────────┘
│
│ POST /api/v1/contracts/discuss
▼
┌─────────────────────────────────────────────────────────────────┐
│ Backend: discuss_contract_handler │
├─────────────────────────────────────────────────────────────────┤
│ 1. Receive message + history + optional transcript context │
│ 2. Build Makima-character system prompt │
│ 3. Run agentic loop with discussion tools │
│ 4. When ready, call create_contract tool │
│ 5. Return response + tool calls + created contract info │
└─────────────────────────────────────────────────────────────────┘
```
## Implementation Tasks
---
### Task 1: Backend - New Discussion Endpoint
**File:** `makima/src/server/handlers/contract_discuss.rs` (NEW)
Create a new handler for ephemeral contract discussions that doesn't require an existing contract.
#### 1.1 Request/Response Types
```rust
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DiscussContractRequest {
/// The user's message
pub message: String,
/// Optional model selection (default: claude-sonnet)
#[serde(default)]
pub model: Option<String>,
/// Conversation history for context continuity
#[serde(default)]
pub history: Option<Vec<ChatMessage>>,
/// Optional transcript context from current session
#[serde(default)]
pub transcript_context: Option<String>,
}
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DiscussContractResponse {
/// Makima's response message
pub response: String,
/// Tool calls that were executed (e.g., create_contract)
pub tool_calls: Vec<ToolCallInfo>,
/// If a contract was created, its details
#[serde(skip_serializing_if = "Option::is_none")]
pub created_contract: Option<CreatedContractInfo>,
/// Pending questions (if LLM needs clarification)
#[serde(skip_serializing_if = "Option::is_none")]
pub pending_questions: Option<Vec<UserQuestion>>,
}
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreatedContractInfo {
pub id: String,
pub name: String,
pub description: Option<String>,
pub contract_type: String,
pub initial_phase: String,
}
```
#### 1.2 System Prompt - Makima Character
```rust
const DISCUSS_SYSTEM_PROMPT: &str = r#"
You are Makima, an AI assistant on the makima.jp platform. You help users define and create contracts for their projects through natural conversation.
## Your Personality
- Professional yet personable
- Focused on understanding the user's actual needs
- Ask clarifying questions when requirements are vague
- Guide the conversation toward actionable outcomes
- Comfortable making recommendations based on experience
## Your Goal
Help the user flesh out their project idea into a well-defined contract. A contract on makima.jp includes:
- A clear name and description
- The right contract type (simple, specification, or execute)
- Understanding of the scope and requirements
## Contract Types
- **simple**: Quick tasks with minimal planning (plan -> execute phases only)
- **specification**: Full lifecycle projects (research -> specify -> plan -> execute -> review)
- **execute**: Direct implementation when requirements are already clear (execute phase only)
## Guidelines
1. **Start by understanding**: Ask about what they want to build
2. **Clarify scope**: Is this a quick fix, a new feature, or a full project?
3. **Gather requirements**: What are the must-haves vs nice-to-haves?
4. **Identify context**: Is there existing code? Which repository?
5. **Recommend type**: Suggest the appropriate contract type
6. **Confirm and create**: When the user is satisfied, create the contract
## When to Create the Contract
Create the contract when:
- You have a clear understanding of what the user wants
- The user has confirmed they're ready to proceed
- You've gathered enough information for a meaningful contract
Do NOT create the contract if:
- The user is still exploring ideas
- Key information is missing
- The user hasn't indicated readiness
{transcript_context}
"#;
```
#### 1.3 Tool Definitions
**File:** `makima/src/llm/discuss_tools.rs` (NEW)
```rust
pub static DISCUSS_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::Lazy::new(|| {
vec![
Tool {
name: "create_contract".to_string(),
description: "Create a new contract based on the discussion. Only call this when the user has confirmed they're ready to create the contract.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name for the contract"
},
"description": {
"type": "string",
"description": "Detailed description of what the contract is for"
},
"contract_type": {
"type": "string",
"enum": ["simple", "specification", "execute"],
"description": "Type of contract workflow"
},
"repository_url": {
"type": "string",
"description": "Optional repository URL if discussed"
},
"local_only": {
"type": "boolean",
"description": "If true, tasks won't auto-push or create PRs"
}
},
"required": ["name", "description", "contract_type"]
}),
},
Tool {
name: "ask_clarification".to_string(),
description: "Ask the user a clarifying question with multiple choice options.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The question to ask"
},
"options": {
"type": "array",
"items": { "type": "string" },
"description": "Multiple choice options"
},
"allow_custom": {
"type": "boolean",
"description": "Allow user to provide a custom answer"
}
},
"required": ["question", "options"]
}),
},
]
});
```
#### 1.4 Handler Implementation
```rust
pub async fn discuss_contract_handler(
State(state): State<SharedState>,
Authenticated(auth): Authenticated,
Json(request): Json<DiscussContractRequest>,
) -> impl IntoResponse {
// 1. Build system prompt with optional transcript context
let transcript_section = match &request.transcript_context {
Some(ctx) => format!(
"\n## Current Session Context\nThe user has been recording a session. Here's the transcript:\n\n{}\n",
ctx
),
None => String::new(),
};
let system_prompt = DISCUSS_SYSTEM_PROMPT.replace("{transcript_context}", &transcript_section);
// 2. Initialize LLM client
let llm_client = match model {
LlmModel::ClaudeSonnet => ClaudeClient::from_env(ClaudeModel::Sonnet)?,
// ...
};
// 3. Run agentic loop with DISCUSS_TOOLS
run_discuss_agentic_loop(
pool,
&state,
&llm_client,
system_prompt,
&request,
auth.owner_id,
).await
}
```
#### 1.5 Register the Endpoint
**File:** `makima/src/server/mod.rs`
Add route:
```rust
.route("/api/v1/contracts/discuss", post(handlers::contract_discuss::discuss_contract_handler))
```
---
### Task 2: Frontend - DiscussContractModal Component
**File:** `makima/frontend/src/components/listen/DiscussContractModal.tsx` (NEW)
Create a chat modal component for the discussion interface.
#### 2.1 Types
```typescript
interface Message {
id: string;
role: "user" | "assistant";
content: string;
timestamp: Date;
toolCalls?: ToolCallInfo[];
}
interface DiscussContractModalProps {
isOpen: boolean;
onClose: () => void;
transcriptContext?: string; // Current session transcript
onContractCreated: (contract: CreatedContractInfo) => void;
}
interface DiscussContractState {
messages: Message[];
isLoading: boolean;
error: string | null;
createdContract: CreatedContractInfo | null;
}
```
#### 2.2 Component Structure
```tsx
export function DiscussContractModal({
isOpen,
onClose,
transcriptContext,
onContractCreated,
}: DiscussContractModalProps) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [createdContract, setCreatedContract] = useState<CreatedContractInfo | null>(null);
const { speak, isSpeaking, cancel } = useSpeakWebSocket();
const messagesEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom on new messages
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
// Initial greeting when modal opens
useEffect(() => {
if (isOpen && messages.length === 0) {
// Add initial assistant message
const greeting = transcriptContext
? "I've reviewed your session transcript. What would you like to build based on this discussion?"
: "Hello! I'm Makima. Tell me about what you'd like to build, and I'll help you create a contract for it.";
setMessages([{
id: crypto.randomUUID(),
role: "assistant",
content: greeting,
timestamp: new Date(),
}]);
}
}, [isOpen, transcriptContext]);
const handleSend = async () => {
if (!input.trim() || isLoading) return;
const userMessage: Message = {
id: crypto.randomUUID(),
role: "user",
content: input,
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setInput("");
setIsLoading(true);
try {
const history = messages.map(m => ({
role: m.role,
content: m.content,
}));
const response = await discussContract(
input,
undefined, // model
history,
transcriptContext
);
const assistantMessage: Message = {
id: crypto.randomUUID(),
role: "assistant",
content: response.response,
timestamp: new Date(),
toolCalls: response.toolCalls,
};
setMessages(prev => [...prev, assistantMessage]);
if (response.createdContract) {
setCreatedContract(response.createdContract);
onContractCreated(response.createdContract);
}
} catch (err) {
// Handle error
} finally {
setIsLoading(false);
}
};
const handleSpeak = (text: string) => {
if (isSpeaking) {
cancel();
} else {
speak(text);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
<div className="panel w-full max-w-2xl h-[600px] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-[rgba(117,170,252,0.25)]">
<h2 className="font-mono text-sm text-[#dbe7ff] uppercase tracking-wide">
Discuss Contract with Makima
</h2>
<button onClick={onClose} className="...">
[X]
</button>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((message) => (
<ChatBubble
key={message.id}
message={message}
onSpeak={() => handleSpeak(message.content)}
isSpeaking={isSpeaking}
/>
))}
<div ref={messagesEndRef} />
{isLoading && (
<div className="flex items-center gap-2 text-[#9bc3ff]">
<span className="animate-pulse">Makima is thinking...</span>
</div>
)}
</div>
{/* Contract Created Banner */}
{createdContract && (
<div className="p-3 bg-green-400/10 border-t border-green-400/50">
<div className="font-mono text-xs text-green-400">
Contract "{createdContract.name}" created successfully!
</div>
</div>
)}
{/* Input */}
<div className="p-4 border-t border-[rgba(117,170,252,0.25)]">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSend()}
placeholder="Describe your project..."
disabled={isLoading || !!createdContract}
className="flex-1 px-3 py-2 bg-[#0d1b2d] border border-[#0f3c78] text-[#dbe7ff] font-mono text-sm focus:border-[#3f6fb3] outline-none"
/>
<button
onClick={handleSend}
disabled={isLoading || !input.trim() || !!createdContract}
className="px-4 py-2 bg-[#0f3c78] text-[#dbe7ff] font-mono text-xs uppercase hover:bg-[#153667] disabled:opacity-50"
>
Send
</button>
</div>
</div>
</div>
</div>
);
}
```
#### 2.3 ChatBubble Component
```tsx
interface ChatBubbleProps {
message: Message;
onSpeak: () => void;
isSpeaking: boolean;
}
function ChatBubble({ message, onSpeak, isSpeaking }: ChatBubbleProps) {
const isUser = message.role === "user";
return (
<div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
<div
className={`max-w-[80%] p-3 font-mono text-sm ${
isUser
? "bg-[#0f3c78] text-[#dbe7ff]"
: "bg-[#0d1b2d] border border-[rgba(117,170,252,0.25)] text-[#dbe7ff]"
}`}
>
<div className="whitespace-pre-wrap">{message.content}</div>
{!isUser && (
<div className="mt-2 flex items-center gap-2">
<button
onClick={onSpeak}
className="text-[10px] text-[#9bc3ff] hover:text-[#dbe7ff] uppercase"
>
{isSpeaking ? "[Stop]" : "[Speak]"}
</button>
</div>
)}
{message.toolCalls && message.toolCalls.length > 0 && (
<div className="mt-2 pt-2 border-t border-[rgba(117,170,252,0.25)]">
{message.toolCalls.map((tc, i) => (
<div key={i} className="text-[10px] text-[#75aafc]">
{tc.result.success ? "+" : "-"} {tc.name}: {tc.result.message}
</div>
))}
</div>
)}
</div>
</div>
);
}
```
---
### Task 3: Frontend - API Integration
**File:** `makima/frontend/src/lib/api.ts`
Add the discuss contract API function.
```typescript
// =============================================================================
// Contract Discussion Types and API
// =============================================================================
export interface DiscussContractRequest {
message: string;
model?: LlmModel;
history?: ChatMessage[];
transcriptContext?: string;
}
export interface CreatedContractInfo {
id: string;
name: string;
description: string | null;
contractType: string;
initialPhase: string;
}
export interface DiscussContractResponse {
response: string;
toolCalls: ContractToolCallInfo[];
createdContract?: CreatedContractInfo;
pendingQuestions?: UserQuestion[];
}
/**
* Discuss a potential contract with Makima.
* This is an ephemeral conversation that can result in contract creation.
*/
export async function discussContract(
message: string,
model?: LlmModel,
history?: ChatMessage[],
transcriptContext?: string
): Promise<DiscussContractResponse> {
const body: DiscussContractRequest = { message };
if (model) body.model = model;
if (history && history.length > 0) body.history = history;
if (transcriptContext) body.transcriptContext = transcriptContext;
const res = await authFetch(`${API_BASE}/api/v1/contracts/discuss`, {
method: "POST",
body: JSON.stringify(body),
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Discussion failed: ${errorText || res.statusText}`);
}
return res.json();
}
```
---
### Task 4: Frontend - Integration into Listen Page
#### 4.1 Update ContractPickerModal
**File:** `makima/frontend/src/components/listen/ContractPickerModal.tsx`
Add "Discuss Contract" option to the picker.
```tsx
interface ContractPickerModalProps {
isOpen: boolean;
onClose: () => void;
contracts: ContractOption[];
selectedContractId: string | null;
onSelect: (contractId: string | null) => void;
onDiscussContract: () => void; // NEW
loading?: boolean;
}
export function ContractPickerModal({
// ... existing props
onDiscussContract,
}: ContractPickerModalProps) {
return (
<div className="...">
{/* ... existing content ... */}
{/* Ephemeral option */}
<button onClick={() => handleSelect(null)} className="...">
<span className="uppercase tracking-wide">Ephemeral</span>
<span className="...">Transcript not saved</span>
</button>
{/* NEW: Discuss Contract option */}
<button
onClick={() => {
onClose();
onDiscussContract();
}}
className="w-full text-left px-3 py-2 font-mono text-xs border bg-[#0d1b2d] border-[#3f6fb3] text-[#dbe7ff] hover:bg-[#153667] transition-colors"
>
<span className="uppercase tracking-wide">Discuss Contract</span>
<span className="block text-[10px] text-[#75aafc] mt-0.5">
Chat with Makima to define a new contract
</span>
</button>
{/* Existing contracts */}
{contracts.map((contract) => (
// ... existing contract buttons
))}
</div>
);
}
```
#### 4.2 Update ControlPanel
**File:** `makima/frontend/src/components/listen/ControlPanel.tsx`
Add prop for discuss contract callback.
```tsx
interface ControlPanelProps {
// ... existing props
onDiscussContract: () => void; // NEW
}
export function ControlPanel({
// ... existing props
onDiscussContract,
}: ControlPanelProps) {
return (
<div className="...">
{/* ... existing content ... */}
<ContractPickerModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
contracts={contracts}
selectedContractId={selectedContractId}
onSelect={onContractChange}
onDiscussContract={onDiscussContract}
loading={contractsLoading}
/>
</div>
);
}
```
#### 4.3 Update Listen Route
**File:** `makima/frontend/src/routes/listen.tsx`
Integrate the DiscussContractModal.
```tsx
import { DiscussContractModal } from "../components/listen/DiscussContractModal";
export default function ListenPage() {
// ... existing state ...
// NEW: Discuss contract modal state
const [isDiscussModalOpen, setIsDiscussModalOpen] = useState(false);
// Get current transcript context for discussion
const transcriptContext = useMemo(() => {
if (ws.transcripts.length === 0) return undefined;
return ws.transcripts
.map(t => `[${t.speaker}]: ${t.text}`)
.join("\n");
}, [ws.transcripts]);
const handleOpenDiscussModal = useCallback(() => {
setIsDiscussModalOpen(true);
}, []);
const handleContractCreated = useCallback((contract: CreatedContractInfo) => {
// Add to contracts list and select it
setContracts(prev => [
{ id: contract.id, name: contract.name },
...prev,
]);
setSelectedContractId(contract.id);
// Close the modal after a short delay to show success
setTimeout(() => setIsDiscussModalOpen(false), 2000);
}, []);
return (
<div className="...">
{/* ... existing content ... */}
<ControlPanel
// ... existing props ...
onDiscussContract={handleOpenDiscussModal}
/>
{/* NEW: Discuss Contract Modal */}
<DiscussContractModal
isOpen={isDiscussModalOpen}
onClose={() => setIsDiscussModalOpen(false)}
transcriptContext={transcriptContext}
onContractCreated={handleContractCreated}
/>
{/* ... existing TranscriptAnalysisPanel ... */}
</div>
);
}
```
---
### Task 5: Integration Testing
#### 5.1 Backend Tests
**File:** `makima/src/server/handlers/contract_discuss_test.rs`
```rust
#[tokio::test]
async fn test_discuss_contract_basic_conversation() {
// Test basic message/response flow
}
#[tokio::test]
async fn test_discuss_contract_creates_contract() {
// Test that create_contract tool works
}
#[tokio::test]
async fn test_discuss_contract_with_transcript_context() {
// Test transcript context is included
}
```
#### 5.2 Frontend Tests
- Test modal opens/closes correctly
- Test message sending and receiving
- Test speak button functionality
- Test contract creation flow
---
## UI/UX Flow
### User Journey
1. **User records a transcript** (optional)
- Captures meeting or brainstorming session
2. **User clicks "Contract" dropdown**
- Sees: Ephemeral, existing contracts, "Discuss Contract" option
3. **User selects "Discuss Contract"**
- Modal opens with greeting from Makima
- If transcript exists, Makima references it
4. **Conversation flows naturally**
- User describes what they want to build
- Makima asks clarifying questions
- User can click [Speak] to hear Makima's responses
5. **When ready, user confirms**
- "Yes, let's create the contract"
- Makima calls create_contract tool
6. **Contract created**
- Success banner appears
- Modal closes, contract selected automatically
- User can continue with normal workflow
### Example Conversation
```
Makima: Hello! I'm Makima. Tell me about what you'd like to build,
and I'll help you create a contract for it.
User: I want to add dark mode to my React app
Makima: Great! Dark mode is a common feature. Let me understand your
situation better:
1. Is this for an existing React application, or are you
starting fresh?
2. Do you have a specific design system or color palette in mind?
3. Should users be able to toggle between light/dark, or should
it follow system preferences?
User: Existing app. I want both - toggle and follow system.
No specific design yet.
Makima: Perfect. For an existing app adding dark mode with toggle
AND system preference support, I'd recommend a "simple" contract
since the requirements are clear.
Here's what I'm thinking:
- **Name:** "Add Dark Mode Support"
- **Description:** Implement theme switching (light/dark mode)
with manual toggle and system preference detection
- **Type:** Simple (plan -> execute)
Does this look right? Should I create this contract?
User: Yes, looks good!
Makima: Done! I've created the contract "Add Dark Mode Support".
You can now add your repository and start planning the
implementation.
[Contract created: Add Dark Mode Support]
```
---
## Technical Details
### API Contract
#### Request
```
POST /api/v1/contracts/discuss
Content-Type: application/json
Authorization: Bearer <token>
{
"message": "I want to add dark mode to my app",
"model": "claude-sonnet",
"history": [
{ "role": "assistant", "content": "Hello! I'm Makima..." },
{ "role": "user", "content": "I want to add dark mode" }
],
"transcriptContext": "[Speaker A]: Let's discuss the dark mode feature..."
}
```
#### Response
```json
{
"response": "Great! Let me understand your requirements...",
"toolCalls": [],
"createdContract": null,
"pendingQuestions": null
}
```
#### Response (with contract creation)
```json
{
"response": "Done! I've created the contract...",
"toolCalls": [
{
"name": "create_contract",
"result": {
"success": true,
"message": "Contract 'Add Dark Mode Support' created"
}
}
],
"createdContract": {
"id": "uuid-here",
"name": "Add Dark Mode Support",
"description": "Implement theme switching...",
"contractType": "simple",
"initialPhase": "plan"
}
}
```
### State Management
The DiscussContractModal manages its own state:
```typescript
interface ModalState {
messages: Message[]; // Conversation history
isLoading: boolean; // API call in progress
error: string | null; // Error message
createdContract: CreatedContractInfo | null; // Created contract info
}
```
The parent (ListenPage) only needs to know:
- Is modal open?
- When was a contract created?
### Error Handling
1. **API errors** - Display in modal, allow retry
2. **Network errors** - Display error, suggest retry
3. **LLM errors** - Display gracefully, offer to restart conversation
---
## Files to Create/Modify
### New Files
1. `makima/src/server/handlers/contract_discuss.rs` - Backend handler
2. `makima/src/llm/discuss_tools.rs` - Tool definitions
3. `makima/frontend/src/components/listen/DiscussContractModal.tsx` - Chat UI
### Modified Files
1. `makima/src/server/mod.rs` - Register new route
2. `makima/src/server/openapi.rs` - Add OpenAPI docs
3. `makima/src/llm/mod.rs` - Export discuss tools
4. `makima/frontend/src/lib/api.ts` - Add API function
5. `makima/frontend/src/components/listen/ContractPickerModal.tsx` - Add discuss option
6. `makima/frontend/src/components/listen/ControlPanel.tsx` - Pass callback
7. `makima/frontend/src/routes/listen.tsx` - Integrate modal
---
## Key Design Decisions
1. **New endpoint vs. existing chatWithContract**
- **Decision:** New endpoint `/api/v1/contracts/discuss`
- **Rationale:** chatWithContract requires an existing contract; this feature creates one
2. **System prompt character**
- **Decision:** Makima-character aware of makima.jp platform
- **Rationale:** Consistent brand experience, engaging conversation
3. **Tool for contract creation**
- **Decision:** `create_contract` tool the LLM invokes
- **Rationale:** Natural conversation flow; LLM decides when ready
4. **Transcript context**
- **Decision:** Pass as optional context in system prompt
- **Rationale:** Seamless integration with listen page recording
5. **Speak functionality**
- **Decision:** Per-message speak button using useSpeakWebSocket
- **Rationale:** Reuses existing TTS infrastructure
6. **Conversation history**
- **Decision:** Frontend manages history, sends with each request
- **Rationale:** Stateless backend, consistent with existing chat patterns