• feat(self-modification): add dynamic Cordis plugin runtime and UI

This commit is contained in:
imccyu
2026-08-12 23:51:31 +08:00
parent 0367506471
commit 4064198560
147 changed files with 20904 additions and 2412 deletions

View File

@@ -0,0 +1,297 @@
/* Cordis definition card: DisclosureRow chrome (leading/chevron/title come from
that primitive) plus this card's own trailing run switch and source body.
No running sweep: `cordis_define` only registers a definition in host memory,
so the call settles in the same turn it starts and the state dot carries the
whole signal. */
.card {
display: flex;
flex-direction: column;
}
/* The shared cordis accent (matches ui-tool's [data-tool^='cordis_'] rows). */
.card .title,
.card .chevron {
color: var(--dsw-alias-state-business-primary);
}
.title {
flex: none;
font-size: 14px;
font-weight: 500;
line-height: 24px;
}
.row {
gap: 0;
}
.separator {
flex: none;
width: 2px;
height: 2px;
border-radius: 1px;
margin: 0 8px;
background: var(--dsw-alias-state-business-primary);
}
.name {
flex: none;
max-width: 40%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
line-height: 24px;
color: var(--dsw-alias-label-secondary);
}
/* The purpose absorbs the remaining width and clips first: the name and the
switch are the two parts that must survive a narrow row. */
.purpose {
flex: 1 1 auto;
min-width: 0;
margin-left: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
line-height: 24px;
color: var(--dsw-alias-label-tertiary);
}
.errorSummary {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
line-height: 24px;
color: var(--dsw-alias-state-error-primary);
}
.requestError {
flex: none;
max-width: 40%;
overflow: hidden;
margin-left: 8px;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
line-height: 24px;
color: var(--dsw-alias-state-error-primary);
}
.readout {
display: inline-flex;
flex: none;
align-items: center;
margin-left: 8px;
}
/* Said once in the expanded body: this card records, the panel operates. */
.panelHint {
margin: 4px 0 2px 4px;
font-size: 11px;
line-height: 16px;
color: var(--dsw-alias-label-caption);
}
.statusLabel {
font-size: 12px;
line-height: 24px;
color: var(--dsw-alias-label-caption);
}
/* The model's question, shown beside the approve/decline pair. */
.approvalPrompt {
flex: none;
max-width: 40%;
overflow: hidden;
margin-left: 8px;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
line-height: 24px;
color: var(--dsw-alias-label-secondary);
}
/* A qualified success (nothing acknowledged the dispatch, or the browser half
parked on missing services): informational, so it must not read as the error
line beside it. */
.notice {
flex: none;
max-width: 40%;
overflow: hidden;
margin-left: 8px;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
line-height: 24px;
color: var(--dsw-alias-label-caption);
}
.switch {
height: 22px;
padding: 0 8px;
font-size: 12px;
}
/* Terminal definitions (unloaded, or lost to a host restart) stay in the flow
as a greyed card: the define call is still in the session log, so removing
the row would leave a replay hole. */
.card[data-terminal] .title,
.card[data-terminal] .name,
.card[data-terminal] .purpose,
.card[data-terminal] .statusLabel {
color: var(--dsw-alias-label-caption);
}
.card[data-terminal] .separator {
background: var(--dsw-alias-label-caption);
}
.bodyWrap {
display: flex;
flex-direction: column;
}
.sourceCard {
display: flex;
flex-direction: column;
margin: 4px 0 4px 4px;
}
.sourceTabs {
display: flex;
height: 32px;
border-bottom: 1px solid var(--dsw-alias-border-l2);
}
.sourceTab {
position: relative;
padding: 0 10px;
border: 0;
background: transparent;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
font: var(--dsw-font-xs-13);
}
.sourceTab:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.sourceTab:disabled {
cursor: default;
opacity: 0.4;
}
.sourceTabActive {
color: var(--dsw-alias-state-business-primary);
}
.sourceTabActive::after {
position: absolute;
right: 10px;
bottom: 0;
left: 10px;
height: 2px;
border-radius: 1px 1px 0 0;
background: var(--dsw-alias-state-business-primary);
content: '';
}
.sourceTab:focus-visible {
outline: 1px solid var(--dsw-alias-state-business-primary);
outline-offset: -1px;
}
.sourcePanel {
max-height: 260px;
overflow: auto;
}
.sourceCode {
margin: 0;
}
.codeSection {
display: flex;
flex-direction: column;
max-height: 260px;
margin: 4px 0 4px 4px;
overflow: auto;
}
.sectionLabel {
flex: none;
padding: 2px 0;
font-size: 11px;
font-weight: 500;
line-height: 16px;
color: var(--dsw-alias-label-caption);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.output {
margin: 0;
padding: 8px 10px;
border: 1px solid var(--dsw-alias-border-l1);
border-radius: 8px;
background: var(--dsw-alias-markdown-code-block);
white-space: pre-wrap;
overflow-wrap: anywhere;
font: var(--dsw-font-markdown-code-block-small);
color: var(--dsw-alias-label-secondary);
}
.output[data-error] {
color: var(--dsw-alias-state-error-primary);
}
.inspectButton {
display: inline-flex;
align-self: flex-start;
align-items: center;
gap: 4px;
margin: 4px 0 2px 4px;
padding: 2px 8px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 999px;
background: var(--dsw-alias-bg-base);
color: var(--dsw-alias-label-secondary);
font-size: 11px;
line-height: 16px;
cursor: pointer;
opacity: 0;
transition: opacity 100ms ease;
}
.card:hover .inspectButton,
.inspectButton:focus-visible {
opacity: 1;
}
.inspectButton:hover {
background: var(--dsw-alias-interactive-bg-hover-solid);
color: var(--dsw-alias-label-primary);
}
.visuallyHidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) {
.inspectButton {
transition: none;
}
}

View File

@@ -0,0 +1,171 @@
/** Read-only `cordis_define` card with Host and Client source tabs. */
import { useId, useState, type ReactNode } from 'react'
import {
CodeBlock, DisclosureRow, IconCodeOutline16, IconInspectOutline12, StateDot,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { InjectFace, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'
import { cordisDefineCard, type CordisToolState } from './card-model.ts'
import type { CordisCardFace } from './slots.ts'
import { cordisVisibleStatus, type CordisVisibleStatus } from './status.ts'
import type { CordisKey } from './locales.ts'
import css from './CordisDefineRow.module.css'
/** Full card props composed by the keyed Tool slot. */
export type CordisDefineRowProps = ToolCallViewProps & InjectFace<CordisCardFace> & PropsLocale<'cordis'>
type CardReading = CordisVisibleStatus | 'removed'
type SourceTab = 'client' | 'host'
const READING_LABELS = {
idle: 'status.idle',
'client-pending': 'status.clientPending',
running: 'status.running',
removed: 'status.removed',
} as const satisfies Record<CardReading, CordisKey>
function stateStatus(state: CordisToolState): CordisKey | null {
switch (state) {
case 'running': return 'a11y.defining'
case 'error': return 'a11y.failed'
case 'stopped': return 'a11y.stopped'
default: return null
}
}
function leadingFor(state: CordisToolState): ReactNode {
switch (state) {
case 'error': return <StateDot state="error" />
case 'stopped': return <StateDot state="warning" />
default: return <IconCodeOutline16 size={14} />
}
}
/** Render one immutable Package definition. */
export function CordisDefineRow({
callId, block, inspect, useInventory, useLoaded, t,
}: CordisDefineRowProps) {
const card = cordisDefineCard(block)
const inventory = useInventory(snapshot => snapshot)
const loaded = useLoaded(snapshot => snapshot)
const [expanded, setExpanded] = useState(false)
const [selectedSource, setSelectedSource] = useState<SourceTab>(card.clientCode !== null ? 'client' : 'host')
const sourcePanelId = useId()
const row = card.pluginId === null
? undefined
: inventory.rows.find(candidate => candidate.pluginId === card.pluginId)
const reading: CardReading = card.pluginId !== null && inventory.removed.has(card.pluginId)
? 'removed'
: row !== undefined && card.packageId !== null
? cordisVisibleStatus(row, card.packageId, loaded)
: 'idle'
const name = card.name ?? callId
const expandable = card.hostCode !== null || card.clientCode !== null || card.output !== null
const open = expanded && expandable
const a11yState = stateStatus(card.state)
const hasSource = card.clientCode !== null || card.hostCode !== null
const activeSource: SourceTab = selectedSource === 'client' && card.clientCode !== null
? 'client'
: selectedSource === 'host' && card.hostCode !== null
? 'host'
: card.clientCode !== null ? 'client' : 'host'
const activeCode = activeSource === 'client' ? card.clientCode : card.hostCode
return (
<div
className={css.card}
data-tool="cordis_define"
data-state={card.state}
data-terminal={reading === 'removed' || undefined}
data-cordis-plugin-id={card.pluginId ?? undefined}
data-cordis-package-id={card.packageId ?? undefined}
data-cordis-status={reading}
>
{a11yState !== null && <span className={css.visuallyHidden}>{t(a11yState)}</span>}
<DisclosureRow
rowClassName={css.row}
titleClassName={css.title}
chevronClassName={css.chevron}
icon={leadingFor(card.state)}
title={t('row.defineTitle')}
open={open}
expandable={expandable}
expandOnRowClick
keepContentWhenOpen
onToggle={() => { setExpanded(value => !value) }}
collapsedContent={(
<>
<span className={css.separator} aria-hidden />
<span className={card.errorSummary === null ? css.name : css.errorSummary}>
{card.errorSummary ?? name}
</span>
{card.errorSummary === null && (
<span className={css.purpose}>{card.purpose ?? t('purpose.missing')}</span>
)}
{card.pluginId !== null && (
<span className={css.readout}>
<span className={css.statusLabel}>{t(READING_LABELS[reading])}</span>
</span>
)}
</>
)}
>
<div className={css.bodyWrap}>
{hasSource && activeCode !== null && (
<section className={css.sourceCard}>
<div className={css.sourceTabs} role="tablist" aria-label={t('body.source')}>
{(['client', 'host'] as const).map(source => {
const available = source === 'client' ? card.clientCode !== null : card.hostCode !== null
return (
<button
key={source}
id={`${sourcePanelId}-${source}`}
type="button"
role="tab"
aria-controls={sourcePanelId}
aria-selected={activeSource === source}
className={activeSource === source ? `${css.sourceTab} ${css.sourceTabActive}` : css.sourceTab}
disabled={!available}
onClick={() => { setSelectedSource(source) }}
>
{t(source === 'client' ? 'body.clientCode' : 'body.hostCode')}
</button>
)
})}
</div>
<div
id={sourcePanelId}
className={css.sourcePanel}
role="tabpanel"
aria-labelledby={`${sourcePanelId}-${activeSource}`}
>
<CodeBlock
code={activeCode}
lang="javascript"
copyLabel={t('body.copy')}
copiedLabel={t('body.copied')}
className={css.sourceCode}
/>
</div>
</section>
)}
{card.output !== null && (
<section className={css.codeSection}>
<div className={css.sectionLabel}>{t('body.output')}</div>
<pre className={css.output} data-error={card.state === 'error' || undefined}>{card.output}</pre>
</section>
)}
{card.pluginId !== null && <div className={css.panelHint}>{t('panel.hint')}</div>}
{inspect !== undefined && (
<button type="button" className={css.inspectButton} onClick={inspect}>
<IconInspectOutline12 />
Inspect
</button>
)}
</div>
</DisclosureRow>
</div>
)
}

View File

@@ -0,0 +1,370 @@
/* Sidebar-foot Cordis action and the fixed list it opens above the footer. */
.layer {
position: relative;
flex: none;
display: flex;
align-items: center;
width: 100%;
height: 49px;
margin: 8px 0 0;
}
.footerButtons {
display: flex;
align-items: center;
width: 100%;
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
width: 100%;
height: 49px;
padding: 0 8px 0 6px;
border: none;
border-radius: 12px;
background: transparent;
color: var(--dsw-alias-label-primary);
font-family: inherit;
font-size: 14px;
cursor: pointer;
overflow: hidden;
}
.badge:hover {
background: var(--dsw-alias-interactive-bg-hover-solid);
}
.badge[data-active] {
background: var(--dsw-alias-interactive-bg-hover);
}
.badgeLabel {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.badgeCount {
flex: none;
margin-left: auto;
color: var(--dsw-alias-label-tertiary);
font-size: 12px;
line-height: 16px;
font-variant-numeric: tabular-nums;
}
.layer.rail {
width: 36px;
height: 36px;
margin: 0;
}
.rail .badge {
justify-content: center;
gap: 0;
width: 36px;
height: 36px;
padding: 0;
border-radius: 50%;
}
.rail .footerButtons {
flex-direction: column;
gap: 2px;
}
.panel {
position: fixed;
left: 12px;
bottom: 128px;
z-index: 30;
display: flex;
flex-direction: column;
width: 420px;
max-width: calc(100vw - 24px);
max-height: 60vh;
overflow: hidden;
border: 1px solid var(--dsw-alias-border-l1);
border-radius: 12px;
background: var(--dsw-alias-bg-base);
box-shadow: var(--dsw-shadow-lv2);
/* Only `.body` scrolls; the header remains fixed above it. */
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
}
.header {
flex: none;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 44px;
padding: 10px 12px;
box-sizing: border-box;
border-bottom: 1px solid var(--dsw-alias-border-l2);
background: var(--dsw-alias-bg-base);
}
.body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 4px 12px 12px;
}
.title {
font-size: 13px;
font-weight: 500;
line-height: 20px;
color: var(--dsw-alias-label-primary);
}
.note,
.readError {
margin: 4px 0;
font-size: 12px;
line-height: 18px;
color: var(--dsw-alias-label-tertiary);
}
.readError {
color: var(--dsw-alias-state-error-primary);
}
/* Session headings replace per-row ownership markers. */
.group {
margin: 8px 0 4px;
font-size: 11px;
font-weight: 500;
line-height: 16px;
color: var(--dsw-alias-label-caption);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.rows {
display: flex;
flex-direction: column;
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
}
.row {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 12px;
background: var(--dsw-alias-bg-base);
}
.row[data-cordis-awaiting] {
border-color: var(--dsw-alias-state-business-primary);
}
.rowHead {
display: flex;
align-items: center;
gap: 8px;
}
.rowId {
flex: none;
color: var(--dsw-alias-label-tertiary);
font-family: var(--dsh-font-mono, monospace);
font-size: 11px;
line-height: 20px;
}
.rowName {
flex: 1;
min-width: 0;
overflow: hidden;
font-size: 13px;
font-weight: 500;
line-height: 20px;
color: var(--dsw-alias-label-primary);
text-overflow: ellipsis;
white-space: nowrap;
}
.rowStatus {
flex: none;
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 6px;
border-radius: 10px;
background: var(--dsw-alias-button-ghost-active-fill);
font-size: 11px;
line-height: 20px;
color: var(--dsw-alias-label-caption);
}
.row[data-cordis-status='idle'] .rowStatus {
background: var(--dsw-alias-button-ghost-active-fill);
color: var(--dsw-alias-label-caption);
}
.row[data-cordis-status='awaiting-approval'] .rowStatus,
.row[data-cordis-status='client-pending'] .rowStatus {
background: var(--dsw-alias-state-warn-tertiary);
color: var(--dsw-alias-state-warn-label);
}
.row[data-cordis-status='failed'] .rowStatus {
background: var(--dsw-alias-interactive-bg-hover-danger);
color: var(--dsw-alias-state-error-primary);
}
.row[data-cordis-status='running'] .rowStatus {
background: var(--dsw-alias-state-success-tertiary);
color: var(--dsw-alias-state-success-primary);
}
.rowDetail {
display: flex;
align-items: center;
gap: 8px;
min-height: 28px;
}
.versionPicker {
display: flex;
align-items: center;
gap: 8px;
color: var(--dsw-alias-label-caption);
font-size: 11px;
line-height: 18px;
}
.versionPicker select {
min-width: 0;
flex: 1;
height: 26px;
padding: 0 8px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 7px;
background: var(--dsw-alias-bg-base);
color: var(--dsw-alias-label-secondary);
font: inherit;
}
.rowPurpose {
flex: 1;
min-width: 0;
overflow: hidden;
font-size: 12px;
line-height: 18px;
color: var(--dsw-alias-label-tertiary);
text-overflow: ellipsis;
white-space: nowrap;
}
.rowError {
font-size: 12px;
line-height: 18px;
color: var(--dsw-alias-state-error-primary);
}
.rowActions {
flex: none;
display: flex;
align-items: center;
gap: 10px;
}
.transition {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
color: var(--dsw-alias-label-caption);
font-size: 11px;
line-height: 18px;
}
.transitionActions {
display: flex;
gap: 6px;
margin-left: auto;
}
.transitionActions button {
padding: 2px 8px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 999px;
background: transparent;
color: var(--dsw-alias-label-secondary);
font: inherit;
cursor: pointer;
}
.transitionActions button:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
}
.transitionActions button:disabled {
opacity: 0.4;
cursor: default;
}
.activeVersion {
color: var(--dsw-alias-label-caption);
font-size: 11px;
line-height: 16px;
}
/* Matches GoalBar's action controls: compact circular icons whose visible copy
lives in a tooltip and accessible name. */
.actionButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
border-radius: 999px;
background: transparent;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
}
.actionButton:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-secondary);
}
.actionButton:disabled {
opacity: 0.4;
cursor: default;
}
.doubleCheck {
position: relative;
display: inline-block;
width: 17px;
height: 14px;
}
.doubleCheck svg {
position: absolute;
top: 1px;
}
.doubleCheck svg:first-child {
left: 0;
opacity: 0.7;
}
.doubleCheck svg:last-child {
left: 5px;
}

View File

@@ -0,0 +1,471 @@
/** Frame-wide dynamic Plugin inventory, approvals, versions, and lifecycle actions. */
import { useEffect, useRef, useState } from 'react'
import type { ButtonHTMLAttributes, ReactNode } from 'react'
import {
IconCheckOutline16, IconCloseOutline16, IconCordisPluginOutline14, IconPlayOutline16,
IconStopFill16, IconTrashOutline16, Tooltip,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
import type { CordisRunActivity } from '@deepseek-ai/dsh-cordis-client-runner/client'
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import type { CordisInventoryRow } from './dynamic-port.ts'
import type { CordisPanelFace } from './slots.ts'
import type { CordisKey } from './locales.ts'
import type {
ApprovalRequestId, CordisDynamicPackageId, CordisDynamicPluginId,
} from './events.ts'
import { cordisVisibleStatus, packageOf, type CordisVisibleStatus } from './status.ts'
import css from './CordisPanel.module.css'
/** Full panel props composed by the sidebar footer-action slot. */
export type CordisPanelProps =
PropsRuntime<'sidebar.footer.action'> & InjectFace<CordisPanelFace> & PropsLocale<'cordis'>
type PanelStatus = CordisVisibleStatus | 'awaiting-approval' | 'failed'
const STATUS_LABELS = {
idle: 'status.idle',
'awaiting-approval': 'status.awaitingApproval',
'client-pending': 'status.clientPending',
running: 'status.running',
failed: 'status.failed',
} as const satisfies Record<PanelStatus, CordisKey>
const RENDER_FAILURE_LABELS = {
abdicated: 'render.failedAbdicated',
held: 'render.failedHeld',
} as const satisfies Record<'abdicated' | 'held', CordisKey>
interface RowView {
readonly pluginId: CordisDynamicPluginId
readonly agentId: SessionId
readonly listed?: CordisInventoryRow
readonly activity?: CordisRunActivity
}
function selectedPackageIdOf(
{ pluginId, listed, activity }: RowView,
selected: Readonly<Record<string, CordisDynamicPackageId>>,
): CordisDynamicPackageId | undefined {
const selectedPackageId = selected[pluginId]
if (selectedPackageId !== undefined
&& listed?.packages.some(pkg => pkg.packageId === selectedPackageId)) return selectedPackageId
return listed?.nextPackageId
?? listed?.currentPackageId
?? listed?.packages.at(-1)?.packageId
?? activity?.packageId
}
function visiblePanelStatus(
view: RowView,
selectedPackageId: CordisDynamicPackageId | undefined,
loaded: Parameters<typeof cordisVisibleStatus>[2],
): PanelStatus {
const { listed, activity } = view
const latest = listed?.latestRun
if (activity?.phase === 'awaiting-approval' || latest?.status === 'awaiting-approval') {
return 'awaiting-approval'
}
if (latest?.status === 'failed' && latest.packageId === selectedPackageId) return 'failed'
if (listed?.activeRun === undefined) return 'idle'
return cordisVisibleStatus(listed, listed.activeRun.packageId, loaded)
}
function blockingFirst(rows: readonly RowView[]): readonly RowView[] {
return [
...rows.filter(row => row.activity?.phase === 'awaiting-approval'),
...rows.filter(row => row.activity?.phase !== 'awaiting-approval'),
]
}
function RowAction({ label, children, ...props }: {
label: string
children: ReactNode
} & ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<Tooltip label={label} side="bottom" delayMs={500}>
<button type="button" className={css.actionButton} aria-label={label} {...props}>
{children}
</button>
</Tooltip>
)
}
function DoubleCheckIcon() {
return (
<span className={css.doubleCheck} aria-hidden>
<IconCheckOutline16 size={12} />
<IconCheckOutline16 size={12} />
</span>
)
}
/** Render the inventory panel and its unified footer action. */
export function CordisPanel({
wide,
useSessions, useInventory, useActiveRuns, useRunErrors, useLoaded, useRenderFailures,
onApprove, onDecline, onRun, onStop, onRemove, onRefresh, t,
}: CordisPanelProps) {
const inventory = useInventory(snapshot => snapshot)
const activeRuns = useActiveRuns(snapshot => snapshot)
const errors = useRunErrors(snapshot => snapshot)
const loaded = useLoaded(snapshot => snapshot)
const renderFailures = useRenderFailures(snapshot => snapshot)
const current = useSessions(state => state.current)
const [open, setOpen] = useState(false)
const [selected, setSelected] = useState<Record<string, CordisDynamicPackageId>>({})
const [pending, setPending] = useState<ReadonlySet<CordisDynamicPluginId>>(new Set())
const [actionErrors, setActionErrors] = useState<ReadonlyMap<CordisDynamicPluginId, string>>(new Map())
const visibleRequests = useRef<Set<ApprovalRequestId>>(new Set())
useEffect(() => {
const now = new Set<ApprovalRequestId>()
for (const activity of activeRuns.values()) {
if (activity.phase === 'awaiting-approval') now.add(activity.requestId)
}
const discovered = [...now].some(requestId => !visibleRequests.current.has(requestId))
visibleRequests.current = now
if (discovered) setOpen(true)
}, [activeRuns])
useEffect(() => { onRefresh() }, [onRefresh])
useEffect(() => { if (open) onRefresh() }, [onRefresh, open])
const byPlugin = new Map<CordisDynamicPluginId, RowView>()
for (const listed of inventory.rows) {
const activity = activeRuns.get(listed.pluginId)
byPlugin.set(listed.pluginId, {
pluginId: listed.pluginId,
agentId: activity?.agentId ?? listed.agentId,
listed,
...activity === undefined ? {} : { activity },
})
}
for (const [pluginId, activity] of activeRuns) {
if (byPlugin.has(pluginId)) continue
byPlugin.set(pluginId, { pluginId, agentId: activity.agentId, activity })
}
const all = [...byPlugin.values()]
const mine = blockingFirst(all.filter(row => current !== undefined && row.agentId === current))
const theirs = blockingFirst(all.filter(row => current === undefined || row.agentId !== current))
const approvals = [...activeRuns.values()].filter(activity => activity.phase === 'awaiting-approval').length
const running = all.filter(view => visiblePanelStatus(
view,
selectedPackageIdOf(view, selected),
loaded,
) === 'running').length
if (all.length === 0) return null
const runAction = async (pluginId: CordisDynamicPluginId, action: () => Promise<void | { ok: boolean; message?: string }>) => {
if (pending.has(pluginId)) return
setPending(currentPending => new Set(currentPending).add(pluginId))
setActionErrors(currentErrors => {
const next = new Map(currentErrors)
next.delete(pluginId)
return next
})
try {
const result = await action()
if (result !== undefined && !result.ok) {
setActionErrors(currentErrors => new Map(currentErrors).set(pluginId, result.message ?? 'operation failed'))
}
} catch (error) {
setActionErrors(currentErrors => new Map(currentErrors).set(
pluginId,
error instanceof Error ? error.message : String(error),
))
} finally {
setPending(currentPending => {
const next = new Set(currentPending)
next.delete(pluginId)
return next
})
onRefresh()
}
}
const renderRow = (view: RowView) => {
const { pluginId, listed, activity } = view
const selectedPackageId = selectedPackageIdOf(view, selected)
const selectedPackage = listed !== undefined && selectedPackageId !== undefined
? packageOf(listed, selectedPackageId)
: undefined
const activePackage = listed?.activeRun === undefined
? undefined
: packageOf(listed, listed.activeRun.packageId)
const name = selectedPackage?.name
?? (activity?.phase === 'awaiting-approval' ? activity.name : pluginId)
const purpose = selectedPackage?.purpose
?? (activity?.phase === 'awaiting-approval' ? activity.purpose : '')
const latest = listed?.latestRun
const awaiting = activity?.phase === 'awaiting-approval'
? activity.requestId
: latest?.status === 'awaiting-approval' ? latest.approvalRequestId : undefined
const status = visiblePanelStatus(view, selectedPackageId, loaded)
const busy = pending.has(pluginId) || activity?.phase === 'orchestrating'
const failure = errors.get(pluginId)
const hostFailure = latest?.status === 'failed' ? latest.error : undefined
const renderFailure = renderFailures.get(pluginId)
const actionError = actionErrors.get(pluginId)
const hasFailedTransition = listed?.nextPackageId !== undefined
&& listed.nextPackageId !== listed.currentPackageId
const runMode = listed?.currentPackageId !== undefined
&& selectedPackageId !== listed.currentPackageId ? 'update' as const : 'run' as const
return (
<li
key={pluginId}
className={css.row}
data-cordis-row={pluginId}
data-cordis-status={status}
data-cordis-awaiting={awaiting !== undefined || undefined}
>
<div className={css.rowHead}>
<span className={css.rowId}>{pluginId}</span>
<span className={css.rowName}>{name}</span>
<span className={css.rowStatus}>{t(STATUS_LABELS[status])}</span>
</div>
{listed !== undefined && listed.packages.length > 1 && selectedPackageId !== undefined && (
<label className={css.versionPicker}>
<span>{t('panel.version')}</span>
<select
value={selectedPackageId}
disabled={busy}
onChange={(event) => {
setSelected(currentSelected => ({
...currentSelected,
[pluginId]: event.target.value as CordisDynamicPackageId,
}))
}}
>
{listed.packages.map(pkg => (
<option key={pkg.packageId} value={pkg.packageId}>{`${pkg.name} · ${pkg.packageId}`}</option>
))}
</select>
</label>
)}
<div className={css.rowDetail}>
<span className={css.rowPurpose}>{purpose}</span>
<div className={css.rowActions}>
{awaiting !== undefined && (
<>
<RowAction
label={t('action.approveOnce')}
data-cordis-approve={awaiting}
disabled={busy}
onClick={() => { void runAction(pluginId, async () => {
await onApprove(awaiting, false)
setOpen(false)
}) }}
>
<IconCheckOutline16 size={14} />
</RowAction>
<RowAction
label={t('action.approvePlugin')}
data-cordis-approve-plugin={awaiting}
disabled={busy}
onClick={() => { void runAction(pluginId, async () => {
await onApprove(awaiting, true)
setOpen(false)
}) }}
>
<DoubleCheckIcon />
</RowAction>
<RowAction
label={t('action.decline')}
data-cordis-decline={awaiting}
disabled={busy}
onClick={() => { void runAction(pluginId, async () => {
await onDecline(awaiting)
setOpen(false)
}) }}
>
<IconCloseOutline16 size={14} />
</RowAction>
</>
)}
{awaiting === undefined && listed !== undefined
&& selectedPackageId !== undefined && listed.activeRun === undefined && (
<RowAction
label={t('action.run')}
data-cordis-switch="run"
disabled={busy}
onClick={() => { void runAction(pluginId, () => onRun({
agentId: listed.agentId,
pluginId,
packageId: selectedPackageId,
mode: runMode,
hasClientHalf: selectedPackage?.hasClientHalf === true,
})) }}
>
<IconPlayOutline16 size={14} />
</RowAction>
)}
{awaiting === undefined && listed !== undefined && listed.activeRun !== undefined
&& selectedPackageId !== listed.activeRun.packageId && selectedPackage !== undefined && (
<RowAction
label={t('action.run')}
data-cordis-switch="run"
disabled={busy}
onClick={() => { void runAction(pluginId, () => onRun({
agentId: listed.agentId,
pluginId,
packageId: selectedPackageId!,
mode: runMode,
hasClientHalf: selectedPackage.hasClientHalf,
})) }}
>
<IconPlayOutline16 size={14} />
</RowAction>
)}
{awaiting === undefined && listed !== undefined && listed.activeRun !== undefined && status === 'client-pending'
&& activePackage !== undefined && selectedPackageId === listed.activeRun.packageId && (
<RowAction
label={t('action.run')}
data-cordis-switch="run"
disabled={busy}
onClick={() => { void runAction(pluginId, () => onRun({
agentId: listed.agentId,
pluginId,
packageId: activePackage.packageId,
mode: 'run',
hasClientHalf: true,
})) }}
>
<IconPlayOutline16 size={14} />
</RowAction>
)}
{awaiting === undefined && listed !== undefined && listed.activeRun !== undefined && (
<RowAction
label={t('action.stop')}
data-cordis-switch="stop"
disabled={busy}
onClick={() => { void runAction(pluginId, () => onStop(listed.agentId, pluginId)) }}
>
<IconStopFill16 size={14} />
</RowAction>
)}
{awaiting === undefined && listed !== undefined && (
<RowAction
label={t('action.remove')}
data-cordis-remove={pluginId}
disabled={busy}
onClick={() => { void runAction(pluginId, () => onRemove(listed.agentId, pluginId)) }}
>
<IconTrashOutline16 size={14} />
</RowAction>
)}
</div>
</div>
{awaiting === undefined && hasFailedTransition && listed !== undefined && (
<div className={css.transition}>
<span>{listed.currentPackageId === undefined ? '' : t('panel.current', { packageId: listed.currentPackageId })}</span>
<span>{t('panel.next', { packageId: listed.nextPackageId! })}</span>
<div className={css.transitionActions}>
<button
type="button"
disabled={busy}
onClick={() => { void runAction(pluginId, () => onRun({
agentId: listed.agentId,
pluginId,
packageId: listed.nextPackageId!,
mode: listed.currentPackageId === undefined ? 'run' : 'update',
hasClientHalf: packageOf(listed, listed.nextPackageId!)?.hasClientHalf === true,
})) }}
>{t('action.retry')}</button>
{listed.currentPackageId !== undefined && (
<button
type="button"
disabled={busy}
onClick={() => { void runAction(pluginId, () => onRun({
agentId: listed.agentId,
pluginId,
packageId: listed.currentPackageId!,
mode: 'run',
hasClientHalf: packageOf(listed, listed.currentPackageId!)?.hasClientHalf === true,
})) }}
>{t('action.rollback')}</button>
)}
</div>
</div>
)}
{failure !== undefined && (
<div className={css.rowError} role="alert">{`${failure.message} (${failure.reason})`}</div>
)}
{failure === undefined && hostFailure !== undefined && (
<div className={css.rowError} role="alert">{`${hostFailure.message} (${hostFailure.phase})`}</div>
)}
{actionError !== undefined && <div className={css.rowError} role="alert">{actionError}</div>}
{renderFailure !== undefined && (
<div
className={css.rowError}
role="alert"
data-cordis-render-failure={renderFailure.slot}
data-cordis-render-abdicated={renderFailure.abdicated || undefined}
>
{`${t(RENDER_FAILURE_LABELS[renderFailure.abdicated ? 'abdicated' : 'held'], {
slot: renderFailure.slot,
})} ${renderFailure.message}`}
</div>
)}
{activePackage !== undefined && activePackage.packageId !== selectedPackageId && (
<span className={css.activeVersion}>{`${t('status.running')}: ${activePackage.name} · ${activePackage.packageId}`}</span>
)}
</li>
)
}
return (
<div className={wide ? css.layer : `${css.layer} ${css.rail}`}>
{open && (
<section className={css.panel} data-cordis-panel aria-label={t('panel.title')}>
<header className={css.header}>
<span className={css.title}>{t('panel.title')}</span>
</header>
<div className={css.body}>
{inventory.error !== undefined && (
<p className={css.readError} role="alert">{t('panel.readFailed', { message: inventory.error })}</p>
)}
{!inventory.read && inventory.error === undefined && <p className={css.note}>{t('panel.loading')}</p>}
{inventory.read && all.length === 0 && <p className={css.note}>{t('panel.empty')}</p>}
{mine.length > 0 && (
<section>
<h3 className={css.group}>{t('panel.group.current')}</h3>
<ul className={css.rows}>{mine.map(renderRow)}</ul>
</section>
)}
{theirs.length > 0 && (
<section>
<h3 className={css.group}>{t('panel.group.others')}</h3>
<ul className={css.rows}>{theirs.map(renderRow)}</ul>
</section>
)}
</div>
</section>
)}
<div className={css.footerButtons}>
<button
type="button"
className={css.badge}
data-cordis-badge={all.length}
data-cordis-approval-badge={approvals}
data-active={approvals > 0 || undefined}
aria-label={t('panel.plugins.aria')}
aria-expanded={open}
onClick={() => { setOpen(value => !value) }}
>
<IconCordisPluginOutline14 />
{wide && (
<>
<span className={css.badgeLabel}>{t('panel.trigger')}</span>
<span className={css.badgeCount}>{t('panel.runningCount', { count: running })}</span>
</>
)}
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,132 @@
.card {
display: flex;
flex-direction: column;
gap: 8px;
}
.row {
display: flex;
align-items: center;
min-height: 32px;
}
.icon {
display: inline-flex;
flex: none;
margin-right: 8px;
color: var(--dsw-alias-state-business-primary);
}
.title {
flex: none;
color: var(--dsw-alias-state-business-primary);
font-size: 14px;
font-weight: 500;
line-height: 24px;
}
.separator {
flex: none;
width: 2px;
height: 2px;
margin: 0 8px;
border-radius: 50%;
background: var(--dsw-alias-state-business-primary);
}
.summary,
.error {
flex: 1;
min-width: 0;
overflow: hidden;
color: var(--dsw-alias-label-secondary);
font-size: 13px;
line-height: 24px;
text-overflow: ellipsis;
white-space: nowrap;
}
.error {
color: var(--dsw-alias-state-error-primary);
}
.status {
flex: none;
margin-left: 8px;
color: var(--dsw-alias-label-caption);
font-size: 12px;
line-height: 24px;
}
.card[data-cordis-status='awaiting-approval'] .status,
.card[data-cordis-status='client-pending'] .status {
color: var(--dsw-alias-state-warn-label);
}
.card[data-cordis-status='running'] .status {
color: var(--dsw-alias-state-success-primary);
}
.card[data-cordis-status='failed'] .status {
color: var(--dsw-alias-state-error-primary);
}
.inspect {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-left: 4px;
padding: 0;
border: none;
border-radius: 999px;
background: transparent;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
opacity: 0;
}
.card:hover .inspect,
.inspect:focus-visible {
opacity: 1;
}
.inspect:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.message {
padding: 8px 12px;
border-radius: 8px;
background: var(--dsw-alias-button-ghost-active-fill);
color: var(--dsw-alias-label-tertiary);
font-size: 12px;
line-height: 18px;
}
.business {
min-width: 0;
overflow: hidden;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 12px;
background: var(--dsw-alias-bg-base);
}
.output {
margin: 0;
padding: 10px 12px;
overflow: auto;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 8px;
background: var(--dsw-alias-markdown-code-block);
color: var(--dsw-alias-label-secondary);
font: var(--dsw-font-markdown-code-block-small);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.business .output {
border: none;
border-radius: 0;
}

View File

@@ -0,0 +1,135 @@
/** `cordis_run` card and the host seat for Package-owned interactive UI. */
import { useEffect } from 'react'
import {
IconCodeOutline16, IconInspectOutline12, StateDot,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { InjectFace, PropsLocale, PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'
import { cordisRunCard } from './card-model.ts'
import { cordisToolViewKey } from './run-card-index.ts'
import type { CordisRunCardFace } from './slots.ts'
import { cordisVisibleStatus, type CordisVisibleStatus } from './status.ts'
import type { CordisKey } from './locales.ts'
import css from './CordisRunRow.module.css'
/** Full Run-card props including its declared Package business-view child slot. */
export type CordisRunRowProps = ToolCallViewProps
& InjectFace<CordisRunCardFace>
& PropsRenderSlots<'tool.view.cordis'>
& PropsLocale<'cordis'>
type RunReading = CordisVisibleStatus | 'awaiting-approval' | 'failed' | 'removed' | 'superseded'
const READING_LABELS = {
idle: 'status.idle',
'awaiting-approval': 'status.awaitingApproval',
failed: 'status.failed',
'client-pending': 'status.clientPending',
running: 'status.running',
removed: 'status.removed',
superseded: 'status.superseded',
} as const satisfies Record<RunReading, CordisKey>
/** Render one activation result and, when eligible, its Package-owned view. */
export function CordisRunRow({
callId, block, inspect, renderSlot, useInventory, useLoaded, useRunCards, useActiveRuns,
onObserveRunCard, t,
}: CordisRunRowProps) {
const card = cordisRunCard(block)
const inventory = useInventory(snapshot => snapshot)
const loaded = useLoaded(snapshot => snapshot)
const latest = useRunCards(snapshot => snapshot)
const activeRuns = useActiveRuns(snapshot => snapshot)
const successful = card.state === 'ok'
&& card.pluginId !== null
&& card.packageId !== null
&& card.pluginRunId !== null
&& card.seq !== null
const key = successful ? cordisToolViewKey(card.pluginId!, card.packageId!) : null
useEffect(() => {
if (!successful || key === null) return
onObserveRunCard({ key, callId, seq: card.seq!, pluginRunId: card.pluginRunId! })
}, [callId, card.pluginRunId, card.seq, key, onObserveRunCard, successful])
const row = card.pluginId === null
? undefined
: inventory.rows.find(candidate => candidate.pluginId === card.pluginId)
const pointer = key === null ? undefined : latest.get(key)
const superseded = pointer !== undefined && pointer.callId !== callId && pointer.seq >= (card.seq ?? -1)
const activity = card.pluginId === null ? undefined : activeRuns.get(card.pluginId)
const attempt = card.pluginRunId !== null && row?.latestRun?.pluginRunId === card.pluginRunId
? row.latestRun
: undefined
const awaitingApproval = attempt?.status === 'awaiting-approval' || (card.packageId !== null
&& activity?.phase === 'awaiting-approval'
&& activity.packageId === card.packageId
&& (card.mode === null || activity.mode === card.mode))
const reading: RunReading = card.pluginId !== null && inventory.removed.has(card.pluginId)
? 'removed'
: superseded
? 'superseded'
: awaitingApproval
? 'awaiting-approval'
: attempt?.status === 'failed'
? 'failed'
: row !== undefined && card.packageId !== null
? cordisVisibleStatus(row, card.packageId, loaded)
: 'idle'
const status = t(READING_LABELS[reading])
const summary = card.errorSummary
?? (card.pluginId === null ? callId : `${card.pluginId}${card.packageId === null ? '' : ` · ${card.packageId}`}`)
const showBusiness = successful && reading === 'running' && key !== null
return (
<div
className={css.card}
data-tool="cordis_run"
data-state={card.state}
data-cordis-plugin-id={card.pluginId ?? undefined}
data-cordis-package-id={card.packageId ?? undefined}
data-cordis-run-id={card.pluginRunId ?? undefined}
data-cordis-status={reading}
>
<div className={css.row}>
<span className={css.icon}>
{card.state === 'error'
? <StateDot state="error" />
: card.state === 'stopped'
? <StateDot state="warning" />
: <IconCodeOutline16 size={14} />}
</span>
<span className={css.title}>{t(card.mode === 'update' ? 'row.updateTitle' : 'row.runTitle')}</span>
<span className={css.separator} aria-hidden />
<span className={card.errorSummary === null ? css.summary : css.error}>{summary}</span>
<span className={css.status}>{status}</span>
{inspect !== undefined && (
<button type="button" className={css.inspect} aria-label="Inspect" onClick={inspect}>
<IconInspectOutline12 />
</button>
)}
</div>
{reading === 'removed' && <div className={css.message}>{t('run.removed')}</div>}
{reading === 'superseded' && <div className={css.message}>{t('run.superseded')}</div>}
{reading === 'failed' && attempt?.error !== undefined && (
<div className={css.message}>{attempt.error.message}</div>
)}
{showBusiness && card.pluginId !== null && card.packageId !== null && card.pluginRunId !== null && (
<div className={css.business} data-cordis-business-view={key ?? undefined}>
{renderSlot('tool.view.cordis', {
pluginId: card.pluginId,
packageId: card.packageId,
pluginRunId: card.pluginRunId,
}, {
entryKey: key ?? undefined,
fallback: card.output === null ? null : <pre className={css.output}>{card.output}</pre>,
})}
</div>
)}
{!showBusiness && reading !== 'removed' && reading !== 'superseded' && card.output !== null && (
<pre className={css.output}>{card.output}</pre>
)}
</div>
)
}

View File

@@ -0,0 +1,126 @@
/** Replay-stable view models for `cordis_define` and `cordis_run` calls. */
import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'
import type {
CordisDynamicPackageId, CordisDynamicPluginId, CordisDynamicPluginRunId, CordisDynamicRunMode,
} from './events.ts'
type Block = ToolCallViewProps['block']
/** Lifecycle of the tool call itself. */
export type CordisToolState = 'running' | 'ok' | 'error' | 'stopped'
/** Frozen `cordis_define` presentation data. */
export interface CordisDefineCard {
readonly pluginId: CordisDynamicPluginId | null
readonly packageId: CordisDynamicPackageId | null
readonly name: string | null
readonly purpose: string | null
readonly hostCode: string | null
readonly clientCode: string | null
readonly output: string | null
readonly errorSummary: string | null
readonly state: CordisToolState
}
/** Frozen `cordis_run` presentation data. */
export interface CordisRunCard {
readonly pluginId: CordisDynamicPluginId | null
readonly packageId: CordisDynamicPackageId | null
readonly pluginRunId: CordisDynamicPluginRunId | null
readonly mode: CordisDynamicRunMode | null
readonly seq: number | null
readonly output: string | null
readonly errorSummary: string | null
readonly state: CordisToolState
}
function firstLine(text: string): string {
const newline = text.indexOf('\n')
return newline === -1 ? text : text.slice(0, newline)
}
function stringAt(source: Record<string, unknown>, key: string): string | null {
const value = source[key]
return typeof value === 'string' && value !== '' ? value : null
}
function objectAt(source: Record<string, unknown>, key: string): Record<string, unknown> | null {
const value = source[key]
return typeof value === 'object' && value !== null ? value as Record<string, unknown> : null
}
function parseArgs(argsRaw: string): Record<string, unknown> | null {
try {
const parsed = JSON.parse(argsRaw) as unknown
return typeof parsed === 'object' && parsed !== null ? parsed as Record<string, unknown> : null
} catch {
// Running calls can expose a truncated JSON prefix.
return null
}
}
function resultText(block: Extract<Block, { kind: 'tool-result' }>): string | null {
const text = block.content
.map(item => item.type === 'text' ? item.text : JSON.stringify(item, null, 2))
.join('\n')
if (text !== '') return text
return block.error === undefined ? null : `${block.error.name}: ${block.error.code}`
}
function stateOf(block: Block): CordisToolState {
if (!('kind' in block)) return 'running'
if (block.error?.code === 'interrupted') return 'stopped'
return block.isError ? 'error' : 'ok'
}
function metaObject(block: Block): Record<string, unknown> | null {
if (!('kind' in block) || block.isError || typeof block.meta !== 'object' || block.meta === null) return null
return block.meta as Record<string, unknown>
}
/** Derive one Define card from its frozen call/result slice. */
export function cordisDefineCard(block: Block): CordisDefineCard {
const settled = 'kind' in block
const argsRaw = (settled ? block.call?.argsRaw : block.argsRaw) ?? ''
const args = parseArgs(argsRaw)
const code = args === null ? null : objectAt(args, 'code')
const state = stateOf(block)
const output = settled ? resultText(block) : null
const meta = metaObject(block)
const rawName = argsRaw === '' ? null : firstLine(argsRaw)
return {
pluginId: meta === null ? null : stringAt(meta, 'pluginId') as CordisDynamicPluginId | null,
packageId: meta === null ? null : stringAt(meta, 'packageId') as CordisDynamicPackageId | null,
name: args === null ? rawName : stringAt(args, 'name') ?? rawName,
purpose: args === null ? null : stringAt(args, 'purpose'),
hostCode: code === null ? null : stringAt(code, 'host'),
clientCode: code === null ? null : stringAt(code, 'client'),
output,
errorSummary: state === 'error' && output !== null ? firstLine(output) : null,
state,
}
}
/** Derive one Run card and its successful activation metadata. */
export function cordisRunCard(block: Block): CordisRunCard {
const settled = 'kind' in block
const argsRaw = (settled ? block.call?.argsRaw : block.argsRaw) ?? ''
const args = parseArgs(argsRaw)
const meta = metaObject(block)
const state = stateOf(block)
const output = settled ? resultText(block) : null
const rawMode = args === null ? null : stringAt(args, 'mode')
const argsPluginId = args === null ? null : stringAt(args, 'pluginId')
const argsPackageId = args === null ? null : stringAt(args, 'packageId')
return {
pluginId: (meta === null ? argsPluginId : stringAt(meta, 'pluginId') ?? argsPluginId) as CordisDynamicPluginId | null,
packageId: (meta === null ? argsPackageId : stringAt(meta, 'packageId') ?? argsPackageId) as CordisDynamicPackageId | null,
pluginRunId: (meta === null ? null : stringAt(meta, 'pluginRunId')) as CordisDynamicPluginRunId | null,
mode: rawMode === 'run' || rawMode === 'update' ? rawMode : null,
seq: settled ? block.seq : null,
output,
errorSummary: state === 'error' && output !== null ? firstLine(output) : null,
state,
}
}

View File

@@ -0,0 +1,24 @@
/** Host operations used directly by the frame-wide Cordis panel. */
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import type {
CordisDynamicPluginId, DynamicCordisInventoryRow,
} from './events.ts'
/** Result of a panel lifecycle gesture. */
export type CordisActionResult =
| { readonly ok: true }
| { readonly ok: false; readonly message: string }
/** RPC seam kept outside the React surface. */
export interface CordisDynamicPort {
/** Stop one Plugin while retaining its immutable Packages. */
stop(sessionId: SessionId, pluginId: CordisDynamicPluginId): Promise<CordisActionResult>
/** Stop and remove one Plugin together with every Package. */
remove(sessionId: SessionId, pluginId: CordisDynamicPluginId): Promise<CordisActionResult>
/** Read the frame-wide Plugin inventory. */
inventory(): Promise<readonly DynamicCordisInventoryRow[]>
}
/** One stable Plugin row as the panel receives it. */
export type CordisInventoryRow = DynamicCordisInventoryRow

View File

@@ -0,0 +1,17 @@
/** Client-safe dynamic Cordis vocabulary re-exported through the Remote assembly. */
// Type-only: merges `ctx.remote` and the forwarded-event key set into this program.
import type {} from '@deepseek-ai/dsh-api-remotes/client'
export type {
ApprovalRequestId,
CordisDynamicPackageId,
CordisDynamicPluginId,
CordisDynamicPluginRunId,
CordisDynamicRunMode,
DynamicCordisInventoryRow,
DynamicCordisPackage,
DynamicCordisRequestResolved,
DynamicCordisRetracted,
DynamicCordisRunRequest,
} from '@deepseek-ai/dsh-api-remotes/client'

View File

@@ -0,0 +1,160 @@
/** Cordis dynamic-plugin cards, inventory panel, business-view host, and `@pluginId` source. */
import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import type {} from '@deepseek-ai/dsh-client-ui-tool/client'
import type {} from '@deepseek-ai/dsh-client-locale/client'
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
import type {} from '@deepseek-ai/dsh-api-remotes/client'
import type { SlashServiceContract, SlashSource } from '@deepseek-ai/dsh-client-ui-slash/client'
import type {} from './events.ts'
import { CordisDefineRow } from './CordisDefineRow.tsx'
import { CordisRunRow } from './CordisRunRow.tsx'
import { CordisPanel } from './CordisPanel.tsx'
import { createCordisInventory } from './inventory.ts'
import { CordisRunCardRegistry } from './run-card-index.ts'
import type { CordisDynamicPort } from './dynamic-port.ts'
import type { CordisCardFace, CordisPanelFace, CordisRunCardFace } from './slots.ts'
import { en, NS, zh } from './locales.ts'
export type { CordisCardFace, CordisPanelFace, CordisRunCardFace, CordisToolViewOwnerProps } from './slots.ts'
export type { CordisActionResult, CordisDynamicPort, CordisInventoryRow } from './dynamic-port.ts'
export type { CordisDefineRowProps } from './CordisDefineRow.tsx'
export type { CordisRunRowProps } from './CordisRunRow.tsx'
export type {
CordisRunCardPointer, CordisRunCardStore, CordisToolViewKey,
} from './run-card-index.ts'
export type {
ApprovalRequestId, CordisDynamicPackageId, CordisDynamicPluginId, CordisDynamicPluginRunId,
DynamicCordisInventoryRow, DynamicCordisPackage, DynamicCordisRetracted,
} from './events.ts'
export type { CordisKey } from './locales.ts'
/** Required services for the two Tool cards, panel, Remote lifecycle, and Slash source. */
export const inject = [
'slots', 'locale', 'slash', 'remote', 'remote.dynamicCordisRunner', 'dynamicCordisRunner',
]
/** Mount every Cordis browser surface over the shared Host inventory. */
export function apply(ctx: ClientContext): void {
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-cordis: dictionaries')
const port: CordisDynamicPort = {
stop: async (sessionId, pluginId) => {
const answered = await ctx.remote.dynamicCordisRunner.stopFromPanel(sessionId, pluginId)
if (!answered.ok) return { ok: false, message: `${answered.error.code}: ${answered.error.message}` }
if (answered.value.ok || answered.value.reason === 'not-running') return { ok: true }
return { ok: false, message: answered.value.message }
},
remove: async (sessionId, pluginId) => {
const answered = await ctx.remote.dynamicCordisRunner.undefineFromPanel(sessionId, pluginId)
if (!answered.ok) return { ok: false, message: `${answered.error.code}: ${answered.error.message}` }
return answered.value.ok ? { ok: true } : { ok: false, message: answered.value.message }
},
inventory: async () => {
const answered = await ctx.remote.dynamicCordisRunner.inventory()
if (!answered.ok) throw new Error(`${answered.error.code}: ${answered.error.message}`)
return answered.value
},
}
const inventory = createCordisInventory(port, (error) => {
console.error('[ui-cordis] reading the Cordis inventory failed:', error)
})
const runner = ctx.dynamicCordisRunner
const loaded = { getSnapshot: () => runner.getSnapshot(), subscribe: (fn: () => void) => runner.subscribe(fn) }
const runCards = new CordisRunCardRegistry()
ctx.effect(() => inventory.subscribe(() => {
const snapshot = inventory.getSnapshot()
if (snapshot.read) runner.reconcileApprovals(snapshot.rows)
}), 'ui-cordis: reconcile pending approvals')
ctx.remote.$on('cordis/dynamic-package', () => { inventory.refresh() })
ctx.remote.$on('cordis/dynamic-retract', () => { inventory.refresh() })
ctx.remote.$on('cordis/request-run', (request) => {
if (!inventory.getSnapshot().rows.some(row => row.pluginId === request.pluginId)) inventory.refresh()
})
ctx.remote.$on('cordis/request-run-resolved', () => { inventory.refresh() })
ctx.on('connection/reset', () => {
inventory.reset()
inventory.refresh()
})
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
name: 'sidebar.footer.action',
id: 'cordis-panel',
locale: NS,
inject: (): CordisPanelFace => ({
hooks: {
inventory,
activeRuns: runner.activeRuns,
runErrors: runner.lastRunError,
loaded,
renderFailures: runner.renderFailures,
},
onApprove: (requestId, approveFutureVersions) => runner.approve(requestId, approveFutureVersions),
onDecline: requestId => runner.decline(requestId),
onRun: request => runner.startUserRun(request),
onStop: async (sessionId, pluginId) => {
const result = await port.stop(sessionId, pluginId)
inventory.refresh()
return result
},
onRemove: async (sessionId, pluginId) => {
const result = await port.remove(sessionId, pluginId)
if (result.ok) inventory.retire(pluginId)
inventory.refresh()
return result
},
onRefresh: () => { inventory.refresh() },
}),
}, CordisPanel))
const cardFace = (): CordisCardFace => ({ hooks: { inventory, loaded } })
ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
name: 'tool.call.toolview',
key: 'cordis_define',
locale: NS,
inject: cardFace,
}, CordisDefineRow))
ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
name: 'tool.call.toolview',
key: 'cordis_run',
locale: NS,
children: { 'tool.view.cordis': { kind: 'keyed', scope: 'session' } },
inject: (sessionId: SessionId): CordisRunCardFace => {
const store = runCards.forSession(sessionId)
return {
hooks: { inventory, loaded, runCards: store, activeRuns: runner.activeRuns },
onObserveRunCard: pointer => { store.observe(pointer) },
}
},
}, CordisRunRow))
const rowsOf = (sessionId: SessionId, query: string) => inventory.getSnapshot().rows
.filter(row => row.agentId === sessionId && String(row.pluginId).includes(query))
const source: SlashSource = {
trigger: '@',
name: 'cordis',
order: 1,
candidates(session, { query }) {
const rows = rowsOf(session.sessionId, query)
return Promise.resolve(rows.map(row => {
const packageId = row.nextPackageId ?? row.currentPackageId ?? row.packages.at(-1)?.packageId
const pkg = packageId === undefined ? undefined : row.packages.find(candidate => candidate.packageId === packageId)
return {
name: String(row.pluginId),
...pkg === undefined ? {} : { description: pkg.purpose },
}
}))
},
warm() { inventory.refresh() },
lexicon(session) { return rowsOf(session.sessionId, '').map(row => String(row.pluginId)) },
subscribeLexicon(_session, listener) { return inventory.subscribe(listener) },
onPick({ candidate }) { return { text: `@${candidate.name} ` } },
}
const slash = ctx.get('slash') as SlashServiceContract
ctx.effect(() => slash.registerSource(source), 'ui-cordis: @pluginId source')
inventory.refresh()
}

View File

@@ -0,0 +1,113 @@
/**
* The host's definition registry as this page last read it, owned by the
* plugin's apply closure.
*
* The panel is a frame-wide surface, so it cannot derive this from any session:
* the registry is global and the read is a single global call. The rows are
* re-read rather than patched, because the wire announcements
* (`cordis/dynamic-package` / `/retract`) carry no labels and a definition
* can appear or disappear between them — a patch-in-place cache would drift into
* showing definitions the host no longer holds.
*
* Reads are single-flight: several announcements settling at once, or a badge
* opening while a reconnect re-reads, must not multiply the call. Single-flight
* alone would be wrong across a reconnect, though — the in-flight read belongs to
* the previous connection, so a reset both discards its answer and frees the slot
* for a fresh one. Without that, a reconnect either loses its re-read to the old
* call or has the old host's rows published on top of it.
*/
import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'
import type { CordisDynamicPort, CordisInventoryRow } from './dynamic-port.ts'
import type { CordisDynamicPluginId } from './events.ts'
/** What the panel reads: the rows, and whether the first read has happened. */
export interface CordisInventorySnapshot {
readonly rows: readonly CordisInventoryRow[]
/** Plugins explicitly removed through this page, retained for historical cards. */
readonly removed: ReadonlySet<CordisDynamicPluginId>
/**
* False until a read settles. The panel shows a loading line rather than an
* empty state, so "nothing defined yet" is never claimed before it is known.
*/
readonly read: boolean
/** Last read failure, so the panel can say why it is empty. */
readonly error?: string | undefined
}
/** Inventory source: an observable of the rows plus the read trigger. */
export interface CordisInventory extends HostObservable<CordisInventorySnapshot> {
/** Read the registry unless a read is already in flight. */
refresh(): void
/** Record an explicit remove and drop the live row immediately. */
retire(pluginId: CordisDynamicPluginId): void
/** Drop what was read; the next refresh starts from nothing (a reconnect may be a new host). */
reset(): void
}
/**
* Create the inventory source.
* @param port - the RPC seam the read goes through.
* @param onError - reporter for a failed read (console in production, captured in specs).
* @returns the inventory observable and its read trigger.
*/
export function createCordisInventory(
port: CordisDynamicPort,
onError: (error: unknown) => void,
): CordisInventory {
const listeners = new Set<() => void>()
let snapshot: CordisInventorySnapshot = { rows: [], removed: new Set(), read: false }
let inFlight: Promise<void> | undefined
// Bumped by reset; a read whose generation is stale publishes nothing.
let generation = 0
const publish = (next: CordisInventorySnapshot): void => {
snapshot = next
for (const listener of [...listeners]) listener()
}
return {
getSnapshot: () => snapshot,
subscribe: (fn) => {
listeners.add(fn)
return () => { listeners.delete(fn) }
},
refresh: () => {
if (inFlight !== undefined) return
const issued = generation
inFlight = port.inventory().then(
(rows) => {
if (issued !== generation) return
const removed = new Set(snapshot.removed)
const live = new Set(rows.map(row => row.pluginId))
for (const previous of snapshot.rows) {
if (!live.has(previous.pluginId)) removed.add(previous.pluginId)
}
publish({ rows, removed, read: true })
},
(error: unknown) => {
if (issued !== generation) return
onError(error)
// A failed read keeps whatever was shown and says why: dropping the
// rows would turn a transient wire failure into "nothing is defined".
publish({
rows: snapshot.rows,
removed: snapshot.removed,
read: snapshot.read,
error: error instanceof Error ? error.message : 'reading the cordis inventory failed',
})
},
).then(() => { if (issued === generation) inFlight = undefined })
},
retire: (pluginId) => {
const removed = new Set(snapshot.removed)
removed.add(pluginId)
publish({ ...snapshot, rows: snapshot.rows.filter(row => row.pluginId !== pluginId), removed })
},
reset: () => {
generation += 1
inFlight = undefined
publish({ rows: [], removed: snapshot.removed, read: false })
},
}
}

View File

@@ -0,0 +1,112 @@
/** Cordis dynamic-plugin UI dictionaries. */
export const NS = 'cordis'
export const zh = {
'row.defineTitle': '注册 Cordis 插件',
'row.runTitle': '运行 Cordis 插件',
'row.updateTitle': '更新 Cordis 插件',
'purpose.missing': '(未填写用途)',
'status.idle': '待激活',
'status.awaitingApproval': '待审批',
'status.failed': '运行失败',
'status.clientPending': 'Client 待激活',
'status.running': '运行中',
'status.removed': '已移除',
'status.superseded': '已有更新',
'run.removed': '包已不存在',
'run.superseded': '已有更新的运行卡片,请查看下方',
'panel.hint': '运行控制在左下角设置上方的 Cordis 面板',
'panel.plugins.aria': 'Cordis 插件',
'panel.approvals.aria': 'Cordis 审批',
'panel.trigger': 'Cordis Plugin',
'panel.runningCount': '{count} running',
'panel.title': 'Cordis 插件',
'panel.empty': '还没有定义任何插件',
'panel.loading': '读取中…',
'panel.readFailed': '读取插件清单失败:{message}',
'panel.group.current': '当前会话',
'panel.group.others': '其他会话',
'panel.version': '版本',
'panel.current': '当前:{packageId}',
'panel.next': '待切换:{packageId}',
'action.approve': '允许',
'action.approveOnce': '仅允许此版本',
'action.approvePlugin': '允许此插件的后续版本',
'action.decline': '拒绝',
'action.run': '运行',
'action.stop': '停止',
'action.remove': '移除',
'action.retry': '重试',
'action.rollback': '回退',
'render.failedAbdicated': '{slot} 渲染失败,已恢复默认界面:',
'render.failedHeld': '{slot} 渲染失败:',
'a11y.defining': '正在定义插件',
'a11y.failed': '定义失败',
'a11y.stopped': '定义已中断',
'body.source': '插件代码',
'body.hostCode': 'Host',
'body.clientCode': 'Client',
'body.output': '结果',
'body.copy': '复制',
'body.copied': '已复制',
} satisfies Record<string, string>
export type CordisKey = keyof typeof zh
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface LocaleNamespaceMap {
/** Dynamic Cordis UI copy. */
cordis: CordisKey
}
}
export const en = {
'row.defineTitle': 'Define Cordis Plugin',
'row.runTitle': 'Run Cordis Plugin',
'row.updateTitle': 'Update Cordis Plugin',
'purpose.missing': '(no purpose given)',
'status.idle': 'Ready',
'status.awaitingApproval': 'Awaiting approval',
'status.failed': 'Run failed',
'status.clientPending': 'Client ready to activate',
'status.running': 'Running',
'status.removed': 'Removed',
'status.superseded': 'Newer run available',
'run.removed': 'This package no longer exists',
'run.superseded': 'A newer run card is available below',
'panel.hint': 'Run controls live in the Cordis panel above Settings',
'panel.plugins.aria': 'Cordis plugins',
'panel.approvals.aria': 'Cordis approvals',
'panel.trigger': 'Cordis Plugin',
'panel.runningCount': '{count} running',
'panel.title': 'Cordis plugins',
'panel.empty': 'No plugins defined yet',
'panel.loading': 'Reading…',
'panel.readFailed': 'Reading the plugin inventory failed: {message}',
'panel.group.current': 'This session',
'panel.group.others': 'Other sessions',
'panel.version': 'Version',
'panel.current': 'Current: {packageId}',
'panel.next': 'Next: {packageId}',
'action.approve': 'Allow',
'action.approveOnce': 'Allow this version only',
'action.approvePlugin': 'Allow future versions of this plugin',
'action.decline': 'Decline',
'action.run': 'Run',
'action.stop': 'Stop',
'action.remove': 'Remove',
'action.retry': 'Retry',
'action.rollback': 'Roll back',
'render.failedAbdicated': 'Rendering failed in {slot}; the default UI was restored:',
'render.failedHeld': 'Rendering failed in {slot}:',
'a11y.defining': 'Defining the plugin',
'a11y.failed': 'Definition failed',
'a11y.stopped': 'Definition interrupted',
'body.source': 'Plugin source',
'body.hostCode': 'Host',
'body.clientCode': 'Client',
'body.output': 'Result',
'body.copy': 'Copy',
'body.copied': 'Copied',
} satisfies Record<CordisKey, string>

View File

@@ -0,0 +1,67 @@
/** Session-local ownership index for Package business views on `cordis_run` cards. */
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'
import type {
CordisDynamicPackageId, CordisDynamicPluginId, CordisDynamicPluginRunId,
} from './events.ts'
/** Stable keyed-slot identity of one Package-owned business view. */
export type CordisToolViewKey = `${CordisDynamicPluginId}.${CordisDynamicPackageId}`
/** One successful tool result competing to host a Package business view. */
export interface CordisRunCardPointer {
readonly key: CordisToolViewKey
readonly callId: string
readonly seq: number
readonly pluginRunId: CordisDynamicPluginRunId
}
/** Per-session observable index consumed by every mounted Run card. */
export interface CordisRunCardStore extends HostObservable<ReadonlyMap<CordisToolViewKey, CordisRunCardPointer>> {
/** Publish one successful Run result; only a greater log sequence can replace it. */
observe(pointer: CordisRunCardPointer): void
}
function createStore(): CordisRunCardStore {
const pointers = new Map<CordisToolViewKey, CordisRunCardPointer>()
const listeners = new Set<() => void>()
let cache: ReadonlyMap<CordisToolViewKey, CordisRunCardPointer> | undefined
return {
getSnapshot: () => cache ??= new Map(pointers),
subscribe: (listener) => {
listeners.add(listener)
return () => { listeners.delete(listener) }
},
observe: (pointer) => {
const current = pointers.get(pointer.key)
if (current !== undefined && current.seq >= pointer.seq) return
pointers.set(pointer.key, pointer)
cache = undefined
for (const listener of [...listeners]) listener()
},
}
}
/** Page-lifetime registry that gives all cards of one session the same Store. */
export class CordisRunCardRegistry {
private readonly sessions = new Map<SessionId, CordisRunCardStore>()
/** Return the persistent page-local Store for a session. */
forSession(sessionId: SessionId): CordisRunCardStore {
let store = this.sessions.get(sessionId)
if (store === undefined) {
store = createStore()
this.sessions.set(sessionId, store)
}
return store
}
}
/** Build the Package business-view key shared by registrations and Run cards. */
export function cordisToolViewKey(
pluginId: CordisDynamicPluginId,
packageId: CordisDynamicPackageId,
): CordisToolViewKey {
return `${pluginId}.${packageId}` as CordisToolViewKey
}

View File

@@ -0,0 +1,67 @@
/** Injected faces and the Package-owned `tool.view.cordis` slot declaration. */
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'
import type {
CordisRunActivity, CordisRunFailure, CordisUserRunRequest, DynamicCordisLivePackage,
DynamicCordisRenderFailure,
} from '@deepseek-ai/dsh-cordis-client-runner/client'
import type { CordisActionResult } from './dynamic-port.ts'
import type { CordisInventory } from './inventory.ts'
import type { CordisRunCardPointer, CordisRunCardStore } from './run-card-index.ts'
import type {
ApprovalRequestId, CordisDynamicPackageId, CordisDynamicPluginId, CordisDynamicPluginRunId,
} from './events.ts'
/** Owner currency delivered to a dynamic Package's business view. */
export interface CordisToolViewOwnerProps {
readonly pluginId: CordisDynamicPluginId
readonly packageId: CordisDynamicPackageId
readonly pluginRunId: CordisDynamicPluginRunId
}
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface SlotMap {
/** Package-owned interactive UI hosted by the latest successful Run card. */
'tool.view.cordis': {
kind: 'keyed'
scope: 'session'
owner: CordisToolViewOwnerProps
}
}
}
/** Live facts used by the read-only Define card. */
export interface CordisCardFace {
hooks: {
inventory: CordisInventory
loaded: HostObservable<readonly DynamicCordisLivePackage[]>
}
}
/** Live facts used by the Run card and its business-view ownership index. */
export interface CordisRunCardFace extends CordisCardFace {
hooks: CordisCardFace['hooks'] & {
runCards: CordisRunCardStore
activeRuns: HostObservable<ReadonlyMap<CordisDynamicPluginId, CordisRunActivity>>
}
/** Publish this successful result into the session's latest-card index. */
onObserveRunCard(pointer: CordisRunCardPointer): void
}
/** Frame-wide panel state and lifecycle verbs. */
export interface CordisPanelFace {
hooks: {
inventory: CordisInventory
activeRuns: HostObservable<ReadonlyMap<CordisDynamicPluginId, CordisRunActivity>>
runErrors: HostObservable<ReadonlyMap<CordisDynamicPluginId, CordisRunFailure>>
renderFailures: HostObservable<ReadonlyMap<CordisDynamicPluginId, DynamicCordisRenderFailure>>
loaded: HostObservable<readonly DynamicCordisLivePackage[]>
}
onApprove(requestId: ApprovalRequestId, approveFutureVersions: boolean): Promise<void>
onDecline(requestId: ApprovalRequestId): Promise<void>
onRun(request: CordisUserRunRequest): Promise<void>
onStop(sessionId: SessionId, pluginId: CordisDynamicPluginId): Promise<CordisActionResult>
onRemove(sessionId: SessionId, pluginId: CordisDynamicPluginId): Promise<CordisActionResult>
onRefresh(): void
}

View File

@@ -0,0 +1,37 @@
/** Shared status derivation over Host inventory and this page's Client live set. */
import type { DynamicCordisLivePackage } from '@deepseek-ai/dsh-cordis-client-runner/client'
import type {
CordisDynamicPackageId, DynamicCordisInventoryRow,
} from './events.ts'
/** The three product-visible lifecycle readings. */
export type CordisVisibleStatus = 'idle' | 'client-pending' | 'running'
/** Locate one immutable Package inside a Plugin row. */
export function packageOf(row: DynamicCordisInventoryRow, packageId: CordisDynamicPackageId) {
return row.packages.find(pkg => pkg.packageId === packageId)
}
/**
* Derive the visible state of one Package.
* @param row - owning Plugin inventory row.
* @param packageId - Package being described.
* @param loaded - Client activations loaded in this page.
* @returns idle, Host-running/Client-pending, or fully running.
*/
export function cordisVisibleStatus(
row: DynamicCordisInventoryRow,
packageId: CordisDynamicPackageId,
loaded: readonly DynamicCordisLivePackage[],
): CordisVisibleStatus {
const run = row.activeRun
if (run === undefined || run.packageId !== packageId) return 'idle'
const pkg = packageOf(row, packageId)
if (pkg?.hasClientHalf !== true) return 'running'
return loaded.some(live => live.pluginId === row.pluginId
&& live.packageId === packageId
&& live.pluginRunId === run.pluginRunId)
? 'running'
: 'client-pending'
}

View File

@@ -0,0 +1,6 @@
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
declare module '*.css'

View File

@@ -0,0 +1,9 @@
/**
* Cordis dynamic-plugin card, node half. Pure UI plugin: the empty apply
* exists so the plugin appears in the host cordis.yml / Loader; the browser
* half ships via exports["./client"], discovered through the package.json
* dshClient declaration.
*/
/** Host plugin body — no host-side behavior for this surface plugin. */
export function apply(): void {}

View File

@@ -0,0 +1,33 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-cordis`.
* @module @deepseek-ai/dsh-client-ui-cordis/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-cordis'
/** Cordis companion plugin name. */
export const name = 'client-ui-cordis-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: a single keyed toolview registration whose disposal is
* proven by the HMR-safety spec. The one mutable relation this package owns —
* the per-definition run-state observable — lives in the browser process, out
* of reach of the host invariant service, and the node half emits no cordis
* events and holds no cross-plugin state.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */