Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 8 additions & 8 deletions .dsh-version.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@
]
},
"dsh-audio": {
"version": "0.1.0-alpha.7",
"version": "0.1.0-alpha.8",
"rows": [
"audio"
],
"rowOverrides": [],
"role": "Audio + Media: the `audio` tab draws WAV/AIFF/FLAC as a waveform (one row per channel, an adaptive ruler, a min/max envelope with the RMS core, zoom down to samples, selection, playback on the audio clock), and the left column's **Media** panel (alpha.5, one row above Plugins) scans the conversation workspace for every audio, image, video and GIF file it can reach - bounded, grouped by kind, previewed inline through the harness's own `/api/file` route, and handed to the tab that owns the type on one click. It is about files only: no microphone, no device enumeration and no output routing. Client-only, read in 2 MiB windows. See packages/dsh-audio/README.md.",
"uses": [
"dsh-rightbar",
"dsh-modal (the pack's shared dialog surface: the panel's seat opens it lazily through ctx.get and never injects it, so a profile without it draws the panel inline as a page instead)",
"dsh-modal (the pack's shared dialog surface: the panel's seat opens it lazily through ctx.get and never injects it, so a profile without it draws the panel inline as a page instead; the package also publishes the `media` service the left bar's Media button opens, which keeps the conversation in the main column)",
"@deepseek-ai/dsh-api-workspace-files (the workspaceFiles remote: readBytes windows, list, stat)"
]
},
Expand Down Expand Up @@ -296,18 +296,18 @@
]
},
"dsh-modal": {
"version": "0.1.0-alpha.3",
"version": "0.1.0-alpha.4",
"rows": [
"modal"
],
"provides": [
"modals"
],
"rowOverrides": [],
"role": "Modal: the pack's shared dialog surface, published as the `modals` service - `open` / `alert` / `confirm` / `prompt` with async submit work that keeps the dialog open, plus a rich `content` dialog whose mask click deliberately does not close it. See packages/dsh-modal/README.md."
"role": "Modal: the pack's shared dialog surface, published as the `modals` service - `open` / `alert` / `confirm` / `prompt` with async submit work that keeps the dialog open, plus a rich `content` dialog whose mask click deliberately does not close it unless the caller opts in with `dismissOnMask`. See packages/dsh-modal/README.md."
},
"dsh-skills": {
"version": "0.1.0-alpha.3",
"version": "0.1.0-alpha.4",
"rows": [
"skills"
],
Expand All @@ -316,7 +316,7 @@
"@deepseek-ai/dsh-skill (ctx.skills, the host skill registry - NOT injected, resolved lazily per request)",
"dsh-modal (the shared dialog surface, opened in its rich `content` form; resolved lazily so a profile without it still loads)"
],
"role": "Skills browser: one header seat opens a modal listing every skill the harness loads for this conversation, with three routes to read and save the markdown behind it. The client names a skill, never a path. See packages/dsh-skills/README.md."
"role": "Skills browser: the left bar's Skills button (and, in a profile without dsh-themes, one header seat) opens a modal listing every skill the harness loads for this conversation, closable by clicking outside it, with three routes to read and save the markdown behind it. The client names a skill, never a path. See packages/dsh-skills/README.md."
},
"dsh-ui-state": {
"version": "0.1.0-alpha.2",
Expand Down Expand Up @@ -381,7 +381,7 @@
]
},
"dsh-themes": {
"version": "0.1.0-alpha.30",
"version": "0.1.0-alpha.31",
"rows": [
"themes"
],
Expand All @@ -391,7 +391,7 @@
"@deepseek-ai/dsh-session-log-export",
"dsh-ui-state (the uiState service, resolved lazily through ctx.get, for the durable page zoom and extension theme)"
],
"role": "Themes and header dress: the conversation header's own controls (page zoom, Screenshot, Themes, session-log download), the Nord/Monokai/Hacker/Cyber palettes, and the pack's appearance overrides - Markdown paper, the branding band, the band lines and the top row's centre, the account menu's Feedback row removed, and the right bar's resize seam. See packages/dsh-themes/README.md.",
"role": "Themes and header dress: the conversation header's own controls (page zoom, Screenshot, Themes, session-log download), the Nord/Monokai/Hacker/Cyber palettes, and the pack's appearance overrides - Markdown paper, the branding band, the band lines and the top row's centre, the account menu's Feedback row removed, the right bar's resize seam, and the band's four action buttons (Settings, Plugins, Media, Skills), where Media opens dsh-audio's `media` dialog instead of swapping the main column and the header's duplicate Skills seat is hidden. See packages/dsh-themes/README.md.",
"overrides": [
"markdown paper: re-declares ui-theme's light declarations on the rendered Markdown root, so the page stays white in the dark theme",
"markdown chrome: hides the preview header's viewer menu on Markdown tabs (the page has exactly one renderer; the editor's Edit button is the way back to editing)",
Expand Down
Binary file modified assets/vncode-banner3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion packages/dsh-audio/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# dsh-audio (alpha.7)
# dsh-audio (alpha.8)

**Audio opens as a waveform — and the left column's Media panel lists every media file the conversation can reach.** The right bar's `audio` tab covers **WAV/RIFF, AIFF/AIFC and FLAC**, instead of the preview's bare `<audio>` element. The **Media** panel is a **Media** row in the left column's global-panel list (`sidebar.panellist`) at `order: -1`, above Plugins: it scans the workspace for audio, image, video and GIF files and previews them in place. Client-only: bytes come from the harness's `workspaceFiles` remote, so it ships no route and no host state; `lib/index.js` is one no-op row.

Expand All @@ -13,6 +13,8 @@

The left column's row is **Media**, not Audio, and it opens one dialog: the library. The panel is a **root-scoped** seat — the harness renders `main` with no props — so the conversation it browses is found the way the harness itself finds it: the session the main view **retains** (`retainedBy.mainView`), which survives a global panel being shown. Without that lookup the dialog had no session and could only say "open a conversation"; it listed nothing.

The package also **publishes the `media` service** (`{ open() }`, through `ctx.reflect.provide`). It exists because the shell's way to reach the seat is `layout.selectPanel('media')`, which swaps the main column from the conversation to the seat underneath — a modal button that changes the background on its way in. The left bar's Media button calls `media.open()` instead, so the dialog opens over the chat; and the seat **hands the column straight back** once its dialog is up, so even the sidebar row's path leaves nothing swapped behind. A profile with no dialog service still draws the panel inline (the seat's fallback), and a profile with no `media` service keeps the old selectPanel path.

- **One view, and it is the files.** The dialog lists the media in this conversation's workspace: **images, GIFs, audio and video**, grouped in that order with a count per group. The walk is **bounded and says so** — depth 5, 120 directories, 120 files, `node_modules`/`.git`/`.vncode`/`dist`/`vendor`/`__pycache__` and their kin skipped — and a cut list is labelled `(list cut)` in the header rather than passed off as complete. Every listing comes from the harness's own `workspaceFiles.list` under the session id, so the host decides what is readable; the panel never names a path it was not given. A refused listing is shown as **the reason it was refused**, never as an empty workspace, and a genuinely empty one says **"No audio, image, video or GIF files were found in this conversation's workspace"** — the sentence names what Rescan scans, because "no media" and "could not read" are different answers. The panel carries a **Close** in its own bar, beside Rescan, as well as the frame's own ×.
- **The pick previews in place.** An image draws as an `<img>`, a GIF too, a video as a `<video controls>`, audio as an `<audio controls>`, all through the harness's authenticated `/api/file?path=` route against the file's **absolute path** (one `stat` per pick, never per row). **Open in a tab** then hands the `dsh-resource://file/session/<id>/<path>` address to the right bar's registry, so a WAV lands on this package's own waveform, a PNG on dsh-image's zoom, an MP4 on dsh-video's player, and a format this pack claims nowhere still reaches the shipped preview.

Expand Down
108 changes: 80 additions & 28 deletions packages/dsh-audio/lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@
* It is about FILES, not about the machine. This package enumerates no
* audio devices, opens no microphone, plays no test tone and routes no
* output: the console that did all four (alpha.3 to alpha.6) was removed,
* and nothing here is left behind for it - no service, no storage key, no
* child surface. The player still creates the one AudioContext it needs.
* and nothing is left behind for it - no storage key, no child surface. The
* player still creates the one AudioContext it needs. What the Media panel
* DOES publish is the `media` service, so the left bar can open its dialog
* without swapping the conversation out for the seat underneath.
*
* Module-table format of every client bundle here; no build step.
*/
Expand All @@ -93,7 +95,7 @@ window.__ModuleLoader__.load({
/** The tab kind this package owns. */
const KIND = 'audio'
/** Version marker shown in the toolbar, so a loaded bundle is easy to verify. */
const PLUGIN_VERSION = '0.1.0-alpha.7'
const PLUGIN_VERSION = '0.1.0-alpha.8'
/** Address grammar owned by @deepseek-ai/dsh-util-workspace-path. */
const FILE_PREFIX = 'dsh-resource://file/'
const SESSION_SEGMENT = 'session/'
Expand Down Expand Up @@ -3148,6 +3150,12 @@ window.__ModuleLoader__.load({
const SIDEBAR_SERVICE = 'sidebarRight'
/** The pack's shared dialog surface (dsh-modal), resolved lazily at click time. */
const MODAL_SERVICE = 'modals'
/**
* The client service this package PUBLISHES: `media.open()` opens the Media
* dialog without selecting the panel behind it, which is how the left bar's
* Media button stays a modal button instead of a page switch.
*/
const MEDIA_SERVICE = 'media'

// ---------------------------------------------------------------------
// The services this file resolves lazily, and the sentence one error becomes
Expand Down Expand Up @@ -3395,14 +3403,67 @@ window.__ModuleLoader__.load({
}
}

/**
* Open the Media dialog through the pack's shared surface.
*
* `sessionOf` is a FUNCTION, not a string: the service re-renders a rich
* dialog's content, so it has to read the CURRENT session each time instead
* of the one captured when the button was pressed - a session switch must
* not reopen the dialog on the conversation it was opened from.
*
* @param sessionOf - `() => string`, the session the panel browses.
* @param onOpenFailure - told when a file could not be handed to its tab.
* @returns the dialog's pending promise, or null when this profile has no
* dialog service (or it refused to open).
*/
function openMediaDialog(sessionOf, onOpenFailure) {
const modals = serviceNow(MODAL_SERVICE)
if (modals === null || modals === undefined || typeof modals.open !== 'function') return null
try {
return modals.open({
title: 'Media',
size: 'lg',
content: (helpers) => {
const sessionId = sessionOf()
return h(MediaPanel, {
sessionId,
close: helpers.close,
onOpen: (item) => {
const failure = openMediaInBar(sessionId, item)
if (failure === '') helpers.close(null)
else if (typeof onOpenFailure === 'function') onOpenFailure(failure)
},
})
},
})
} catch (err) {
return null
}
}

/**
* The client service the left bar's Media button drives: the dialog, and
* nothing else. `layout.selectPanel('media')` is deliberately NOT used - it
* would swap the main column from the conversation to the seat below, which
* is exactly what a modal button must not do.
*
* @returns `{ open() }`, where open() answers whether a dialog was opened.
*/
function mediaService() {
return {
open: () => openMediaDialog(() => sessionIdFor({}), () => {}) !== null,
}
}

/**
* The main column's seat: the panel the sidebar row selects, and the only
* door to the Media panel.
*
* The dialog is opened ON MOUNT, which is what makes the left-bar row behave
* like a modal button. The seat underneath is not a dead end either: it
* carries its own "Open the Media panel" control, so a dialog dismissed
* without the column being handed back can always be reopened.
* The dialog is opened ON MOUNT and the column is handed straight back, so
* the row behaves like a modal button even though the shell opened a panel
* to reach it. The seat underneath is not a dead end either: it carries its
* own "Open the Media panel" control, so a dialog dismissed without the
* column being handed back can always be reopened.
*/
function MediaSeat(props) {
const inline = props !== null && props !== undefined && props.inline === true
Expand All @@ -3423,28 +3484,13 @@ window.__ModuleLoader__.load({
const modalsRef = useRef(modals)
modalsRef.current = modals
useEffect(() => {
const service = modalsRef.current
if (!hasModal || service === null) return undefined
if (!hasModal) return undefined
let live = true
let pending = null
try {
pending = service.open({
title: 'Media',
size: 'lg',
content: (helpers) =>
h(MediaPanel, {
sessionId: sessionRef.current,
close: helpers.close,
onOpen: (item) => {
const failure = openMediaInBar(sessionRef.current, item)
if (failure === '') helpers.close(null)
},
}),
})
} catch (err) {
restorePanel()
return undefined
}
const pending = openMediaDialog(() => sessionRef.current, () => {})
// Hand the column back ONCE THE DIALOG IS OPEN, not when it closes: the
// dialog is the surface, and leaving the chat swapped out for the seat
// is the behaviour this button was reported for.
restorePanel()
if (pending !== null && pending !== undefined && typeof pending.then === 'function') {
pending.then(
() => {
Expand Down Expand Up @@ -4058,6 +4104,12 @@ window.__ModuleLoader__.load({
() => ctx.slots.inject(MAIN_SLOT, () => ctx.slots.register({ name: MAIN_SLOT, key: PANEL_ID, inject: () => ({}) }, MediaSeat)),
'dsh-audio: the Media panel seat',
)
// The `media` service: the left bar opens the dialog with it instead of
// selecting the seat's panel. Optional on the context (a profile or a
// harness build without `reflect.provide` keeps the row's own path).
if (ctx.reflect !== undefined && ctx.reflect !== null && typeof ctx.reflect.provide === 'function') {
ctx.effect(() => ctx.reflect.provide(MEDIA_SERVICE, mediaService()), 'dsh-audio: the media dialog service')
}
ctx.logger?.debug?.('[dsh-audio] client half active (' + PLUGIN_VERSION + ')')
} catch (err) {
// eslint-disable-next-line no-console
Expand Down
2 changes: 1 addition & 1 deletion packages/dsh-audio/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dsh-audio",
"version": "0.1.0-alpha.7",
"version": "0.1.0-alpha.8",
"description": "Audio: WAV/AIFF/FLAC as a waveform (one row per channel, adaptive ruler, RMS core, sample-level zoom, selection, audio-clock playback) plus the MEDIA panel in the left column - a bounded scan of every audio, image, video and GIF file the conversation can reach, with an inline preview and a one-click hand-off to the tab that owns the type. Client-only, read in 2 MiB windows, about files only: no microphone, no device enumeration, no output routing.",
"type": "module",
"main": "lib/index.js",
Expand Down
5 changes: 0 additions & 5 deletions packages/dsh-docker/lib/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,6 @@ function table(headers, rows) {
return [line(headers), ...rows.map(line)].join('\n')
}

/** One container row, as a line of the state answer. */
function containerLine(row) {
return row.name + (row.running ? '' : ' (' + row.state + ')') + ' ' + row.image + (row.ports === '' ? '' : ' ' + row.ports)
}

/** The engine's one-line identity. */
function engineLine(info) {
const version = info.serverVersion !== '' ? info.serverVersion : info.cliVersion
Expand Down
Loading
Loading