dsh-plugin-dev-kb 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.en.md +14 -8
- package/README.md +14 -8
- package/docs/banner.svg +25 -0
- package/kb/INDEX.md +6 -2
- package/kb/README.md +4 -4
- package/kb/extra/AGENTS.md +2 -2
- package/kb/extra/cookbook/adding-a-session-format-version.md +109 -0
- package/kb/extra/cookbook/adding-a-session-format-version.zh.md +109 -0
- package/kb/extra/cookbook/adding-a-vendored-package.md +1 -1
- package/kb/extra/cookbook/adding-a-vendored-package.zh.md +1 -1
- package/kb/extra/cookbook/responding-to-pr-review-on-a-stack.md +1 -1
- package/kb/extra/cookbook/responding-to-pr-review-on-a-stack.zh.md +1 -1
- package/kb/extra/deepseek-llm-api-wire-extensions.md +2 -2
- package/kb/extra/deepseek-llm-api-wire-extensions.zh.md +2 -2
- package/kb/extra/development.md +13 -3
- package/kb/extra/development.zh.md +13 -3
- package/kb/extra/event-producer-consumer.md +33 -31
- package/kb/extra/event-producer-consumer.zh.md +34 -32
- package/kb/extra/i18n/README.md +2 -1
- package/kb/extra/i18n/README.zh.md +3 -2
- package/kb/extra/i18n/translation-prompt.md +1 -1
- package/kb/extra/module-graph.md +62 -24
- package/kb/extra/module-graph.zh.md +62 -24
- package/kb/extra/rescope.md +1 -1
- package/kb/extra/rescope.zh.md +1 -1
- package/kb/extra/session-format-status.md +47 -0
- package/kb/extra/session-format-status.zh.md +47 -0
- package/kb/extra/subsystems/agent-team.md +1 -1
- package/kb/extra/subsystems/agent-team.zh.md +1 -1
- package/kb/extra/subsystems/feedback.md +164 -30
- package/kb/extra/subsystems/feedback.zh.md +164 -30
- package/kb/extra/testing.md +4 -3
- package/kb/extra/testing.zh.md +4 -3
- package/kb/extra/web-styling.md +1 -0
- package/kb/extra/web-styling.zh.md +1 -0
- package/kb/meta/search-index.json +151 -103
- package/kb/meta/site-pages.txt +188 -184
- package/kb/meta/source.json +5 -5
- package/kb/meta/topics.md +2 -2
- package/kb/site/develop/basic/publish.md +1 -1
- package/kb/site/en/develop/basic/publish.md +1 -1
- package/kb/site/en/guide/providers.md +1 -1
- package/kb/site/en/guide/python-sdk.md +19 -3
- package/kb/site/en/reference/agent-lifecycle.md +12 -5
- package/kb/site/en/reference/capability-seams.md +10 -3
- package/kb/site/en/reference/config-catalog.md +173 -42
- package/kb/site/en/reference/cookbook/adding-a-tool.md +1 -1
- package/kb/site/en/reference/cookbook/extension-cookbook.md +1 -1
- package/kb/site/en/reference/index.md +18 -8
- package/kb/site/en/reference/persistence-catalog.md +121 -48
- package/kb/site/en/reference/subsystems/approval.md +1 -1
- package/kb/site/en/reference/subsystems/client-modules.md +9 -0
- package/kb/site/en/reference/subsystems/client-resources.md +94 -0
- package/kb/site/en/reference/subsystems/compaction.md +1 -1
- package/kb/site/en/reference/subsystems/core.md +79 -16
- package/kb/site/en/reference/subsystems/filesystem.md +15 -0
- package/kb/site/en/reference/subsystems/goal.md +37 -1
- package/kb/site/en/reference/subsystems/index.md +2 -0
- package/kb/site/en/reference/subsystems/invariants.md +1 -1
- package/kb/site/en/reference/subsystems/llm-streaming.md +20 -10
- package/kb/site/en/reference/subsystems/lsp.md +1 -1
- package/kb/site/en/reference/subsystems/persistence.md +36 -15
- package/kb/site/en/reference/subsystems/schedule.md +1 -1
- package/kb/site/en/reference/subsystems/scope.md +1 -1
- package/kb/site/en/reference/subsystems/session-reference.md +4 -0
- package/kb/site/en/reference/subsystems/session-telemetry.md +22 -6
- package/kb/site/en/reference/subsystems/session.md +64 -42
- package/kb/site/en/reference/subsystems/shell.md +9 -6
- package/kb/site/en/reference/subsystems/sidebar-right.md +151 -0
- package/kb/site/en/reference/subsystems/slots.md +41 -33
- package/kb/site/en/reference/subsystems/spill.md +13 -6
- package/kb/site/en/reference/subsystems/subagent.md +20 -13
- package/kb/site/en/reference/subsystems/subprocess.md +25 -26
- package/kb/site/en/reference/subsystems/system-prompt.md +3 -1
- package/kb/site/en/reference/subsystems/token-meter.md +2 -1
- package/kb/site/en/reference/subsystems/tools.md +5 -5
- package/kb/site/en/reference/subsystems/workspace.md +80 -1
- package/kb/site/en/reference/tool-catalog.md +46 -2
- package/kb/site/en/reference/tool-execution-pipeline.md +2 -2
- package/kb/site/guide/providers.md +1 -1
- package/kb/site/guide/python-sdk.md +19 -3
- package/kb/site/reference/agent-lifecycle.md +12 -5
- package/kb/site/reference/capability-seams.md +10 -3
- package/kb/site/reference/config-catalog.md +173 -42
- package/kb/site/reference/cookbook/adding-a-tool.md +1 -1
- package/kb/site/reference/cookbook/extension-cookbook.md +1 -1
- package/kb/site/reference/index.md +18 -8
- package/kb/site/reference/persistence-catalog.md +127 -54
- package/kb/site/reference/subsystems/approval.md +1 -1
- package/kb/site/reference/subsystems/client-modules.md +9 -0
- package/kb/site/reference/subsystems/client-resources.md +94 -0
- package/kb/site/reference/subsystems/compaction.md +1 -1
- package/kb/site/reference/subsystems/core.md +79 -16
- package/kb/site/reference/subsystems/filesystem.md +15 -0
- package/kb/site/reference/subsystems/goal.md +37 -1
- package/kb/site/reference/subsystems/index.md +14 -12
- package/kb/site/reference/subsystems/invariants.md +1 -1
- package/kb/site/reference/subsystems/llm-streaming.md +20 -10
- package/kb/site/reference/subsystems/lsp.md +1 -1
- package/kb/site/reference/subsystems/persistence.md +36 -15
- package/kb/site/reference/subsystems/schedule.md +1 -1
- package/kb/site/reference/subsystems/scope.md +1 -1
- package/kb/site/reference/subsystems/session-reference.md +4 -0
- package/kb/site/reference/subsystems/session-telemetry.md +22 -6
- package/kb/site/reference/subsystems/session.md +64 -42
- package/kb/site/reference/subsystems/shell.md +9 -6
- package/kb/site/reference/subsystems/sidebar-right.md +151 -0
- package/kb/site/reference/subsystems/slots.md +41 -33
- package/kb/site/reference/subsystems/spill.md +13 -6
- package/kb/site/reference/subsystems/subagent.md +20 -13
- package/kb/site/reference/subsystems/subprocess.md +25 -26
- package/kb/site/reference/subsystems/system-prompt.md +3 -1
- package/kb/site/reference/subsystems/token-meter.md +2 -1
- package/kb/site/reference/subsystems/tools.md +5 -5
- package/kb/site/reference/subsystems/workspace.md +80 -1
- package/kb/site/reference/tool-catalog.md +46 -2
- package/kb/site/reference/tool-execution-pipeline.md +2 -2
- package/lib/index.js +25 -44
- package/package.json +3 -2
- package/skills/dsh-plugin-dev-kb.md +3 -3
|
@@ -58,22 +58,38 @@ interface SessionTelemetryRecord {
|
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
Every canonical [session event](./session.md), including each `assistant/message` or `assistant/attempt` with its complete compact stream and every plugin-merged type the seam never heard of, passes through whole as one ordered ledger record. Process-local `agent/assistant-stream` frames do not enter this durable feed. A new Session object
|
|
61
|
+
Every canonical [session event](./session.md), including each `assistant/message` or `assistant/attempt` with its complete compact stream and every plugin-merged type the seam never heard of, passes through whole as one ordered ledger record. Process-local `agent/assistant-stream` frames do not enter this durable feed. A new Session object starts at its lifecycle boundary unless the backend selects `includeHistory`; re-adopting the same object resumes after its handoff cursor. Delivery is best-effort: the cursor marks handed-off, not delivered, and records can be lost (crash, reload window) or duplicated (new-object replay, SDK retries), so receivers dedupe ledger records on `(session.id, session.format_version, event.seq)`; ops records deliberately omit that identity — they are signals to alert on, not entries to sum, and tolerate duplicates instead.
|
|
62
62
|
|
|
63
63
|
## The sharing disclosure
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Every backend exposes its deployment-selected mode through the required abstract `sharing` member on `ctx.sessionTelemetry` ([Service Definition README](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/session/session-telemetry/README.md#the-sharing-disclosure)). This is neither a per-Session admission decision nor a delivery receipt. The `/feedback` acknowledgement does not consult it.
|
|
66
66
|
|
|
67
67
|
```ts type-equiv
|
|
68
68
|
/**
|
|
69
|
-
* Deployment-selected session-sharing
|
|
70
|
-
* {@link SessionTelemetryBackend} backend to human-facing acknowledgement surfaces (the
|
|
71
|
-
* `/feedback` command's confirmation text). The Service Definition owns the
|
|
72
|
-
* vocabulary so consumers and backends do not depend on a specific provider.
|
|
69
|
+
* Deployment-selected session-sharing mode, not confirmation of SDK delivery.
|
|
73
70
|
*/
|
|
74
71
|
type SessionTelemetrySharingStatus = 'full' | 'feedback-only' | 'disabled'
|
|
75
72
|
```
|
|
76
73
|
|
|
74
|
+
## Capture policy
|
|
75
|
+
|
|
76
|
+
```ts type-equiv
|
|
77
|
+
/** Whether capture follows live events or reads the canonical log only when requested. */
|
|
78
|
+
type SessionTelemetryCapture = 'live' | 'on-demand'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```ts type-equiv
|
|
82
|
+
/** Backend-selected capture mode and history policy. */
|
|
83
|
+
interface SessionTelemetryCaptureOptions {
|
|
84
|
+
/** Follow live events, or wait for explicit capture; defaults to live. */
|
|
85
|
+
capture?: SessionTelemetryCapture
|
|
86
|
+
/** Include stored history before this lifecycle; defaults to false. */
|
|
87
|
+
includeHistory?: boolean
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`includeHistory` permits stored and inherited records but does not itself authorize capture. The [OTel backend](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/session/session-telemetry-otel/README.md) uses on-demand capture and requires new own explicit feedback; it releases only the complete prefix through that feedback, for every provider.
|
|
92
|
+
|
|
77
93
|
## The backend contract
|
|
78
94
|
|
|
79
95
|
```ts type-equiv
|
|
@@ -56,6 +56,19 @@ interface SessionEventMap {
|
|
|
56
56
|
* project their `content` verbatim; `source` tells them apart.
|
|
57
57
|
*/
|
|
58
58
|
'user/message': UserMessage
|
|
59
|
+
/**
|
|
60
|
+
* The rendered system prompt on the model-visible surface. The loop appends
|
|
61
|
+
* the first one as surface node 0 before the step's first `user/message`.
|
|
62
|
+
* A prepared in-history route can append nonempty changes in a continuing
|
|
63
|
+
* series. An incapable route or new series normalizes text to the first system
|
|
64
|
+
* node. Normalization empties nonempty later nodes, then rewrites the head if
|
|
65
|
+
* needed, through logged per-node replacements. An empty rendering always
|
|
66
|
+
* clears all active system nodes, leaving no older instructions model-visible.
|
|
67
|
+
* Empty later nodes are dormant and project to no message; an empty head with
|
|
68
|
+
* no active later node records "no system prompt". Restored nonempty text follows
|
|
69
|
+
* the same route and series rule; empty nodes never restore older text.
|
|
70
|
+
*/
|
|
71
|
+
'system/message': { turn: number; step: number; message: SystemMessage }
|
|
59
72
|
/**
|
|
60
73
|
* Assembled assistant message for one step (derived history uses this).
|
|
61
74
|
* Carries the step's `usage` when the adapter reported token accounting, so
|
|
@@ -102,6 +115,7 @@ interface SessionEventMap {
|
|
|
102
115
|
turn: number
|
|
103
116
|
step: number
|
|
104
117
|
message: ToolResultMessage
|
|
118
|
+
/** Optional failure identity; allowed only when the tool-result block has `isError: true`. */
|
|
105
119
|
error?: { name: string; code: string }
|
|
106
120
|
meta?: JsonValue
|
|
107
121
|
}
|
|
@@ -116,8 +130,10 @@ interface SessionEventMap {
|
|
|
116
130
|
startsSeries?: true
|
|
117
131
|
}
|
|
118
132
|
/**
|
|
119
|
-
* Route metadata for the next request, logged only when the route
|
|
120
|
-
* changes. It does not participate in request
|
|
133
|
+
* Route metadata for the next request, logged only when the route, capacity,
|
|
134
|
+
* or system prompt update mode changes. It does not participate in request
|
|
135
|
+
* reconstruction or header equality. Prompt admission uses the bound prepared
|
|
136
|
+
* call's capability, not this snapshot from an earlier request.
|
|
121
137
|
*/
|
|
122
138
|
'request/context': RequestContext
|
|
123
139
|
/**
|
|
@@ -152,12 +168,13 @@ interface SessionEventMap {
|
|
|
152
168
|
|
|
153
169
|
### The request header event: `request/header`
|
|
154
170
|
|
|
155
|
-
The request envelope — the `EpochHeader` (call config + markers for adapter-supplied defaults +
|
|
171
|
+
The request envelope — the `EpochHeader` (call config + markers for adapter-supplied defaults + assembled tool schemas) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). The rendered system prompt is not part of the header: it is derived history, the `system/message` event at surface node 0 and any later in-history system node ([decision](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md)), so a prompt change replaces or appends a system node and leaves the header unchanged. A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a changed request appends a snapshot with reason `'change'`; and an unchanged envelope beginning an explicitly declared message series or following a surface replacement appends a snapshot with reason `'series'`. A changed snapshot carries `startsSeries: true` when that request also begins a series. Ordinary append-only later Turns, further Steps, and retries in the same model-message series inherit the latest snapshot. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message.
|
|
156
172
|
|
|
157
173
|
```ts type-equiv
|
|
158
174
|
/**
|
|
159
|
-
* Logged request state outside derived history: call config
|
|
160
|
-
*
|
|
175
|
+
* Logged request state outside derived history: call config and tools. The
|
|
176
|
+
* system prompt is derived history — surface node 0, a `system/message` event.
|
|
177
|
+
* The latest full `request/header` snapshot reconstructs the header; canonical
|
|
161
178
|
* empty optional fields are absent.
|
|
162
179
|
*/
|
|
163
180
|
interface EpochHeader {
|
|
@@ -165,18 +182,16 @@ interface EpochHeader {
|
|
|
165
182
|
config: LlmCallConfig
|
|
166
183
|
/** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
|
|
167
184
|
adapterDefaults?: LlmCallConfigAdapterDefaults
|
|
168
|
-
/** Rendered system prompt text; absent for a system-less request. */
|
|
169
|
-
system?: string
|
|
170
185
|
/** Assembled tool schemas; absent for a tool-less request. */
|
|
171
186
|
tools?: ToolSchema[]
|
|
172
187
|
}
|
|
173
188
|
```
|
|
174
189
|
|
|
175
|
-
|
|
190
|
+
Current event acceptance requires canonical `request/header.header`: any `system` field is forbidden, and `tools: []` and `adapterDefaults: {}` must be omitted. Whitespace-only system-message content, `config.stop: []`, and nested extensions remain unchanged. Seed, append, and current persistence reads reject noncanonical headers rather than silently normalizing them; [the V3 envelope decision](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-09-06-v3-canonical-session-envelopes.md) owns historical conversion. Legacy v0 logs containing `request/header-delta` or its full-snapshot `fallback` reason are rejected rather than replayed incompletely.
|
|
176
191
|
|
|
177
192
|
### The route capacity event: `request/context`
|
|
178
193
|
|
|
179
|
-
The context metadata of the route a request resolved to is separate logged state, appended beside `request/header` inside the same step and only when the provider, model, or
|
|
194
|
+
The context metadata of the route a request resolved to is separate logged state, appended beside `request/header` inside the same step and only when the provider, model, capacity, or `systemPromptUpdate` mode differs from the previous record. It stays outside `EpochHeader` because that type is the reconstruction contract compared field-wise by `headerEquals`: capacity and the update mode describe a route, not a request input, so folding them in would let a route change register as a request-envelope `change` and would pull adapter metadata into the loop's reconstruction invariant. Like `request/header`, it is not a `SurfaceEventType` and produces no LLM message. `session.requestContext()` folds the latest record incrementally; the agent loop reads that record's `systemPromptUpdate` when it decides whether a changed system prompt replaces the latest system node or is appended after the cached history ([decision rule](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/agent-loop/README.md#understand-the-implementation)). A route whose adapter advertises no capacity is recorded with `contextWindow` absent, so the new record clears an older route's capacity; a route without a declared update mode likewise clears an older route's `systemPromptUpdate`.
|
|
180
195
|
|
|
181
196
|
```ts type-equiv
|
|
182
197
|
/** Registration-bound metadata for one resolved model route. */
|
|
@@ -187,6 +202,8 @@ interface RequestContext {
|
|
|
187
202
|
model: string
|
|
188
203
|
/** Maximum combined request and response context in tokens, when advertised. */
|
|
189
204
|
contextWindow?: number
|
|
205
|
+
/** `'in-history'` when the route reads the latest `system` message at any position as the effective system prompt. */
|
|
206
|
+
systemPromptUpdate?: SystemPromptUpdate
|
|
190
207
|
}
|
|
191
208
|
```
|
|
192
209
|
|
|
@@ -224,7 +241,7 @@ type OptionalSessionSeq = SessionSeq | null
|
|
|
224
241
|
* unions), so `switch (event.type)` narrows `event.data` without casts.
|
|
225
242
|
*
|
|
226
243
|
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
|
|
227
|
-
* they only exist on {@link SurfaceEventType} variants (`user/message`,
|
|
244
|
+
* they only exist on {@link SurfaceEventType} variants (`system/message`, `user/message`,
|
|
228
245
|
* `assistant/message`, `tool/result`).
|
|
229
246
|
* Non-surface events (boundary markers, attempts, errors) never carry
|
|
230
247
|
* surface metadata — the compiler enforces this at `Session.append()`
|
|
@@ -249,26 +266,20 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
|
|
249
266
|
* inconvenience) rather than silently resuming a gutted session.
|
|
250
267
|
*/
|
|
251
268
|
ignorable?: true
|
|
252
|
-
} & (K extends SurfaceEventType ? {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
* `assistant/message` embeds its provider stream and cannot carry this field.
|
|
257
|
-
*/
|
|
258
|
-
sourceEventSeqs?: SessionSeq[]
|
|
259
|
-
/** How this event entered the surface; absent for non-surface events. */
|
|
260
|
-
surfaceOp?: SurfaceOp
|
|
261
|
-
} : object)
|
|
269
|
+
} & (K extends SurfaceEventType ? SurfaceIntent<K> : {
|
|
270
|
+
surfaceOp?: never
|
|
271
|
+
sourceEventSeqs?: never
|
|
272
|
+
})
|
|
262
273
|
}[T]
|
|
263
274
|
```
|
|
264
275
|
|
|
265
276
|
`SessionEventType = keyof SessionEventMap`. Because `SessionEventMap` is merge-extensible, switches over `SessionEvent` must NOT use `assertNever` — a plugin-added variant is a valid unknown value; handle the known cases and fall through `default`.
|
|
266
277
|
|
|
267
|
-
|
|
278
|
+
Every surface event requires `surfaceOp`; known log-only events forbid both surface metadata fields. Native unknown or obsolete ignorable envelopes remain opaque. `assistant/message` embeds its provider stream and forbids `sourceEventSeqs`. System, user, and tool surface events may cite a complete non-empty set of unique earlier events when their provenance or replacement operation requires it. A `tool/result` may carry `data.error` only when its tool-result block has `isError: true`; failure identity remains optional for failed results.
|
|
268
279
|
|
|
269
280
|
## Surface types
|
|
270
281
|
|
|
271
|
-
The
|
|
282
|
+
The four message-producing types (`SurfaceEventType` — `system/message`, `user/message`, `assistant/message`, `tool/result`) carry surface metadata declaring how they join the ordered derived surface. `system/message` holds the rendered system prompt: the loop appends the first one as surface node 0 and, when the prompt changes, replaces exactly the latest system node or appends a new one on an in-history route; the surface fold rejects any other replacement covering a `system/message` at node 0, while a later system node is ordinary history that a compaction replacement may shadow. See the [session surface Agent Note](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-06-18-session-surface.md).
|
|
272
283
|
|
|
273
284
|
### `SurfaceEventType` — the message-producing subset of event types
|
|
274
285
|
|
|
@@ -276,10 +287,11 @@ The three message-producing types (`SurfaceEventType` — `user/message`, `assis
|
|
|
276
287
|
/**
|
|
277
288
|
* The subset of {@link SessionEventType} values whose events produce LLM
|
|
278
289
|
* messages and are eligible to appear on the ordered surface. Only these
|
|
279
|
-
* event types may carry {@link SurfaceOp}; user and tool events may also cite
|
|
290
|
+
* event types may carry {@link SurfaceOp}; system, user, and tool events may also cite
|
|
280
291
|
* earlier sources through {@link SessionEvent.sourceEventSeqs}.
|
|
281
292
|
*/
|
|
282
293
|
type SurfaceEventType =
|
|
294
|
+
| 'system/message'
|
|
283
295
|
| 'user/message'
|
|
284
296
|
| 'assistant/message'
|
|
285
297
|
| 'tool/result'
|
|
@@ -294,19 +306,19 @@ type SurfaceEventType =
|
|
|
294
306
|
*
|
|
295
307
|
* - `'append'`: added to the tail — normal path for user/assistant/tool
|
|
296
308
|
* messages.
|
|
297
|
-
* - `{ op: 'replace',
|
|
298
|
-
* (inclusive) through `
|
|
299
|
-
* surface nodes in the current surface. `
|
|
309
|
+
* - `{ op: 'replace', startSeq, endSeq }`: replaces surface nodes from `startSeq`
|
|
310
|
+
* (inclusive) through `endSeq` (inclusive) with this node. Both must exist as
|
|
311
|
+
* surface nodes in the current surface. `startSeq === endSeq` replaces a single
|
|
300
312
|
* node. The node's {@link SessionEvent.sourceEventSeqs} must include every
|
|
301
313
|
* shadowed surface node. Used by compaction; any surface-replacing producer
|
|
302
314
|
* may use it.
|
|
303
315
|
*/
|
|
304
316
|
type SurfaceOp =
|
|
305
317
|
| 'append'
|
|
306
|
-
| { op: 'replace';
|
|
318
|
+
| { op: 'replace'; startSeq: SessionSeq; endSeq: SessionSeq }
|
|
307
319
|
```
|
|
308
320
|
|
|
309
|
-
`'append'` is the normal tail-append path. `replace`
|
|
321
|
+
`'append'` is the normal tail-append path. `replace` contains exactly `op`, `startSeq`, and `endSeq`, with no aliases or extra keys. It shadows the inclusive span between those current surface event sequences and inserts the new event in their place; equal endpoints replace one entry. Endpoints must precede the replacing event, but their relative order is surface order, not numeric sequence order.
|
|
310
322
|
|
|
311
323
|
### `SurfaceIntent` — the parameter to `session.append()`
|
|
312
324
|
|
|
@@ -318,7 +330,7 @@ type SurfaceOp =
|
|
|
318
330
|
type SurfaceIntent<T extends SurfaceEventType = SurfaceEventType> = {
|
|
319
331
|
surfaceOp: SurfaceOp
|
|
320
332
|
} & (T extends 'assistant/message' ? {
|
|
321
|
-
/**
|
|
333
|
+
/** Assistant messages embed their provider stream instead of citing source events. */
|
|
322
334
|
sourceEventSeqs?: never
|
|
323
335
|
} : {
|
|
324
336
|
/** Complete non-empty set of known earlier source-event seqs. */
|
|
@@ -443,13 +455,16 @@ declare class Session {
|
|
|
443
455
|
inheritedEventCount?: SessionLogOffset,
|
|
444
456
|
): Session;
|
|
445
457
|
/**
|
|
446
|
-
* Restore a detached session by
|
|
447
|
-
*
|
|
448
|
-
* and header fields are validated
|
|
458
|
+
* Restore a detached session by adopting an independently owned or deeply frozen seed.
|
|
459
|
+
* Runtime-required event fields, event envelopes, sequence continuity, surface
|
|
460
|
+
* transitions, and header fields are validated without copying or freezing events.
|
|
461
|
+
* Embedded Assistant streams remain opaque until a stream consumer or storage
|
|
462
|
+
* verifier reads them.
|
|
449
463
|
* @param id - restored session identity.
|
|
450
|
-
* @param seed -
|
|
451
|
-
* @param header -
|
|
464
|
+
* @param seed - independently owned or deeply frozen events.
|
|
465
|
+
* @param header - independently owned storage metadata.
|
|
452
466
|
* @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
|
|
467
|
+
* @param eventState - aliasing state carried from the operation that produced the seed.
|
|
453
468
|
* @returns a restored detached session.
|
|
454
469
|
*/
|
|
455
470
|
static fromRestore(
|
|
@@ -457,6 +472,7 @@ declare class Session {
|
|
|
457
472
|
seed: readonly SessionEvent[],
|
|
458
473
|
header: SessionHeader,
|
|
459
474
|
inheritedEventCount: SessionLogOffset,
|
|
475
|
+
eventState: SessionSeedEventState,
|
|
460
476
|
): Session;
|
|
461
477
|
/**
|
|
462
478
|
* Return the immutable event stored at one exact sequence number.
|
|
@@ -515,6 +531,7 @@ declare class Session {
|
|
|
515
531
|
* (BigInt, function, symbol, undefined, negative zero, non-finite number,
|
|
516
532
|
* circular reference, sparse array, or an exotic object such as
|
|
517
533
|
* Map/Set/Date/class instance), or when the candidate violates the
|
|
534
|
+
* request-header empty-field or tool-error consistency rules, or the
|
|
518
535
|
* canonical surface contract (marker shape and eligibility, unique
|
|
519
536
|
* earlier source-event references, positional replacement validity, and complete
|
|
520
537
|
* shadowed-node coverage). One iterative pass reads, validates, and
|
|
@@ -642,7 +659,7 @@ The optional `dsh-session/invariant` companion enforces the relations owned by c
|
|
|
642
659
|
|
|
643
660
|
A fresh fork constructor requires its seed to equal the inherited prefix and appends `session/end-seed { inherited: true }` at the exact durable cut. A restore retains that tagged marker and appends an ordinary `session/end-seed {}` only when its complete stored seed does not already end in a marker. Both forms are log-only and produce no message; `Session`'s constructor is the only legitimate writer.
|
|
644
661
|
|
|
645
|
-
For fork lineage, locate the LAST marker whose payload carries `inherited: true`;
|
|
662
|
+
For fork lineage, locate the LAST marker whose payload carries `inherited: true`; current-format decoding requires it exactly when `SessionHeader.isSeeded` is true and derives `inheritedEventCount` from its seq. For lifecycle ownership, locate the last `session/end-seed` of either form. Reopening a seed that already ends in any marker does not append another ordinary marker.
|
|
646
663
|
|
|
647
664
|
It exists because seed history and live work are otherwise byte-identical, which defeats any plugin owning a standalone open/close bracket: an unmatched `compaction/start` reads the same whether the writer crashed mid-compaction or is compacting right now. An opening marker before `session/end-seed` came from the constructor seed and belongs to an ended lifecycle, whatever ended it (a crash, a succeeding process, or a fork out of a still-running parent), so its owner may treat it as dead. That covers only brackets *this* session inherited: a concurrently live session holding an open bracket over the same history has its own boundary elsewhere, so tolerating concurrent writers needs a liveness signal beyond the log. Core writes the boundary and reads nothing from it — a bracket's vocabulary stays with its owning plugin, which is why crash repair closes turn/step/tool boundaries and never `compaction/*`.
|
|
648
665
|
|
|
@@ -654,11 +671,11 @@ A plugin may declaration-merge extra `SessionEventMap` types. These are **log-on
|
|
|
654
671
|
|
|
655
672
|
When several events in one plugin-owned family assemble into one Web Client Conversation Node, every start, update, result, resource, or interruption event in that family carries or independently derives the same stable business id. This requirement applies to correlated Node families, not to every Session event; it lets the client group each event without guessing from adjacency or scanning history. See the [Conversation subsystem](./conversation.md).
|
|
656
673
|
|
|
657
|
-
The hook bridges' `hook/invoked` / `hook/result` pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` gets no `hook/*` record because it runs before turn 1; its context remains pending in the inbox until a waking delivery opens a turn
|
|
674
|
+
The hook bridges' `hook/invoked` / `hook/result` pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` gets no `hook/*` record because it runs before turn 1; its context remains pending in the inbox until a waking delivery opens a turn.
|
|
658
675
|
|
|
659
676
|
## Durability contract
|
|
660
677
|
|
|
661
|
-
What a persistence backend relies on: the durable log persists every event losslessly, and every Assistant attempt is one `assistant/message` or `assistant/attempt` whose embedded compact stream preserves the original timed chunks. `seq` stays contiguous across these settlements and all interleaved events. A backend may choose its own storage framing for an event batch as long as a handle's `read()` returns the exact appended events; current JSONL
|
|
678
|
+
What a persistence backend relies on: the durable log persists every event losslessly, and every Assistant attempt is one `assistant/message` or `assistant/attempt` whose embedded compact stream preserves the original timed chunks. `seq` stays contiguous across these settlements and all interleaved events. A backend may choose its own storage framing for an event batch as long as a handle's `read()` returns the exact appended events; current JSONL writes one row per event (see [persistence.md](./persistence.md)). All `event.data` must be JSON-serializable; `Session.append` enforces this at the source (throwing on non-serializable data), so a bad event never enters the log and `session.snapshotEvents()` always equals what a backend can persist. Adding an event type that carries non-serializable data, corrupts core execution nesting, or violates its owner's declared relation is a breaking change to the on-disk format.
|
|
662
679
|
|
|
663
680
|
The backends that consume this contract are on [persistence.md](./persistence.md).
|
|
664
681
|
|
|
@@ -666,7 +683,7 @@ The backends that consume this contract are on [persistence.md](./persistence.md
|
|
|
666
683
|
|
|
667
684
|
`ModelCatalog` is the Host-generation model directory returned by `session/modelCatalog`: it carries the deployment default, routable provider ids, successful provider groups, and isolated provider failures. It is not derived from one Session and remains separate from Session projections.
|
|
668
685
|
|
|
669
|
-
`SessionOpenWorkspacePathRequest` carries an absolute or workspace-resolved `path
|
|
686
|
+
`SessionOpenWorkspacePathRequest` carries an absolute or workspace-resolved `path`; optional `action: "reveal"` selects file-manager navigation instead of default-application opening. `SessionOpenWorkspacePathValue` confirms that the Host accepted the native handoff. A Session-aware Client resolves relative paths against its current Session cwd when known; the controller hands the path to the opener unchanged and reports invalid requests, cancellation, and opener failures through the Session Remote error vocabulary.
|
|
670
687
|
|
|
671
688
|
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
|
672
689
|
|
|
@@ -740,6 +757,12 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspectio
|
|
|
740
757
|
*/
|
|
741
758
|
@Remote canOpenWorkspacePath(): boolean
|
|
742
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Describe the serving desktop for authenticated file-action routes.
|
|
762
|
+
* @returns Host name, configured availability, and platform-specific file-manager behavior.
|
|
763
|
+
*/
|
|
764
|
+
workspaceDesktop(): { name: string; available: boolean; fileManager: 'finder' | 'explorer' | 'directory' | null }
|
|
765
|
+
|
|
743
766
|
/**
|
|
744
767
|
* Open one path prepared by a Session-aware caller on the Host desktop.
|
|
745
768
|
* @param request - path after best-effort Session workspace resolution.
|
|
@@ -864,10 +887,9 @@ create(id?: SessionId, options?: CreateSessionOptions): Session
|
|
|
864
887
|
*
|
|
865
888
|
* @param id - the session id; omitted, the store mints `session-<n>`.
|
|
866
889
|
* @param options - seed events and/or creation metadata for the header. With
|
|
867
|
-
* `
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
* retain no mutable aliases.
|
|
890
|
+
* `eventState`, every seed event is either independently owned or any
|
|
891
|
+
* shared value is deeply frozen; {@link Session.fromRestore} validates and
|
|
892
|
+
* adopts those values without copying or freezing them.
|
|
871
893
|
* @returns the constructed session, NOT yet in the store.
|
|
872
894
|
* @throws if a session with `id` already exists, metadata is not a plain
|
|
873
895
|
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
|
|
@@ -5,7 +5,7 @@ outline: [2,3]
|
|
|
5
5
|
|
|
6
6
|
# Bash Executor
|
|
7
7
|
|
|
8
|
-
The bash execution seam is split across a Service Definition ([dsh-shell](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/shell/shell), `ctx.shell`), Service Providers ([dsh-bash-local](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/shell/bash-local) and [dsh-bash-sandbox](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/shell/bash-sandbox)), and Consumer ([dsh-tool-bash](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/shell/tool-bash), the `bash` schema). Generic background-job ids, ownership, and controls live in [jobs.md](./jobs.md); this seam returns a task-free process handle.
|
|
8
|
+
The bash execution seam is split across a Service Definition ([dsh-shell](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/shell/shell), `ctx.shell`), Service Providers ([dsh-bash-local](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/shell/bash-local) and [dsh-bash-sandbox](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/shell/bash-sandbox)), and Consumer ([dsh-tool-bash](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/shell/tool-bash), the `bash` schema). Generic background-job ids, ownership, and controls live in [jobs.md](./jobs.md); this seam returns a task-free process handle. Managed-range mechanics live behind the [subprocess seam](./subprocess.md).
|
|
9
9
|
|
|
10
10
|
Source: [`packages/shell/shell/src/types.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/shell/shell/src/types.ts)
|
|
11
11
|
|
|
@@ -101,7 +101,7 @@ interface ShellExecSpec {
|
|
|
101
101
|
}
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
`stdin` and `env` are trusted in-process plugin inputs and are not exposed by `dsh-tool-bash`. The local executor scrubs ambient credentials before merging explicit caller-supplied env.
|
|
104
|
+
`stdin` and `env` are trusted in-process plugin inputs and are not exposed by `dsh-tool-bash`. The local executor scrubs ambient credentials before merging explicit caller-supplied env.
|
|
105
105
|
|
|
106
106
|
`stdoutMaxBytes` is also trusted-plugin-only. It lets a foreground consumer request complete stdout up to a bounded parser budget without changing stderr, background jobs, or the model-facing bash tool's ordinary output cap.
|
|
107
107
|
|
|
@@ -169,7 +169,7 @@ The `SANDBOX_UNAVAILABLE` error code (owned by the [sandbox seam](./sandbox.md))
|
|
|
169
169
|
|
|
170
170
|
## Background processes: `ShellProcess`
|
|
171
171
|
|
|
172
|
-
`start()` returns a handle with no id or owner. `dsh-tool-bash` adapts it into `ctx.jobs.start()` hooks; the generic runtime then owns job identity and lifecycle. `done` resolves when the process
|
|
172
|
+
`start()` returns a handle with no id or owner. `dsh-tool-bash` adapts it into `ctx.jobs.start()` hooks; the generic runtime then owns job identity and lifecycle. `done` resolves when the underlying process settles and never rejects; a subprocess provider rejection becomes a `killed` process with a stage-neutral error on stderr. Reads remain valid after settlement, and sandbox facts are stamped before `done` resolves.
|
|
173
173
|
|
|
174
174
|
```ts type-equiv
|
|
175
175
|
/**
|
|
@@ -185,7 +185,10 @@ interface ShellProcess {
|
|
|
185
185
|
exitCode: number | null
|
|
186
186
|
/** Terminating signal name, when signal-killed. */
|
|
187
187
|
signal: NodeJS.Signals | null
|
|
188
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* Resolves when the underlying process settles (never rejects — provider
|
|
190
|
+
* rejection settles as `killed` with a stage-neutral error on stderr).
|
|
191
|
+
*/
|
|
189
192
|
readonly done: Promise<void>
|
|
190
193
|
/** Sandbox facts, stamped once a confined process settles. */
|
|
191
194
|
sandbox?: ShellSandboxInfo
|
|
@@ -196,7 +199,7 @@ interface ShellProcess {
|
|
|
196
199
|
*/
|
|
197
200
|
readOutput(): ShellProcessRead
|
|
198
201
|
/**
|
|
199
|
-
*
|
|
202
|
+
* Terminate the provider-managed range. Returns false when it had already finished
|
|
200
203
|
* (no-op); idempotent.
|
|
201
204
|
*/
|
|
202
205
|
kill(): boolean
|
|
@@ -221,7 +224,7 @@ interface ShellProcessRead {
|
|
|
221
224
|
|
|
222
225
|
## The service
|
|
223
226
|
|
|
224
|
-
`ShellExecutor` owns `resolve`, foreground `run`, background-process `start`, and the `sandboxMode` capability fact. `dsh-bash-local` owns command defaulting, timeout/abort classification, the terminal environment, and the background read merge;
|
|
227
|
+
`ShellExecutor` owns `resolve`, foreground `run`, background-process `start`, and the `sandboxMode` capability fact. `dsh-bash-local` owns command defaulting, timeout/abort classification, the terminal environment, and the background read merge; managed-range termination, bounded collectors, spill files, credential scrubbing, and disposal quiescence are the [subprocess service](./subprocess.md)'s. `dsh-tool-bash` owns model-facing rendering and adapts background handles into the [generic job runtime](./jobs.md). `dsh-shell` owns the shell tools' shared exit-status contract: the exported `parseExitStatus`/`ParsedExitStatus` inverts the `[exit code: N]` / `[killed by signal: X]` markers `dsh-tool-bash`'s `renderResult` and `dsh-tool-pwsh`'s `renderPwshResult` append, and both tools' `presentResult` use it to split the rendered text into the terminal card's output body and its exit-status pill.
|
|
225
228
|
|
|
226
229
|
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
|
227
230
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/subsystems/sidebar-right.md"
|
|
3
|
+
outline: [2,3]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Right Sidebar
|
|
7
|
+
|
|
8
|
+
The right Sidebar is the Web Client's per-Session docking surface: a column of panes and tabs beside the conversation in which addressed content — a workspace file, a directory tree, the product's own pages — opens, splits, floats, and closes. [`dsh-client-ui-sidebar-right`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-right/README.md) owns the surface, the tab-type registry, and the navigation service; [`dsh-client-ui-dockkit`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-dockkit/README.md) is its internal layout engine; [`dsh-client-resources`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/resources/README.md) turns addresses into live values for any component; [`dsh-api-workspace-files`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/api/workspace-files/README.md) provides both the Host workspace service and the Client `file` resource provider.
|
|
9
|
+
|
|
10
|
+
This page is the reference for the subsystem's contracts: addresses, tab-type registration, the navigation service, the extension slots and their owner props, the resource model, the Workspace Files service, the shipped types, and what is deliberately not built. How the layout engine, the frame, and the surface fit together is in the [Agent Note](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/feature/2026-09-04-right-sidebar-docking-infrastructure.md); slot mechanics are in the [Slots reference](./slots.md).
|
|
11
|
+
|
|
12
|
+
## Position and ownership
|
|
13
|
+
|
|
14
|
+
One docking surface exists per Session, held in a session-scoped slot store and drawn by `rightbar.session`. The root-scoped `rightbar` controller mounts that seat only while Conversation is selected; a reload returns every session to the collapsed default, and switching sessions keeps each surface where it was ([state](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-right/README.md#state)). The surface's every change is one recorded history entry computed by the kit's pure planners; a docked pane never stays empty, and an empty root pane receives the default page selected from registered guide entries.
|
|
15
|
+
|
|
16
|
+
A tab type is two registrations that share the definition's `id`: a static definition in `ctx.sidebarRightTabs` saying which addresses its `kind` opens, and a keyed slot registration supplying its body. The framework injects `useTabInfo()` for live Sidebar, pane and tab information; each type keeps its own state in its slot store. Packages import each other's declarations only as types.
|
|
17
|
+
|
|
18
|
+
| Package | Role |
|
|
19
|
+
|---|---|
|
|
20
|
+
| [`client/ui-sidebar-right`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-right/README.md) | The panel and rail seats, the layout store, `ctx.sidebarRightTabs`, `ctx.sidebarRight`, the Tab domain, the guide type |
|
|
21
|
+
| [`client/ui-dockkit`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-dockkit/README.md) | Pure layout engine and React surface; an internal dependency of `ui-sidebar-right`, not a stable interface |
|
|
22
|
+
| [`client/resources`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/resources/README.md) | `ctx.resources`, `useResource`, the protocol → value roster `ResourceProtocolMap` |
|
|
23
|
+
| [`api/workspace-files`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/api/workspace-files/README.md) | Host `ctx.workspaceFiles`, the `workspaceFiles` Remote namespace, and the Client `file` resource provider |
|
|
24
|
+
| [`util/workspace-path`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/util/workspace-path/README.md) | The file address grammar: `fileAddressFor`, `parseFileAddress` |
|
|
25
|
+
| [`client/ui-sidebar-documentpreview`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-documentpreview/README.md), [`client/ui-sidebar-files`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-files/README.md) | The shipped `text` and `files` types |
|
|
26
|
+
|
|
27
|
+
## Addresses
|
|
28
|
+
|
|
29
|
+
Every tab is opened by an address string, and the address is the tab's content identity. Two families exist.
|
|
30
|
+
|
|
31
|
+
A **resource address** is a `dsh-resource://<type>/…` URL. The host names the resource protocol — the key of `ResourceProtocolMap` — and everything after it is the protocol's own path; one scheme serves every protocol, so adding a protocol adds a host, never a scheme. The `file` protocol's path opens with its scope: `session/<sessionId>` followed by the path relative to that session's workspace root (`dsh-resource://file/session/abc/src/notes.txt`), or `absolute` followed by the absolute path with its leading `/` dropped (`dsh-resource://file/absolute/home/ys/notes.txt`, `dsh-resource://file/absolute/C:/x/y.txt` on Windows). Every id and path segment is component-encoded, with `:` kept literal for drive letters. `fileAddressFor(sessionId, cwd, path)` builds one — a relative path or an absolute path inside the workspace becomes `session`-relative, any other absolute path becomes `absolute` — and `parseFileAddress(address)` reads it back or returns `undefined` ([grammar](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/util/workspace-path/README.md)).
|
|
32
|
+
|
|
33
|
+
A **page address** is what the Sidebar records for a tab opened by kind rather than by resource: `sidebar://<kind>`, written by the Sidebar itself when `openTab(kind)` runs. Callers never build one — the guide and the file tree are opened as `openTab('guide')` and `openTab('files')` — and no other navigation address exists ([not built](#not-built)).
|
|
34
|
+
|
|
35
|
+
Tab identity is the pair `(kind, address)`: the registry's claim uses the address verbatim as the record's `contentId`, so opening the same address through the same type finds the existing tab, and the same address through two types is two tabs.
|
|
36
|
+
|
|
37
|
+
## Tab-type registration
|
|
38
|
+
|
|
39
|
+
`ctx.sidebarRightTabs.register(definition)` registers one implementation of a type for the caller's lifetime and returns the disposer; the caller holds it inside its own `ctx.effect`, so an implementation lives exactly as long as the plugin that contributed it, and a second registration of the same `id` throws ([extension seats](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-right/README.md#extension-seats)). The definition is static: no runtime hook, nothing per tab or per session.
|
|
40
|
+
|
|
41
|
+
| Field | Meaning |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `id` | The implementation's identity, unique across every registration; a package name is the natural value (`@deepseek-ai/dsh-client-ui-sidebar-files`). It is the key the body and title register under. |
|
|
44
|
+
| `kind` | The type's discriminator: what its tabs are, and what `openTab` names. Not unique — an extension may take over a builtin's kind. The shipped kinds are `guide`, `text`, `files`. |
|
|
45
|
+
| `patterns` | Optional resource-address globs the type recognizes; a page type opened by kind omits them. A pattern containing `:` matches the whole address (`dsh-resource://file/**`); one without matches the URL's path at any depth (`*.md`), and an address that is not a URL matches no such pattern. Matching is case-insensitive and does not hide dotfiles; the syntax is picomatch's POSIX dialect. |
|
|
46
|
+
| `priority` | One of three literal bands: `extension` (the default and the highest: a type from outside the product outranks every shipped viewer), `builtin` (types shipped with the product), `fallback` (plain-content viewers anything more specific should beat). |
|
|
47
|
+
| `canOpen(address)` | Optional synchronous veto of a glob match; it runs on every routing decision. |
|
|
48
|
+
| `title(address)` | The chip's text, captured into the layout record when the tab opens and never rewritten. |
|
|
49
|
+
| `guide` | Optional entry boxes for the guide page: `{ order, title(), description?(), icon? }`. Picking a box opens the contributing type as a page; omit to stay off the page. |
|
|
50
|
+
|
|
51
|
+
Routing is a ranked claim. `candidates(address)` ranks the types whose patterns match and whose `canOpen` does not veto: by band, then by the length of the longest matched pattern, then by registration order. `claim(address, kind?)` picks the first candidate, or the named `kind` outright — its globs are skipped, its `canOpen` still applies — and returns `{ kind, contentId: address, title }`. An address no type claims throws: it is a wiring mistake, not a user error.
|
|
52
|
+
|
|
53
|
+
One `kind` may carry one `builtin` and one `extension` registration at the same time. The extension is the one in force for claims, `get(kind)`, `openTab(kind)`, and the guide page, and the seat finds a tab's body and title under the definition in force's `id`, so no slot priority is involved; when the extension unregisters, the builtin resumes. Every other collision on a kind, and every duplicate `id`, throws.
|
|
54
|
+
|
|
55
|
+
```ts ignore-check
|
|
56
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
57
|
+
import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client'
|
|
58
|
+
|
|
59
|
+
export const inject = ['sidebarRightTabs', 'slots']
|
|
60
|
+
|
|
61
|
+
export function apply(ctx: Context): void {
|
|
62
|
+
ctx.effect(() => ctx.sidebarRightTabs.register({
|
|
63
|
+
id: '@acme/dsh-client-ui-image',
|
|
64
|
+
kind: 'image',
|
|
65
|
+
patterns: ['*.png', '*.jpg', '*.gif', '*.svg'],
|
|
66
|
+
canOpen: address => address.startsWith('dsh-resource://file/'),
|
|
67
|
+
title: address => address.slice(address.lastIndexOf('/') + 1),
|
|
68
|
+
}), 'image type')
|
|
69
|
+
ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register(
|
|
70
|
+
{ name: 'sidebar.right.pane.tab', key: '@acme/dsh-client-ui-image' },
|
|
71
|
+
ImageBody,
|
|
72
|
+
)), 'image body')
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Navigation: `ctx.sidebarRight`
|
|
77
|
+
|
|
78
|
+
Two opens are the navigation controller, and every way into the column calls one of them: `openResource(address, options?)` for a `dsh-resource://` address — the conversation's file links, a tool row's line reference, a file tree's rows — and `openTab(kind, options?)` for a page — the strip's add control, a guide entry box. Both run four steps as one history entry — claim (the registry ranks the resource's types, or the named `kind`'s implementation in force answers); focus a tab already showing the same `(kind, address)`; otherwise seat a new tab; expand the column — and then record the navigation in the Tab domain ([service](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-right/README.md#ctxsidebarright)). Content the user cannot see is not opened, so a collapsed column expands in the same step. `openResource` throws for an address outside `dsh-resource://` or one no type claims; `openTab` throws for a kind nothing registered: both are wiring mistakes, not user errors.
|
|
79
|
+
|
|
80
|
+
| Option | Meaning |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `paneId` | Land a new tab in this pane; default is the active docked pane (the first docked pane while a floating pane is active). |
|
|
83
|
+
| `replaceTab` | Take this tab's pane and strip slot, closing it in the same step; a floating tab lends no place, so the new tab lands as if unplaced. |
|
|
84
|
+
| `revealIfOpened` | Default `true`: a tab already showing the same `(kind, address)` is focused and handed `params`. `false` opens another tab regardless. |
|
|
85
|
+
| `kind` (`openResource` only) | Name the opening type instead of ranking claims; its implementation in force opens the address, and its `canOpen` still applies. |
|
|
86
|
+
| `params` | Navigation parameters for the body, delivered as `navigation.params`. `openResource` types them by resource type through the merge-extensible `SidebarRightResourceParamsMap` (the text preview declares `{ line?: number }`); `openTab<K>` types them by kind through `SidebarRightTabParamsMap`, `undefined` for a kind that declares none; a body reads `SidebarRightNavigationParams`, the union of both. Values are JSON-shaped by convention and not validated at run time. |
|
|
87
|
+
|
|
88
|
+
Placement is the caller's option, never a type's property. The conversation calls `openResource(fileAddressFor(sessionId, cwd, path))` and, from a `read` tool row, adds `{ params: { line } }` from the call's 1-based `offset`; a guide entry box calls `tab.actions.openTab(entry.kind, { replaceTab: true })`; a file-tree row calls `tab.actions.openResource(address)`; the strip's add control calls `openTab('guide', { paneId, revealIfOpened: false })`.
|
|
89
|
+
|
|
90
|
+
`close(tabId)` closes a tab; `active()` returns the active pane's active tab; `isExpanded()` and `toggleExpanded()` read and flip the column, the flip recorded in the sequence. Reads answer for the no-Session case with `undefined` or `false`; writes need a mounted Session surface and throw without one rather than write into a surface nobody draws.
|
|
91
|
+
|
|
92
|
+
`focus(tabId)` makes a tab its pane's active tab; `split(paneId?)` splits the active docked pane, or the named one, and returns the new pane's id — or `undefined`, recording nothing, when the pane budget or the column's width forbids a split; `float(tabId, rect?)` lifts a tab into a floating pane; `dock(paneId)` returns a floating pane to the docked area. All four run the store's existing actions and record one history entry each; a target that does not exist or is already in the requested state is a no-op, and like `open` they throw without a mounted Session surface. `TabId`, `PaneId`, `TabRecord`, and `FloatRect` are re-exported from the package's `/client` entry so a caller needs no dockkit import.
|
|
93
|
+
|
|
94
|
+
## Slots and owner props
|
|
95
|
+
|
|
96
|
+
The Sidebar declares four extension slots; its document tab declares the additional keyed document body below ([hierarchy](./slots.md)).
|
|
97
|
+
|
|
98
|
+
| Slot | Cardinality | Purpose |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `sidebar.right.pane.tab` | keyed by the definition's `id`, Session scope | One tab's body. The seat dispatches a tab to the `id` of its kind's implementation in force, so the registrant receives every tab of its kind, docked or floating. A kind whose implementation registered no body renders the owner's "nothing can view this" notice. |
|
|
101
|
+
| `sidebar.right.pane.tab.title` | keyed by the definition's `id`, Session scope | The chip's title, with the same owner share as the body. Optional: without an entry the chip shows the `title(address)` text captured at open time; a type with a live title reads its own store here. |
|
|
102
|
+
| `sidebar.right.tab.guide` | chain, Session scope | Replaces the guide tab's contents without replacing the tab; the first non-declining entry takes the body, otherwise the shipped guide renders. |
|
|
103
|
+
| `sidebar.right.tab.menu.item` | list, Session scope | Content-level actions appended after the kit's own layout actions. An item that acts must call the owner's `dismiss()`. |
|
|
104
|
+
| `sidebar.right.tab.document` | keyed by the document implementation's `id`, Session scope | The selected file renderer inside the document tab; the parent owns shared loading and toolbar controls. |
|
|
105
|
+
|
|
106
|
+
A body, title and guide replacement receive the framework-injected `useTabInfo()`. It returns `{ sidebar, panel, tab }`: `sidebar` holds `expanded` and `fullscreen`, `panel.id` names the containing pane, and `tab` contains its record fields plus `visible`, `navigation`, `signal`, and `actions`. Docked bodies are visible only while expanded and active; docked titles need only expansion; floats stay visible. `signal` aborts when the record disappears or the plugin unloads, not on hiding or Session switching. `tab.actions` provides `openResource`, `openTab`, and `close`, bound to the tab's own Session. Open placement defaults to its current pane; `revealIfOpened` defaults to `true`, and `replaceTab: true` replaces this record in the same history entry. Menu entries retain plain `tab` and `dismiss` owner parameters.
|
|
107
|
+
|
|
108
|
+
`navigation.revision` increments on every navigation to the tab whether or not `params` changed, so a body can act on "navigated again" alone; it is `1` for a tab opened by address and `0` for a record nobody opened by address — a seeded guide, or a tab restored by undo. The Tab domain holds one occurrence per open record: a record that appears is pinned in the resource model, so switching tabs unmounts a body without dropping its content; a record that vanishes is aborted and dropped; a record restored by undo is a new occurrence ([Tab domain](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-right/README.md#the-tab-domain)).
|
|
109
|
+
|
|
110
|
+
## Document renderers
|
|
111
|
+
|
|
112
|
+
The `text` tab is the shared Document Preview owner. Its [root registration](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-documentpreview/src/client/index.ts) declares `sidebar.right.tab.document` and provides `ctx.documentPreviews`. A renderer registers `DocumentPreviewDefinition` metadata in its own effect, then waits through `ctx.slots.inject('sidebar.right.tab.document', ...)` and registers its component with `key: definition.id` and its locale namespace. Changing the renderer does not change the tab or resource address; the [extension decision](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md) separates preview policy from resource ownership.
|
|
113
|
+
|
|
114
|
+
The [registry](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-documentpreview/src/client/document/registry.ts) records unique `id`, `extensions`, localized `title()`, `loading`, optional `priority`, and optional `wrap`. Case-insensitive suffix matching ranks `extension` (the default) before `builtin`, then longer suffixes before shorter ones, then registration order. Unlike tab-kind replacement, the registry keeps all implementations available; the toolbar lists matching alternatives and remembers the selection per tab. Unknown extensions use plain text. `loading` is `text-pages` or `bytes-complete`; `wrap` advertises support for the shared source-wrap control.
|
|
115
|
+
|
|
116
|
+
[`DocumentPreviewProps`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-documentpreview/src/client/document/contract.ts) derives from `PropsRuntime<'sidebar.right.tab.document'>`. The owner supplies the original `resourceAddress`, `content`, and current `wrap`: text content is `{ kind: 'text', text, pages: [{ offset, text, lines }], eof }`, with cumulative `text`; complete bytes are `{ kind: 'bytes', data }`, with `Uint8Array<ArrayBuffer>` data. These transient buffers are borrowed read-only and must not enter durable layout or Session JSON. PDF copies the bytes before Worker transfer, preserving the owner's buffer. The child receives the same framework-bound `useTabInfo` and the global metadata-only `useResource`. The parent reads through ordinary inject callbacks to `remote.workspaceFiles.read`/`readAll` and owns page appends, per-tab refresh, and loading status. HTML's own inject callback uses `readRelated`; Host code resolves paths. Markdown and code retain one incremental renderer across appends and settle at EOF; HTML and PDF receive complete bytes.
|
|
117
|
+
|
|
118
|
+
Preview records its loaded version and the version observed when a read starts. Refresh rereads only that tab, without changing shared metadata or another tab's content. Reads are non-transactional; versions are opaque equality tokens, not ordered timestamps ([resource observation and Preview RPC](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md)).
|
|
119
|
+
|
|
120
|
+
## Resource model
|
|
121
|
+
|
|
122
|
+
The model is documented in [Client Resources](./client-resources.md); this section states what the Sidebar relies on. A resource is one address, and a resource address is a `dsh-resource://<type>/…` URL whose lower-cased host is the protocol key. The protocol's owning client package registers one provider with `ctx.resources.register(provider)` for its own lifetime; a second provider for the same protocol throws ([provide a protocol](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/resources/README.md#provide-a-protocol)). A provider is `{ protocol, open(address, { signal }) }`: `open` yields `RemoteResult` frames — the current state first, one frame per later change — and stops when `signal` aborts; a failure is an `{ ok: false, error }` frame, never a throw, and a throw inside the stream is a programming error the model does not catch.
|
|
123
|
+
|
|
124
|
+
`useResource<P>(address)` is a global standard prop on every slot component, whatever its scope. It returns `{ status, value, failure }`: `none` when the address's protocol has no provider or the address is not a resource address (`sidebar://guide` names no resource), `loading` until the first frame, `live` with the latest `ok` value, `failed` with the latest frame's failure beside the last value. ([read a resource](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/resources/README.md#read-a-resource)).
|
|
125
|
+
|
|
126
|
+
A resource stays open while it has a holder — a subscribed `useResource` or a `ctx.resources.pin(address, signal)`; the first holder opens the provider's stream, later holders share it and read the latest value at once, and the last release aborts the stream and discards the value. Streams carry metadata, not content: the `file` value is `{ absolutePath, version, bytes? }`, and a consumer reads file text itself, by page, through the Workspace Files service ([lifecycle](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/resources/README.md#lifecycle)).
|
|
127
|
+
|
|
128
|
+
## Workspace Files
|
|
129
|
+
|
|
130
|
+
The Host `ctx.workspaceFiles` service and generated `workspaceFiles` Remote namespace read files allowed by the Session filesystem backend: `stat(path)` returns `{ absolutePath, version, bytes? }`; `read(path, { offset?, limit? })` returns one page of lines (`offset` 1-based, `limit` capped by the configured page size) as `{ …stat, offset, text, eof }`; `readBytes(path, { offset?, length? })` returns one raw byte window (`offset` 0-based, `length` capped by the configured byte limit) as base64 `{ …stat, offset, data, eof }` with no text decoding. `list(path)` remains inside the workspace root and returns a directory's direct children (`name`, `type: 'file' | 'directory' | 'other'`, `size?`) cut to the configured cap with `truncated` set. `changes()` likewise remains workspace-scoped and yields `{ kind: 'ready' }` once subscribed, then `{ kind: 'change', change }` frames whose payload is `{ absolutePath, version }` or `{ absolutePath, absent: true }` ([README](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/api/workspace-files/README.md#use-this-package)). File operations reject final symlinks and enforce transfer caps; `read` additionally requires UTF-8 text. Failures use `workspace-file/*` codes ([failures](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/api/workspace-files/README.md)).
|
|
131
|
+
|
|
132
|
+
[`dsh-api-workspace-files`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/api/workspace-files/README.md) registers the `file` provider, with `ResourceProtocolMap.file` directly naming `WorkspaceFileStat`. A Session address carries the authorizing Session and a relative or absolute path, passed unchanged to the Host for resolution. The provider waits for Host `ready` before stat and filters changes by `stat.absolutePath`. Bare `absolute` addresses have no authorizing Session and fail with `workspace-file/unknown-workspace`, without borrowing current or Tab Session. Any UI, including Global components, shares the observation for the same complete address. Preview's ordinary Remote callbacks use the Session in that address; Host `readAll` and `readRelated` remain, and Preview's `rpc.ts` decodes byte results.
|
|
133
|
+
|
|
134
|
+
## Shipped types
|
|
135
|
+
|
|
136
|
+
- **`guide`** — `builtin`, opened as `openTab('guide')`. A muted compass sits above one capsule per contributed `guide` entry, in `order`; short lists show registered descriptions, and every missing icon uses the shipped placeholder. Picking a capsule opens the contributing type as a page in the guide tab's place. A pane holds at most one guide tab, and the strip's add control appears only while its pane has none. A new pane receives the registered default page: the sole guide entry directly, or the guide when the entry count is not one ([guide](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-right/README.md#the-guide)).
|
|
137
|
+
- **`text`** — `fallback`, `dsh-resource://file/**`, claiming Session addresses only. Document Preview observes metadata through `useResource<'file'>`, loads content through Remote callbacks, and owns renderer selection, the toolbar, per-tab refresh, scroll, and source navigation; unknown extensions render as plain text ([README](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-documentpreview/README.md)).
|
|
138
|
+
- **`files`** — `builtin`, opened as `openTab('files')`. The workspace directory tree, listed lazily through `list`, opening a file with `tab.actions.openResource(fileAddressFor(sessionId, root, path))` into its own pane ([README](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-sidebar-files/README.md)).
|
|
139
|
+
|
|
140
|
+
<a id="not-built"></a>
|
|
141
|
+
## Not built
|
|
142
|
+
|
|
143
|
+
- Persistence: layout state is memory-only; a reload starts every session collapsed, and no session's tabs are visible from another.
|
|
144
|
+
- A read-only layout snapshot or subscription on `ctx.sidebarRight`: the service exposes operations only, and dockkit's `LayoutState`/`LayoutOp` are internal.
|
|
145
|
+
- A capability-discovery array (`features`) on the service.
|
|
146
|
+
- An `option` priority band for tab types: nothing lists a tab type without letting it claim.
|
|
147
|
+
- Retitling a record: `title(address)` is captured once; a live chip comes from the title slot, not from the record.
|
|
148
|
+
- Naming a tab implementation when opening: `openResource` names a kind at most; document-renderer selection belongs to the file tab's toolbar.
|
|
149
|
+
- An address lookup on the service (`find`): a caller opens with `revealIfOpened` and lets the surface de-duplicate.
|
|
150
|
+
- Navigation addresses beyond the Sidebar's own `sidebar://<kind>` bookkeeping; their grammar waits for the navigation controller as a whole.
|
|
151
|
+
- A user-facing undo, a content navigation stack, and tab icons ([deferred](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/feature/2026-09-04-right-sidebar-docking-infrastructure.md#deferred)).
|