{"_id":"@2nd1st/dsh-plugin-open-app","_rev":"2-74265dc18d79998eda136eeb49264cfa","name":"@2nd1st/dsh-plugin-open-app","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.1":{"name":"@2nd1st/dsh-plugin-open-app","version":"0.1.1","keywords":["dsh","dsh-plugin","deepseek-harness","mcp","mcp-apps","model-context-protocol","open-mcp-apps","cordis","plugin"],"author":{"name":"2nd1st"},"license":"MIT","_id":"@2nd1st/dsh-plugin-open-app@0.1.1","maintainers":[{"name":"secondfirst","email":"admin@secondfirst.ai"}],"homepage":"https://github.com/2nd1st/dsh-plugin-open-app#readme","bugs":{"url":"https://github.com/2nd1st/dsh-plugin-open-app/issues"},"dsh":{"bundle":{"patch":"./cordis.patch.yml"},"client":{"inject":["@deepseek-ai/dsh-client-connection","@deepseek-ai/dsh-client-runtime","@deepseek-ai/dsh-client-ui-slots","@deepseek-ai/dsh-client-ui-conversation"],"platform":"web"}},"dist":{"shasum":"e036d2d35120cfe81850bdb2121c757bfc692179","tarball":"https://registry.npmjs.org/@2nd1st/dsh-plugin-open-app/-/dsh-plugin-open-app-0.1.1.tgz","fileCount":7,"integrity":"sha512-WZ6jaqL9F607vEi8Ed2YgdTolFggqh9aAhgyLso3KH1fQeUj5oA7PkcWXdvYJPgjiTBeta2SOd/roJ4M0JPSzQ==","signatures":[{"sig":"MEQCIDOAm4c2A/g074cE7dCeAmRGqar3NVFX2dFRZNfOsb//AiBRlgKoRhu+44PL+H+rijlgXaSiRBC7nwIFH5rqIYBExg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":208916},"main":"lib/index.js","type":"module","engines":{"node":">=22"},"exports":{".":{"default":"./lib/index.js"},"./client":{"default":"./lib/client.js"},"./package.json":"./package.json"},"gitHead":"b7d53d8d7b5f73dc7ff1e08db7d270cee1ce6199","_npmUser":{"name":"secondfirst","email":"admin@secondfirst.ai"},"repository":{"url":"git+https://github.com/2nd1st/dsh-plugin-open-app.git","type":"git"},"_npmVersion":"11.17.0","description":"Brings open-mcp-apps into DeepSeek Harness (dsh): an Apps section in the sidebar, one container per app with its own workspace and conversation, an agent-presence strip under the app, and inline app rendering for MCP tool calls.","directories":{},"_nodeVersion":"26.5.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/dsh-plugin-open-app_0.1.1_1786802103433_0.6791159170843231","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@2nd1st/dsh-plugin-open-app","version":"0.1.2","description":"Brings open-mcp-apps into DeepSeek Harness (dsh): an Apps section in the sidebar, one container per app with its own workspace and conversation, an agent-presence strip under the app, and inline app rendering for MCP tool calls.","license":"MIT","author":{"name":"2nd1st"},"homepage":"https://github.com/2nd1st/dsh-plugin-open-app#readme","repository":{"type":"git","url":"git+https://github.com/2nd1st/dsh-plugin-open-app.git"},"bugs":{"url":"https://github.com/2nd1st/dsh-plugin-open-app/issues"},"keywords":["dsh","dsh-plugin","deepseek-harness","mcp","mcp-apps","model-context-protocol","open-mcp-apps","cordis","plugin"],"type":"module","main":"lib/index.js","exports":{".":{"default":"./lib/index.js"},"./client":{"default":"./lib/client.js"},"./package.json":"./package.json"},"engines":{"node":">=22"},"dsh":{"bundle":{"patch":"./cordis.patch.yml"},"client":{"inject":["@deepseek-ai/dsh-client-connection","@deepseek-ai/dsh-client-runtime","@deepseek-ai/dsh-client-ui-slots","@deepseek-ai/dsh-client-ui-conversation"],"platform":"web"}},"publishConfig":{"access":"public"},"gitHead":"01706d45221a1c98ac33be9c900a9dc3eec4f9af","_id":"@2nd1st/dsh-plugin-open-app@0.1.2","_nodeVersion":"26.5.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-ObdVOyFDoCkvP8+57/gaUsTZVosl+i/MT7Zwzcw1FXAC/WzksP6+VflEV7fPPtrc8U48c29xmVI6GVnG0xKkHg==","shasum":"160498349f04f68f10763c2a9c3b4ecbb4ed5d2d","tarball":"https://registry.npmjs.org/@2nd1st/dsh-plugin-open-app/-/dsh-plugin-open-app-0.1.2.tgz","fileCount":7,"unpackedSize":215992,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDQL21vb5UmK9MtaPCPu6cg4x3RWzS0SYDmHjzah55b6AIhALjbTfs+YJPJ4SpEGmpANspO5QXGoOH2d/OWZO8MIl3x"}]},"_npmUser":{"name":"secondfirst","email":"admin@secondfirst.ai"},"directories":{},"maintainers":[{"name":"secondfirst","email":"admin@secondfirst.ai"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dsh-plugin-open-app_0.1.2_1786815292133_0.013604543477336417"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-15T13:55:03.262Z","modified":"2026-08-15T17:34:52.473Z","0.1.1":"2026-08-15T13:55:03.564Z","0.1.2":"2026-08-15T17:34:52.285Z"},"bugs":{"url":"https://github.com/2nd1st/dsh-plugin-open-app/issues"},"author":{"name":"2nd1st"},"license":"MIT","homepage":"https://github.com/2nd1st/dsh-plugin-open-app#readme","keywords":["dsh","dsh-plugin","deepseek-harness","mcp","mcp-apps","model-context-protocol","open-mcp-apps","cordis","plugin"],"repository":{"type":"git","url":"git+https://github.com/2nd1st/dsh-plugin-open-app.git"},"description":"Brings open-mcp-apps into DeepSeek Harness (dsh): an Apps section in the sidebar, one container per app with its own workspace and conversation, an agent-presence strip under the app, and inline app rendering for MCP tool calls.","maintainers":[{"name":"secondfirst","email":"admin@secondfirst.ai"}],"readme":"# @2nd1st/dsh-plugin-open-app\n\n[![npm](https://img.shields.io/npm/v/%402nd1st%2Fdsh-plugin-open-app?logo=npm&label=npm)](https://www.npmjs.com/package/@2nd1st/dsh-plugin-open-app)\n[![license](https://img.shields.io/npm/l/%402nd1st%2Fdsh-plugin-open-app)](LICENSE)\n[![node](https://img.shields.io/node/v/%402nd1st%2Fdsh-plugin-open-app)](package.json)\n[![open-mcp-apps](https://img.shields.io/badge/open--mcp--apps-%E2%89%A5v0.5.1-8A63D2)](https://github.com/2nd1st/open-mcp-apps)\n\nMakes every [open-mcp-apps](https://github.com/2nd1st/open-mcp-apps) app **a place you go**\ninside [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`): an Apps\nsection in the sidebar, one container per app with its own workspace and conversation, and\ninline rendering when the model opens an app mid-chat.\n\n| | |\n|---|---|\n| **Package** | `@2nd1st/dsh-plugin-open-app` on npm — MIT ([LICENSE](LICENSE)) |\n| **Install** | `dsh plugin --profile web add @2nd1st/dsh-plugin-open-app` — one step |\n| **Requires** | `dsh` with a web profile · **pnpm** on `PATH` (`dsh plugin` manages profile dependencies through it) · a running **open-mcp-apps v0.5.1+** · Node 22+ |\n| **Platform** | dsh **web** (`dsh web`) — every surface it takes is a web slot |\n| **Releases** | [CHANGELOG.md](CHANGELOG.md) |\n\n> **Developer preview.** dsh is itself a `0.1.0-rc` preview, and this plugin tracks it\n> closely — including six places where it reaches into dsh through no published seam (all\n> six are listed under [What it reaches into](#what-it-reaches-into-and-what-breaks-if-dsh-moves),\n> with what each degrades to). Expect breaking changes between releases, and pin a version\n> if you need one that holds still.\n\n![An app container — the app in its own tab, the agent's line under it, the composer below](docs/screenshot-app-container.jpg)\n\n## Install\n\n```sh\n# add it — this is the whole install\ndsh plugin --profile web add @2nd1st/dsh-plugin-open-app\n\n# restart; plugin metadata is cached per name for the life of the process\ndsh web\n```\n\nThe package declares `dsh.bundle`, so `dsh plugin add` does not only put it in the\nprofile's dependencies — it appends it to `dsh.profile.bundles`, and the patch it ships\nbecomes a layer of your profile's tree. That layer inserts two rows: `open-app` (this\nplugin) and `mcp-oma` (dsh's MCP client, pointed at the same engine, so the model can open\nand build apps as well as you can). Bundle layers apply *below* your profile's own\n`cordis.patch.yml`, so both rows stay yours to configure or switch off.\n\nNothing here changes what an app is allowed to do: the engine is a local process you\nstarted, and the rows only say where it listens.\n\n**Upgrading from a hand-written install** (before 0.1.1 the second step was pasting an\n`- insert:` block yourself): delete those blocks for `open-app` and `mcp-oma` from your\nprofile's `cordis.patch.yml`, and remove the old package name —\n\n```sh\ndsh plugin --profile web remove dsh-plugin-open-app\ndsh plugin --profile web add @2nd1st/dsh-plugin-open-app\n```\n\nA patch `insert` always appends: two layers inserting the same id are two rows, and dsh\nrefuses to boot such a tree (`duplicate loader entry id: open-app`) rather than running the\nplugin twice. Keep your settings as [id-targeted overrides](#configuration) instead.\n\n## Requirements\n\n- **`dsh` with a web profile** (`dsh web`). Every surface this plugin takes is a web slot.\n- **pnpm on `PATH`.** Not something this plugin depends on: `dsh plugin` is a thin forwarder\n  that runs `pnpm` in the profile directory, so this is how any dsh plugin is installed.\n  Without it the install command above stops at\n  `dsh: pnpm not found on PATH — install pnpm to manage profile plugins` and exits 127.\n  No minimum version is stated here because none has been measured.\n- **A running open-mcp-apps engine with its HTTP face up** — `node src/http.mjs`, default\n  port `8787`. The engine is a separate install ([its README](https://github.com/2nd1st/open-mcp-apps#readme)):\n  it gives a model persistent, interactive UI apps backed by durable data collections,\n  shared with every other host talking to the same engine. This plugin is the dsh face of them.\n- **Engine v0.5.1 or newer.** That release carries the three seams the panel leans on:\n  `?chrome=0` (the bare widget instead of the engine's own viewer bar), `?nav=intent`\n  (an app→app link becomes a message to the host instead of a navigation inside the frame),\n  and the standalone viewer's root `overflow-y:auto`. Against an older engine the plugin\n  still works and degrades gracefully — each case is called out under\n  [Known limitations](#known-limitations).\n- **Optional, for per-app inline views:** start the engine with `OMA_DYNAMIC_TOOLS=1`, which\n  is what makes it publish one `open_<app>` tool per app. The universal `open_app` tool\n  is always covered.\n\nBoth opt-in query parameters are additive: an engine that has not heard of either ignores\nit and keeps its own behaviour. Nothing else is asked of the engine, and no dsh source is\nchanged — the plugin runs entirely out-of-tree.\n\n## Configuration\n\nEvery setting is optional; the defaults match a stock local engine and the `mcp-oma` row\nthe bundle ships.\n\nConfigure it from your profile's own `cordis.patch.yml` by naming the bundle's row id —\nnever by inserting the row again. An id-targeted patch **replaces** the key it names\nrather than merging into it, so write the whole `config` you want:\n\n```yaml\n- id: open-app\n  config:\n    # Where the engine's HTTP face listens.\n    engineBase: 'http://127.0.0.1:8787'\n    # The `serverName` of the engine's mcp-client entry. It decides the\n    # `mcp__<serverName>__` prefix the inline tool views are keyed on.\n    serverName: 'oma'\n    # Where the per-app workspace directories are made. One directory per app,\n    # named after it. Defaults to $DSH_HOME/storages/open-app/apps.\n    appsRoot: '~/.dsh/storages/open-app/apps'\n    # The container's rules, carried as a system-prompt section on every\n    # request a container's agent makes. Replaces the shipped app-only\n    # prompt entirely (see below for what it has to keep doing). `{app}` is\n    # the app's name and `{card}` the declaration card the host half builds\n    # from the engine's registry. An empty string retires the rules: the\n    # containers then run as ordinary dsh sessions that happen to have an\n    # app in a tab.\n    containerPrompt: |\n      You live in the {app} app — this conversation is its container.\n\n      {card}\n\n      Never call open_app, app_html or any open_* tool here; the panel beside\n      this chat is already showing the app. Answer in one sentence — that line\n      is what the user reads on the status bar under it.\n    # The one line a brand-new container opens with, which is only there to\n    # end the blank state and to draw the first receipt onto the strip. It\n    # carries no rules. `{app}` is substituted. Set it to an empty string to\n    # open containers silently — they then stay blank until you speak, and\n    # the app waits in a row above the composer instead of in its own tab.\n    installMessage: 'What is in the app right now?'\n```\n\nPinned apps and what each app's place is made of are kept per-browser in\n`localStorage`, under `dsh-plugin-open-app:pins` and `dsh-plugin-open-app:containers`\n(unscoped, and staying that way — the package moved to a scope in 0.1.1, but renaming a\nstorage key would silently unpin every app somebody had pinned).\nOnly the pins are irreplaceable: the workspace registration is the durable binding, so\nclearing the container map costs nothing — the next visit re-adopts the same directory,\nthe same workspace and the conversation already accounted under it.\n\n## Usage\n\nAn app is a place, not a page. Opening `shopping-list` opens the shopping list *and*\nthe talk about it; opening it tomorrow resumes both. So each app gets a dsh\n**workspace** of its own — a directory the plugin makes under its apps root, registered\nwith dsh — and its conversation is created inside that workspace, never in one of\nyours. Inside it:\n\n- **Apps** — the app's UI, first in the view ring, with a one-line strip under it\n  saying what the agent is doing right now.\n- **Chat** — this app's conversation, still there when you come back.\n- **Trajectory** — unchanged.\n- The composer stays docked below all of them, so you can be looking at the app and\n  telling the model to change it in the same breath.\n\nIn an app container the app is the reply: you say \"mark the milk as bought\" and the row\nticks itself, because the widget holds its own live connection to the engine. That is\nwhy the strip under it is not a small chat window — it is the agent's presence. It shows\nthe tool being called in the same glance as the change it causes, and it makes the two\nthings you could otherwise miss impossible to miss: a question waiting for you, and a\nfailure.\n\nA container is exclusive: inside an app's session the Apps tab shows that app and\nnothing else. There is no way to browse out of it, because a container you can escape\nis not a container — switching apps means going to the other app's node. The directory\nof apps lives in the two places that are nobody's container: the sidebar's own overlay,\nand the Apps tab of an ordinary chat session.\n\n### What it adds\n\n| | |\n| --- | --- |\n| **Apps section in the sidebar** | *All apps* (the directory), *App Store* (where new ones come from), and one node per pinned app. |\n| **App containers** | Clicking an app node opens that app's workspace and its own session, with the app's UI on top and its history underneath. |\n| **App mode** | A container's agent gets a prompt of its own — who it is here, the app's card (what it is, the collections its rows live in, their shape, the functions it declares), what it must not call, how short a reply should be. It is a system-prompt section, assembled fresh for every request in a session that lives in an app directory, and it costs an ordinary chat session nothing. The container's session also runs on an **App mode** preset, so dsh names the mode where dsh names modes. |\n| **An opening line** | A brand-new container is asked one question — \"What is in the app right now?\" — because a session dsh has never been spoken to gets no view ring, and because the answer is the first line of the strip under the app. |\n| **Apps tab ahead of Chat** | Registered at order `-10`, so the app leads and the conversation follows. Chat and Trajectory keep their seats. |\n| **The agent's presence** | A one-line strip under the app: what the agent is doing right now (the tool it is calling, the answer arriving), what it last said, and — unmissably — when it is waiting for you or has failed. It reads as speech, not as source: the line is set in dsh's own content column and the model's markdown is read out flat, since nothing here can typeset it. Click it for the whole conversation. |\n| **App workspaces stay out of your way** | The directories the plugin registers are hidden from the workspace tree and the New Session picker, and New Session never lands inside an app. |\n| **App → app stays inside the model** | \"Open X\" from the App Store opens X's own container instead of navigating the store's frame (engine `?nav=intent`). |\n| **Inline app rendering** | In an ordinary chat session, when the model calls `open_app` (or a per-app `open_*` tool), the tool card becomes the running app, with an **Open in Apps** button under it that takes you into the app's own container. That seat is for ordinary chats: inside a container the same call is what the opening prompt forbids, because the panel is already showing the app. |\n\nThat last seat is the one an ordinary chat sees most: no container, no workspace of its\nown — the app simply arrives where the tool call was, and the conversation goes on around\nit. The **Open in Apps** pill under the frame is the way onward: the card is a live app in\nsomebody else's conversation, and everything you can do to it there you can also do in its\nown place, with its history under it and a composer aimed at it.\n\n![Inline app rendering — an `open_app` tool call becomes the running app, inside an ordinary chat](docs/screenshot-inline.jpg)\n\n## Troubleshooting\n\nEvery row below is explained in full further down; this table is the index, not a second\ncopy of the answer.\n\n| Symptom | What it is | Where |\n|---|---|---|\n| dsh refuses to boot: `duplicate loader entry id: open-app` | A hand-written `- insert:` block and the bundle's row are two rows with one id | [Install → upgrading](#install) |\n| Installed, but nothing appears | Plugin metadata is cached per name for the life of the process — restart dsh | [Install](#install) |\n| The app panel is empty, or the directory is | The engine is not up, or is not on `engineBase` | [Requirements](#requirements) |\n| An app made mid-session has no `open_<name>` tool yet | dsh's tool table does not re-sync; the universal `open_app` covers it | [Known limitations](#known-limitations) |\n| The App Store's *Open* navigates its own frame | Engine older than v0.5.1 (`?nav=intent`) | [Known limitations](#known-limitations) |\n| A tall app is cut off at the fold | Engine older than v0.5.1 (viewer root `overflow-y:auto`) | [Known limitations](#known-limitations) |\n| A new container has no tab ring, app sits above the composer | The session is still blank; the opening line has not gone out | [Known limitations](#known-limitations) |\n| The container's agent ignores the rules | `containerPrompt: ''` retires them; the rules are a system section, not a message | [The container prompt](#the-container-prompt) |\n\n## How it works\n\nThe package ships both halves of a dsh plugin.\n\n**The browser half** takes five slots dsh publishes for exactly this kind of\nextension: `conversation.view` for the Apps tab, `conversation.input.dock` for the same\nsurface before a session has a view ring, `sidebar.footer.action` for the Apps section,\n`shell.overlay` for browsing, and `tool.call.toolview` for the inline takeover. Nothing\nis shadowed or replaced — every seat is an additive one.\n\nEach app is a dsh **workspace**: the host half makes `<appsRoot>/<app>/` and the browser\nhalf registers it (`workspaces.create({path})`, which adopts an existing directory\nidempotently), then creates the app's session inside it. That is not decoration. dsh's\nNew Session reuses a workspace's idle blank session — so a container sitting blank in\n*your* workspace is exactly the session your next New Session picks up, and your fresh\nchat silently becomes the app's. Giving apps their own workspace puts that reuse where\nit belongs: inside the app, where reusing its own idle session is the right answer.\n\nThe app's data does not live in that directory — it lives in the engine's store, shared\nwith every other host talking to the same engine. The directory is where the workspace\nis anchored, and where the app's exported files will land.\n\nEvery app surface is an `<iframe>` pointed at the engine's own `/view/<app>` page. That\nis a requirement, not a convenience: the engine answers its `/rpc` and `/events`\nendpoints only for callers whose origin is its own, so a frame that loads the engine's\nURL is trusted and a `srcdoc` frame — which would inherit the dsh page's origin — is\nnot. Loading the real URL is also what keeps the app's live updates working: the frame\nholds its own SSE connection, so a change the model writes appears in the widget\nwithout dsh mediating anything.\n\n**The host half** exists because the same origin rule blocks the plugin's own React\ntree from reading the app registry. It publishes a small API on dsh's web server —\nsame-origin with the page — and forwards a **whitelist** of read-only engine tools\n(`list_apps`, `app_store_list`, `get_app`) from inside the dsh process, where a server-to-server\nrequest carries no `Origin` header at all. The whitelist is the point: these routes\nhave none of the engine's own protection, so everything that writes keeps going over\nMCP, where the host's permission prompts are.\n\nIt also owns two things the browser cannot own at all: the app directories, and the\ncontainer prompt. The prompt has to live here because a prompt section is a host-plane\nregistration — dsh assembles every request out of the sections registered on its own\ncontext — and because the card in it is engine data the browser half could only reach\ncross-origin. Its dependencies say as much: `webServer` for the routes, `systemPrompt`\nfor the section (a hard dependency: a dsh that could not carry the rules would run\ncontainers with none), and `agentPresets` reached optionally, since the surface that\ncomposes an agent per session is the Web one.\n\n## The container prompt\n\nAn app container's agent behaves like it lives somewhere, and that comes from a prompt\nof its own. open-mcp-apps writes its guidance for a **chat** host, where the core verb is\n`open_app`: a model with no screen of its own puts an app on the user's by calling it.\nHere the screen came first, so the container prompt replaces that posture — an identity,\na bounded set of verbs, one ban, and a reply contract.\n\n**It is a system-prompt section, not a message.** The host half registers one section\nwith dsh's prompt registry (`ctx.systemPrompt.section`) and dsh renders it into every\nrequest. The section is global and decides per request whether it has anything to say:\ndsh hands the assembling agent to the section (`assembleContextFor`, `dsh-agent`), the\nsection asks whether that agent's session is working inside one of this plugin's app\ndirectories, and answers with the empty string when it is not — which the assembler drops\nbefore the prompt is joined. So an ordinary chat session carries no trace of this plugin,\nnot even a blank line, and a container carries the rules in the request's `system` field\nwhere they belong.\n\nThree things follow from that, and they are the reason it stopped being the opening\nmessage it was through the plugin's first development builds:\n\n- **It is current.** The prompt is assembled for every request, so upgrading this plugin\n  re-rules every container that already exists, on its next turn. A message in the log is\n  frozen the moment it is sent.\n- **It survives compaction.** Rules in the message history are summarizable; a system\n  prompt is not part of the history being summarized.\n- **The first turn is the user's.** A container no longer spends a model call answering\n  its own installation.\n\nIt is not free of the log, and pretending otherwise would be wrong: dsh snapshots the\nrendered system prompt and the tool catalog into the session as a `request/header` event\nwhenever they CHANGE (reasons `initial`, `resume`, `change`). That is an audit record,\nnot conversation — it is not part of the message history the model is sent — but it is\nwhy the app's card is served from a cache that only ever moves forward: a card that\nflickered with every engine hiccup would write a header snapshot each time.\n\n**The blank session is what the opening line is for.** A session dsh has never been\nspoken to is `blank`, and a blank session gets no view ring, no tab strip and no header:\nthe app has to live in a row above the composer, under the hero greeting, in a column\nlaid out for a chat that has not started. Everything awkward about that state comes from\none fact — the session is empty. So a fresh container is asked one question, and from\nthat moment it is an ordinary session with the app in its own tab. The question is also\nthe strip's first line: what the model answers is what the user reads under the app.\n\n**App mode** is the same fact said in dsh's own vocabulary. A container's session is put\non an `app-mode` agent preset while it is still blank (dsh accepts a preset only then),\nwhich is what the new-session chip and the session header report. The preset is a copy of\nwhatever preset your deployment defaults to, made once through dsh's own authoring path\nthe first time this plugin boots — a container is an ordinary agent with an app in front\nof it, and hand-writing a composition would quietly take away tools it has today. It is a\nlabel, and nothing load-bearing hangs on it: the rules are keyed on the app directory, so\nswitching a container back to Standard mode changes what the header says and nothing else.\nDelete the preset and the next boot copies it back from the current default; a deployment\nthat composes no preset roster at all (the TUI) simply has no label to show.\n\nOne default cannot be copied that way. A preset is composed once per process, and dsh's\n`cordis` preset (创造模式) registers Host Cordis inspect providers into a *process-global*\nregistry — so a duplicate of it throws `already registered` and can never mount, because\nthe original is always mounted first: it is what the session was created on. When that is\nyour default, the first container to be labelled tells the host half so, the copy is\nre-made from the first preset your deployment lists that is neither the default nor this\none (its own ordinary agent, `standard` in a stock dsh), and the label lands on that same\ncontainer. The `preset.yml` beside the composition says which preset it came from and why.\nContainers made before the repair keep the agent they started on — dsh fixes a session's\npreset the moment it stops being blank — so it is the next container that shows the label.\n\n**The card** is assembled by the host half from `list_apps` and the app's own declaration\n(`get_app` slot `manifest`): name, version, purpose, the collections its rows live in and\nthe shape of those rows, and — only when the app declares them — its functions, with the\n`call_function` call shape spelled out. It is deliberately short, because it rides in\nevery request this container ever makes, so it states schema and never sample data.\nMeasured on the shipped apps the card runs 194–474 characters, which puts the whole\nprompt at ~1,300 characters (~330 tokens) against the largest of them. It is read from\nthe engine, cached, and refreshed in the background when it is more than ten seconds old\n— a section renders synchronously, so a request cannot wait for the engine, and a\ncontainer whose engine is down keeps the last card it had rather than forgetting what it\nlives in.\n\n**Why `open_app` is banned here.** The panel above the conversation is already the app,\nand it holds its own live connection to the engine, so opening it again renders nothing\nnew. What it does do is expensive and permanent: the result is the full widget HTML\n(~17K tokens measured on `settings`), and a container's conversation is durable, so that\npayload is re-sent on every later turn of that container. Measured on the rig: a first\nturn costs 23.1K input tokens without the call and 46.6K with it. The ban therefore\ncovers `open_app`, `app_html` and the per-app `open_*` tools, and it is stated flatly —\n*not even when a tool result tells you to* — because tool results do tell you to:\n`get_app_guide` says \"open it with `open_app {app}`; it renders immediately after\nsaving\", and `save_app`'s own result ends with `Show it NOW with: open_app {…}`. It\nholds: measured in this panel on DeepSeek-V4-Pro, a container that read its data, edited\nthe app's HTML and added a row made twelve tool calls and not one of them was an `open_*`\n— including the step right after `get_app_guide` had told it to open one. This is a\ncontainer-only rule; in an ordinary chat session `open_app` is exactly right, and the\nplugin renders it inline.\n\nIt does **not** have to argue with the engine's `initialize` instructions, because dsh\nnever reads them: `packages/mcp/mcp-client/src/connection.ts:272` discards the SDK\nclient's connect result, and nothing in the tree calls `getInstructions()`. The only\nserver-authored text that reaches the model is tool names, descriptions and schemas\n(`packages/mcp/mcp-client/src/tools.ts:146-152` → `ctx.tools.register`). If a later dsh\nstarts forwarding them, nothing here changes: the ban is unconditional already.\n\n**The reply contract** is machined to fit the strip. In a container the app is the reply\n— you say \"mark the milk as bought\" and the row ticks itself — so the model's sentence is a\nreceipt, and the strip under the app shows the head of the last thing it said, flattened\nto one line and cut at 200 characters. That is why the prompt asks for one sentence\nfirst: the first sentence *is* what the user reads. After a write it asks for the change\nby name rather than a recital of the board, which the panel is already showing.\n\nIf you replace the template through `containerPrompt`, keep those four jobs — identity,\nthe `{card}`, the ban, the one-sentence receipt — in whatever language you write it in.\nBoth placeholders are optional: a template without `{card}` simply does not get one.\n\n**Containers made by a build that still posted the rules as a message** keep that message\nin their history and gain the section on their next turn. Nothing is migrated: the old message is one more thing\nthat was said in that conversation, it agrees with the section, and a container is not\nworth rewriting history for. New containers get the one-line question instead.\n\n## What it reaches into, and what breaks if dsh moves\n\nSix things this plugin does are not asked of it by any published seam — it reads or\nadjusts something inside dsh. Each degrades to plainer behaviour rather than breaking, and\neach is listed here so an upgrade has somewhere to look:\n\n- **The hero gives way in a container.** A stylesheet keyed on an attribute the plugin\n  stamps on dsh's composer stack hides the hero chrome and lets the app fill the column.\n  It selects structurally (`> :not([data-slot])`) because dsh's class names are\n  content-hashed. If the stack's shape changes the rules stop matching and the blank\n  container looks like an ordinary blank session with an app in a row above the composer.\n  Only reachable in the state where the opening line did not go out.\n- **App workspaces are filtered out of dsh's own projection.** The plugin wraps\n  `workspaces.list.getSnapshot`, so the sidebar tree, the New Session picker and dsh's\n  own \"most recent workspace\" all stop seeing them. One app workspace survives the\n  filter — the one whose blank session you are looking at — because ConversationRoot\n  disables the composer of a blank session whose workspace it cannot find. (Measured:\n  with that exemption removed the input reads \"Choose a workspace to start\" and Send is\n  disabled.)\n- **New Session is re-aimed.** `workspaces.startSession` is wrapped so that an implicit\n  New Session standing inside a container goes to your own most recent workspace instead\n  of the app's. A New Session started from a workspace row still goes exactly where the\n  row says.\n- **Two tabs are pressed rather than selected.** View selection lives in a store the\n  chat entry owns, with no service verb: the Apps tab is found by its label, and Chat as\n  the first tab that is not ours.\n- **The app pane is measured, because a `conversation.view` gets no box to fill.** Its\n  height is the scrollport minus the composer seat, read off the two attributes dsh puts\n  on them (`data-conversation-scroll`, `data-composer-seat`) and re-read on every pass —\n  the nodes are dsh's and get replaced, and an observer left on a detached one freezes\n  the height at a window size that is no longer there. The invariant is that the app pane\n  is never taller than the room: a column with something to scroll is a column a chat\n  scrolls to the bottom, which slides the app out of sight. If neither attribute is\n  found the pane keeps its 320px starting height and the panel is small, not broken.\n- **The agent's line sits in dsh's content column.** The strip's rule and its\n  alert tint span the panel; the row of text inside them is capped at\n  `--dsh-composer-card-max-width` (the variable dsh declares on the conversation root —\n  `calc(748px + 32px)` today) and centred, so the agent speaks in the same column as the\n  composer and every chat message. A dsh that stops publishing the variable falls back to\n  780px; one that changes it moves our row with everything else.\n\n## Known limitations\n\n- **The opening line is one model call per app.** See above; `installMessage: ''` opts\n  out, at the cost of the blank-session layout and of the strip's first receipt. The\n  rules are unaffected — they are a system section, and `containerPrompt: ''` is what\n  retires those.\n- **A container that never got its opening line has no tab ring.** dsh renders no\n  view until a session has its first message. Until then the app renders in a row of its\n  own above the composer (`conversation.input.dock`) — live and interactive, never\n  covering the input — and moves into the Apps tab the instant the ring appears.\n- **An app's conversations are hidden from the session tree, not just its workspace.**\n  Hiding the workspace alone would drop its sessions into the Ungrouped bucket, so they\n  are hidden with it (through dsh's own archived-session set, in this client's\n  projection only — nothing is archived on the host). They are still reachable the way\n  they are meant to be: through the app.\n- **The App Store's \"open\" needs an engine that knows `?nav=intent`** (v0.5.1+). An older\n  engine ignores the parameter, and the store's links navigate its own frame instead of\n  entering the other app's container.\n- **An app taller than the panel needs an engine that keeps its viewer scrollable.**\n  The panel gives every app the same height, and an app whose document is taller than\n  that has to scroll inside it. Seven of the shipped apps declare\n  `html,body{overflow:hidden}` — correct advice to a host that sizes the frame from the\n  app, and in a fixed frame it means the wheel does nothing (measured: habit-streaks at\n  1712×537, document 1127px, nothing in the tree scrollable, 590px unreachable). The\n  engine's viewer (v0.5.1+) sets `overflow-y:auto` on the root of a standalone page,\n  which costs nothing when the app fits — on an older engine those apps are clipped at\n  the fold, in a browser tab exactly as in this panel.\n- **A sandboxed app's links are not intercepted.** Apps installed `--sandboxed` run in\n  the runner's own child document; the click interception lives in the document the\n  engine serves. Every app the AI writes, and everything from the App Store, is local\n  and covered.\n- **The Apps section sits at the foot of the sidebar, not above the workspace list.**\n  The sidebar shell offers plugins exactly one hole (`sidebar.footer.action`, below the\n  workspace region); the region itself is a single-occupant slot that dsh's own\n  workspace browser fills, and registering a second entry there would shadow it rather\n  than sit beside it.\n- **Chat stays dsh's default view.** The Apps tab sorts first, but a session with no\n  stored preference opens on Chat — the fallback is a constant inside ui-conversation.\n  Entering through an app node selects the Apps tab explicitly, and so does the handover\n  when a container's first message lands.\n- **A per-app `open_<name>` tool for an app made mid-session needs a dsh restart — and the\n  plugin is no longer the reason.** Keyed slots take no wildcards, so the plugin asks the\n  engine which `open_*` tools it publishes and registers each one; it now re-asks whenever\n  the directory is opened or a card shows an app it has not heard of, so its key set is\n  never older than the app list beside it. What does not move is dsh's own tool table: the\n  engine registers the new tool when `save_app` runs (with `OMA_DYNAMIC_TOOLS=1`) and dsh's\n  MCP client re-syncs on `notifications/tools/list_changed`, but the engine's `/mcp` face is\n  stateless — `createMcpHandler` builds a fresh engine per request — so there is no live\n  session to send that notification on. Measured on the rig: an app created in turn 1 was\n  still missing from the model's tools in turn 2 (*\"There is no tool named\n  open_mood_tracker\"*), and the session log carries exactly one `request/header` for the\n  whole session, so the catalog never changed. None of this reaches the user, because the\n  universal `open_app` covers every app the moment it exists — it is what the model called\n  in every rig run, including the step straight after `save_app`.\n- **Tool results arrive flattened.** dsh keeps only the rendered text of an MCP result,\n  so the app name is recovered from the call's arguments (or the tool name) rather than\n  from `structuredContent`.\n\n## Development\n\nPoint the same command at a checkout instead of the registry — pnpm links it, so edits are\nlive on the next dsh restart:\n\n```sh\ndsh plugin --profile web add /path/to/dsh-plugin-open-app\n```\n\nA linked plugin is resolved from its real path, so the profile's own `node_modules` is not\non its resolution path. That is why `lib/index.js` imports nothing but Node builtins, and\nwhy it hand-checks its config instead of declaring a schema.\n\nThe package is two files: `lib/index.js` (the host half — routes, app directories, the\nprompt section) and `lib/client.js` (the browser half — the five slots). `cordis.patch.yml`\nis the bundle layer `dsh plugin add` appends to your profile.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","readmeFilename":"README.md"}