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.
- package/lib/client.js +1508 -362
- package/lib/client.js.map +1 -1
- package/lib/index.js +102 -8
- package/package.json +1 -1
- package/src/client/AgentRowEntrance.module.css +1 -4
- package/src/client/AnimatedDisclosure.tsx +3 -3
- package/src/client/DebugPanel.tsx +34 -15
- package/src/client/FrameCoordinator.ts +180 -0
- package/src/client/SmoothStreamCard.tsx +28 -3
- package/src/client/StreamBuffer.ts +145 -0
- package/src/client/TypewriterAssistantNodeView.module.css +82 -12
- package/src/client/TypewriterAssistantNodeView.tsx +216 -102
- package/src/client/debugRuntime.ts +58 -0
- package/src/client/harnessIcons.ts +27 -0
- package/src/client/index.ts +151 -11
- package/src/client/locales.ts +85 -0
- package/src/client/smooth-stream-card-controller.ts +7 -3
- package/src/client/smooth-stream-settings-api.ts +4 -2
- package/src/client/teleprompterGlide.ts +491 -52
- package/src/client/useDecoupledMarkdown.ts +173 -0
- package/src/client/useFpsGuard.ts +9 -6
- package/src/client/useLogarithmicFade.ts +439 -60
- package/src/client/useProgressiveDomText.ts +27 -11
- package/src/client/useSmoothStreamContent.ts +5 -13
- package/src/config.ts +1 -1
- package/src/plugin.ts +83 -8
- package/src/settings-api.ts +2 -0
- package/src/settings-channel.ts +34 -8
- package/src/settings.ts +11 -0
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|