@linxin666/dsh-remote-web-ui 0.3.4 → 0.3.6
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 +92 -3
- package/lib/client.js.map +1 -1
- package/lib/index.js +4 -0
- package/lib/mobile.js +47 -10
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/FenceNotice.d.ts +12 -9
- package/lib/types/client/FenceNotice.d.ts.map +1 -1
- package/lib/types/client/FenceNotice.js +67 -2
- package/lib/types/client/locales.d.ts +12 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +12 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +4 -0
- package/lib/types/mobile/messages.d.ts +13 -5
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +13 -5
- package/lib/types/mobile/views/ChatView.d.ts +7 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +53 -5
- package/lib/types/mobile/views/ChatView.stream.test.js +8 -3
- package/package.json +1 -1
- package/src/client/FenceNotice.tsx +81 -1
- package/src/client/locales.ts +12 -0
- package/src/client/remote.module.css +72 -4
- package/src/index.ts +6 -0
- package/src/mobile/messages.ts +13 -5
- package/src/mobile/views/ChatView.stream.test.tsx +8 -3
- package/src/mobile/views/ChatView.tsx +52 -5
|
@@ -484,23 +484,91 @@
|
|
|
484
484
|
margin-top: 8px;
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
.
|
|
487
|
+
.fenceForm {
|
|
488
|
+
margin-top: 20px;
|
|
488
489
|
width: 100%;
|
|
489
|
-
|
|
490
|
-
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.fenceInputRow {
|
|
493
|
+
display: flex;
|
|
494
|
+
gap: 8px;
|
|
495
|
+
width: 100%;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.fenceInput {
|
|
499
|
+
flex: 1;
|
|
500
|
+
min-width: 0;
|
|
501
|
+
padding: 10px 14px;
|
|
502
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
503
|
+
border-radius: 10px;
|
|
504
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
505
|
+
color: var(--dsw-alias-label-primary);
|
|
506
|
+
font: inherit;
|
|
507
|
+
font-size: 13px;
|
|
508
|
+
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.fenceInput:focus {
|
|
512
|
+
outline: none;
|
|
513
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
514
|
+
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.15);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.fencePairButton {
|
|
518
|
+
flex-shrink: 0;
|
|
519
|
+
padding: 10px 18px;
|
|
491
520
|
border: 1px solid var(--dsw-alias-brand-primary);
|
|
492
521
|
border-radius: 10px;
|
|
493
522
|
background: var(--dsw-alias-brand-primary);
|
|
494
523
|
color: #fff;
|
|
495
524
|
font: inherit;
|
|
525
|
+
font-size: 13px;
|
|
496
526
|
font-weight: 500;
|
|
497
527
|
cursor: pointer;
|
|
528
|
+
transition: filter 120ms ease, opacity 120ms ease;
|
|
498
529
|
}
|
|
499
530
|
|
|
500
|
-
.
|
|
531
|
+
.fencePairButton:hover:not(:disabled) {
|
|
501
532
|
filter: brightness(0.96);
|
|
502
533
|
}
|
|
503
534
|
|
|
535
|
+
.fencePairButton:disabled {
|
|
536
|
+
opacity: 0.55;
|
|
537
|
+
cursor: not-allowed;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.fencePairButton:focus-visible {
|
|
541
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
542
|
+
outline-offset: 2px;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.fenceError {
|
|
546
|
+
margin: 10px 0 0;
|
|
547
|
+
color: var(--dsw-alias-state-error-primary);
|
|
548
|
+
font-size: 13px;
|
|
549
|
+
line-height: 1.4;
|
|
550
|
+
text-align: left;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.fenceRetry {
|
|
554
|
+
width: 100%;
|
|
555
|
+
margin-top: 12px;
|
|
556
|
+
padding: 10px 16px;
|
|
557
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
558
|
+
border-radius: 10px;
|
|
559
|
+
background: transparent;
|
|
560
|
+
color: var(--dsw-alias-label-secondary);
|
|
561
|
+
font: inherit;
|
|
562
|
+
font-weight: 500;
|
|
563
|
+
cursor: pointer;
|
|
564
|
+
transition: background-color 120ms ease, color 120ms ease;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.fenceRetry:hover {
|
|
568
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
569
|
+
color: var(--dsw-alias-label-primary);
|
|
570
|
+
}
|
|
571
|
+
|
|
504
572
|
.fenceRetry:focus-visible {
|
|
505
573
|
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
506
574
|
outline-offset: 3px;
|
package/src/index.ts
CHANGED
|
@@ -462,6 +462,12 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
462
462
|
if (!resolve().enabled) return false
|
|
463
463
|
return isPairedDeviceRequest(service, request)
|
|
464
464
|
})
|
|
465
|
+
|
|
466
|
+
if (lanAddresses.length > 0) {
|
|
467
|
+
const urls = lanAddresses.map(ip => `http://${ip}:${String(ctx.webServer.port)}/m/`).join(' , ')
|
|
468
|
+
console.log(`remote-web-ui: mobile UI reachable on LAN at ${urls}`)
|
|
469
|
+
}
|
|
470
|
+
|
|
465
471
|
const sync = (): void => {
|
|
466
472
|
const value = resolve()
|
|
467
473
|
service.config = pairingConfigOf(value)
|
package/src/mobile/messages.ts
CHANGED
|
@@ -611,11 +611,19 @@ export function foldEvents(events: readonly WireEvent[], existing?: readonly Ren
|
|
|
611
611
|
* Incremental folder for one message stream. Live chat folds one event at a
|
|
612
612
|
* time; rebuilding the five index maps by scanning every message per event
|
|
613
613
|
* made that path O(n) per event (O(n * events) per turn). A folder keeps the
|
|
614
|
-
* indexes alive across folds, applies each event in O(1) map
|
|
615
|
-
* returns the previous snapshot identity unchanged when
|
|
616
|
-
* React skips the re-render entirely. Replayed events
|
|
617
|
-
* watermark advanced by the first application skips
|
|
618
|
-
* double-invoked React state updater harmless.
|
|
614
|
+
* indexes alive across folds, applies each event in (near-)O(1) map
|
|
615
|
+
* operations, and returns the previous snapshot identity unchanged when
|
|
616
|
+
* nothing applied, so React skips the re-render entirely. Replayed events
|
|
617
|
+
* are no-ops: the maxSeq watermark advanced by the first application skips
|
|
618
|
+
* them, which also makes a double-invoked React state updater harmless.
|
|
619
|
+
*
|
|
620
|
+
* Cost contract: every fold that applies at least one event rebuilds the
|
|
621
|
+
* snapshot as one fresh O(messages) array copy (React needs the new
|
|
622
|
+
* identity), and replacement paths locate rows by an O(messages) identity
|
|
623
|
+
* scan. Both are amortized by the consumer folding bursts as ONE batch on a
|
|
624
|
+
* fixed cadence (see FOLD_FLUSH_MS in ChatView) instead of per event. An
|
|
625
|
+
* id-to-index map was measured and deliberately rejected: at realistic
|
|
626
|
+
* message counts the Map maintenance cost exceeds the identity-scan cost.
|
|
619
627
|
*/
|
|
620
628
|
export class EventFolder {
|
|
621
629
|
private state: FoldState
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
10
10
|
import { act, cleanup, render, screen } from '@testing-library/react'
|
|
11
|
-
import { ChatView, STREAM_RENDER_INTERVAL_MS, LONG_TEXT_LIMIT } from './ChatView.tsx'
|
|
11
|
+
import { ChatView, STREAM_RENDER_INTERVAL_MS, LONG_TEXT_LIMIT, FOLD_FLUSH_MS } from './ChatView.tsx'
|
|
12
12
|
import { type SessionView } from './App.tsx'
|
|
13
13
|
import { renderMarkdown } from '../markdown.ts'
|
|
14
14
|
import type { SessionModels } from '@deepseek-ai/dsh-host-apiproxy/api/sessions'
|
|
@@ -150,8 +150,13 @@ describe('ChatView streaming markdown throttle', () => {
|
|
|
150
150
|
]
|
|
151
151
|
const full = parts.join('')
|
|
152
152
|
|
|
153
|
-
// The first chunk mounts the pending message
|
|
154
|
-
|
|
153
|
+
// The first chunk mounts the pending message on the next fold flush
|
|
154
|
+
// (chunk streams fold in batches on the FOLD_FLUSH_MS cadence); the mount
|
|
155
|
+
// render parses it.
|
|
156
|
+
await act(async () => {
|
|
157
|
+
chunk(mux, parts[0] ?? '', 6)
|
|
158
|
+
vi.advanceTimersByTime(FOLD_FLUSH_MS + 5)
|
|
159
|
+
})
|
|
155
160
|
const afterMount = renderMarkdownMock.mock.calls.length
|
|
156
161
|
expect(afterMount).toBeGreaterThanOrEqual(1)
|
|
157
162
|
|
|
@@ -38,6 +38,14 @@ export interface ChatViewProps {
|
|
|
38
38
|
*/
|
|
39
39
|
export const MAX_TAIL_BUFFER_EVENTS = 500
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Cadence for folding buffered live events into the message list. Streaming
|
|
43
|
+
* chunks can arrive at a far higher rate; flushing on this cadence caps fold
|
|
44
|
+
* and re-render work per second without visible lag, and keeps working in
|
|
45
|
+
* background tabs where requestAnimationFrame is paused.
|
|
46
|
+
*/
|
|
47
|
+
export const FOLD_FLUSH_MS = 50
|
|
48
|
+
|
|
41
49
|
/** localStorage key for the tool-call display toggle (persisted on the /m origin). */
|
|
42
50
|
const SHOW_TOOL_CALLS_KEY = 'dsh.mobile.showToolCalls'
|
|
43
51
|
/** localStorage key for the injected-system-message display toggle. */
|
|
@@ -147,6 +155,14 @@ export function ChatView({ session, mux, onBack }: ChatViewProps) {
|
|
|
147
155
|
const liveBufferRef = useRef<WireEvent[]>([])
|
|
148
156
|
/** Incremental folder for this session's stream (indexes stay hot across events). */
|
|
149
157
|
const folderRef = useRef<EventFolder | undefined>(undefined)
|
|
158
|
+
/**
|
|
159
|
+
* Live events buffered between fold flushes. Folding every single event
|
|
160
|
+
* costs one O(messages) snapshot copy per event; flushing at a fixed
|
|
161
|
+
* cadence folds each burst once while keeping streaming text fluid.
|
|
162
|
+
*/
|
|
163
|
+
const foldBufferRef = useRef<WireEvent[]>([])
|
|
164
|
+
/** Timer id of the scheduled fold flush, if any. */
|
|
165
|
+
const foldFlushTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
150
166
|
/** True once the live buffer hit its cap (oldest events were dropped). */
|
|
151
167
|
const liveBufferOverflowRef = useRef(false)
|
|
152
168
|
|
|
@@ -273,7 +289,21 @@ export function ChatView({ session, mux, onBack }: ChatViewProps) {
|
|
|
273
289
|
// Live frames: fold session events for this session in as they arrive.
|
|
274
290
|
useEffect(() => {
|
|
275
291
|
if (mux === undefined) return
|
|
276
|
-
|
|
292
|
+
/** Fold every buffered live event in one batch and publish the snapshot. */
|
|
293
|
+
const flushFoldBuffer = (): void => {
|
|
294
|
+
if (foldFlushTimerRef.current !== undefined) {
|
|
295
|
+
clearTimeout(foldFlushTimerRef.current)
|
|
296
|
+
foldFlushTimerRef.current = undefined
|
|
297
|
+
}
|
|
298
|
+
const events = foldBufferRef.current
|
|
299
|
+
if (events.length === 0) return
|
|
300
|
+
foldBufferRef.current = []
|
|
301
|
+
setMessages(previous => {
|
|
302
|
+
const folder = folderRef.current
|
|
303
|
+
return folder === undefined ? foldEvents(events, previous) : folder.fold(events)
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
const unsubscribe = mux.onFrame((frame: MuxFrame) => {
|
|
277
307
|
if (frame.type === 'session/event') {
|
|
278
308
|
if (frame.sessionId !== session.sessionId) return
|
|
279
309
|
const event = frame.event as WireEvent
|
|
@@ -298,10 +328,16 @@ export function ChatView({ session, mux, onBack }: ChatViewProps) {
|
|
|
298
328
|
liveBufferRef.current.push(event)
|
|
299
329
|
return
|
|
300
330
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
331
|
+
foldBufferRef.current.push(event)
|
|
332
|
+
// Only chunk streams batch on the flush cadence; every other event
|
|
333
|
+
// (final message, turn close, user echo, update/delete) flushes the
|
|
334
|
+
// buffer synchronously so settled UI states never wait for a timer.
|
|
335
|
+
const isChunk = event.type === 'assistant/chunk' || event.type === 'message/chunk'
|
|
336
|
+
if (!isChunk) {
|
|
337
|
+
flushFoldBuffer()
|
|
338
|
+
} else if (foldFlushTimerRef.current === undefined) {
|
|
339
|
+
foldFlushTimerRef.current = setTimeout(flushFoldBuffer, FOLD_FLUSH_MS)
|
|
340
|
+
}
|
|
305
341
|
return
|
|
306
342
|
}
|
|
307
343
|
// Live projection pushes keep the permission picker current.
|
|
@@ -342,6 +378,17 @@ export function ChatView({ session, mux, onBack }: ChatViewProps) {
|
|
|
342
378
|
return
|
|
343
379
|
}
|
|
344
380
|
})
|
|
381
|
+
return () => {
|
|
382
|
+
unsubscribe()
|
|
383
|
+
// Drop the pending flush: its events belong to the session (or
|
|
384
|
+
// component instance) being torn down, and the next session reseeds
|
|
385
|
+
// its folder from a fresh history tail.
|
|
386
|
+
if (foldFlushTimerRef.current !== undefined) {
|
|
387
|
+
clearTimeout(foldFlushTimerRef.current)
|
|
388
|
+
foldFlushTimerRef.current = undefined
|
|
389
|
+
}
|
|
390
|
+
foldBufferRef.current = []
|
|
391
|
+
}
|
|
345
392
|
}, [mux, session.sessionId])
|
|
346
393
|
|
|
347
394
|
// Weak-network polling fallback: when the assistant is running, poll for
|