@naxodev/opencode-music-player 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ This file records the initial package release. Later versions use generated [GitHub release notes](https://github.com/naxodev/ai/releases?q=opencode-music-player).
4
+
5
+ ## [0.1.0] - 2026-08-20
6
+
7
+ ### Added
8
+
9
+ - Initial macOS system Now Playing integration for the OpenCode 2 TUI.
10
+ - Playback controls, progress display, expandable details, and waveform visualization.
11
+
12
+ ### Changed
13
+
14
+ - Move the player from the footer overlay into the OpenCode sidebar.
15
+ - Keep a responsive one-row music bar below the active route when the sidebar is collapsed.
16
+ - Simplify compact metadata from title and artist to title, truncated title, then playback marker as terminal width decreases.
17
+ - Display native album artwork in Kitty-compatible terminals with a true-color fallback elsewhere.
18
+ - Support native artwork through Herdr and tmux graphics passthrough.
19
+ - Resolve missing system artwork through exact iTunes catalog matches.
20
+ - Refresh promptly for provider changes, retain bounded polling fallback, and clean up subscriptions on plugin disposal.
21
+
22
+ ### Fixed
23
+
24
+ - Re-anchor native album artwork and remove stale Kitty images when its sidebar slot moves or resizes.
25
+
26
+ [0.1.0]: https://github.com/naxodev/ai/releases/tag/opencode-music-player@v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nacho Vazquez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @naxodev/opencode-music-player
2
+
3
+ A sidebar player and compact bottom bar for the OpenCode 2 TUI that display and control the active macOS system media session.
4
+
5
+ It supports browsers, Spotify, Apple Music, Kaset, and other apps exposed through [`media-control`](https://github.com/ungive/media-control). The player keeps the existing OpenCode theme and provides keyboard and mouse controls.
6
+
7
+ ## Architecture
8
+
9
+ One reconnecting music-session client supplies replayed and live state, provider status, transport, and daemon-owned native artwork bytes. The shared same-user daemon owns provider discovery, provider events and polling, the playback clock, and global transport ordering.
10
+
11
+ OpenCode keeps plugin/controller lifecycle, the Solid compact and sidebar UI, optimistic transport presentation, seek coalescing, notifications, waveform projection, iTunes catalog fallback and downloads, conversion, bounded presentation cache/jobs, and Kitty or half-block rendering. Plugin disposal removes local listeners and presentation work, then disposes only its session client. Other clients keep the shared daemon alive.
12
+
13
+ Read the [music session architecture field guide](../../docs/music-session-architecture.html) for the daemon protocol, replay, reconnect, and cleanup model.
14
+
15
+ ## Artwork
16
+
17
+ The daemon performs the bounded native `media-control get --now` read and validates the complete recording identity before and after the read. OpenCode uses those bytes when available, then keeps iTunes Search fallback, image downloads, conversion, cache/job ownership, and terminal rendering locally. Artwork failure never blocks playback state.
18
+
19
+ Ghostty and other terminals with Kitty graphics support display the cover as a native image. Other terminals receive a true-color half-block rendering of the same cover.
20
+
21
+ Terminal multiplexers must pass Kitty graphics through to use native images. The player uses the half-block rendering when the host does not expose that support.
22
+
23
+ Herdr users can enable its experimental renderer in `~/.config/herdr/config.toml`:
24
+
25
+ ```toml
26
+ [experimental]
27
+ kitty_graphics = true
28
+ ```
29
+
30
+ tmux 3.3 and later users must allow wrapped graphics passthrough in `~/.tmux.conf`:
31
+
32
+ ```tmux
33
+ set -g allow-passthrough on
34
+ ```
35
+
36
+ > [!IMPORTANT]
37
+ > This package targets the beta OpenCode 2 TUI plugin API in `opencode2 v0.0.0-next-17395`. OpenCode may change this API before its stable release.
38
+
39
+ ## Requirements
40
+
41
+ - macOS
42
+ - OpenCode 2 `v0.0.0-next-17395`
43
+ - Bun, which OpenCode uses to load TypeScript plugin packages
44
+ - [`media-control`](https://github.com/ungive/media-control), recommended:
45
+
46
+ ```sh
47
+ brew tap ungive/media-control
48
+ brew install media-control
49
+ ```
50
+
51
+ [`nowplaying-cli`](https://github.com/kirtan-shah/nowplaying-cli) is a fallback. Its play state can freeze for some media apps.
52
+
53
+ ## Install
54
+
55
+ Add the package to the `plugin` array in your global `~/.config/opencode/tui.jsonc` or project `.opencode/tui.jsonc`:
56
+
57
+ ```jsonc
58
+ {
59
+ "$schema": "https://opencode.ai/tui.json",
60
+ "plugin": ["@naxodev/opencode-music-player"],
61
+ }
62
+ ```
63
+
64
+ OpenCode installs npm plugin packages and their production dependencies in its isolated cache. Restart OpenCode after changing the package entry.
65
+
66
+ ### Local checkout
67
+
68
+ OpenCode imports local packages directly and does not install their dependencies. Install them first:
69
+
70
+ ```sh
71
+ git clone https://github.com/naxodev/ai.git
72
+ cd ai
73
+ bun install --frozen-lockfile
74
+ ```
75
+
76
+ Then reference the absolute package path:
77
+
78
+ ```jsonc
79
+ {
80
+ "$schema": "https://opencode.ai/tui.json",
81
+ "plugin": ["/absolute/path/to/ai/packages/opencode-music-player"],
82
+ }
83
+ ```
84
+
85
+ ## Verify
86
+
87
+ Start OpenCode and list active plugin IDs:
88
+
89
+ ```sh
90
+ opencode2 api get /api/plugin
91
+ ```
92
+
93
+ The response should include `music-player`. If it does not, inspect `~/.local/share/opencode/log/opencode.log` for package resolution or setup errors.
94
+
95
+ ## Controls
96
+
97
+ The compact bar appears below the active route whenever a current track exists, including while playback is paused. It remains visible when the session sidebar is collapsed. Wide terminals show the playback marker, title, and artist. Medium terminals omit the artist. Narrow terminals truncate the title, then keep only the playback marker when metadata cannot fit safely. The bar always stays on one row.
98
+
99
+ | Input | Action |
100
+ | ------------------ | -------------- |
101
+ | `ctrl+shift+p` | Play or pause |
102
+ | `ctrl+shift+left` | Previous track |
103
+ | `ctrl+shift+right` | Next track |
104
+
105
+ ## Development
106
+
107
+ ```sh
108
+ bun install --frozen-lockfile
109
+ bun run check
110
+ ```
111
+
112
+ The workspace smoke packs OpenCode and music-core, installs them into an isolated project, and launches the exact manifest-selected OpenCode CLI. It verifies the packed plugin's deterministic playing, paused, collapsed, narrow, and smallest layouts. See the workspace [contribution guide](../../CONTRIBUTING.md) for the contribution and release process.
113
+
114
+ ## Community
115
+
116
+ - Ask usage questions in [GitHub Discussions](https://github.com/naxodev/ai/discussions).
117
+ - Report reproducible bugs with the [bug form](https://github.com/naxodev/ai/issues/new?template=bug.yml).
118
+ - Read [SUPPORT.md](SUPPORT.md) before requesting support.
119
+ - Report vulnerabilities privately as described in the workspace [security policy](../../SECURITY.md).
120
+
121
+ ## License
122
+
123
+ [MIT](LICENSE)
package/SUPPORT.md ADDED
@@ -0,0 +1,7 @@
1
+ # Support
2
+
3
+ Use [GitHub Discussions](https://github.com/naxodev/ai/discussions) for installation and usage questions.
4
+
5
+ Use [GitHub Issues](https://github.com/naxodev/ai/issues) for reproducible defects and focused feature requests. Include your macOS version, `opencode2 --version`, package version, media backend, and relevant redacted logs.
6
+
7
+ Security reports must follow the workspace [security policy](../../SECURITY.md).
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Pure planner for native Kitty album-artwork placement.
3
+ *
4
+ * Decides which cleanup/draw actions to run when slot geometry or image
5
+ * identity changes. No I/O, no PNG bytes, no terminal writes.
6
+ *
7
+ * **Write-failure / `nextState` commit rule (Phase 2 executor):**
8
+ * `nextState` is the state to commit **only after every planned action is
9
+ * applied successfully**. On any failed write, the executor must keep the
10
+ * previous `state` so the next frame retries from a known-good snapshot.
11
+ * This module only proposes the post-success state; it does not simulate I/O.
12
+ */
13
+
14
+ export type NativeArtworkPlacement = {
15
+ imageId: number
16
+ x: number
17
+ y: number
18
+ width: number
19
+ height: number
20
+ }
21
+
22
+ export type NativeArtworkState = {
23
+ /** Kitty image id currently believed transmitted (`0` = none). */
24
+ transmitted: number
25
+ /** Last successfully applied geometry, or `null`. */
26
+ placement: NativeArtworkPlacement | null
27
+ }
28
+
29
+ export type NativeArtworkPlacementInput = {
30
+ state: NativeArtworkState
31
+ /** Desired Kitty image id for current artwork; ignored when clearing. */
32
+ imageId: number
33
+ /** Desired absolute cell geometry (caller applies any tmux offset). */
34
+ x: number
35
+ y: number
36
+ width: number
37
+ height: number
38
+ /** Host can show native graphics. */
39
+ kittySupported: boolean
40
+ /** Container present, not destroyed, `width >= 1`, `height >= 1`. */
41
+ slotValid: boolean
42
+ /** Component tearing down. */
43
+ disposed: boolean
44
+ }
45
+
46
+ export type NativeArtworkPlacementAction =
47
+ | { type: "delete-image"; imageId: number }
48
+ | { type: "delete-placement"; imageId: number }
49
+ | {
50
+ type: "transmit-and-display"
51
+ imageId: number
52
+ x: number
53
+ y: number
54
+ width: number
55
+ height: number
56
+ }
57
+ | {
58
+ type: "place"
59
+ imageId: number
60
+ placementId: number
61
+ x: number
62
+ y: number
63
+ width: number
64
+ height: number
65
+ }
66
+
67
+ export type NativeArtworkPlacementPlan = {
68
+ actions: NativeArtworkPlacementAction[]
69
+ /**
70
+ * Proposed state after successful apply of every action.
71
+ * Do not commit on partial/failed writes — see module JSDoc.
72
+ */
73
+ nextState: NativeArtworkState
74
+ }
75
+
76
+ const CLEARED_STATE: NativeArtworkState = {
77
+ transmitted: 0,
78
+ placement: null,
79
+ }
80
+
81
+ /** Stable key matching the historical `imageId:x:y:width:height` string shape. */
82
+ export function placementKey(placement: NativeArtworkPlacement): string {
83
+ return [
84
+ placement.imageId,
85
+ placement.x,
86
+ placement.y,
87
+ placement.width,
88
+ placement.height,
89
+ ].join(":")
90
+ }
91
+
92
+ function samePlacement(
93
+ a: NativeArtworkPlacement | null,
94
+ b: NativeArtworkPlacement,
95
+ ): boolean {
96
+ return a !== null && placementKey(a) === placementKey(b)
97
+ }
98
+
99
+ /**
100
+ * Plan Kitty cleanup/draw actions for the next native-artwork frame.
101
+ *
102
+ * Decision table:
103
+ * 1. disposed / unsupported / invalid slot → delete transmitted image (if any)
104
+ * 2. different image id (incl. first paint) → delete old image, then transmit-and-display
105
+ * 3. same image id, geometry changed → delete-placement then place
106
+ * 4. same image id, same geometry → no-op
107
+ */
108
+ export function planNativeArtworkPlacement(
109
+ input: NativeArtworkPlacementInput,
110
+ ): NativeArtworkPlacementPlan {
111
+ const { state, imageId, x, y, width, height } = input
112
+ const desired: NativeArtworkPlacement = { imageId, x, y, width, height }
113
+
114
+ if (input.disposed || !input.kittySupported || !input.slotValid) {
115
+ if (state.transmitted !== 0) {
116
+ return {
117
+ actions: [{ type: "delete-image", imageId: state.transmitted }],
118
+ nextState: CLEARED_STATE,
119
+ }
120
+ }
121
+ return { actions: [], nextState: state }
122
+ }
123
+
124
+ if (state.transmitted !== imageId) {
125
+ const actions: NativeArtworkPlacementAction[] = []
126
+ if (state.transmitted !== 0) {
127
+ actions.push({ type: "delete-image", imageId: state.transmitted })
128
+ } else {
129
+ // A remount loses local state, but the terminal may still hold this ID.
130
+ actions.push({ type: "delete-image", imageId })
131
+ }
132
+ actions.push({
133
+ type: "transmit-and-display",
134
+ imageId,
135
+ x,
136
+ y,
137
+ width,
138
+ height,
139
+ })
140
+ return {
141
+ actions,
142
+ nextState: { transmitted: imageId, placement: desired },
143
+ }
144
+ }
145
+
146
+ // Same image id.
147
+ if (samePlacement(state.placement, desired)) {
148
+ return { actions: [], nextState: state }
149
+ }
150
+
151
+ const actions: NativeArtworkPlacementAction[] = []
152
+ // Applied placement or transmitted image → clear old placement first.
153
+ if (state.placement !== null || state.transmitted !== 0) {
154
+ actions.push({ type: "delete-placement", imageId })
155
+ }
156
+ actions.push({
157
+ type: "place",
158
+ imageId,
159
+ placementId: imageId,
160
+ x,
161
+ y,
162
+ width,
163
+ height,
164
+ })
165
+ return {
166
+ actions,
167
+ nextState: { transmitted: imageId, placement: desired },
168
+ }
169
+ }