Skip to content

CH 22 · UI Plugins: Reskin, Add Panels, Inject Content into dsh

Word count~2,420 wordsTime~20 minPrereqCH 17 (Plugin Installation)LevelReproducible

Chapter Goal

The plugins in the previous chapters worked in places "you can't see": register tools, intercept calls, provide dependencies. This chapter switches to the visible layer — plugins can directly change the Web UI's look and capabilities. This chapter makes clear what UI plugins can do, how they work, and hands-on to see a real event stream.

You're Already Using UI Plugins

Looking back at CH 17, we installed three plugins, and they were all UI plugins:

PluginWhat it does
dsh-themeSkin the Web UI (a theme card appears in Settings → Appearance)
dsh-oil-sticky-promptPin the latest user message to the top while scrolling
dsh-better-sidebarSidebar workbench, see the directory directly from the file panel

You might have just "installed a plugin" at the time, but now you understand: they all modify the UI. That's the first level of understanding of UI plugins — they are responsible for all the customization you "can see".

What UI Plugins Can Do

Roughly three categories of capabilities:

  • Reskinning: change colors, fonts, layout style. The lightest category, dsh-theme is one.
  • Modify layout / add panels / add features: move the sidebar, top bar, input area, and also directly add a complete feature module or visualization panel to the interface. dsh-better-sidebar, dsh-oil-sticky-prompt are in this category; the DSH Skill & MCP Panel you installed in CH 12 is also one — it directly adds an "MCP Management" entry to Settings, each server's status and tool count visible at a glance, equivalent to adding a visualization O&M panel to the Web UI.
  • Contribute content to the conversation: render custom rows in the session (e.g. draw a card for a certain kind of tool result, insert special nodes). This is the deepest category, officially called "contribute business rows to the Web Client".

The first two categories are mostly pure front-end work; the third really needs to "hook into" the conversation rendering pipeline.

How UI Plugins Work: It All Starts with the Event Stream

UI plugins don't directly manipulate the DOM on the page, they subscribe to the event stream. This is the most critical point in understanding UI plugins.

Throughout a session, every event becomes a standard event: user sent a message, model started streaming output, a step started, a tool call happened... these events flow through the session/event channel like water. The built-in interface renders conversation bubbles, trajectory, and status from them; UI plugins do the same, subscribing to the same stream via ctx.on('session/event', ...).

The official minimum example for a UI plugin does exactly this:

js
export const name = 'my-ui'
export const inject = ['agents']

export function apply(ctx) {
  ctx.on('session/event', (_session, event) => {
    if (event.type === 'assistant/chunk' && event.data.chunk.type === 'text-delta') {
      console.log(event.data.chunk.text)   // Each piece of the model's streaming output
    }
  })
}

The skeleton of the whole mechanism:

You input, the model replies, the session controller translates them all into an event stream; UI plugins and the built-in interface subscribe to the same stream, each takes what they need. The interface is a "consumer" of the event stream, not a direct manipulator of the DOM — this also explains why "everything is a plugin" can cover the UI: even the interface is "listened out" by plugins from the event stream.

Hands-on: Install an "Event Observer" to See the Event Stream

Just looking at the mechanism isn't enough fun, let's write a minimalist plugin that prints every session event — see how much really happens behind a single conversation.

Step 1: Create Directory, Write Plugin

In a workspace where you want to put plugins:

powershell
New-Item -ItemType Directory -Path "ui-demo\src" -Force

Create ui-demo\src\event-watch.js:

js
export const name = 'event-watch'

export function apply(ctx) {
  ctx.on('session/event', (_session, event) => {
    console.log(`[event] ${event.type}${event.data?.type ? ' / ' + event.data.type : ''}`)
  })
}

Just subscribe to session/event and print out the type of each event.

Step 2: Declare the Plugin

Create ui-demo\cordis.yml (replace the path with your own, remember %20 for spaces):

yaml
- insert:
    - id: event-watch
      name: 'file:///E:/your-workspace/ui-demo/src/event-watch.js'

Step 3: Run headless Once

powershell
cd your-workspace-directory
dsh --profile headless --patch "./ui-demo/cordis.yml" "Just reply: hi"

My actual terminal output:

Just answering "hi", behind the scenes went through 22 events: session initialization (permission, sandbox, approval), turn/start, step/start, three user/messages, request headers, assistant/chunk streaming out text one by one, assistant/message settling, step/end, turn/end...

The conversation you see in the Web UI is this string of events being rendered after consumption. The event observer let us "see" the raw material behind the UI for the first time.

Let dsh Do It: Develop a Visible UI Plugin

The event observer above just "watches events" and hasn't really changed the interface. Have dsh directly develop a UI plugin where the effect is visible in the settings page, it can do that.

Send this prompt directly in the Web UI's input box:

text
In my current workspace, help me develop a simple UI plugin visible in the Web UI: add a custom tab to the settings page (e.g. called "My Plugins", with some placeholder content on the page, no complex functionality needed). First read the dsh official docs to understand the UI plugin mechanism and how to add tabs to the settings page, implement per the official spec, tell me which profile to install to, how to see it in the interface (and tell me if you need to restart dsh or change config).

It will go read the docs itself, judge the tech stack (such interface plugins usually need TypeScript and front-end build, it'll handle that itself), implement, and tell you how to verify. Follow its instructions to restart dsh, and you'll see the new tab in the settings page.

This is me actually running it once — it went from reading the docs, writing the plugin, installing into the profile, to telling me how to see it in the interface, how to exit, all done by itself, even highlighting the key paths:

After restarting, go to Settings → Plugins, and the top has a new "My Plugins" tab:

A detail: this plugin is pure UI, no Host-side behavior, so no changes to settings.yaml / cordis.yml — everything is loaded through the profile's bundles. Rolling back is also simple: just remove it from the bundles array. This is another embodiment of "everything is a plugin": even adding a tab on the interface is "install a plugin".

Common Pitfalls

ProblemWhat's happeningHow to handle
Event observer doesn't printsession/event subscribed wrong, or plugin not loadedConfirm cordis.yml path, ctx.on syntax
So many events you can't read themOne conversation naturally has dozens of eventsFirst look between turn/start and turn/end, that's the main trunk
Want to draw content directly in the conversationThat requires a React renderer, advancedFirst get comfortable with the event stream and settings, advanced later
Reports Cannot find packagePlugin uses external dependencies that aren't installednpm install the corresponding package in the plugin directory

What you learned in this chapter

  • [ ] State the three categories of UI plugin capabilities: reskin / layout & panel / session content contribution
  • [ ] Understand the core mechanism that "the interface is a consumer of the event stream"
  • [ ] Use ctx.on('session/event', ...) to subscribe to session events
  • [ ] Use the event observer to see the event stream behind a single conversation
  • [ ] Know that UI plugins can add custom tabs to the settings page

Open Source · MIT · Community Driven