dsh-winrm 0.3.1 → 0.4.1

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.
@@ -6,6 +6,7 @@
6
6
  // Run from the repo root so `winrm-client` resolves:
7
7
  // node --experimental-strip-types scripts/verify-live.mts
8
8
  import { HostStore } from '../src/store.ts'
9
+ import { WinRmEngine } from '../src/engine.ts'
9
10
  import { connOf, runScript, TRANSFER_CHUNK, uploadBuffer, downloadChunks, remoteFileSize } from '../src/engine/client.ts'
10
11
  import { psWriteChunk, psReadChunk, psFileSize } from '../src/powershell.ts'
11
12
 
@@ -106,6 +107,121 @@ console.log('=== upload/download round trip ===')
106
107
  await runScript(conn, `Remove-Item -LiteralPath 'C:\\temp\\dsh-winrm-verify' -Recurse -Force -ErrorAction SilentlyContinue`, { timeoutMs: 60_000 })
107
108
  }
108
109
 
110
+ // Remote text read/write/edit: the three tools that act on the target's files
111
+ // rather than on the host's. Round-trips through the real encoding detection.
112
+ console.log('=== remote text read/write/edit ===')
113
+ {
114
+ const engine = new WinRmEngine(store)
115
+ const dir = 'C:\\temp\\dsh-winrm-verify-text'
116
+ const alias = entry.alias
117
+ const body = ['alpha', '中文行 ✓', '', 'beta alpha', 'omega'].join('\r\n') + '\r\n'
118
+
119
+ const created = await engine.writeFile(alias, `${dir}\\utf8.txt`, body, 'utf8')
120
+ const createOk = created.operation === 'create' && created.bytes === Buffer.byteLength(body, 'utf8')
121
+ if (!createOk) bad++
122
+ console.log(`${createOk ? 'ok ' : 'BAD '} write create utf8: op=${created.operation} bytes=${created.bytes}`)
123
+
124
+ const updated = await engine.writeFile(alias, `${dir}\\utf8.txt`, body, 'utf8')
125
+ const updateOk = updated.operation === 'update'
126
+ if (!updateOk) bad++
127
+ console.log(`${updateOk ? 'ok ' : 'BAD '} write update utf8: op=${updated.operation}`)
128
+
129
+ const window = await engine.readFile(alias, `${dir}\\utf8.txt`, 1, 100)
130
+ const readOk = window.totalLines === 5
131
+ && window.encoding === 'utf8'
132
+ && window.lines[0]?.text === 'alpha'
133
+ && window.lines[1]?.text === '中文行 ✓'
134
+ && window.lines[2]?.text === ''
135
+ && window.lines[4]?.text === 'omega'
136
+ if (!readOk) bad++
137
+ console.log(`${readOk ? 'ok ' : 'BAD '} read whole: total=${window.totalLines} enc=${window.encoding} lines=${JSON.stringify(window.lines.map(l => l.text))}`)
138
+
139
+ // Paging must start where the caller asked, or a model paging a log would
140
+ // silently re-read the top forever.
141
+ const tail = await engine.readFile(alias, `${dir}\\utf8.txt`, 4, 2)
142
+ const pageOk = tail.lines.length === 2 && tail.lines[0]?.number === 4 && tail.lines[0]?.text === 'beta alpha'
143
+ if (!pageOk) bad++
144
+ console.log(`${pageOk ? 'ok ' : 'BAD '} read page offset=4 limit=2: ${JSON.stringify(tail.lines)}`)
145
+
146
+ const edited = await engine.editFile(alias, `${dir}\\utf8.txt`, 'omega', 'OMEGA', false)
147
+ const editOk = edited.replacements === 1 && edited.encoding === 'utf8'
148
+ if (!editOk) bad++
149
+ console.log(`${editOk ? 'ok ' : 'BAD '} edit single: replacements=${edited.replacements} size=${edited.size}`)
150
+
151
+ const afterEdit = await engine.readFile(alias, `${dir}\\utf8.txt`, 1, 100)
152
+ const persisted = afterEdit.lines[4]?.text === 'OMEGA'
153
+ if (!persisted) bad++
154
+ console.log(`${persisted ? 'ok ' : 'BAD '} edit persisted: line5=${JSON.stringify(afterEdit.lines[4]?.text)}`)
155
+
156
+ // `alpha` appears twice, so a single-replacement edit must refuse rather than
157
+ // guess which one the caller meant.
158
+ let ambiguous = ''
159
+ try {
160
+ await engine.editFile(alias, `${dir}\\utf8.txt`, 'alpha', 'ALPHA', false)
161
+ } catch (error) {
162
+ ambiguous = error instanceof Error ? error.message : String(error)
163
+ }
164
+ const ambiguousOk = ambiguous.includes('more than once')
165
+ if (!ambiguousOk) bad++
166
+ console.log(`${ambiguousOk ? 'ok ' : 'BAD '} edit refuses ambiguous needle: ${JSON.stringify(ambiguous)}`)
167
+
168
+ const all = await engine.editFile(alias, `${dir}\\utf8.txt`, 'alpha', 'ALPHA', true)
169
+ const allOk = all.replacements === 2
170
+ if (!allOk) bad++
171
+ console.log(`${allOk ? 'ok ' : 'BAD '} edit replaceAll: replacements=${all.replacements}`)
172
+
173
+ // A needle that is gone must fail loudly: silently succeeding is how a stale
174
+ // edit looks successful while changing nothing.
175
+ let missing = ''
176
+ try {
177
+ await engine.editFile(alias, `${dir}\\utf8.txt`, 'not-there-at-all', 'x', false)
178
+ } catch (error) {
179
+ missing = error instanceof Error ? error.message : String(error)
180
+ }
181
+ const missingOk = missing.includes('was not found')
182
+ if (!missingOk) bad++
183
+ console.log(`${missingOk ? 'ok ' : 'BAD '} edit reports a missing needle: ${JSON.stringify(missing)}`)
184
+
185
+ // UTF-16LE with a BOM must come back as UTF-16LE, not as mojibake.
186
+ const utf16 = await engine.writeFile(alias, `${dir}\\utf16.txt`, '第一行\r\n第二行\r\n', 'utf16le')
187
+ const utf16Read = await engine.readFile(alias, `${dir}\\utf16.txt`, 1, 10)
188
+ const utf16Ok = utf16Read.encoding === 'utf16le' && utf16Read.lines[0]?.text === '第一行' && utf16Read.lines[1]?.text === '第二行'
189
+ if (!utf16Ok) bad++
190
+ console.log(`${utf16Ok ? 'ok ' : 'BAD '} utf16le round trip: bytes=${utf16.bytes} enc=${utf16Read.encoding} lines=${JSON.stringify(utf16Read.lines.map(l => l.text))}`)
191
+
192
+ // A UTF-8 BOM must be preserved by an edit, or the next reader misdetects it.
193
+ await engine.writeFile(alias, `${dir}\\bom.txt`, 'one\r\ntwo\r\n', 'utf8bom')
194
+ const bomEdit = await engine.editFile(alias, `${dir}\\bom.txt`, 'two', 'TWO', false)
195
+ const bomRead = await engine.readFile(alias, `${dir}\\bom.txt`, 1, 10)
196
+ const bomOk = bomEdit.encoding === 'utf8bom' && bomRead.encoding === 'utf8bom' && bomRead.lines[1]?.text === 'TWO'
197
+ if (!bomOk) bad++
198
+ console.log(`${bomOk ? 'ok ' : 'BAD '} utf8bom preserved through an edit: edit=${bomEdit.encoding} read=${bomRead.encoding}`)
199
+
200
+ // A binary file has no line structure; the reader must say so instead of
201
+ // returning U+FFFD soup.
202
+ let binary = ''
203
+ try {
204
+ await engine.readFile(alias, 'C:\\Windows\\System32\\notepad.exe', 1, 5)
205
+ } catch (error) {
206
+ binary = error instanceof Error ? error.message : String(error)
207
+ }
208
+ const binaryOk = binary.length > 0
209
+ if (!binaryOk) bad++
210
+ console.log(`${binaryOk ? 'ok ' : 'BAD '} binary read fails loudly: ${JSON.stringify(binary.slice(0, 120))}`)
211
+
212
+ let absent = ''
213
+ try {
214
+ await engine.readFile(alias, `${dir}\\nope.txt`, 1, 5)
215
+ } catch (error) {
216
+ absent = error instanceof Error ? error.message : String(error)
217
+ }
218
+ const absentOk = absent.includes('not found')
219
+ if (!absentOk) bad++
220
+ console.log(`${absentOk ? 'ok ' : 'BAD '} read reports a missing file: ${JSON.stringify(absent)}`)
221
+
222
+ await runScript(conn, `Remove-Item -LiteralPath '${dir}' -Recurse -Force -ErrorAction SilentlyContinue`, { timeoutMs: 60_000 })
223
+ }
224
+
109
225
  // Sanity: a script that never reaches the envelope must not claim success.
110
226
  {
111
227
  const r = await runScript(conn, `Write-Output 'ok'`, { timeoutMs: 60_000 })
@@ -23,6 +23,7 @@ import type { MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client'
23
23
  import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
24
24
  import { WinrmApi } from './api.ts'
25
25
  import { en, zh, type WinKey } from './locales.ts'
26
+ import { WinrmEditCard, WinrmReadCard, WinrmWriteCard } from './tool/cards.tsx'
26
27
  import { WinrmPanel } from './panel/WinrmPanel.tsx'
27
28
  import { WinrmPanelIcon } from './panel/WinrmPanelIcon.tsx'
28
29
 
@@ -71,6 +72,22 @@ export function apply(ctx: ClientContext): void {
71
72
  inject: () => ({ api, onBack: () => { ctx.layout.selectPanel(null) } }),
72
73
  }, WinrmPanel))
73
74
 
75
+ // Transcript cards for the three remote file tools, keyed by wire tool name.
76
+ // Without an entry here each call renders as the generic tool row, so the
77
+ // user reads the same envelope text the model reads. The cards read the
78
+ // metadata the host persists with each result.
79
+ for (const [key, card] of [
80
+ ['winrm_read', WinrmReadCard],
81
+ ['winrm_write', WinrmWriteCard],
82
+ ['winrm_edit', WinrmEditCard],
83
+ ] as const) {
84
+ ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
85
+ name: 'tool.call.toolview',
86
+ key,
87
+ locale: NS,
88
+ }, card))
89
+ }
90
+
74
91
  // The sidebar row: an icon plus a localized label; selecting it opens the panel.
75
92
  ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
76
93
  name: 'sidebar.panellist',
@@ -114,6 +114,28 @@ export const zh = {
114
114
  'transfer.mib': 'MB',
115
115
  'transfer.dir': '目录',
116
116
  'transfer.file': '文件',
117
+ 'tool.read': '读取远程文件',
118
+ 'tool.write': '写入远程文件',
119
+ 'tool.edit': '编辑远程文件',
120
+ 'tool.running': '执行中…',
121
+ 'tool.noDetail': '无卡片数据',
122
+ 'tool.created': '新建 {bytes} 字节',
123
+ 'tool.updated': '覆盖 {bytes} 字节',
124
+ 'tool.replacements': '替换 {count} 处',
125
+ 'tool.beyondBasis': '文件超过 diff 体积上限,仅显示路径',
126
+ 'tool.truncatedByBytes': '已按字节上限截断',
127
+ 'tool.truncatedLines': '{count} 行过长已截断',
128
+ 'tool.codeLabel': '代码',
129
+ 'tool.wrapLabel': '自动换行',
130
+ 'tool.unwrapLabel': '取消自动换行',
131
+ 'tool.window': '第 {shown} / {total} 行',
132
+ 'tool.lines': '共 {count} 行',
133
+ 'tool.copy': '复制',
134
+ 'tool.copied': '已复制',
135
+ 'tool.collapseAria': '收起',
136
+ 'tool.expandAria': '展开剩余 {hidden} 行',
137
+ 'tool.collapse': '收起',
138
+ 'tool.expand': '展开其余 {hidden} 行',
117
139
  } as const
118
140
 
119
141
  export const en = {
@@ -227,6 +249,28 @@ export const en = {
227
249
  'transfer.mib': 'MB',
228
250
  'transfer.dir': 'dir',
229
251
  'transfer.file': 'file',
252
+ 'tool.read': 'Read remote file',
253
+ 'tool.write': 'Write remote file',
254
+ 'tool.edit': 'Edit remote file',
255
+ 'tool.running': 'Working…',
256
+ 'tool.noDetail': 'No card data',
257
+ 'tool.created': 'Created {bytes} bytes',
258
+ 'tool.updated': 'Overwrote {bytes} bytes',
259
+ 'tool.replacements': '{count} replacements',
260
+ 'tool.beyondBasis': 'File is past the diff size cap; showing the path only',
261
+ 'tool.truncatedByBytes': 'Cut at the byte budget',
262
+ 'tool.truncatedLines': '{count} long lines truncated',
263
+ 'tool.codeLabel': 'Code',
264
+ 'tool.wrapLabel': 'Wrap lines',
265
+ 'tool.unwrapLabel': 'Do not wrap',
266
+ 'tool.window': '{shown} of {total} lines',
267
+ 'tool.lines': '{count} lines',
268
+ 'tool.copy': 'Copy',
269
+ 'tool.copied': 'Copied',
270
+ 'tool.collapseAria': 'Collapse',
271
+ 'tool.expandAria': 'Expand the remaining {hidden} lines',
272
+ 'tool.collapse': 'Collapse',
273
+ 'tool.expand': 'Show the remaining {hidden} lines',
230
274
  } as const
231
275
 
232
276
  export type WinKey = keyof typeof zh
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Card models for the three remote file tools.
3
+ *
4
+ * The host projects a small JSON payload through `output.presentationMeta` and
5
+ * the session log persists it; these functions narrow that unknown payload into
6
+ * something the card components can hand to the harness's `ReadBlock` and
7
+ * `DiffBlock` primitives. Kept apart from the components so the narrowing can be
8
+ * unit tested without a DOM.
9
+ * @module dsh-winrm/src/client/tool/card-model
10
+ */
11
+
12
+ /** One line of a read window. */
13
+ export interface ReadCardLine {
14
+ number: number
15
+ text: string
16
+ }
17
+
18
+ /** A completed `winrm_read`: a line-numbered window of a remote file. */
19
+ export interface ReadCard {
20
+ kind: 'read'
21
+ path: string
22
+ offset: number
23
+ lines: ReadCardLine[]
24
+ totalLines: number
25
+ encoding: string
26
+ truncatedByBytes: boolean
27
+ truncatedLines: number
28
+ }
29
+
30
+ /** One hunk of a remote write or edit, in the primitive's own shape. */
31
+ export interface DiffHunk {
32
+ /** Prior fragment including shared context; `''` when the hunk had no prior content. */
33
+ oldText: string
34
+ /** Resulting fragment including the same shared context. */
35
+ newText: string
36
+ }
37
+
38
+ /** A completed `winrm_write` or `winrm_edit`: the changes it applied. */
39
+ export interface DiffCard {
40
+ kind: 'diff'
41
+ /** Which tool produced it, so the header can name the operation. */
42
+ tool: 'write' | 'edit'
43
+ path: string
44
+ operation: 'create' | 'update'
45
+ bytes: number
46
+ size: number
47
+ replacements: number
48
+ encoding: string
49
+ hunks: DiffHunk[]
50
+ /** True when the previous text was past the basis cap, so no hunks exist. */
51
+ beyondBasis: boolean
52
+ }
53
+
54
+ /** A failed call: keeps the path and the message, drops card-specific detail. */
55
+ export interface ErrorCard {
56
+ kind: 'error'
57
+ path: string
58
+ message: string
59
+ }
60
+
61
+ /** Any card these tools render. */
62
+ export type FileCard = ReadCard | DiffCard | ErrorCard
63
+
64
+ /** Read one string field, falling back when it is absent or the wrong type. */
65
+ function str(source: Record<string, unknown>, key: string, fallback: string): string {
66
+ const value = source[key]
67
+ return typeof value === 'string' ? value : fallback
68
+ }
69
+
70
+ /** Read one finite number field, falling back when it is absent or not numeric. */
71
+ function num(source: Record<string, unknown>, key: string, fallback: number): number {
72
+ const value = source[key]
73
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback
74
+ }
75
+
76
+ /** Read one boolean field, defaulting to `false`. */
77
+ function bool(source: Record<string, unknown>, key: string): boolean {
78
+ return source[key] === true
79
+ }
80
+
81
+ /** Narrow an unknown value to a plain record. */
82
+ function record(value: unknown): Record<string, unknown> | undefined {
83
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
84
+ ? value as Record<string, unknown>
85
+ : undefined
86
+ }
87
+
88
+ /**
89
+ * Narrow a `winrm_read` metadata payload into a card.
90
+ * @param meta - the persisted payload, of unknown shape.
91
+ * @param fallbackPath - the authored path, used when the payload omits one.
92
+ * @returns the card, or `undefined` when the payload is not a read card.
93
+ */
94
+ export function readCard(meta: unknown, fallbackPath: string): FileCard | undefined {
95
+ const source = record(meta)
96
+ if (source === undefined || source.card !== 'winrm-read') return undefined
97
+ const path = str(source, 'path', fallbackPath)
98
+ const error = str(source, 'error', '')
99
+ if (error !== '') return { kind: 'error', path, message: error }
100
+ const rawLines = Array.isArray(source.lines) ? source.lines : []
101
+ const lines: ReadCardLine[] = []
102
+ for (const entry of rawLines) {
103
+ const line = record(entry)
104
+ if (line === undefined) continue
105
+ lines.push({ number: num(line, 'number', lines.length + 1), text: str(line, 'text', '') })
106
+ }
107
+ return {
108
+ kind: 'read',
109
+ path,
110
+ offset: num(source, 'offset', 1),
111
+ lines,
112
+ totalLines: num(source, 'totalLines', lines.length),
113
+ encoding: str(source, 'encoding', 'utf8'),
114
+ truncatedByBytes: bool(source, 'truncatedByBytes'),
115
+ truncatedLines: num(source, 'truncatedLines', 0),
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Narrow a `winrm_write` / `winrm_edit` metadata payload into a card.
121
+ * @param meta - the persisted payload, of unknown shape.
122
+ * @param fallbackPath - the authored path, used when the payload omits one.
123
+ * @param tool - which tool produced the payload.
124
+ * @returns the card, or `undefined` when the payload belongs to another card.
125
+ */
126
+ export function diffCard(meta: unknown, fallbackPath: string, tool: 'write' | 'edit'): FileCard | undefined {
127
+ const source = record(meta)
128
+ if (source === undefined || source.card !== `winrm-${tool}`) return undefined
129
+ const path = str(source, 'path', fallbackPath)
130
+ const error = str(source, 'error', '')
131
+ if (error !== '') return { kind: 'error', path, message: error }
132
+ const rawDiffs = Array.isArray(source.diffs) ? source.diffs : []
133
+ const hunks: DiffHunk[] = []
134
+ for (const entry of rawDiffs) {
135
+ const hunk = record(entry)
136
+ if (hunk === undefined) continue
137
+ hunks.push({ oldText: str(hunk, 'oldText', ''), newText: str(hunk, 'newText', '') })
138
+ }
139
+ return {
140
+ kind: 'diff',
141
+ tool,
142
+ path,
143
+ operation: source.operation === 'create' ? 'create' : 'update',
144
+ bytes: num(source, 'bytes', 0),
145
+ size: num(source, 'size', 0),
146
+ replacements: num(source, 'replacements', 0),
147
+ encoding: str(source, 'encoding', 'utf8'),
148
+ hunks,
149
+ beyondBasis: bool(source, 'beyondBasis'),
150
+ }
151
+ }
@@ -0,0 +1,128 @@
1
+ /*
2
+ * Only the pieces this plugin draws itself: the failed-call row, the collapsed
3
+ * header line, and the summary note under a card. The card bodies are the
4
+ * harness's own `ReadBlock` / `DiffBlock` primitives, which carry their own
5
+ * styles.
6
+ *
7
+ * The header line copies the measurements of the harness's own tool rows
8
+ * (`ui-tool`'s `ToolRow.module.css`) rather than approximating them, so a
9
+ * remote file row sits in the transcript exactly like a local one:
10
+ * `[16 leading] gap6 [title] gap8 [2px dot] gap8 [path fill] [trailing figure]`.
11
+ */
12
+
13
+ /* Hover scope for the trailing +/- totals: the harness tints them only while
14
+ the row is hovered or expanded, keeping a quiet transcript quiet. */
15
+ .wrap {
16
+ display: flex;
17
+ flex-direction: column;
18
+ min-width: 0;
19
+ }
20
+
21
+ .wrap:hover .added,
22
+ .wrap:focus-within .added {
23
+ color: var(--dsw-alias-state-success-primary);
24
+ }
25
+
26
+ .wrap:hover .removed,
27
+ .wrap:focus-within .removed {
28
+ color: var(--dsw-alias-state-error-primary);
29
+ }
30
+
31
+ /* The separator between the verb and the path: a 2px dot, not a character, so
32
+ it never picks up the text font's metrics. */
33
+ .sep {
34
+ flex: none;
35
+ width: 2px;
36
+ height: 2px;
37
+ border-radius: 1px;
38
+ margin: 0 8px;
39
+ background: var(--dsw-alias-label-caption);
40
+ }
41
+
42
+ /* The remote path hugs its text and clips when the row is narrow, so the
43
+ trailing figure reads as part of the same sentence instead of being pushed to
44
+ the far edge of a wide row. Same treatment the harness gives a file path. */
45
+ .summary {
46
+ flex: 0 1 auto;
47
+ min-width: 0;
48
+ overflow: hidden;
49
+ text-overflow: ellipsis;
50
+ white-space: nowrap;
51
+ font-size: var(--dsh-content-font-size-secondary, 13px);
52
+ line-height: calc(24px + var(--dsh-content-font-delta, 0px));
53
+ font-variant-numeric: tabular-nums;
54
+ }
55
+
56
+ /* Trailing figure kept outside the path's ellipsis: a line count or a status
57
+ word whose whole value is that it survives a narrow row. */
58
+ .meta {
59
+ flex: none;
60
+ margin-left: 8px;
61
+ white-space: nowrap;
62
+ font-size: var(--dsh-content-font-size-secondary, 13px);
63
+ line-height: calc(24px + var(--dsh-content-font-delta, 0px));
64
+ font-variant-numeric: tabular-nums;
65
+ }
66
+
67
+ /* The `+n -m` figure a write or edit row shows beside its path: the code font,
68
+ matching the diff body it summarizes, two px under the secondary tier
69
+ because mono digits read optically larger than the sans path. */
70
+ .diffStat {
71
+ flex: none;
72
+ margin-left: 10px;
73
+ white-space: nowrap;
74
+ font-family: var(--ds-font-family-code);
75
+ font-size: calc(var(--dsh-content-font-size-secondary, 13px) - 2px);
76
+ line-height: calc(24px + var(--dsh-content-font-delta, 0px));
77
+ transform: translateY(0.5px);
78
+ font-variant-numeric: tabular-nums;
79
+ }
80
+
81
+ .added,
82
+ .removed {
83
+ color: inherit;
84
+ }
85
+
86
+ /* One-line row for a call that failed: the message replaces the card. */
87
+ .rowError {
88
+ display: flex;
89
+ align-items: baseline;
90
+ gap: 8px;
91
+ padding: 2px 0;
92
+ font-family: var(--dsw-font-family);
93
+ font-size: 12px;
94
+ line-height: 1.55;
95
+ }
96
+
97
+ .title {
98
+ flex: none;
99
+ color: var(--dsw-alias-label-secondary);
100
+ }
101
+
102
+ .detail {
103
+ min-width: 0;
104
+ color: var(--dsw-alias-label-tertiary);
105
+ overflow: hidden;
106
+ text-overflow: ellipsis;
107
+ white-space: nowrap;
108
+ }
109
+
110
+ .rowError .title {
111
+ color: var(--dsw-alias-state-error-primary);
112
+ }
113
+
114
+ .rowError .detail {
115
+ color: var(--dsw-alias-state-error-primary);
116
+ white-space: normal;
117
+ overflow-wrap: anywhere;
118
+ }
119
+
120
+ /* The note under an expanded card: encoding, truncation, replacement count. */
121
+ .note {
122
+ padding: 4px 0 0;
123
+ color: var(--dsw-alias-label-tertiary);
124
+ font-family: var(--dsw-font-family);
125
+ font-size: 12px;
126
+ line-height: 1.55;
127
+ font-variant-numeric: tabular-nums;
128
+ }