Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9dccfa5
feat(gamification): status pulse rings + event feed stagger + spec up…
jsell-rh Mar 12, 2026
ccb611c
Merge branch 'main' into feat/gamification
jsell-rh Mar 12, 2026
a57b83f
feat(gamification): kanban spring-drop, @mention pulse, fleet vibe, s…
jsell-rh Mar 12, 2026
a57efd9
feat(gamification): spawn warp, PR shimmer, confetti priority variations
jsell-rh Mar 12, 2026
e4c69cb
docs(gamification): update spec with all brainstorm results and imple…
jsell-rh Mar 12, 2026
96e080c
fix(sse): agent_spawned payload from lifecycle.go missing space/agent…
jsell-rh Mar 12, 2026
ea439b2
feat(gamification): agent signature chimes, activity tick, spawn guard
jsell-rh Mar 12, 2026
35588eb
merge: resolve conflict with origin/main in gamification spec
jsell-rh Mar 12, 2026
3d5170e
fix(ux): usability audit fixes — H1, H2, H6, H7, TASK-038
jsell-rh Mar 12, 2026
ddd0062
fix(ux): H3, H4, M1, M13 usability audit fixes
jsell-rh Mar 12, 2026
ac427b3
fix(ux): usability batch 3 — C2, M2, M3, M4, M10, M11, M14
jsell-rh Mar 12, 2026
5b20303
fix(ux): TASK-045 — close agent create dialog before opening personas…
jsell-rh Mar 12, 2026
4b78f9f
feat(gamification): TASK-048 agent voice system enhancements
jsell-rh Mar 12, 2026
e2355c1
merge: resolve conflict in useNotifications.ts — keep TASK-048 voice …
jsell-rh Mar 12, 2026
48f1bb7
feat(gamification): audio-sme ideas — blocked alert, spawn sound, tas…
jsell-rh Mar 12, 2026
9fef007
feat(gamification): audio control center — volume, per-category toggl…
jsell-rh Mar 12, 2026
2be8208
merge: resolve conflicts in App.vue + useNotifications.ts — keep audi…
jsell-rh Mar 12, 2026
a8d7364
feat(gamification): agent moods, boss level fanfare, heartbeat mode —…
jsell-rh Mar 12, 2026
5db8082
merge: resolve conflicts for PR #182 — keep full audio palette (HEAD)
jsell-rh Mar 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ import {
notifyBossMessage,
playSprintComplete,
playAgentSignatureChime,
playActivityTick,
playBlockedAlert,
playAgentSpawn,
playMentionPing,
playPRShipped,
playCollaborationChord,
playAgentMoodActive,
playAgentMoodIdle,
playAgentTick,
resetAgentChimes,
} from '@/composables/useNotifications'
import { useConfetti } from '@/composables/useConfetti'
Expand Down Expand Up @@ -717,6 +719,8 @@ function scheduleSpacesReload(delayMs = 1000) {

function setupSSE() {
sse.on('agent_updated', (data) => {
// Capture prevStatus BEFORE the in-place patch so mood/alert transitions fire correctly.
const prevStatus = currentSpace.value?.agents[data.agent]?.status
// Patch agent in-place immediately for instant UI feedback — no HTTP round-trip.
// SSE payload has status+summary; schedule a debounced full reload for
// items/questions/blockers that aren't included in the SSE payload.
Expand All @@ -740,13 +744,18 @@ function setupSSE() {
checkSprintComplete()
// Agent signature chime — plays once per agent per page load on first update
playAgentSignatureChime(data.agent)
// Dissonance alert — plays when agent transitions into blocked or error state
if ((data.status === 'blocked' || data.status === 'error')) {
const prev = currentSpace.value?.agents[data.agent]?.status
if (prev !== 'blocked' && prev !== 'error') playBlockedAlert()
// Dissonance alert — fires when transitioning INTO blocked/error (not already there)
if ((data.status === 'blocked' || data.status === 'error')
&& prevStatus !== 'blocked' && prevStatus !== 'error') {
playBlockedAlert()
}
// Activity tick — subtle ambient sound for busy server-room feel (opt-in)
playActivityTick()
// Agent moods (#5): ascending voice on going active, descending on going idle
if (prevStatus && prevStatus !== data.status) {
if (data.status === 'active') playAgentMoodActive(data.agent)
else if (data.status === 'idle' && prevStatus === 'active') playAgentMoodIdle(data.agent)
}
// Activity tick — agent-tuned pentatonic micro-tone (#7 Heartbeat Mode)
playAgentTick(data.agent)
statusAnnouncement.value = `Agent ${data.agent} updated: ${data.status}`
pushLog('agent_updated', `[${data.agent}] ${data.status}: ${data.summary}`)
})
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/KanbanView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ async function onTaskDrop(taskId: string, newStatus: TaskStatus) {
try {
const updated = await api.moveTask(props.space.name, taskId, newStatus)
Object.assign(task, updated)
if (newStatus === 'done') { celebrate(undefined, undefined, (task.priority ?? 'medium') as ConfettiPriority); playSuccess() }
if (newStatus === 'done') { celebrate(undefined, undefined, (task.priority ?? 'medium') as ConfettiPriority); playSuccess(task.priority ?? 'medium') }
else { playTaskTransition(newStatus) }
} catch {
// Revert on error
Expand Down Expand Up @@ -239,7 +239,7 @@ const unsubTaskUpdated = sse.on('task_updated', (data) => {
const existing = tasks.value.find(t => t.id === data.id)
if (data.status === 'done' && existing && existing.status !== 'done') {
celebrate(undefined, undefined, (existing.priority ?? 'medium') as ConfettiPriority)
playSuccess()
playSuccess(existing.priority ?? 'medium')
} else if (data.status && data.status !== existing?.status && data.status !== 'done') {
playTaskTransition(data.status)
}
Expand Down
97 changes: 81 additions & 16 deletions frontend/src/composables/useNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,35 +159,53 @@ export function playChime(): void {
}
}

// Task-done success chord
export function playSuccess(): void {
// Task-done success chord.
// priority='critical' (#4 Boss Level): adds an ascending run before the chord for extra fanfare.
export function playSuccess(priority?: string): void {
if (!isCategoryEnabled('celebrations')) return
const isCritical = priority === 'critical'
try {
const ctx = new AudioContext()
const t = ctx.currentTime
const theme = soundTheme.value
// Critical-priority head-start: ascending run (C5→G5→C6) gives a "Boss Level" feeling
const offset = isCritical ? 0.38 : 0
if (isCritical && !prefersReducedMotion) {
if (theme === 'retro') {
tone(ctx, 523, t, 0.09, effectiveVolume(0.065), 'square')
tone(ctx, 784, t + 0.10, 0.09, effectiveVolume(0.065), 'square')
tone(ctx, 1047, t + 0.22, 0.1, effectiveVolume(0.075), 'square')
} else if (theme === 'space') {
sweep(ctx, 300, 1400, t, 0.32, effectiveVolume(0.07), 'sine')
} else {
// Classic/Nature: short C5→G5→C6 arpeggio lead-in
const wave: OscillatorType = theme === 'nature' ? 'triangle' : 'sine'
tone(ctx, 523.25, t, 0.12, effectiveVolume(0.055), wave)
tone(ctx, 783.99, t + 0.13, 0.12, effectiveVolume(0.055), wave)
tone(ctx, 1046.5, t + 0.26, 0.1, effectiveVolume(0.065), wave)
}
}

if (theme === 'retro') {
// Chiptune ascending arpeggio
tone(ctx, 262, t, 0.12, 0.07, 'square') // C4
tone(ctx, 330, t + 0.10, 0.12, 0.07, 'square') // E4
tone(ctx, 392, t + 0.20, 0.12, 0.07, 'square') // G4
tone(ctx, 523, t + 0.30, 0.22, 0.09, 'square') // C5 held
tone(ctx, 262, t + offset, 0.12, 0.07, 'square') // C4
tone(ctx, 330, t + offset + 0.10, 0.12, 0.07, 'square') // E4
tone(ctx, 392, t + offset + 0.20, 0.12, 0.07, 'square') // G4
tone(ctx, 523, t + offset + 0.30, 0.22, 0.09, 'square') // C5 held
} else if (theme === 'space') {
sweep(ctx, 400, 800, t, 0.15, 0.07, 'sine')
sweep(ctx, 800, 1200, t + 0.18, 0.25, 0.08, 'sine')
sweep(ctx, 400, 800, t + offset, 0.15, 0.07, 'sine')
sweep(ctx, 800, 1200, t + offset + 0.18, 0.25, 0.08, 'sine')
} else if (theme === 'nature') {
tone(ctx, 523.25, t, 0.6, 0.05, 'triangle') // C5
tone(ctx, 659.25, t + 0.12, 0.55, 0.05, 'triangle') // E5
tone(ctx, 783.99, t + 0.24, 0.5, 0.05, 'triangle') // G5
tone(ctx, 523.25, t + offset, 0.6, 0.05, 'triangle') // C5
tone(ctx, 659.25, t + offset + 0.12, 0.55, 0.05, 'triangle') // E5
tone(ctx, 783.99, t + offset + 0.24, 0.5, 0.05, 'triangle') // G5
} else {
// Classic: C major triad (C5, E5, G5)
tone(ctx, 523.25, t, 0.5) // C5
tone(ctx, 659.25, t + 0.08, 0.45) // E5
tone(ctx, 783.99, t + 0.16, 0.4) // G5
tone(ctx, 523.25, t + offset, 0.5) // C5
tone(ctx, 659.25, t + offset + 0.08, 0.45) // E5
tone(ctx, 783.99, t + offset + 0.16, 0.4) // G5
}

setTimeout(() => ctx.close(), 1500)
setTimeout(() => ctx.close(), isCritical ? 2000 : 1500)
} catch {
// AudioContext not available
}
Expand Down Expand Up @@ -321,6 +339,20 @@ export function playActivityTick(): void {
}
}

// ── #7 Heartbeat Mode — agent-personality tick ─────────────────────────────
// Each agent's tick is a 3ms micro-tone at their pentatonic frequency instead
// of uniform white noise. Active fleets sound like a chord of working agents.
export function playAgentTick(agentName: string): void {
if (!activityTickEnabled.value) return
try {
const ctx = new AudioContext()
const t = ctx.currentTime
const freq = PENTATONIC_HZ[hashName(agentName) % PENTATONIC_HZ.length]!
tone(ctx, freq, t, 0.003, 0.008 * soundVolume.value, 'sine') // 3ms micro-tone
setTimeout(() => ctx.close(), 100)
} catch { /* AudioContext not available */ }
}

// ── Reduced-motion awareness ────────────────────────────────────────────────
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches

Expand Down Expand Up @@ -432,3 +464,36 @@ export function playCollaborationChord(senderName: string, receiverName: string)
setTimeout(() => ctx.close(), 600)
} catch { /* AudioContext not available */ }
}

// ── #5 Agent Moods — status transition voice variants ─────────────────────
// Each agent's pentatonic root frequency played in ascending or descending
// intervals to convey "waking up" vs "settling down" — completing the arc.
// Uses the same pentatonic hash so moods are tonally consistent with chimes.

export function playAgentMoodActive(agentName: string): void {
if (!isCategoryEnabled('events')) return
try {
const ctx = new AudioContext()
const t = ctx.currentTime
const root = PENTATONIC_HZ[hashName(agentName) % PENTATONIC_HZ.length]!
const fifth = root * 1.498 // perfect fifth (3:2 ratio) — energizing, upward
// Ascending: root → fifth, short and punchy
tone(ctx, root, t, 0.18, effectiveVolume(0.038), 'sine')
tone(ctx, fifth, t + 0.1, 0.16, effectiveVolume(0.038), 'triangle')
setTimeout(() => ctx.close(), 500)
} catch { /* AudioContext not available */ }
}

export function playAgentMoodIdle(agentName: string): void {
if (!isCategoryEnabled('events')) return
try {
const ctx = new AudioContext()
const t = ctx.currentTime
const root = PENTATONIC_HZ[hashName(agentName) % PENTATONIC_HZ.length]!
const fifth = root * 1.498
// Descending: fifth → root, slower and softer — "settling down"
tone(ctx, fifth, t, 0.22, effectiveVolume(0.028), 'triangle')
tone(ctx, root, t + 0.13, 0.28, effectiveVolume(0.022), 'sine')
setTimeout(() => ctx.close(), 600)
} catch { /* AudioContext not available */ }
}
Loading