Skip to content

Anxiety Aid Tools: guided meditation, peaceful visualization, relaxing sounds - #210

Open
alvinunreal wants to merge 3 commits into
feat/aat-groundingfrom
feat/aat-media-player
Open

alvinunreal wants to merge 3 commits into
feat/aat-groundingfrom
feat/aat-media-player

Conversation

@alvinunreal

Copy link
Copy Markdown
Collaborator

Stacked on #207 (base feat/aat-grounding). Retarget to main after #206 and #207 merge.

Summary

Platform (host)

  • player session kind (narrated tracks) and soundscape session kind (layered ambient scenes). Both require network. Every media URL must be on one of the plugin's approved network.hosts; the bridge refuses the open otherwise.
  • session-media-cache.ts: downloads each file once through a new safeHttpFetchBytes (same SSRF, host and redirect guards as ctx.net) and caches it under userData (1 GB LRU, 25 MB per file, atomic writes, de-duplicated downloads). Files are served on a privileged openpets-session-media: scheme, added to the pet window CSP. The coordinator only resolves URLs listed in the open descriptor. Media plays offline after first use.
  • Renderer:
    • player-practice.cjs: cover thumbnail, track picker, three-line caption, part n/N, time, back 15s / previous / next / mute. An unavailable segment keeps its caption on screen for about 9s and the track moves on.
    • soundscape-practice.cjs: a Web Audio port of AAT's ambient engine (crossfaded loops; random or wave accents with file, volume, pan and pitch variation; pause via context suspend; volume, mute, sleep timer).
  • Shell hooks: hidesSharedRows, hidesCount, extraControls, onPause / onResume / onStop, ui.resolveMedia. The practice cap goes from 6 to 8.

Anxiety Aid Tools

  • Guided meditation: 8 AAT sessions (92 segments) with covers.
  • Peaceful visualization: 9 scenes × 7 steps. Covers pending: the image service hit its quota; the placeholder tile shows until then.
  • Relaxing sounds: 8 environments (43 layers) with covers.
  • Narration language: the closest AAT one (es-419 → es, pt-BR → pt, zh-Hans / zh-Hant → zh). ja and ko get English narration with translated captions and a "Narration in English" note.
  • Captions and copy ×7: AAT transcripts for en / es / pt / zh, OpenCC for zh-Hant, my own translations for ja / ko.
  • Honest Info with verified citations. AAT's misdescribed studies are not repeated.

Verification

  • Desktop typecheck, build and tests pass, including a new bridge test: media on an unapproved host is refused, and approved hosts reach the host.
  • Plugin golden test covers the tracks and scenes contract, narration mapping, commands, menus and remembered selections.
  • plugins:test, plugins:locales, plugins:check, plugins:package, plugins:validate-release (18 plugins): pass.
  • Every plugin descriptor (7 practices × 7 locales) validates against the host validator.
  • linkedom player test: segment order, gaps, pause and resume in a gap, track switch mid-run, text-only fallback, completion.
  • Headless Chromium with real AAT ocean-beach audio: both beds decode and start, and the 25s bed crossfades into a new instance on time.
  • Earlier renderer regression harnesses still pass; breathing and PMR events are unchanged.

Not yet

  • Not run in the app.
  • Visualization covers (image quota).
  • docs/*.md pending sign-off.

…tion

Add a "player" session kind for narrated practices: tracks of https
audio segments with captions, cover images, a gap between segments and
an optional narration-language note. The bridge requires the network
permission and refuses segments on unapproved hosts. The host downloads
each segment once through a new safeHttpFetchBytes (same guards as
ctx.net), caches it under userData with an LRU cap, and serves it on a
privileged openpets-session-media: scheme; the coordinator only
resolves URLs listed in the open descriptor. Unavailable segments fall
back to a timed caption so the practice still works offline.

The overlay gets a compact player view (cover, track picker, captions,
progress, back 15s / previous / next / mute) and shell hooks for it.

Anxiety Aid Tools gains Guided meditation: AAT's eight sessions
streamed from R2 in the closest narration language (ja/ko hear English
with translated captions), covers, captions in seven locales, and
honest Info with verified citations.
Add Peaceful visualization: AAT's nine scenes, seven narrated steps each,
streamed from R2 in the closest narration language with captions in
seven locales and honest Info (three verified imagery studies described
as what they are). Scene covers follow once generated; tracks show the
placeholder tile meanwhile. Raise the session practice cap to eight.
Add a "soundscape" session kind: scenes of layers (looping beds or
accents on random/wave intervals, with volume, pan, pitch, fades and
loop crossfades) and sleep-timer choices. Files follow the player's
approved-host rule and host media cache. The overlay mixes them with Web
Audio, ported from AAT's ambient engine: crossfaded loops, randomized
accents, pause via context suspend, master volume, mute, and a sleep
timer that fades out and completes the run.

Anxiety Aid Tools gains Relaxing sounds: AAT's eight environments with
covers, names in seven locales and honest Info with verified studies.
@greptile-apps

greptile-apps Bot commented Sep 22, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

The PR is not yet safe to merge because mid-run sleep-timer changes can end sessions early and the published soundscape type accepts descriptors that the host rejects.

Fix All in Claude CodeFindings

  1. P1 Sleep Timer Ends Early
  2. P1 Sound Layer Types Mismatch
  3. P2 Downloads Outlive Session Teardown
  4. P2 Failed Layers Never Retry

Summary

This PR adds host-mediated media sessions and extends Anxiety Aid Tools with narrated meditation, visualization, and layered soundscapes.

  • Introduces player and Web Audio soundscape renderer practices with localized controls and lifecycle hooks.
  • Adds approved-host media downloading, a persistent LRU cache, and a privileged local media scheme.
  • Extends session descriptors and the public SDK with player and soundscape contracts.
  • Adds seven-practice Anxiety Aid Tools content, translations, covers, commands, persistence, and tests.
  • The sleep-timer calculation and public sound-layer type contract need correction before merge; media lifecycle cancellation and transient-layer retry also need strengthening.

Diagram

sequenceDiagram
  participant Plugin as Anxiety Aid plugin
  participant Bridge as SDK bridge
  participant Overlay as Session coordinator
  participant Cache as Media cache
  participant Network as Approved media host
  participant Renderer as Pet session renderer

  Plugin->>Bridge: ui.session(player or soundscape)
  Bridge->>Bridge: Validate descriptor, permission, hosts
  Bridge->>Overlay: Open descriptor + approved hosts
  Renderer->>Overlay: Resolve descriptor-listed media URL
  Overlay->>Cache: resolveSessionMedia(url, hosts)
  alt Cache miss
    Cache->>Network: Guarded HTTPS fetch
    Network-->>Cache: Bounded media bytes
    Cache->>Cache: Atomic write and LRU eviction
  end
  Cache-->>Overlay: openpets-session-media URL
  Overlay-->>Renderer: Privileged local URL
  Renderer->>Renderer: Play audio or decode Web Audio buffer
Loading

Reviews (1) · Last reviewed commit: "feat(aat): relaxing sounds on a layered ..."

Comment on lines +444 to +448
chip.addEventListener("click", () => {
timerMinutes = minutes;
renderTimerChips();
renderStatus();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Sleep Timer Ends Early

Changing the sleep timer while a soundscape is active only updates timerMinutes, while the remaining time subtracts elapsedMs accumulated since the run began. For example, selecting 15 minutes after listening for 10 minutes leaves only 5 minutes, and selecting it after 15 minutes starts the fade immediately. Track the timer from when it is selected instead.

Fix in Claude Code

Comment thread packages/sdk/src/index.ts
Comment on lines +645 to +648
/** Looping bed (exactly one of `loop` or `interval`). */
loop?: boolean;
/** Seconds between accents (1–600); "wave" drifts min→max→min by `increment`. */
interval?: { type: "random" | "wave"; min: number; max: number; increment?: number };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Sound Layer Types Mismatch

The public SDK makes loop and interval independently optional, so TypeScript accepts layers with both properties or neither. Runtime validation rejects both forms, meaning a plugin can type-check successfully but fail when it calls ui.session(). Model looping beds and interval accents as mutually exclusive variants so the published contract matches runtime validation.

Knowledge Base Used:

Fix in Claude Code

Comment on lines +75 to +77
async function downloadToCache(url: string, cacheId: string, allowedHosts: ReadonlySet<string>, diagnostics: NetworkDiagnostics): Promise<string> {
const started = Date.now();
const response = await safeHttpFetchBytes(url, allowedHosts, diagnostics, maxMediaFileBytes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Downloads Outlive Session Teardown

This call does not pass the lifecycle signal supported by safeHttpFetchBytes. Stopping or replacing a session, or disabling or reloading its plugin, can therefore leave a media request running for up to 60 seconds and allow it to write an unused cache file afterward. Connect these downloads to session or plugin teardown cancellation.

Knowledge Base Used: Desktop plugin platform

Fix in Claude Code

Comment on lines +105 to +119
const loadBuffer = (url) => {
let pending = buffers.get(url);
if (!pending) {
pending = (async () => {
const localUrl = await ui.resolveMedia(url);
if (!localUrl || !context) return null;
const response = await fetch(localUrl);
if (!response.ok) return null;
const bytes = await response.arrayBuffer();
if (!context) return null;
return context.decodeAudioData(bytes);
})().catch(() => null);
buffers.set(url, pending);
}
return pending;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Failed Layers Never Retry

loadBuffer stores a promise that resolves to null when media resolution, fetching, or decoding fails. That failed result remains cached for the entire run, so a transient failure can permanently silence a loop or accent even after connectivity recovers. If another bed succeeds, the UI still reports the scene as playing without warning about the missing layers. Remove failed entries from buffers or retry them with a bounded backoff.

Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant