DSH HUB
首页插件商店插件包社区排行榜资源发布指南
插件源码
返回插件目录

omdsh-plugins /

omdsh-basemode

已验证

The session-mode system for the DeepSeek Harness web GUI: the segment registry every mode plugin registers into, the switch that renders them, and the sidebar dots that colour their conversations

★ 1 Stars0 Forks0 IssuesN/A 社区评分0 已确认安装
查看 GitHub
README来源: main@96550192

omdsh-basemode

English | 中文

The session-mode system for the DeepSeek Harness web GUI: the segment registry every mode plugin registers into, the switch that renders them, and the sidebar marks that say which conversation belongs to which mode and which one you are actually looking at.

It invents no mode of its own. Chat arrives with omdsh-chatmode, Code with omdsh-codemode, and neither is more native here than the other. The one posture this package does contribute is the one that was already on screen: Work, the harness's own conversation column, offered as a segment so that a switch has somewhere to switch back to. It appears only when it is needed — see The baseline posture.

What it adds

Surface Where it comes from
The sessionModes service ctx.provide, the door every mode plugin registers its segment through
The mode switch An entry in shell.overlay, ui-layout's frame-wide floating layer, centred on the conversation column and auto-hidden when nobody reaches for it
The Work segment — the harness's own column registerBaseline, a registration this package makes for itself and withdraws the moment a mode plugin brings that posture
A leading dot on every sidebar row, in its mode's colour row-marks.ts — painted onto the shipped rows rather than rendered, driven by the registry's tone and owns
The sidebar's highlight, on the conversation the column is showing The same painter — written only while the column and the selection disagree, which is exactly where a mode whose column is not the web conversation leaves the shipped highlight behind
New Session reaching the mode holding the column first An override of workspaces.startSession that offers the request to the active segment, and announces it when no segment takes it
New Session landing on a conversation that is actually new The same override: a request no segment took mints one rather than reusing the workspace's old blank, which is what puts its row at the top of the group
sessionModes.column — what the column is actually showing The active segment's own scope when it declares one, the selected conversation otherwise

Nothing in the harness is modified. The one slot it registers into is a published seat, the override is a prototype method shadowed by an own property, and withdrawing the row hands both straight back.

It registers no settings namespace either, and that is deliberate rather than an omission. There is nothing about a segment registry a person configures — which posture holds the column is derived per tab, and which postures exist at all is a property of the profile — so its card in the plugin hub carries no form.

Why this is its own package

It used to be part of Chat mode, and that was a layering inversion with a concrete cost.

A mode plugin needs the registry. If the registry ships inside one particular mode, every other mode depends on that mode's whole package — its managed workspace, its agent preset, its dock note — to contribute a pill to a switch. Wanting Code mode without Chat mode had no answer, and a profile assembled that way did not merely lose a segment: omdsh-codemode's browser half sat pending on a service nobody composed, and the client boot audit failed the entire page over it.

Splitting the seat from the postures makes the dependency honest. Every mode plugin depends on this package, this package depends on no mode, and "which postures exist" becomes a property of the profile rather than of whichever mode happened to own the registry.

The baseline posture

A switch needs somewhere to switch back to. A profile composed with this package and omdsh-codemode and nothing else used to get a control with a single Code pill on it — one segment, no way out of the terminal, and every conversation in the sidebar unmarked because nothing claimed them. The missing posture was never a plugin's to contribute: it is the harness's own conversation column, the screen that is already there.

So this package registers it, under the same name omdsh-chatmode uses for it — Work — with that plugin's words, colour and glyph, so a person cannot tell which package their switch got it from. Pressing it takes the column back and shows whatever is selected; it starts nothing, remembers nothing, and needs nothing from the profile.

It stands only while it is needed, and both halves of that matter:

  • Nothing else registered, no switch at all. The baseline alone is not rendered, because a control with one segment cannot switch — which keeps the promise that a profile with the mode system and no mode plugin shows nothing.
  • A contributor claiming the everything-else posture takes its place. omdsh-chatmode's Work is this posture with a memory of the conversation you left in it, so its segment is the one that renders; the baseline steps aside for any registration that declares fallback or takes its id, and comes straight back if that plugin unloads. Two everything-else segments would put one conversation under two dots.

Its active flag is derived rather than written: it holds the column exactly while it stands and no contributor has taken it. That is what makes a mode plugin crashing, unloading, or simply going inactive hand the column back with nothing left to notice.

The contract

A mode plugin registers one segment and answers for itself:

// Never in a top-level `inject` — see rule 9 of the conventions.
ctx.inject(['sessionModes'], (mctx) => {
  const modes = mctx.get('sessionModes') as SessionModes | undefined
  if (modes === undefined) return

  mctx.effect(() => modes.register({
    id: 'code',
    order: 20,
    label: t('mode.code'),          // already in the reader's language
    hint: t('mode.code.hint'),
    tone: 'var(--dsw-alias-state-error-primary)',
    icon: createElement(IconCodeOutline16, { size: 14 }),
    owns: isCodeSessionId,          // "is this conversation mine?"
    available: true,
    enter: () => { /* the navigation a press performs */ },
    newSession: (workspaceId) => { /* true when this mode started one */ },
  }))
})

SessionModes is a type, and it comes from @omdsh-plugins/omdsh-basemode/client. Import it with import type and never as a value: a cross-plugin VALUE import either inlines a second copy of this package's runtime into your bundle or asks the shell's frozen module table for a specifier it cannot answer, and the client bundle purity gate fails the build over it. That is why the service is resolved by the string 'sessionModes' above rather than by this package's exported SESSION_MODES constant — a service name is a wire name shared with a runtime, not a symbol shared with a package, and both mode plugins in this collection spell it out for themselves for exactly that reason.

Five things are worth knowing before writing one.

Exactly one segment is active, and the registry enforces it rather than trusting contributors to agree: marking one active clears the rest. That is also how a contributor learns it lost the column — it watches its own entry go false and takes its surface down.

Copy arrives localized. The switch renders label, hint and unavailableHint as given; it owns two words of its own (switch.aria, and the fallback shown when a mode is unavailable and named no reason) and knows no mode's name. Re-update the segment on locale/change.

A press is a navigation, never a state write. enter is called and the segment is expected to make the world true — by opening a conversation, starting one, or taking the column — after which whatever derives its active flag reports it. Nothing here remembers a mode across a reload; there is no node half, deliberately, so there is no stored posture two tabs can disagree about.

owns is asked per conversation, in display order, and the first claim wins; a segment marked fallback takes whatever no one claims. That is what lets the sidebar mark a whole list by mode with nothing about any particular mode in the code that paints it. A classifier that throws declines that conversation rather than taking the browsing region down.

inProject says whether a mode's conversations live somewhere a person works. It defaults to true, because that is what a conversation in this product is; a mode that files its own in a store of its own declares false. Chat is the one that does. It matters to any surface that derives a DIRECTORY from the conversation on screen — Code mode pressing beside a chat used to open a terminal inside the folder chats are filed in — and asking the registry is what keeps "which of these is Chat" out of every other plugin.

What the column is showing is not what is selected

sessions.current answers "which conversation is SELECTED", and that is the same question as "what is on screen" only while every mode's column is the web conversation. Code mode's is a terminal, and it deliberately never selects the conversation that terminal drives — a selected conversation is one the web host would resume, on a log another process owns. So the selection stays on whatever was open behind the terminal.

A surface beside the column that read the selection was therefore silently wrong rather than missing: omdsh-sidepanel's file tree sat beside a terminal in one project describing another. column is the honest answer:

// A mode whose column is not the web conversation declares what it shows.
modes.register({ id: 'code', /* … */, scope: controller.scope })

// Everything beside the column follows that, not the selection.
const scope = modes.column.getSnapshot()   // { sessionId, cwd }

Read only while the segment is active, on purpose: a contributor's scope is usually live whether or not it holds the column (Code mode goes on deriving what it WOULD show), and reporting that would describe a screen nobody is looking at. A mode that declares none — Chat and Work, whose column IS the web conversation — is reported as the selection, so a consumer needs one path rather than two.

Clicking a conversation is a request to see it

The selection cannot carry that request on its own, and the gap is a bug a person finds in a minute: enter Code mode from a working conversation — which leaves that conversation selected, because a terminal is not it — then click its row in the sidebar. The runtime selects what is already selected, nothing changes, nothing publishes, and the terminal goes on covering the conversation the click was asking for. The switch was the only way back, which is not what a sidebar row means.

So sessions.open is wrapped here, and every open hands the column to the mode that shows that conversation (showConversation) whether or not the selection moves. A mode that declares its own scope is skipped: it takes such a click before this wrapper does — omdsh-codemode shows the terminal for its own ids — and reaching here means it declined.

New Session belongs to the mode it was pressed in

"Another conversation like this one" means different things in different postures: the shipped modes want the frame's blank session, and a posture whose column is a terminal wants something the frame has never heard of. So the request is offered to the active segment first (newSession), and only reaches the frame when no segment takes it.

A segment that declines gives the column up with the request, which is the right default — the user asked for a conversation and the frame is about to show one.

The passthrough is announced (onNewSession) because that gesture can be the one navigation with nothing to derive from: pressing New Session while already standing on a workspace's blank conversation moves no selection, changes no list, and publishes no store. A mode deriving its own flag from where the user is would go on reporting the conversation the request was asking to leave. Asking is the whole fact.

And it lands on a conversation that is actually new

The shipped gesture reuses a workspace's existing blank conversation, and that reuse is what puts the row in the wrong place. The browsing list lifts a row to the top of its group for one the account has never seen, or one whose updatedAt grew; a conversation being handed back does neither, so it keeps whatever slot it had — and past the fifth, a collapsed group does not draw it at all. Pressing New Session and being shown no new row is the same thing seen from the other end.

So a request no segment took mints a conversation (sessions.create) instead. Reuse is kept for the one case it is right for: the row already on screen. Pressing New Session while standing on a workspace's blank conversation is asking for the conversation that is already open, so nothing happens — which is also what stops a run of presses from minting a run of them.

The cost is a blank conversation left behind by a New Session that was abandoned and navigated away from. It is invisible (the list draws no blank row but the current one), writes nothing to disk until a turn runs in it, and the frame's own startup reuse consumes it.

Install

dsh plugin --profile web add @omdsh-plugins/omdsh-basemode

This package is published, so the name is enough — no git specifier, no pnpm build-allowlist. The same install is also a button, on this plugin's card in Settings → Plugins → Plugin hub, once the hub itself is in the profile.

The mode plugins that fill the switch install through the plugin hub's command, which resolves each from the collection's registry:

npx @omdsh-plugins/omdsh-plughub add omdsh-chatmode omdsh-codemode

Each of the two is optional, and the switch says what was composed: this package alone shows nothing, with omdsh-codemode it shows Work · Code, with omdsh-chatmode Chat · Work, and with both Chat · Work · Code.

Order is a readability preference, not a requirement: mode plugins resolve sessionModes by name on a restricted fiber, so one composed before this package waits rather than fails.

Or from a checkout, which is what a build you are changing wants. lib/ must exist before dsh web runs — the loader imports lib/index.js directly, and a path-installed package never has its prepare run, so nothing builds it for you:

pnpm install && pnpm run build
dsh plugin --profile web add "$PWD"

Rebuild after every source change, for the same reason.

Remove it the same way:

dsh plugin --profile web remove @omdsh-plugins/omdsh-basemode

Every mode plugin then goes quiet on its own — segments and switch gone, each plugin's other surfaces still standing. That is what a restricted fiber buys, and it is the difference between "the modes are off" and a dead page.

Commands

pnpm install
pnpm run build       # tsc emits lib/types, tsdown bundles the browser half
pnpm run typecheck   # package sources, then the specs
pnpm run test        # vitest

Which harness this package compiles against is a switch:

pnpm run harness:npm                             # the committed state: the pinned release
pnpm run harness:local ../../deepseek-harness    # a sibling checkout, for development
pnpm run check:harness-pin                       # fails while any dependency is linked

Only the registry state may be committed. A link: specifier is resolved against the manifest that declares it, so a committed one bakes one machine's directory layout into the package — and pnpm does not fail loudly when it is wrong: it creates a dangling symlink, reports a successful install, and the build dies later with TS2307 on every harness import. check:harness-pin exists to catch that before a commit.

Known limitations

  • The switch borrows its seat. shell.overlay spans the sidebar and details panel too, so the pill centres itself on the box carrying data-conversation-scroll and hides when the pointer is not near it. A column that does not carry that attribute loses the anchor, and the switch snaps to the centre of the whole frame.
  • The row marks are painted, not rendered. The sidebar's rows belong to the harness, so both marks — the mode dot and the column's highlight — are written onto the DOM through a MutationObserver rather than composed into it. A row shape the harness changes is a selector this package has to follow.
  • The marks read a private structure. A row's conversation is taken from its React props, which is not a published contract; a future browser or a future React that changes that shape leaves rows unmarked rather than mismarked, and the switch's glyphs are unaffected. The alternative — matching on the row's title — is worse than nothing, because untitled sessions in one project share a title.
  • Search results carry no dot. The dots are painted on the browsing rows; a search result is a two-line stack that already names its workspace.
  • A conversation with no row gets no highlight. The cursor is moved onto a row, so a mode showing something the session list has never heard of — a Code terminal whose first turn has not landed — leaves the sidebar with nothing highlighted at all. The row appears, and takes the highlight, once the conversation is persisted.
  • The moved highlight outranks a row's own menu. Suppressing the shipped highlight is a background rule with more specificity than the browser's, and it reaches one state further than it means to: a row that is selected, off-column, and has its overflow menu open loses that menu's background for as long as all three hold. The menu itself is unaffected.
  • An abandoned New Session leaves a blank conversation behind. Pressing it, saying nothing, and going somewhere else keeps that conversation in the workspace's account, where the frame's own startup reuse will eventually pick it up. It is drawn nowhere in the meantime, and nothing of it reaches disk.
  • No mode survives a reload. The active posture is derived per tab from where the current conversation lives, and each contributor derives its own. Opening the app always lands wherever the current conversation says, never where you last were.
  • The baseline Work remembers nothing. Pressing it hands the column back and shows whatever is selected — it does not return you to the last working conversation, and on a fresh tab with nothing selected it lands on the workspace picker. That memory is omdsh-chatmode's Work, which is why its segment replaces this one when the plugin is composed.
—/ 5

暂无评分

已验证 DSH bundle

Commit 96550192e30f

社区评论

还没有评论,来写第一条。

DSH HUB

社区维护的 DSH 插件索引。不是 GitHub 或 DeepSeek AI 的官方产品。

社区资源API关于