dsh-smooth-stream 0.6.1 → 0.7.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.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * High-performance append-only stream buffer with code-point cursor addressing.
3
+ *
4
+ * Avoids per-frame `[...content].slice().join('')` or repeated full-string allocations.
5
+ * Chunks are stored in an indexed rope array, and code-point to UTF-16 offset maps
6
+ * are updated incrementally per chunk.
7
+ */
8
+
9
+ export class StreamBuffer {
10
+ private chunks: string[] = []
11
+ private chunkCodePoints: number[] = []
12
+ private chunkCharLengths: number[] = []
13
+ private totalCodePoints = 0
14
+ private fullStringCache: string | null = null
15
+
16
+ constructor(initialText = '') {
17
+ if (initialText.length > 0) {
18
+ this.append(initialText)
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Count Unicode code points (perceived characters, UTF-16 surrogate pairs counted as 1).
24
+ */
25
+ static countCodePoints(str: string): number {
26
+ let count = 0
27
+ for (let i = 0; i < str.length; i++) {
28
+ const code = str.charCodeAt(i)
29
+ if (code >= 0xd800 && code <= 0xdbff && i + 1 < str.length) {
30
+ const next = str.charCodeAt(i + 1)
31
+ if (next >= 0xdc00 && next <= 0xdfff) {
32
+ i++
33
+ }
34
+ }
35
+ count++
36
+ }
37
+ return count
38
+ }
39
+
40
+ /**
41
+ * Convert code-point index into UTF-16 unit offset within a single chunk.
42
+ */
43
+ private static codePointToOffset(str: string, targetCp: number): number {
44
+ let cp = 0
45
+ let offset = 0
46
+ while (offset < str.length && cp < targetCp) {
47
+ const code = str.charCodeAt(offset)
48
+ if (code >= 0xd800 && code <= 0xdbff && offset + 1 < str.length) {
49
+ const next = str.charCodeAt(offset + 1)
50
+ if (next >= 0xdc00 && next <= 0xdfff) {
51
+ offset++
52
+ }
53
+ }
54
+ offset++
55
+ cp++
56
+ }
57
+ return offset
58
+ }
59
+
60
+ /**
61
+ * Append a new chunk to the buffer.
62
+ */
63
+ append(chunk: string): void {
64
+ if (chunk.length === 0) return
65
+ const cps = StreamBuffer.countCodePoints(chunk)
66
+ this.chunks.push(chunk)
67
+ this.chunkCodePoints.push(cps)
68
+ this.chunkCharLengths.push(chunk.length)
69
+ this.totalCodePoints += cps
70
+ this.fullStringCache = null
71
+ }
72
+
73
+ /**
74
+ * Total perceived characters (code points) in the buffer.
75
+ */
76
+ get length(): number {
77
+ return this.totalCodePoints
78
+ }
79
+
80
+ /**
81
+ * Reset the buffer to empty or to a new baseline string.
82
+ */
83
+ reset(text = ''): void {
84
+ this.chunks = []
85
+ this.chunkCodePoints = []
86
+ this.chunkCharLengths = []
87
+ this.totalCodePoints = 0
88
+ this.fullStringCache = null
89
+ if (text.length > 0) {
90
+ this.append(text)
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Extract a slice of text between `fromCp` and `toCp` (both in code points).
96
+ */
97
+ slice(fromCp: number, toCp: number): string {
98
+ const start = Math.max(0, fromCp)
99
+ const end = Math.min(this.totalCodePoints, toCp)
100
+ if (start >= end) return ''
101
+
102
+ // Fast path: if requesting the full string
103
+ if (start === 0 && end === this.totalCodePoints) {
104
+ return this.toString()
105
+ }
106
+
107
+ let result = ''
108
+ let accumulatedCp = 0
109
+
110
+ for (let i = 0; i < this.chunks.length; i++) {
111
+ const chunkCps = this.chunkCodePoints[i]!
112
+ const chunkEndCp = accumulatedCp + chunkCps
113
+
114
+ if (chunkEndCp <= start) {
115
+ accumulatedCp = chunkEndCp
116
+ continue
117
+ }
118
+ if (accumulatedCp >= end) {
119
+ break
120
+ }
121
+
122
+ const chunk = this.chunks[i]!
123
+ const localStartCp = Math.max(0, start - accumulatedCp)
124
+ const localEndCp = Math.min(chunkCps, end - accumulatedCp)
125
+
126
+ const startOffset = localStartCp === 0 ? 0 : StreamBuffer.codePointToOffset(chunk, localStartCp)
127
+ const endOffset = localEndCp === chunkCps ? chunk.length : StreamBuffer.codePointToOffset(chunk, localEndCp)
128
+
129
+ result += chunk.slice(startOffset, endOffset)
130
+ accumulatedCp = chunkEndCp
131
+ }
132
+
133
+ return result
134
+ }
135
+
136
+ /**
137
+ * Get the full concatenated string (cached until next append/reset).
138
+ */
139
+ toString(): string {
140
+ if (this.fullStringCache === null) {
141
+ this.fullStringCache = this.chunks.join('')
142
+ }
143
+ return this.fullStringCache
144
+ }
145
+ }
@@ -1,10 +1,28 @@
1
+ /*
2
+ * Font size and line height follow the Host's content-font axis, with
3
+ * fallbacks chosen so that EACH supported Harness version renders exactly what
4
+ * its own assistant root renders.
5
+ *
6
+ * The axis is published on `document.body` by the Harness layout plugin
7
+ * (`--dsh-content-font-size`) and derived by its theme stylesheet
8
+ * (`--dsh-content-font-delta: calc(size - 14px)`,
9
+ * `--dsh-content-font-size-secondary`, `--dsh-content-font-delta-secondary`).
10
+ * No package in the pinned 0.1.0-rc.6 dependency tree assigns any of them -
11
+ * rc.6 has neither the layout nor the theme plugin - and rc.6's own assistant
12
+ * root is a hard `16px/28px`.
13
+ *
14
+ * So the fallback has to be rc.6's value, not 14px: `var(size, 16px)` with
15
+ * `calc(24px + var(delta, 4px))` yields 16px/28px on rc.6 (matching its root)
16
+ * and the Host's live value on 0.1.5+ (where the same expression is what the
17
+ * Harness itself writes).
18
+ */
1
19
  .root {
2
20
  display: flex;
3
21
  flex-direction: column;
4
22
  min-width: 0;
5
23
  color: var(--dsw-alias-label-primary);
6
- font-size: 16px;
7
- line-height: 28px;
24
+ font-size: var(--dsh-content-font-size, 16px);
25
+ line-height: calc(24px + var(--dsh-content-font-delta, 4px));
8
26
  }
9
27
 
10
28
  .body {
@@ -14,6 +32,10 @@
14
32
  min-width: 0;
15
33
  }
16
34
 
35
+ .body > [data-turn-process-inline][hidden] {
36
+ margin-bottom: -16px;
37
+ }
38
+
17
39
  .think {
18
40
  display: flex;
19
41
  flex-direction: column;
@@ -66,13 +88,19 @@
66
88
  background: var(--dsw-alias-label-caption);
67
89
  }
68
90
 
91
+ /*
92
+ * Think text reads the secondary tier, mirroring the Harness's own reasoning
93
+ * row (`ui-chat/src/client/chat/ReasoningRow.module.css`): one step under the
94
+ * body size, with `secondary` fallbacks that reproduce rc.6's hard `14px/24px`
95
+ * when no Host axis is published.
96
+ */
69
97
  .thinkSummary {
70
98
  min-width: 0;
71
99
  overflow: hidden;
72
100
  flex: 1 1 auto;
73
101
  color: var(--dsw-alias-label-tertiary);
74
- font-size: 14px;
75
- line-height: 24px;
102
+ font-size: var(--dsh-content-font-size-secondary, 14px);
103
+ line-height: calc(20px + var(--dsh-content-font-delta-secondary, 4px));
76
104
  text-overflow: ellipsis;
77
105
  white-space: nowrap;
78
106
  }
@@ -82,12 +110,38 @@
82
110
  }
83
111
 
84
112
  .thinkBody {
85
- padding: 4px 0 4px 22px;
113
+ /* Indent under the title, which starts at leading (16 + delta) + gap 6. */
114
+ padding: 4px 0 4px calc(22px + var(--dsh-content-font-delta, 0px));
86
115
  color: var(--dsw-alias-label-tertiary);
87
- font-size: 14px;
88
- line-height: 24px;
116
+ font-size: var(--dsh-content-font-size-secondary, 14px);
117
+ line-height: calc(20px + var(--dsh-content-font-delta-secondary, 4px));
89
118
  white-space: pre-wrap;
90
119
  word-break: break-word;
120
+ max-height: var(--dsh-thinking-max-height, 300px);
121
+ overflow-y: auto;
122
+ overscroll-behavior: contain;
123
+ scrollbar-width: thin;
124
+ scrollbar-color: var(--dsw-alias-border-l4, rgba(128, 128, 128, 0.4)) transparent;
125
+ contain: layout;
126
+ transform: translateZ(0);
127
+ will-change: scroll-position;
128
+ }
129
+
130
+ .thinkBody::-webkit-scrollbar {
131
+ width: 6px;
132
+ }
133
+
134
+ .thinkBody::-webkit-scrollbar-track {
135
+ background: transparent;
136
+ }
137
+
138
+ .thinkBody::-webkit-scrollbar-thumb {
139
+ background-color: var(--dsw-alias-border-l4, rgba(128, 128, 128, 0.4));
140
+ border-radius: 3px;
141
+ }
142
+
143
+ .thinkBody::-webkit-scrollbar-thumb:hover {
144
+ background-color: var(--dsw-alias-border-l3, rgba(128, 128, 128, 0.6));
91
145
  }
92
146
 
93
147
  /*
@@ -106,7 +160,9 @@
106
160
  overflow: hidden;
107
161
  display: flex;
108
162
  align-items: center;
109
- height: 24px;
163
+ /* The Harness DisclosureRow reserves this row with the same expression, so
164
+ * the disclosure grows with the reading font instead of clipping it. */
165
+ height: calc(24px + var(--dsh-content-font-delta, 0px));
110
166
  min-width: 0;
111
167
  cursor: pointer;
112
168
  }
@@ -114,8 +170,8 @@
114
170
  .disclosureLeading {
115
171
  position: relative;
116
172
  flex: none;
117
- width: 16px;
118
- height: 16px;
173
+ width: calc(16px + var(--dsh-content-font-delta, 0px));
174
+ height: calc(16px + var(--dsh-content-font-delta, 0px));
119
175
  display: inline-flex;
120
176
  align-items: center;
121
177
  justify-content: center;
@@ -123,6 +179,11 @@
123
179
  color: var(--dsw-alias-label-tertiary);
124
180
  }
125
181
 
182
+ .disclosureLeading svg:not([data-state]) {
183
+ width: calc(14px + var(--dsh-content-font-delta, 0px));
184
+ height: calc(14px + var(--dsh-content-font-delta, 0px));
185
+ }
186
+
126
187
  .disclosureIconIdle {
127
188
  display: inline-flex;
128
189
  opacity: 1;
@@ -147,8 +208,8 @@
147
208
 
148
209
  .disclosureTitle {
149
210
  flex: none;
150
- font-size: 14px;
151
- line-height: 24px;
211
+ font-size: var(--dsh-content-font-size-secondary, 13px);
212
+ line-height: calc(24px + var(--dsh-content-font-delta, 0px));
152
213
  color: var(--dsw-alias-label-secondary);
153
214
  }
154
215
 
@@ -165,6 +226,7 @@
165
226
  display: grid;
166
227
  grid-template-rows: 1fr;
167
228
  visibility: visible;
229
+ overflow: hidden;
168
230
  transition:
169
231
  grid-template-rows var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, cubic-bezier(0.4, 0, 0.2, 1)),
170
232
  visibility 0s;
@@ -178,6 +240,11 @@
178
240
  visibility 0s var(--ds-transition-duration, 0.2s);
179
241
  }
180
242
 
243
+ .disclosureContent[data-collapsed] > * {
244
+ padding-top: 0 !important;
245
+ padding-bottom: 0 !important;
246
+ }
247
+
181
248
  /* Auto-close at stream end snaps instead of animating: the follower's settle
182
249
  * spring already absorbs the height step through the compositor, so a grid
183
250
  * animation would double-animate the same collapse for a dozen frames. */
@@ -187,6 +254,9 @@
187
254
 
188
255
  .disclosureContent > * {
189
256
  min-height: 0;
257
+ }
258
+
259
+ .disclosureContent[data-collapsed] > * {
190
260
  overflow: hidden;
191
261
  }
192
262