dsh-draw 0.2.15 → 0.2.17

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.
@@ -1,8 +1,14 @@
1
1
  /**
2
- * The keyed `image_generate` tool result card: engine/quota facts and the
3
- * regenerate action. The images themselves are the attachment content blocks
4
- * the shell already renders from the tool result, so the card only adds the
5
- * accounting line and the action — it never duplicates image transport.
2
+ * The keyed `image_generate` tool view: engine/quota facts and the regenerate
3
+ * action once the call settles, and an in-flight row of its own while the
4
+ * arguments are still arriving. The images themselves are the attachment
5
+ * content blocks the shell already renders from the tool result, so the card
6
+ * only adds the accounting line and the action — it never duplicates image
7
+ * transport.
8
+ *
9
+ * The Host dispatches this keyed entry in EVERY stage of the call, and a keyed
10
+ * cell that is occupied never reaches the owner fallback — so the earlier
11
+ * stages cannot be answered with nothing without blanking the row.
6
12
  *
7
13
  * The `tool.call.toolview` SlotMap member is declared locally (mirroring the
8
14
  * harness's own ui-tool contract declaration, which its package index does not
@@ -13,20 +19,30 @@
13
19
 
14
20
  import { createElement as h, useState, type ReactElement } from 'react'
15
21
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
16
- import { presentDrawResult, type ToolCallBlock } from './present.ts'
22
+ import { presentDrawResult, toolCallPhase, type ToolCallBlock, type ToolCallPhase } from './present.ts'
17
23
 
18
24
  /**
19
- * Standard owner currency for one keyed tool view (mirror of the harness
20
- * ui-tool contract `ToolCallOwnerProps`, which the package index does not
21
- * re-export — keep the shape identical so the declarations merge).
25
+ * Stage-specific tool data, mirror of the harness ui-tool contract
26
+ * `ToolCallPhaseProps`. The Host dispatches the SAME keyed entry in every
27
+ * stage, so the card's own contract must discriminate on `phase` exactly as
28
+ * the Host's does — a narrower declaration here hides the preparing owner
29
+ * rather than rejecting it (the slot is occupied for every stage).
22
30
  */
23
- export interface ToolCallOwnerProps {
24
- /** Tool call identity, stable across running and settled forms. */
31
+ export type ToolCallPhaseProps =
32
+ | { readonly phase: 'preparing'; readonly block: ToolCallBlock }
33
+ | { readonly phase: 'start'; readonly block: ToolCallBlock }
34
+ | { readonly phase: 'result'; readonly block: ToolCallBlock }
35
+
36
+ /**
37
+ * Common owner currency declared beside the phase share, mirror of the
38
+ * harness ui-tool contract `ToolCallCommonProps` (which the package index
39
+ * does not re-export — keep the shape identical so the declarations merge).
40
+ */
41
+ export interface ToolCallCommonProps {
42
+ /** Tool call identity, stable across all stages. */
25
43
  callId: string
26
44
  /** Wire tool name and keyed dispatch value. */
27
45
  toolName: string
28
- /** Frozen running call or settled result node. */
29
- block: ToolCallBlock
30
46
  /** Session workspace root for relative summaries. */
31
47
  cwd?: string
32
48
  /** Open a tool argument path through the host. */
@@ -35,6 +51,12 @@ export interface ToolCallOwnerProps {
35
51
  inspect?: () => void
36
52
  }
37
53
 
54
+ /**
55
+ * Standard owner currency for one keyed tool view: the common share plus the
56
+ * stage share, mirroring the harness ui-tool contract `ToolCallOwnerProps`.
57
+ */
58
+ export type ToolCallOwnerProps = ToolCallCommonProps & ToolCallPhaseProps
59
+
38
60
  declare module '@deepseek-ai/dsh-client-ui-slots' {
39
61
  interface SlotMap {
40
62
  /** Keyed atomic tool-call view, dispatched by the wire tool name. */
@@ -55,17 +77,55 @@ export type DrawResultCardProps =
55
77
  & InjectFace<DrawResultCardInjected>
56
78
 
57
79
  /**
58
- * The keyed tool result card.
59
- * @param props - owner currency, locale, and injected bindings.
60
- * @returns the card element.
80
+ * The keyed tool view. Dispatched by the wire tool name in EVERY stage of the
81
+ * call, so the card owes a presentation to the in-flight stages too: returning
82
+ * an empty node would leave the row blank, because an occupied keyed cell
83
+ * never reaches the Host's own generic row.
84
+ *
85
+ * @param props - owner currency (common + stage shares), locale, and injected bindings.
86
+ * @returns the in-flight row, or the settled result card.
61
87
  */
62
88
  export function DrawResultCard(props: DrawResultCardProps): ReactElement {
89
+ // The owner's discriminant and the block are two statements of the same fact,
90
+ // and the block is the one the presenter reads — so the card classifies the
91
+ // stage from the block, through the same function the presenter uses. The
92
+ // owner's arm only breaks the tie in the impossible case of a `result` owner
93
+ // carrying a block that is not a settled node.
94
+ const phase = toolCallPhase(props.block)
95
+ if (phase !== 'result') {
96
+ const stage = props.phase === 'result' ? phase : props.phase
97
+ return h('div', { className: 'dshdraw-inflight', 'aria-busy': 'true', 'data-phase': stage },
98
+ h('span', { className: 'dshdraw-inflight-title' }, props.t('row.title')),
99
+ h('span', { className: 'dshdraw-sep', 'aria-hidden': 'true' }, '·'),
100
+ h('span', { className: 'dshdraw-inflight-summary' }, props.toolName),
101
+ h('span', { className: 'dshdraw-inflight-state' }, props.t(inFlightKey(stage))),
102
+ )
103
+ }
104
+ return h(DrawResultCardBody, props)
105
+ }
106
+
107
+ /** Locale key naming the stage of a call that has not settled yet. */
108
+ function inFlightKey(phase: ToolCallPhase): 'row.preparing' | 'row.running' {
109
+ return phase === 'preparing' ? 'row.preparing' : 'row.running'
110
+ }
111
+
112
+ /**
113
+ * The settled result card: engine/quota facts and the regenerate action.
114
+ *
115
+ * @param props - the owner at its `result` stage, locale, and injected bindings.
116
+ * @returns the card element.
117
+ */
118
+ function DrawResultCardBody(props: DrawResultCardProps): ReactElement {
63
119
  const { block, regenerate, t } = props
64
120
  const presented = presentDrawResult(block)
65
121
  const [busy, setBusy] = useState(false)
66
122
  const [failed, setFailed] = useState(false)
67
123
 
68
- if (presented === undefined) return h('div', null)
124
+ if (presented === undefined) {
125
+ // The Host handed this card a settled node it does not own (a foreign call
126
+ // head after window truncation, or an error result) — nothing to add.
127
+ return h('div', null)
128
+ }
69
129
 
70
130
  const runRegenerate = async (): Promise<void> => {
71
131
  if (presented.args === undefined) return
@@ -15,6 +15,9 @@ export type DrawLocaleKey =
15
15
  | 'result.regenerate'
16
16
  | 'result.regenerating'
17
17
  | 'result.failed'
18
+ | 'row.title'
19
+ | 'row.preparing'
20
+ | 'row.running'
18
21
  | 'tab'
19
22
  | 'tab.engines'
20
23
  | 'tab.preferred'
@@ -40,6 +43,9 @@ export const en: Record<DrawLocaleKey, string> = {
40
43
  'result.regenerate': 'Regenerate',
41
44
  'result.regenerating': 'Regenerating…',
42
45
  'result.failed': 'Regenerate failed',
46
+ 'row.title': 'Tool call',
47
+ 'row.preparing': 'Preparing tool call',
48
+ 'row.running': 'Running',
43
49
  'tab': 'Image generation',
44
50
  'tab.engines': 'Engines',
45
51
  'tab.preferred': 'preferred',
@@ -66,6 +72,9 @@ export const zh: Record<DrawLocaleKey, string> = {
66
72
  'result.regenerate': '重新生成',
67
73
  'result.regenerating': '正在重新生成…',
68
74
  'result.failed': '重新生成失败',
75
+ 'row.title': '工具调用',
76
+ 'row.preparing': '正在准备调用',
77
+ 'row.running': '运行中',
69
78
  'tab': '图像生成',
70
79
  'tab.engines': '引擎',
71
80
  'tab.preferred': '首选',
@@ -51,6 +51,88 @@ export type ToolCallBlock = RunningToolCallBlock | SettledToolResultBlock
51
51
  /** Settled tool-result node only. */
52
52
  export type ToolResultNode = SettledToolResultBlock
53
53
 
54
+ /**
55
+ * The Host's tool-call-view stage discriminant. The Host splits the owner
56
+ * currency into `preparing` / `start` / `result`, each carrying its own stage
57
+ * block, and dispatches the SAME keyed entry in every stage — so a keyed card
58
+ * receives a preparing owner whose block has no `kind` and no `argsRaw`.
59
+ *
60
+ * {@link TOOL_CALL_PHASES} is the single source of truth for this union: the
61
+ * literal type is derived from the array, so a value and its type can never
62
+ * drift apart, and `scripts/verify-host-contract.mjs` compares the array
63
+ * against the Host's own `ToolCallPhaseProps` declaration.
64
+ */
65
+ export const TOOL_CALL_PHASES = ['preparing', 'start', 'result'] as const
66
+
67
+ /** One stage of the Host's tool-call view. */
68
+ export type ToolCallPhase = (typeof TOOL_CALL_PHASES)[number]
69
+
70
+ /**
71
+ * Classify a frozen tool block into its Host stage, mirroring the Host's own
72
+ * `toolCallPhase`: a settled node wins on `kind`, and the running family splits
73
+ * on `phase`. `ToolCallBlock` is structurally narrower than the Host union
74
+ * (`RunningToolCallBlock` declares no `phase` field), so the running arm is
75
+ * read through a structural probe rather than a declared member.
76
+ *
77
+ * @param block - the frozen tool block the owner delivered.
78
+ * @returns the stage this block belongs to.
79
+ */
80
+ export function toolCallPhase(block: ToolCallBlock): ToolCallPhase {
81
+ if ('kind' in block) return 'result'
82
+ return (block as { phase?: unknown }).phase === 'preparing' ? 'preparing' : 'start'
83
+ }
84
+
85
+ /** How a card presents one stage of its own keyed tool call. */
86
+ export interface PresentedToolCallPhase {
87
+ /** The Host stage this presentation covers. */
88
+ phase: ToolCallPhase
89
+ /**
90
+ * Whether this stage carries a settled result to present, or whether the
91
+ * card must render its own in-flight row for a call still arriving.
92
+ */
93
+ state: 'result' | 'in-flight'
94
+ }
95
+
96
+ /**
97
+ * Project the card's one appearance per Host stage.
98
+ *
99
+ * Only the `result` stage carries the engine/quota facts and the regenerate
100
+ * action. The earlier stages have no arguments and no result, and the keyed
101
+ * slot is OCCUPIED for them — the Host's own generic row is unreachable
102
+ * (the renderer falls back only for an empty cell) — so the card owning an
103
+ * in-flight row is what keeps the row visible while the call streams.
104
+ *
105
+ * A stage outside the declared vocabulary is reported loudly instead of being
106
+ * silently swept into `preparing`; the `switch` is exhaustive over
107
+ * {@link ToolCallPhase}, so a new stage fails the build before it can reach
108
+ * this default.
109
+ *
110
+ * @param phase - the stage classified from the owner's block.
111
+ * @returns the presentation this card owes that stage.
112
+ */
113
+ export function presentToolCallPhase(phase: ToolCallPhase): PresentedToolCallPhase {
114
+ switch (phase) {
115
+ case 'result':
116
+ return { phase, state: 'result' }
117
+ case 'preparing':
118
+ case 'start':
119
+ return { phase, state: 'in-flight' }
120
+ default:
121
+ return exhaustiveToolCallPhase(phase)
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Compile-time completeness check for the stage vocabulary: a new Host stage
127
+ * added to {@link ToolCallPhase} without a branch above fails the build here.
128
+ *
129
+ * @param phase - the unhandled stage.
130
+ * @returns never; throws at runtime.
131
+ */
132
+ function exhaustiveToolCallPhase(phase: never): never {
133
+ throw new Error(`unhandled tool-call phase: ${JSON.stringify(phase)}`)
134
+ }
135
+
54
136
  /** One image of the presented result card. */
55
137
  export interface PresentedImage {
56
138
  attachmentId: string
@@ -78,11 +160,19 @@ export interface PresentedDrawResult {
78
160
  /**
79
161
  * Project one settled `image_generate` tool block onto the card model.
80
162
  *
163
+ * The stage guard is explicit, not incidental: only the Host's `result` stage
164
+ * carries `kind`, and the preparing stage's block is an ordinary object, so
165
+ * an `undefined` return is the presenter's answer for every non-result stage
166
+ * as well as for a foreign tool, an error result, or a window-truncated call
167
+ * head. The card pairs that answer with {@link toolCallPhase} to render its
168
+ * in-flight row instead of an empty node.
169
+ *
81
170
  * @param block - the frozen tool-call block (running or settled).
82
171
  * @returns the presented model, or `undefined` when the block is not a settled
83
172
  * image_generate result.
84
173
  */
85
174
  export function presentDrawResult(block: ToolCallBlock): PresentedDrawResult | undefined {
175
+ if (toolCallPhase(block) !== 'result') return undefined
86
176
  if (!('kind' in block) || block.kind !== 'tool-result' || block.isError) return undefined
87
177
  if (block.call?.name !== 'image_generate') return undefined
88
178
  const value = (block.meta as { engine?: unknown; model?: unknown; fallbackUsed?: unknown; images?: unknown; quota?: unknown; limits?: unknown } | undefined) ?? {}
@@ -31,6 +31,11 @@ export function installDrawStyles(): () => void {
31
31
  .dshdraw-figure img { max-width: 100%; border-radius: 6px; border: 1px solid var(--dsh-border, #d0d7de); }
32
32
  .dshdraw-figure figcaption { font-size: 11px; opacity: 0.75; overflow-wrap: anywhere; }
33
33
  .dshdraw-meta { font-size: 12px; opacity: 0.85; display: flex; flex-wrap: wrap; gap: 12px; }
34
+ .dshdraw-inflight { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; font-size: 13px; min-height: 20px; }
35
+ .dshdraw-inflight .dshdraw-sep { opacity: 0.45; }
36
+ .dshdraw-inflight-title { font-weight: 500; }
37
+ .dshdraw-inflight-summary { opacity: 0.85; overflow-wrap: anywhere; }
38
+ .dshdraw-inflight-state { font-size: 12px; opacity: 0.7; }
34
39
  .dshdraw-actions { display: flex; gap: 8px; }
35
40
  .dshdraw-button {
36
41
  border: 1px solid var(--dsh-border, #d0d7de); border-radius: 6px; background: transparent;
package/src/version.ts CHANGED
@@ -8,4 +8,4 @@
8
8
  */
9
9
 
10
10
  /** Plugin version; must equal the `version` field in `package.json`. */
11
- export const PLUGIN_VERSION = '0.2.15'
11
+ export const PLUGIN_VERSION = '0.2.17'