dsh-smooth-stream 0.6.0 → 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/README.en.md +4 -1
- package/README.md +5 -1
- package/lib/client.js +1757 -282
- package/lib/client.js.map +1 -1
- package/lib/index.js +299 -3
- 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/LogarithmicFade.module.css +160 -0
- package/src/client/SmoothStreamCard.tsx +41 -3
- package/src/client/StreamBuffer.ts +145 -0
- package/src/client/TypewriterAssistantNodeView.module.css +82 -12
- package/src/client/TypewriterAssistantNodeView.tsx +232 -104
- package/src/client/debugRuntime.ts +58 -0
- package/src/client/harnessIcons.ts +27 -0
- package/src/client/index.ts +153 -11
- package/src/client/locales.ts +90 -0
- package/src/client/smooth-stream-card-controller.ts +10 -3
- package/src/client/smooth-stream-settings-api.ts +7 -3
- package/src/client/teleprompterGlide.ts +499 -70
- package/src/client/useDecoupledMarkdown.ts +173 -0
- package/src/client/useFpsGuard.ts +9 -6
- package/src/client/useLogarithmicFade.ts +672 -0
- package/src/client/useProgressiveDomText.ts +27 -11
- package/src/client/useSmoothStreamContent.ts +5 -13
- package/src/config.ts +1 -1
- package/src/plugin.ts +99 -12
- package/src/settings-api.ts +4 -0
- package/src/settings-channel.ts +303 -0
- package/src/settings.ts +14 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect whether the given markdown text ends inside an unclosed code block (fence).
|
|
5
|
+
* Supports both backtick (```) and tilde (~~~) fences per CommonMark rules.
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* 1. An opening fence begins on a line with 0-3 leading spaces, followed by 3+ backticks or tildes.
|
|
9
|
+
* 2. An opening backtick fence info-string cannot contain backticks.
|
|
10
|
+
* 3. A closing fence must have at least as many characters as the opening fence, matching fence char,
|
|
11
|
+
* and cannot have non-whitespace content after the fence characters.
|
|
12
|
+
* 4. While inside a code block, all lines belong to the block until a matching closing fence is found.
|
|
13
|
+
*/
|
|
14
|
+
export function hasUnclosedCodeFence(text: string): boolean {
|
|
15
|
+
if (!text.includes('```') && !text.includes('~~~')) {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let inFence = false
|
|
20
|
+
let fenceChar = ''
|
|
21
|
+
let fenceLen = 0
|
|
22
|
+
|
|
23
|
+
let start = 0
|
|
24
|
+
const len = text.length
|
|
25
|
+
|
|
26
|
+
while (start < len) {
|
|
27
|
+
let end = text.indexOf('\n', start)
|
|
28
|
+
if (end === -1) end = len
|
|
29
|
+
|
|
30
|
+
let line = text.slice(start, end)
|
|
31
|
+
if (line.endsWith('\r')) line = line.slice(0, -1)
|
|
32
|
+
|
|
33
|
+
let indent = 0
|
|
34
|
+
while (indent < line.length && line[indent] === ' ' && indent < 4) {
|
|
35
|
+
indent++
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (indent < 4) {
|
|
39
|
+
const rest = line.slice(indent)
|
|
40
|
+
const firstChar = rest[0]
|
|
41
|
+
|
|
42
|
+
if (firstChar === '`' || firstChar === '~') {
|
|
43
|
+
let count = 0
|
|
44
|
+
while (count < rest.length && rest[count] === firstChar) {
|
|
45
|
+
count++
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (count >= 3) {
|
|
49
|
+
const afterFence = rest.slice(count)
|
|
50
|
+
|
|
51
|
+
if (!inFence) {
|
|
52
|
+
if (firstChar !== '`' || !afterFence.includes('`')) {
|
|
53
|
+
inFence = true
|
|
54
|
+
fenceChar = firstChar
|
|
55
|
+
fenceLen = count
|
|
56
|
+
}
|
|
57
|
+
} else if (firstChar === fenceChar && count >= fenceLen) {
|
|
58
|
+
if (afterFence.trim() === '') {
|
|
59
|
+
inFence = false
|
|
60
|
+
fenceChar = ''
|
|
61
|
+
fenceLen = 0
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
start = end + 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return inFence
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface DecoupledMarkdownOptions {
|
|
75
|
+
/** Throttle interval in milliseconds during unclosed code blocks. Defaults to 33ms (~30Hz). */
|
|
76
|
+
throttleMs?: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Decouple the smooth 60 FPS animation clock from the heavyweight Markdown AST compilation clock.
|
|
81
|
+
*
|
|
82
|
+
* Outside unclosed code blocks (or when settled), Markdown renders 1:1 with `shown`.
|
|
83
|
+
* Inside unclosed code blocks (where host incremental parsing degenerates to O(n) full-text compilation),
|
|
84
|
+
* Markdown re-renders are throttled to ~33ms (30Hz).
|
|
85
|
+
* Upon code block closure or stream end, updates are synchronized immediately without latency.
|
|
86
|
+
*/
|
|
87
|
+
export function useDecoupledMarkdown(
|
|
88
|
+
shown: string,
|
|
89
|
+
live: boolean,
|
|
90
|
+
options?: DecoupledMarkdownOptions
|
|
91
|
+
): string {
|
|
92
|
+
const throttleMs = options?.throttleMs ?? 33
|
|
93
|
+
const [markdownShown, setMarkdownShown] = useState(shown)
|
|
94
|
+
const lastCommitTimeRef = useRef(0)
|
|
95
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
96
|
+
const shownRef = useRef(shown)
|
|
97
|
+
shownRef.current = shown
|
|
98
|
+
const wasUnclosedRef = useRef(false)
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
// If not live (settled / motion reduced / stream closed), sync immediately
|
|
102
|
+
if (!live) {
|
|
103
|
+
if (timerRef.current !== null) {
|
|
104
|
+
clearTimeout(timerRef.current)
|
|
105
|
+
timerRef.current = null
|
|
106
|
+
}
|
|
107
|
+
setMarkdownShown(shown)
|
|
108
|
+
lastCommitTimeRef.current = performance.now()
|
|
109
|
+
wasUnclosedRef.current = false
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const unclosed = hasUnclosedCodeFence(shown)
|
|
114
|
+
const now = performance.now()
|
|
115
|
+
|
|
116
|
+
// Edge case: Closing edge - code block just closed right now!
|
|
117
|
+
// Immediately sync so completed code block styles render cleanly without any lag
|
|
118
|
+
if (wasUnclosedRef.current && !unclosed) {
|
|
119
|
+
if (timerRef.current !== null) {
|
|
120
|
+
clearTimeout(timerRef.current)
|
|
121
|
+
timerRef.current = null
|
|
122
|
+
}
|
|
123
|
+
wasUnclosedRef.current = false
|
|
124
|
+
setMarkdownShown(shown)
|
|
125
|
+
lastCommitTimeRef.current = now
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
wasUnclosedRef.current = unclosed
|
|
130
|
+
|
|
131
|
+
// Outside code blocks, keep instant 1:1 synchronization
|
|
132
|
+
if (!unclosed) {
|
|
133
|
+
if (timerRef.current !== null) {
|
|
134
|
+
clearTimeout(timerRef.current)
|
|
135
|
+
timerRef.current = null
|
|
136
|
+
}
|
|
137
|
+
setMarkdownShown(shown)
|
|
138
|
+
lastCommitTimeRef.current = now
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Inside unclosed code block: apply intelligent throttle
|
|
143
|
+
const elapsed = now - lastCommitTimeRef.current
|
|
144
|
+
if (elapsed >= throttleMs) {
|
|
145
|
+
if (timerRef.current !== null) {
|
|
146
|
+
clearTimeout(timerRef.current)
|
|
147
|
+
timerRef.current = null
|
|
148
|
+
}
|
|
149
|
+
setMarkdownShown(shown)
|
|
150
|
+
lastCommitTimeRef.current = now
|
|
151
|
+
} else {
|
|
152
|
+
if (timerRef.current === null) {
|
|
153
|
+
const delay = Math.max(16, throttleMs - elapsed)
|
|
154
|
+
timerRef.current = setTimeout(() => {
|
|
155
|
+
timerRef.current = null
|
|
156
|
+
setMarkdownShown(shownRef.current)
|
|
157
|
+
lastCommitTimeRef.current = performance.now()
|
|
158
|
+
}, delay)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}, [shown, live, throttleMs])
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
return () => {
|
|
165
|
+
if (timerRef.current !== null) {
|
|
166
|
+
clearTimeout(timerRef.current)
|
|
167
|
+
timerRef.current = null
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}, [])
|
|
171
|
+
|
|
172
|
+
return live ? markdownShown : shown
|
|
173
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { useCallback, useEffect, useRef } from 'react'
|
|
12
12
|
import { debugRuntime } from './debugRuntime.ts'
|
|
13
|
+
import { FrameCoordinator } from './FrameCoordinator.ts'
|
|
13
14
|
|
|
14
15
|
const FPS_THRESHOLD = 30
|
|
15
16
|
const FPS_ALPHA = 0.12
|
|
@@ -33,14 +34,12 @@ export function useFpsGuard(active: boolean): {
|
|
|
33
34
|
|
|
34
35
|
useEffect(() => {
|
|
35
36
|
if (!active) return
|
|
36
|
-
let rafId = 0
|
|
37
37
|
let lastDebugReport = 0
|
|
38
|
-
const frame = (now: number) => {
|
|
39
|
-
rafId = requestAnimationFrame(frame)
|
|
38
|
+
const frame = (now: number): boolean => {
|
|
40
39
|
const fps = fpsRef.current
|
|
41
40
|
if (fps.lastMs === 0) {
|
|
42
41
|
fps.lastMs = now
|
|
43
|
-
return
|
|
42
|
+
return true
|
|
44
43
|
}
|
|
45
44
|
const delta = Math.min(MAX_FRAME_MS, Math.max(1, now - fps.lastMs))
|
|
46
45
|
fps.lastMs = now
|
|
@@ -57,10 +56,14 @@ export function useFpsGuard(active: boolean): {
|
|
|
57
56
|
debugRuntime.reportFps(currentFps, fps.emaMs, fps.degraded)
|
|
58
57
|
lastDebugReport = now
|
|
59
58
|
}
|
|
59
|
+
return true
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
// The frame-rate monitor no longer owns a rAF chain of its own: it rides
|
|
62
|
+
// the shared document clock and only re-arms while the reply is streaming.
|
|
63
|
+
const coordinator = FrameCoordinator.forDocument()
|
|
64
|
+
const taskId = coordinator.registerTask({ onSimulate: (_dtMs, now) => frame(now) })
|
|
62
65
|
return () => {
|
|
63
|
-
|
|
66
|
+
coordinator.unregisterTask(taskId)
|
|
64
67
|
fpsRef.current = { emaMs: 0, lastMs: 0, healthyRun: 0, degraded: false }
|
|
65
68
|
debugRuntime.clearFps()
|
|
66
69
|
}
|