feat: add editable handoff preview flow
This commit is contained in:
@@ -4,7 +4,7 @@ BuildPulse is a calm planning cockpit for AI-assisted product building.
|
|||||||
It helps capture features, park distracting ideas, log progress as Pulse events, and export clean project context for AI coding agents such as Claude Code, Codex, OpenCode, OpenClaw, or future autonomous agents.
|
It helps capture features, park distracting ideas, log progress as Pulse events, and export clean project context for AI coding agents such as Claude Code, Codex, OpenCode, OpenClaw, or future autonomous agents.
|
||||||
|
|
||||||
Current release line:
|
Current release line:
|
||||||
- v0.4.0 — one-tap feature handoffs with target-specific briefs and optional INTENT logging
|
- v0.4.1 — preview/edit-before-copy handoffs with cleaner INTENT controls and tighter mobile UX
|
||||||
|
|
||||||
Personal runtime target:
|
Personal runtime target:
|
||||||
- `build.friborg.uk`
|
- `build.friborg.uk`
|
||||||
|
|||||||
+1
-1
@@ -67,9 +67,9 @@ Current shipped slices:
|
|||||||
- v0.3.1 — focused handoff shortcuts
|
- v0.3.1 — focused handoff shortcuts
|
||||||
- v0.3.2 — target-specific handoff presets
|
- v0.3.2 — target-specific handoff presets
|
||||||
- v0.4.0 — one-tap feature handoffs
|
- v0.4.0 — one-tap feature handoffs
|
||||||
|
- v0.4.1 — preview/edit before copy + explicit INTENT controls
|
||||||
|
|
||||||
Planned slices:
|
Planned slices:
|
||||||
- v0.4.1 — preview/edit before copy + optional INTENT pulse controls
|
|
||||||
- v0.4.2 — paste agent result into RESULT/BLOCKER/TEST_RESULT pulses
|
- v0.4.2 — paste agent result into RESULT/BLOCKER/TEST_RESULT pulses
|
||||||
- v0.4.3 — session modes (30-minute, feature-based, bugfix, QA review)
|
- v0.4.3 — session modes (30-minute, feature-based, bugfix, QA review)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "buildpulse",
|
"name": "buildpulse",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"api": "node --env-file=../.env server/index.mjs",
|
"api": "node --env-file=../.env server/index.mjs",
|
||||||
|
|||||||
+178
-45
@@ -118,8 +118,11 @@ function App() {
|
|||||||
const [pulseFeatureFilter, setPulseFeatureFilter] = useState('all')
|
const [pulseFeatureFilter, setPulseFeatureFilter] = useState('all')
|
||||||
const [pulseSourceFilter, setPulseSourceFilter] = useState('all')
|
const [pulseSourceFilter, setPulseSourceFilter] = useState('all')
|
||||||
const [promptTarget, setPromptTarget] = useState<(typeof PROMPT_TARGETS)[number]>('OpenClaw')
|
const [promptTarget, setPromptTarget] = useState<(typeof PROMPT_TARGETS)[number]>('OpenClaw')
|
||||||
const [logIntentOnHandoff, setLogIntentOnHandoff] = useState(true)
|
|
||||||
const [promptFeatureId, setPromptFeatureId] = useState('')
|
const [promptFeatureId, setPromptFeatureId] = useState('')
|
||||||
|
const [handoffPreviewOpen, setHandoffPreviewOpen] = useState(false)
|
||||||
|
const [handoffPreviewTarget, setHandoffPreviewTarget] = useState<(typeof PROMPT_TARGETS)[number]>('OpenClaw')
|
||||||
|
const [handoffPreviewFeatureId, setHandoffPreviewFeatureId] = useState('')
|
||||||
|
const [handoffPreviewDraft, setHandoffPreviewDraft] = useState('')
|
||||||
const [backendMode, setBackendMode] = useState<'connecting' | 'appwrite' | 'local-cache'>('connecting')
|
const [backendMode, setBackendMode] = useState<'connecting' | 'appwrite' | 'local-cache'>('connecting')
|
||||||
const [syncStatus, setSyncStatus] = useState<'connecting' | 'synced' | 'pending' | 'syncing' | 'degraded'>('connecting')
|
const [syncStatus, setSyncStatus] = useState<'connecting' | 'synced' | 'pending' | 'syncing' | 'degraded'>('connecting')
|
||||||
const [lastSyncedAt, setLastSyncedAt] = useState<string | null>(null)
|
const [lastSyncedAt, setLastSyncedAt] = useState<string | null>(null)
|
||||||
@@ -231,6 +234,11 @@ function App() {
|
|||||||
[appState.features, selectedFeatureId],
|
[appState.features, selectedFeatureId],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handoffPreviewFeature = useMemo(
|
||||||
|
() => appState.features.find((feature) => feature.id === handoffPreviewFeatureId) ?? null,
|
||||||
|
[appState.features, handoffPreviewFeatureId],
|
||||||
|
)
|
||||||
|
|
||||||
const selectedParkingItem = useMemo(
|
const selectedParkingItem = useMemo(
|
||||||
() => appState.parking_lot.find((item) => item.id === selectedParkingId) ?? null,
|
() => appState.parking_lot.find((item) => item.id === selectedParkingId) ?? null,
|
||||||
[appState.parking_lot, selectedParkingId],
|
[appState.parking_lot, selectedParkingId],
|
||||||
@@ -1204,44 +1212,61 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyFocusedHandoff = async (featureId?: string, target = promptTarget, shouldLogIntent = logIntentOnHandoff) => {
|
const buildHandoffPrompt = (featureId?: string, target = promptTarget) => {
|
||||||
const resolvedFeatureId = featureId ?? selectedFeature?.id ?? (promptFeatureId || groupedFeatures.now[0]?.id)
|
const resolvedFeatureId = featureId ?? selectedFeature?.id ?? (promptFeatureId || groupedFeatures.now[0]?.id)
|
||||||
const prompt = createAgentSessionPrompt(appState, {
|
return {
|
||||||
featureId: resolvedFeatureId || undefined,
|
resolvedFeatureId,
|
||||||
target,
|
prompt: createAgentSessionPrompt(appState, {
|
||||||
})
|
featureId: resolvedFeatureId || undefined,
|
||||||
|
target,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logHandoffIntent = (featureId: string, target: (typeof PROMPT_TARGETS)[number]) => {
|
||||||
|
const feature = appState.features.find((entry) => entry.id === featureId)
|
||||||
|
if (!feature) return null
|
||||||
|
|
||||||
|
const timestamp = nowIso()
|
||||||
|
const pulse: PulseEvent = {
|
||||||
|
id: `pulse_${Date.now().toString(36)}`,
|
||||||
|
timestamp,
|
||||||
|
project_id: appState.project.id,
|
||||||
|
feature_id: feature.id,
|
||||||
|
source: 'buildpulse',
|
||||||
|
agent_id: target,
|
||||||
|
pulse_type: 'INTENT',
|
||||||
|
message: `Generated ${target} handoff for feature “${feature.title}”.`,
|
||||||
|
confidence_score: 0.9,
|
||||||
|
evidence_refs: [
|
||||||
|
'Feature-detail handoff action',
|
||||||
|
feature.release_id ? `Release: ${appState.releases.find((release) => release.id === feature.release_id)?.name || feature.release_id}` : 'No linked release',
|
||||||
|
feature.phase_id ? `Phase: ${appState.phases.find((phase) => phase.id === feature.phase_id)?.title || feature.phase_id}` : 'No linked phase',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppState((current) => ({
|
||||||
|
...current,
|
||||||
|
pulses: [pulse, ...current.pulses],
|
||||||
|
}))
|
||||||
|
|
||||||
|
return feature
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyFocusedHandoff = async (featureId?: string, target = promptTarget, shouldLogIntent = false, promptOverride?: string) => {
|
||||||
|
const { resolvedFeatureId, prompt } = buildHandoffPrompt(featureId, target)
|
||||||
|
const finalPrompt = promptOverride ?? prompt
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(prompt)
|
await navigator.clipboard.writeText(finalPrompt)
|
||||||
if (resolvedFeatureId) {
|
if (resolvedFeatureId) {
|
||||||
setPromptFeatureId(resolvedFeatureId)
|
setPromptFeatureId(resolvedFeatureId)
|
||||||
}
|
}
|
||||||
setPromptTarget(target)
|
setPromptTarget(target)
|
||||||
|
|
||||||
const feature = resolvedFeatureId ? appState.features.find((entry) => entry.id === resolvedFeatureId) : null
|
let feature = resolvedFeatureId ? appState.features.find((entry) => entry.id === resolvedFeatureId) : null
|
||||||
if (feature && shouldLogIntent) {
|
if (resolvedFeatureId && shouldLogIntent) {
|
||||||
const timestamp = nowIso()
|
feature = logHandoffIntent(resolvedFeatureId, target) ?? feature
|
||||||
const pulse: PulseEvent = {
|
|
||||||
id: `pulse_${Date.now().toString(36)}`,
|
|
||||||
timestamp,
|
|
||||||
project_id: appState.project.id,
|
|
||||||
feature_id: feature.id,
|
|
||||||
source: 'buildpulse',
|
|
||||||
agent_id: target,
|
|
||||||
pulse_type: 'INTENT',
|
|
||||||
message: `Generated ${target} handoff for feature “${feature.title}”.`,
|
|
||||||
confidence_score: 0.9,
|
|
||||||
evidence_refs: [
|
|
||||||
'Feature-detail handoff action',
|
|
||||||
feature.release_id ? `Release: ${appState.releases.find((release) => release.id === feature.release_id)?.name || feature.release_id}` : 'No linked release',
|
|
||||||
feature.phase_id ? `Phase: ${appState.phases.find((phase) => phase.id === feature.phase_id)?.title || feature.phase_id}` : 'No linked phase',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
setAppState((current) => ({
|
|
||||||
...current,
|
|
||||||
pulses: [pulse, ...current.pulses],
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatusMessage(
|
setStatusMessage(
|
||||||
@@ -1254,8 +1279,33 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openHandoffPreview = (featureId?: string, target = promptTarget) => {
|
||||||
|
const { resolvedFeatureId, prompt } = buildHandoffPrompt(featureId, target)
|
||||||
|
if (resolvedFeatureId) {
|
||||||
|
setPromptFeatureId(resolvedFeatureId)
|
||||||
|
setHandoffPreviewFeatureId(resolvedFeatureId)
|
||||||
|
}
|
||||||
|
setHandoffPreviewTarget(target)
|
||||||
|
setHandoffPreviewDraft(prompt)
|
||||||
|
setHandoffPreviewOpen(true)
|
||||||
|
setStatusMessage('Preview the handoff, tweak it if needed, then copy when ready.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncHandoffPreviewTarget = (target: (typeof PROMPT_TARGETS)[number]) => {
|
||||||
|
const { prompt } = buildHandoffPrompt(handoffPreviewFeatureId || undefined, target)
|
||||||
|
setHandoffPreviewTarget(target)
|
||||||
|
setHandoffPreviewDraft(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetHandoffPreview = () => {
|
||||||
|
if (!handoffPreviewFeatureId) return
|
||||||
|
const { prompt } = buildHandoffPrompt(handoffPreviewFeatureId, handoffPreviewTarget)
|
||||||
|
setHandoffPreviewDraft(prompt)
|
||||||
|
setStatusMessage('Handoff preview reset to the current preset.')
|
||||||
|
}
|
||||||
|
|
||||||
const copySessionPrompt = async () => {
|
const copySessionPrompt = async () => {
|
||||||
await copyFocusedHandoff(promptFeatureId || undefined, promptTarget)
|
await copyFocusedHandoff(promptFeatureId || undefined, promptTarget, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleImport = async (event: ChangeEvent<HTMLInputElement>) => {
|
const handleImport = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -1385,7 +1435,7 @@ function App() {
|
|||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<header className="mobile-shell-header card">
|
<header className="mobile-shell-header card">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">BuildPulse v0.4.0</p>
|
<p className="eyebrow">BuildPulse v0.4.1</p>
|
||||||
<h1>{appState.project.name}</h1>
|
<h1>{appState.project.name}</h1>
|
||||||
<p className="hero-goal compact-goal">
|
<p className="hero-goal compact-goal">
|
||||||
<strong>Current goal:</strong> {appState.project.current_goal || 'Classify new ideas before they become work.'}
|
<strong>Current goal:</strong> {appState.project.current_goal || 'Classify new ideas before they become work.'}
|
||||||
@@ -1649,6 +1699,93 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{handoffPreviewOpen && handoffPreviewFeature && (
|
||||||
|
<div className="editor-sheet-backdrop" role="presentation">
|
||||||
|
<section className="card editor-sheet handoff-sheet" role="dialog" aria-modal="true" aria-label="Handoff preview">
|
||||||
|
<div className="section-heading compact">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">v0.4.1 Handoff preview</p>
|
||||||
|
<h3>{handoffPreviewFeature.title}</h3>
|
||||||
|
<p>Choose the target, tweak the brief if needed, then copy it cleanly.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" className="ghost small" onClick={() => setHandoffPreviewOpen(false)}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sheet-summary-grid handoff-preview-grid">
|
||||||
|
<div className="focus-panel">
|
||||||
|
<h4>Preview context</h4>
|
||||||
|
<div className="focus-badges">
|
||||||
|
<span className="pill">{handoffPreviewTarget}</span>
|
||||||
|
<span className="pill">{handoffPreviewFeature.status}</span>
|
||||||
|
<span className={`pill ${handoffPreviewFeature.priority}`}>{handoffPreviewFeature.priority}</span>
|
||||||
|
{handoffPreviewFeature.release_role && <span className="pill">{handoffPreviewFeature.release_role}</span>}
|
||||||
|
</div>
|
||||||
|
<p><strong>Phase:</strong> {appState.phases.find((phase) => phase.id === handoffPreviewFeature.phase_id)?.title || 'No phase linked'}</p>
|
||||||
|
<p><strong>Release:</strong> {appState.releases.find((release) => release.id === handoffPreviewFeature.release_id)?.name || 'No release linked'}</p>
|
||||||
|
<p><strong>Acceptance criteria:</strong> {handoffPreviewFeature.acceptance_criteria.length ? handoffPreviewFeature.acceptance_criteria.join('; ') : 'None yet'}</p>
|
||||||
|
<p><strong>Scope guard:</strong> {handoffPreviewFeature.scope_notes || 'No scope notes yet.'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="focus-panel">
|
||||||
|
<h4>Handoff controls</h4>
|
||||||
|
<div className="button-inline-row">
|
||||||
|
{PROMPT_TARGETS.map((target) => (
|
||||||
|
<button
|
||||||
|
key={target}
|
||||||
|
type="button"
|
||||||
|
className={handoffPreviewTarget === target ? 'small' : 'ghost small'}
|
||||||
|
onClick={() => syncHandoffPreviewTarget(target)}
|
||||||
|
>
|
||||||
|
{target}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="tap-hint">Preview mode is the safer path when you want to edit wording or create a clean INTENT pulse.</p>
|
||||||
|
<div className="button-inline-row">
|
||||||
|
<button type="button" className="ghost small" onClick={resetHandoffPreview}>Reset to preset</button>
|
||||||
|
<button type="button" className="ghost small" onClick={() => openFeatureHandoff(handoffPreviewFeature.id)}>Open in Export</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="full-span handoff-preview-editor">
|
||||||
|
<span>Editable handoff brief</span>
|
||||||
|
<textarea
|
||||||
|
rows={18}
|
||||||
|
value={handoffPreviewDraft}
|
||||||
|
onChange={(event) => setHandoffPreviewDraft(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="button-row sheet-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
void copyFocusedHandoff(handoffPreviewFeature.id, handoffPreviewTarget, false, handoffPreviewDraft)
|
||||||
|
setHandoffPreviewOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy Handoff
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
void copyFocusedHandoff(handoffPreviewFeature.id, handoffPreviewTarget, true, handoffPreviewDraft)
|
||||||
|
setHandoffPreviewOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy + INTENT Pulse
|
||||||
|
</button>
|
||||||
|
<button type="button" className="ghost" onClick={() => setHandoffPreviewOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{featureSheetOpen && (
|
{featureSheetOpen && (
|
||||||
<div className="editor-sheet-backdrop" role="presentation">
|
<div className="editor-sheet-backdrop" role="presentation">
|
||||||
<section className="card editor-sheet" role="dialog" aria-modal="true" aria-label={selectedFeature ? 'Feature details' : 'Manual feature'}>
|
<section className="card editor-sheet" role="dialog" aria-modal="true" aria-label={selectedFeature ? 'Feature details' : 'Manual feature'}>
|
||||||
@@ -1713,26 +1850,22 @@ function App() {
|
|||||||
<button type="button" className="ghost small" onClick={() => openFeatureHandoff(selectedFeature.id)}>
|
<button type="button" className="ghost small" onClick={() => openFeatureHandoff(selectedFeature.id)}>
|
||||||
Prepare AI Handoff
|
Prepare AI Handoff
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" className="ghost small" onClick={() => openHandoffPreview(selectedFeature.id, 'OpenClaw')}>
|
||||||
|
Preview / Edit Handoff
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<label className="tap-chip">
|
<p className="tap-hint">Quick copy if you already know the target. Use preview when you want to tweak the brief or log an INTENT cleanly.</p>
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={logIntentOnHandoff}
|
|
||||||
onChange={(event) => setLogIntentOnHandoff(event.target.checked)}
|
|
||||||
/>
|
|
||||||
<span>Log INTENT pulse when copying handoff</span>
|
|
||||||
</label>
|
|
||||||
<div className="button-inline-row">
|
<div className="button-inline-row">
|
||||||
<button type="button" className="ghost small" onClick={() => void copyFocusedHandoff(selectedFeature.id, 'OpenClaw')}>
|
<button type="button" className="ghost small" onClick={() => void copyFocusedHandoff(selectedFeature.id, 'OpenClaw', false)}>
|
||||||
Copy for OpenClaw
|
Copy for OpenClaw
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="ghost small" onClick={() => void copyFocusedHandoff(selectedFeature.id, 'Claude Code')}>
|
<button type="button" className="ghost small" onClick={() => void copyFocusedHandoff(selectedFeature.id, 'Claude Code', false)}>
|
||||||
Copy for Claude Code
|
Copy for Claude Code
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="ghost small" onClick={() => void copyFocusedHandoff(selectedFeature.id, 'Codex')}>
|
<button type="button" className="ghost small" onClick={() => void copyFocusedHandoff(selectedFeature.id, 'Codex', false)}>
|
||||||
Copy for Codex
|
Copy for Codex
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="ghost small" onClick={() => void copyFocusedHandoff(selectedFeature.id, 'Generic Agent')}>
|
<button type="button" className="ghost small" onClick={() => void copyFocusedHandoff(selectedFeature.id, 'Generic Agent', false)}>
|
||||||
Copy Generic Brief
|
Copy Generic Brief
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -131,13 +131,13 @@ export const createSeedState = (): AppState => ({
|
|||||||
definition_of_done: [
|
definition_of_done: [
|
||||||
'Each feature detail exposes one-tap copy actions for OpenClaw, Claude Code, Codex, and a generic brief.',
|
'Each feature detail exposes one-tap copy actions for OpenClaw, Claude Code, Codex, and a generic brief.',
|
||||||
'Prompt generation includes phase, release, readiness, blockers, parking-lot, and forbidden-work guardrails.',
|
'Prompt generation includes phase, release, readiness, blockers, parking-lot, and forbidden-work guardrails.',
|
||||||
'Copy actions can optionally log an INTENT pulse without triggering any live execution.',
|
'Preview/edit before copy is available with explicit INTENT pulse controls and mobile-safe actions.',
|
||||||
],
|
],
|
||||||
required_feature_ids: ['export_screen', 'feature_focused_handoff_shortcuts'],
|
required_feature_ids: ['export_screen', 'feature_focused_handoff_shortcuts'],
|
||||||
optional_feature_ids: ['pulse_log_screen'],
|
optional_feature_ids: ['pulse_log_screen'],
|
||||||
forbidden_feature_titles: ['Live OpenClaw/Hermes Agent Status', 'WebSocket agent telemetry', 'GitHub / Gitea sync'],
|
forbidden_feature_titles: ['Live OpenClaw/Hermes Agent Status', 'WebSocket agent telemetry', 'GitHub / Gitea sync'],
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
notes: 'v0.4.0 focuses on one-tap feature handoffs before preview/edit and result-capture slices.',
|
notes: 'v0.4.1 adds preview/edit before copy and cleaner INTENT controls; result capture comes next.',
|
||||||
created_at: seedDate,
|
created_at: seedDate,
|
||||||
updated_at: seedDate,
|
updated_at: seedDate,
|
||||||
},
|
},
|
||||||
@@ -152,10 +152,10 @@ export const createSeedState = (): AppState => ({
|
|||||||
status: 'building',
|
status: 'building',
|
||||||
acceptance_criteria: [
|
acceptance_criteria: [
|
||||||
'Feature detail exposes separate copy actions for OpenClaw, Claude Code, Codex, and Generic.',
|
'Feature detail exposes separate copy actions for OpenClaw, Claude Code, Codex, and Generic.',
|
||||||
'Copied prompt includes release context, blockers, and forbidden-work warnings.',
|
'Preview/edit is available before copy, with target switching and reset-to-preset behavior.',
|
||||||
'Copy can optionally log an INTENT pulse for the chosen target.',
|
'INTENT pulse logging is explicit from the preview flow instead of hidden behind a toggle.',
|
||||||
],
|
],
|
||||||
scope_notes: 'This is the v0.4.0 loop: pick a feature, copy the right brief in one tap, then let the agent work.',
|
scope_notes: 'This is the v0.4 loop: quick-copy when you know the target, preview/edit when you want a safer brief, then let the agent work.',
|
||||||
phase_id: 'phase_session_handoff',
|
phase_id: 'phase_session_handoff',
|
||||||
release_id: 'release_v040_focused_handoffs',
|
release_id: 'release_v040_focused_handoffs',
|
||||||
release_role: 'required',
|
release_role: 'required',
|
||||||
|
|||||||
@@ -263,6 +263,31 @@ textarea {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.handoff-sheet {
|
||||||
|
max-width: 1080px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-preview-grid {
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-preview-editor {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-preview-editor span {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #9fb9d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-preview-editor textarea {
|
||||||
|
min-height: 22rem;
|
||||||
|
font-family: 'IBM Plex Mono', 'SFMono-Regular', ui-monospace, monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
.focus-panel h4 {
|
.focus-panel h4 {
|
||||||
margin: 0 0 0.65rem;
|
margin: 0 0 0.65rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user