/** * Dual-stack shims for the dsh 0.1.5-rc and 0.1.7-rc lines (including the * 0.1.5-alpha handle API that landed with the former). * * 0.1.5 resolves settings through `SettingsProvider.get`/`installSection` and * exposes session persistence as snapshot `list` plus per-session `open` * handles; live tokens arrive on the process-local `agent/assistant-stream`. * 0.1.7 replaced the settings service with schema-projected forms. Every shim * here picks the API that is actually present so one build runs on either * host line. */ import type { Agent } from '@deepseek-ai/dsh-agent'; import type { Context } from '@deepseek-ai/cordis'; import type { ContextFormed } from '@deepseek-ai/dsh-llm'; import type { SessionId, SessionEvent } from '@deepseek-ai/dsh-session'; import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'; import type z from '@deepseek-ai/schemastery'; /** * The source kind this plugin declares for the messages it commits itself: the * plan-close nudge and the approval-denied steering notice. * * These used to ride the released catch-all wrapper — * `` — which 0.1.5 shipped as a member and * 0.1.7 deleted ("each producer declares its own `kind` in its own module; there * is no shared catch-all `plugin` kind"). Worse than missing on 0.1.7: its V4 * native admission *refuses* the wrapper outright, so an append under that * spelling throws "format v4 message requires a producer-owned source kind" and * takes the whole turn down with it — which is what the plan nudge and the * approval-denied notice did to any turn they fired in on a V4 session. * * The producer-owned spelling is the one shape both supported lines admit: the * wrapper was only ever mandatory for `system/message` (`SystemMessage['source']` * is the `plugin` member on 0.1.5), while user-role messages have always carried * a producer's own kind — the 0.1.5 line's own `goal`, `webhook`, and * `agent-instructions` producers all commit `kind: ''`. */ export declare const TUI_SOURCE_KIND = "dsh-ssh-tui"; declare module '@deepseek-ai/dsh-llm' { interface MessageSourceMap { 'dsh-ssh-tui': { kind: 'dsh-ssh-tui'; } & ContextFormed; } } /** * The kind the V3-to-V4 lane gives the same messages in a converted log. * * The conversion lifts a released plugin wrapper to the producer's kind when it * knows the plugin, and falls back to `plugin:` when it does not — which is * this plugin's case, so a resumed V3 session carries the prefixed spelling. */ export declare const TUI_CONVERTED_SOURCE_KIND = "plugin:dsh-ssh-tui"; /** * Whether a durable message source came from this plugin. * * Three spellings reach a reader: the producer-owned kind written now, the * `plugin:`-prefixed compatibility kind the V3-to-V4 conversion assigns, and the * original wrapper (`kind: 'plugin'` + `plugin: 'dsh-ssh-tui'`) that a log * committed before this change — or an unconverted V3 file read on the 0.1.5 * line — still carries. * * @param kind - the source's `kind`, when it has one. * @param plugin - the source's legacy `plugin` field, when it carries the wrapper. * @returns whether this plugin produced the message. */ export declare function isTuiMessageSource(kind: unknown, plugin?: unknown): boolean; /** * Settings namespaces are branded strings at the type level on both supported * lines; this cast supplies the brand from a plain literal. */ export declare function settingsNamespace(value: string): SettingsNamespace; /** * Hooks a settings consumer hands to {@link installSettingsSection}. * * Spelled out locally instead of imported: 0.1.7 deleted the * `SettingsSectionHooks` export, and this shape is the whole contract the three * call sites use. */ export interface SettingsSectionHooks { /** * Receive the active configuration source: the resolved settings value while * a section is attached. Called at attach and again after every change. * @param current - thunk returning the currently authoritative value. */ setSource(current: () => T): void; /** * Re-judge anything derived from the source — registration-level facts, * memoized resolutions — after an attach or a committed change. */ onChange(): void; /** Reject a resolved section this consumer could not act on. */ validate?(value: T): void; } /** Which settings protocol the running host speaks. */ export type SettingsGeneration = 'legacy' | 'forms'; /** * The host's settings generation, as feature detection rather than a version. * * `legacy` (0.1.5) resolves a namespace through `settings.get`; `forms` (0.1.7) * has no `get` and projects a form per loader entry. Callers that differ by * generation — which profile rows a terminal profile must mount, for one — read * it here instead of sniffing package versions. * * Before the service exists (a plugin applies before it is mounted) the same * split shows up as the roster package's absence, and the answer is memoized as * soon as the service is seen so a later call cannot disagree with an earlier * one. */ export declare function hostSettingsGeneration(ctx: Context): SettingsGeneration; /** Forget the memoized descriptors so the next read re-walks the forms. */ export declare function invalidateSettingsCache(ctx: Context): void; /** * Read one settings section. * * 0.1.5 resolves it through `get(ns)`. 0.1.7 has no `get`, so the value comes * from the entry's descriptor, which projects the live config — volatile fields * only, i.e. exactly the fields its form exposes — and returns `undefined` when * no entry carries that id. */ export declare function readSettingsSection(ctx: Context, ns: SettingsNamespace): unknown; /** * The user's own settings document, keyed by namespace, on either line. * * 0.1.5 publishes it as `settings.document`. 0.1.7 dropped the property — the * document is the profile patch now — but each descriptor still carries the * user layer it was built from, so the same view is reconstructible. Callers * that ask "did the user configure this?" must use this, not * {@link readSettingsSection}: a resolved read also carries the composition * base and schema defaults, which is exactly what such a caller must not * mistake for a user choice. */ export declare function settingsDocument(ctx: Context): Record | undefined; /** * Mark one schema field as form-writable on the 0.1.7 line. * * 0.1.7 projects only fields whose schema node carries the `volatile` meta, and * refuses a settings write to any other path. The 0.1.5 schemastery (3.18.2) * has no such builder, so the call is feature-detected rather than typed: on * that line the marker means nothing and the schema is returned untouched. */ export declare function liveField(schema: z): z; /** * Register a settings section. * * 0.1.5 publishes the `settings` service with `installSection`, callable only * once that service is injected (plugins apply before it, so `ctx.inject` must * defer — same pattern the harness's own packages use). * * 0.1.7 removed it. The section is now the loader entry's own `Config` * schema — this plugin's is `ssh-tui`, and the two auxiliary namespaces are * carried by the `dsh-ssh-tui/settings-*` rows in `cordis.patch.yml` — so the * only thing left for a consumer to wire is the live read (`setSource`) and the * change notification (`onChange`). */ export declare function installSettingsSection(ctx: Context, ns: SettingsNamespace, schema: z, entry: T, hooks: SettingsSectionHooks): void; /** * Whether a tool result reports failure. * * 0.1.5 carries `isError` on the `tool-result` content block; 0.1.7 removed * that block from `ContentBlockMap` and moved the flag onto the message * itself. Both are read, so one build understands either host. */ export declare function toolResultFailed(message: unknown): boolean; /** * Read the full durable event log. Both supported lines read it on demand * through `snapshotEvents()`. */ export declare function sessionEvents(session: object): readonly SessionEvent[]; /** * Walk the durable log without copying it. Prefer `eventAt` so resume does * not materialize a second array of every chunk. */ export declare function forEachSessionEvent(session: object, visit: (event: SessionEvent) => void): void; /** Events folded between event-loop turns while a long log is replayed. */ export declare const REPLAY_YIELD_EVERY = 200; /** * {@link forEachSessionEvent} for the resume replay. A synchronous walk of a * large log blocks the loop for its whole duration, which freezes the TUI on * the pre-replay frame: the relay's RTT frame and the render timer cannot run, * so the footer keeps painting `SSH ○○○○` and the transcript never fills in * until the walk ends. Yielding every few hundred events keeps both flowing. * * @param halt - stop early when the TUI was disposed mid-replay. */ export declare function forEachSessionEventAsync(session: object, visit: (event: SessionEvent) => void, yieldEvery?: number, halt?: () => boolean): Promise; /** Header fields the picker and resume path actually read. */ export interface SessionHeaderLike { id: string; createdAt: number; cwd?: string; origin?: string; delegationDepth?: number; } /** Logical log plus the header it belongs to. */ export interface SessionInspectionLike { events: readonly unknown[]; header?: SessionHeaderLike; /** * Backend state for the slice. `detached` means the backend never * materialized a physical artifact (the writer is in this process and has * not flushed yet), so `events: []` says nothing about whether the session * is blank. Callers must not delete artifacts on a `detached` read. */ eventState?: string; } /** * Both supported lines return `{ header, revision, … }` snapshots from * `list()`; normalize to the header so the picker does not care which host * it is talking to. */ export declare function listPersistenceHeaders(persistence: object): Promise; /** * Read one session through `open(id, 'read')` + `handle.read()`, the access * both supported lines expose. Close the handle so a listing pass does not pin * write ownership. */ export declare function inspectPersistenceSession(persistence: object, id: unknown): Promise; /** * A session's artifact path from its header. Both supported lines still * implement `locate()` on the JSONL backend, but their typings keep it * private, so the call stays feature-detected. */ export declare function persistenceLocate(persistence: object, meta: object): { path?: string; } | undefined; /** Whether a command's input admits the composer's attachments. */ export declare function commandAcceptsAttachments(input: unknown): boolean; /** One chunk from a live `agent/assistant-stream` frame. */ export interface StreamChunkLike { type: string; text?: string; /** `tool-call-delta` argument fragment. */ argumentsDelta?: string; /** `tool-call-delta` tool name, present on the first fragment. */ name?: string; usage?: { inputTokens?: number; outputTokens?: number; }; } /** * Whether one chunk carries the model's first output token, matching the host's * own `isTokenDelta`: a non-empty text/reasoning fragment, or a tool-call * fragment (name-bearing deltas included). Thinking-first models therefore * start the latency clock on their first reasoning token, not on the first * visible answer token. */ export declare function isTokenDeltaChunk(chunk: unknown): boolean; /** * Turn/step of a live `agent/assistant-stream` `start` frame. The chunk frames * this opens carry no turn/step at all, so callers must remember them or every * live chunk is attributed to step 0 (which silently disabled TTFT, decode * speed, and usage de-duplication). */ export declare function streamFrameOwner(frame: unknown): { attemptId: unknown; turn: number; step: number; } | undefined; /** Attempt id of a live chunk/end frame, used to match it to its `start`. */ export declare function streamFrameAttemptId(frame: unknown): unknown; /** * First token time inside a durable compact assistant stream * (`assistant/message.stream`). Mirrors the host's * `assistantStreamFirstTokenTime`: packed delta runs place member `i` at * `time0 + dt[0..i-1]`, raw chunk records carry their own time. */ export declare function streamFirstTokenTime(stream: unknown): number | undefined; /** * A live `agent/assistant-stream` chunk frame's inner chunk plus its framing. * * `fallback` supplies the turn/step, which chunk frames do not carry (see * {@link streamFrameOwner}). */ export declare function streamChunkOf(frame: unknown, fallback?: { turn: number; step: number; }): { chunk: StreamChunkLike; turn: number; step: number; time: number; /** * False when neither the frame nor a fallback carried a real turn/step. * Usage folded under such a chunk would be filed under a bogus key (0:0) * that `step/end` never clears, inflating the session totals forever. */ stepKnown: boolean; } | undefined; /** * Subscribe to a host event whose scoped payload type does not match this * build's `Events` map. Both supported lines emit `agent/assistant-stream`, * and Cordis accepts the plain event name at runtime. */ export declare function listenHostEvent(ctx: { on: (event: never, handler: never) => unknown; }, event: string, handler: (...args: unknown[]) => unknown): () => void; /** * Wind down the selected direct children of one parent, under both names the * subagent service has shipped. * * 0.2.1-rc.2 renamed `drainContinuableChildren(parent, childIds)` to * `drainChildren(parent, childIds)` — same contract (release the selected * resident direct children of one exact live parent, leaving its other children * admitted). Both 0.2.0 rcs and 0.2.1-rc.2 are declared, so the call site asks * for whichever spelling the host has instead of pinning one and failing to * compile on the other. A composition without the service at all resolves: the * caller has already handled "no service", and this keeps the helper total. * * @param subagents - the host's subagent service. * @param parent - the exact live parent whose children are being released. * @param childIds - durable direct-child ids to release when resident. * @returns once every selected activation released its execution handle. */ export declare function drainChildSubagents(subagents: Context['subagents'], parent: Agent, childIds: readonly SessionId[]): Promise;