From aa4d79535cb211f04c12c81303346b1a3ba0fd69 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Thu, 7 May 2026 16:02:35 +0200 Subject: [PATCH] feat: add AI session prompt export --- README.md | 3 +- docs/ROADMAP.md | 3 + src/App.tsx | 117 ++++++++++++++++++++++++------- src/features/export/exporters.ts | 77 ++++++++++++++++++++ 4 files changed, 173 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index f57cefe..a2c7da5 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ BuildPulse v0.1 includes: - Parking Lot screen - Pulse Log screen - Export screen +- AI session prompt export for coding-agent handoff - Appwrite-backed persistence on the Unraid server - Local cache fallback for resilience during backend hiccups - Pulse-shaped event records @@ -76,7 +77,7 @@ This means the data shape should be future-compatible from day one. 1. Feature Plan 2. Parking Lot 3. Pulse Log -4. Export +4. Export + AI Session Prompt ## Intended First Use diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 73f0f8c..a0d4b95 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -71,6 +71,9 @@ Potential features: - Include recent pulse context - End-session summary template +Note: +BuildPulse now has a lightweight export-side session prompt generator in v0.1.x so handoff quality improves before the fuller v0.4 workflow arrives. + ## v0.5 — Local/Cloud AI Assistant Goal: diff --git a/src/App.tsx b/src/App.tsx index 867f438..f665a99 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import type { ChangeEvent } from 'react' import './index.css' -import { createMarkdownPackage, createJsonExport, createPulseJsonl } from './features/export/exporters' +import { createAgentSessionPrompt, createMarkdownPackage, createJsonExport, createPulseJsonl } from './features/export/exporters' import { loadAppState, replaceAppState, saveAppState, validateAppState } from './store/storage' import { fetchRemoteState, pushRemoteState } from './store/remote' import { FEATURE_COLUMNS, FEATURE_PRIORITIES, FEATURE_STATUSES, PULSE_TYPES, RISK_LEVELS } from './store/types' @@ -15,6 +15,8 @@ const TABS: Array<{ key: TabKey; label: string }> = [ { key: 'export', label: 'Export' }, ] +const PROMPT_TARGETS = ['OpenClaw', 'Claude Code', 'Codex', 'Generic Agent'] as const + const initialFeatureDraft = { title: '', description: '', @@ -74,6 +76,8 @@ function App() { const [pulseTypeFilter, setPulseTypeFilter] = useState('all') const [pulseFeatureFilter, setPulseFeatureFilter] = useState('all') const [pulseSourceFilter, setPulseSourceFilter] = useState('all') + const [promptTarget, setPromptTarget] = useState<(typeof PROMPT_TARGETS)[number]>('OpenClaw') + const [promptFeatureId, setPromptFeatureId] = useState('') const [backendMode, setBackendMode] = useState<'connecting' | 'appwrite' | 'local-cache'>('connecting') const hasHydratedRemote = useRef(false) const initialLocalStateRef = useRef(appState) @@ -167,6 +171,10 @@ function App() { ) const markdownPackage = useMemo(() => createMarkdownPackage(appState), [appState]) + const sessionPrompt = useMemo( + () => createAgentSessionPrompt(appState, { featureId: promptFeatureId || undefined, target: promptTarget }), + [appState, promptFeatureId, promptTarget], + ) const updateProject = (field: keyof AppState['project'], value: string) => { setAppState((current) => ({ @@ -459,6 +467,15 @@ function App() { } } + const copySessionPrompt = async () => { + try { + await navigator.clipboard.writeText(sessionPrompt) + setStatusMessage('AI session prompt copied to clipboard.') + } catch { + setStatusMessage('Clipboard copy failed. Browser said no.') + } + } + const handleImport = async (event: ChangeEvent) => { const file = event.target.files?.[0] if (!file) return @@ -974,32 +991,80 @@ function App() { -
-
-
-

Markdown Package

-

`CLAUDE_CONTEXT.md` is the decision-boundary file. Keep it sharp.

-
-
-
- {Object.entries(markdownPackage).map(([filename, content]) => ( -
-
- {filename} -
- - -
-
-
{content}
+
+
+
+
+

AI Session Prompt

+

Pick a focus feature and hand a sharp brief to your coding agent instead of pasting the whole kitchen sink.

- ))} -
-
+
+
+ + +
+
+ + +
+
+
+ AI_SESSION_PROMPT.md +
+
{sessionPrompt}
+
+
+ +
+
+
+

Markdown Package

+

`CLAUDE_CONTEXT.md` is the decision-boundary file. Keep it sharp.

+
+
+
+ {Object.entries(markdownPackage).map(([filename, content]) => ( +
+
+ {filename} +
+ + +
+
+
{content}
+
+ ))} +
+
+ )} diff --git a/src/features/export/exporters.ts b/src/features/export/exporters.ts index 41acaca..c17402a 100644 --- a/src/features/export/exporters.ts +++ b/src/features/export/exporters.ts @@ -39,6 +39,83 @@ const renderFeature = (feature: Feature) => { const sortPulsesNewestFirst = (pulses: PulseEvent[]) => [...pulses].sort((a, b) => b.timestamp.localeCompare(a.timestamp)) +export const createAgentSessionPrompt = ( + state: AppState, + options?: { + featureId?: string + target?: string + }, +) => { + const grouped = groupFeatures(state.features) + const target = options?.target || 'AI coding agent' + const focusFeature = options?.featureId ? state.features.find((feature) => feature.id === options.featureId) ?? null : grouped.now[0] ?? null + const relatedPulses = sortPulsesNewestFirst(state.pulses) + .filter((pulse) => (focusFeature ? pulse.feature_id === focusFeature.id : true)) + .slice(0, 6) + + const renderFeatureBlock = (feature: Feature | null) => { + if (!feature) return '- No specific feature selected yet. Choose the smallest useful next move.' + + return [ + `- Title: ${feature.title}`, + `- Description: ${feature.description || '—'}`, + `- Column: ${columnLabels[feature.column]}`, + `- Priority: ${feature.priority}`, + `- Status: ${feature.status}`, + `- Acceptance criteria: ${feature.acceptance_criteria.length ? feature.acceptance_criteria.join('; ') : 'None yet'}`, + `- Scope notes: ${feature.scope_notes || '—'}`, + ].join('\n') + } + + return [ + `You are the ${target} for ${state.project.name}.`, + '', + 'Read the project context below and ship the smallest high-quality improvement that satisfies the focus feature without scope creep.', + '', + '## Project', + `- Name: ${state.project.name}`, + `- Pitch: ${state.project.one_line_pitch || '—'}`, + `- Current goal: ${state.project.current_goal || '—'}`, + `- Notes: ${state.project.notes || '—'}`, + '', + '## Focus Feature', + renderFeatureBlock(focusFeature), + '', + '## Other Active Features', + grouped.now.filter((feature) => feature.id !== focusFeature?.id).length + ? grouped.now + .filter((feature) => feature.id !== focusFeature?.id) + .map((feature) => `- ${feature.title} (${feature.status}, ${feature.priority})`) + .join('\n') + : '- None beyond the focus feature.', + '', + '## Next Up', + grouped.next.length ? grouped.next.map((feature) => `- ${feature.title} (${feature.status})`).join('\n') : '- Nothing queued yet.', + '', + '## Parking Lot / Do Not Implement Yet', + state.parking_lot.length + ? state.parking_lot.map((item) => `- ${item.title} — ${item.reason_parked || item.description || 'Parked for later.'}`).join('\n') + : '- Nothing parked yet.', + '', + '## Recent Relevant Pulses', + relatedPulses.length + ? relatedPulses.map((pulse) => `- ${formatDateTime(pulse.timestamp)} · ${pulse.pulse_type} · ${pulse.message}`).join('\n') + : '- No relevant pulse events yet.', + '', + '## Rules', + '- Stay inside the focus feature unless a tiny supporting fix is required.', + '- Do not implement Parking Lot items.', + '- Preserve working behavior and avoid needless rewrites.', + '- Prefer the smallest shippable step with clear evidence.', + '', + '## Deliver back', + '- What changed', + '- Files touched', + '- How you verified it', + '- Any blocker or follow-up you intentionally parked', + ].join('\n') +} + export const createJsonExport = (state: AppState) => JSON.stringify( {