Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions apps/mobile/src/host/text-playback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,10 @@ export function createTextPlayback(initial = '', now = Date.now()) {
const oldLength = ends.length;
const shown = body.length;
const from = next.startsWith(target) ? (ends.at(-2) ?? 0) : 0;
const kept = from === 0 ? [] : ends.filter((end) => end <= from);
ends = [
...kept,
...[...graphemeSegments(next.slice(from))].map(
(part) => from + part.index + part.segment.length,
),
];
cursor = ends.findIndex((end) => end > shown);
if (cursor < 0) cursor = ends.length;
ends.length = from === 0 ? 0 : ends.length - 1;
for (const part of graphemeSegments(next.slice(from)))
ends.push(from + part.index + part.segment.length);
cursor = firstEndAfter(ends, shown);
target = next;
const added = Math.max(0, ends.length - oldLength);
if (added > 0) {
Expand Down Expand Up @@ -159,4 +154,15 @@ export function createTextPlayback(initial = '', now = Date.now()) {
};
}

function firstEndAfter(ends: number[], offset: number) {
let low = 0;
let high = ends.length;
while (low < high) {
const middle = (low + high) >>> 1;
if (ends[middle]! > offset) high = middle;
else low = middle + 1;
}
return low;
}

export type TextPlayback = ReturnType<typeof createTextPlayback>;
16 changes: 8 additions & 8 deletions apps/mobile/src/ui/chat/message-bubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,14 @@ function PressableBubble({
if (anchor.current !== undefined) onLongPress(anchor.current);
else measure(onLongPress);
};
const hold =
onLongPress === undefined
? undefined
: () => {
haptic('menu');
if (shrinks) scale.value = withSpring(1, spring.spatial.fast);
open();
};
const holdRef = useRef<() => void>(undefined);
holdRef.current = () => {
haptic('menu');
if (shrinks) scale.value = withSpring(1, spring.spatial.fast);
open();
};
const holds = onLongPress !== undefined;
const hold = useMemo(() => (holds ? () => holdRef.current?.() : undefined), [holds]);
return (
<Animated.View
ref={ref}
Expand Down
11 changes: 11 additions & 0 deletions changes/ios-streaming-playback-scan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
section: Fixed
---

## en

- Streaming a long reply no longer slows down as the reply grows: the text playback no longer rescans every earlier character on each update ([#3974](https://github.com/BotHarness/DeepSeekBot/issues/3974)).

## zh

- 流式输出长回复时不再越来越慢:文字播放不再在每次更新时重新扫描之前所有字符([#3974](https://github.com/BotHarness/DeepSeekBot/issues/3974))。
24 changes: 19 additions & 5 deletions packages/core/src/devices/noise/channel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { bytesToHex, hexToBytes, concatBytes, utf8ToBytes } from '@noble/hashes/utils.js';
import { bytesToHex, concatBytes, utf8ToBytes } from '@noble/hashes/utils.js';
import { NoiseXk, NOISE_PROTOCOL, type NoiseTransport, type NoiseKeyAgreement } from './xk.js';

export const CONNECT_PROTOCOL = 'connect-noise-xk-v1';
Expand All @@ -15,6 +15,20 @@ export interface ChannelResult {
ready?: true;
}

const NIBBLE = new Int8Array(128).fill(-1);
for (let index = 0; index < 16; index++) NIBBLE['0123456789abcdef'.charCodeAt(index)] = index;

function wireBytes(wire: string): Uint8Array {
const bytes = new Uint8Array((wire.length - 1) / 2);
for (let index = 0, at = 1; index < bytes.length; index++, at += 2) {
const high = NIBBLE[wire.charCodeAt(at)] ?? -1;
const low = NIBBLE[wire.charCodeAt(at + 1)] ?? -1;
if (high < 0 || low < 0) throw new Error('channel-frame-invalid');
bytes[index] = (high << 4) | low;
}
return bytes;
}

export class ConnectChannel {
private readonly noise: NoiseXk;
private readonly role: 'initiator' | 'responder';
Expand Down Expand Up @@ -71,7 +85,8 @@ export class ConnectChannel {
if (
this.closed ||
wire.length > 2 * (HEADER + CHUNK + 17) + 1 ||
!/^[hd][0-9a-f]+$/u.test(wire) ||
(wire[0] !== 'h' && wire[0] !== 'd') ||
wire.length < 3 ||
wire.length % 2 !== 1
)
throw new Error('channel-frame-invalid');
Expand All @@ -83,8 +98,7 @@ export class ConnectChannel {
(this.role === 'initiator' && this.stage !== 1)
)
throw new Error('channel-handshake-invalid');
if (this.noise.read(hexToBytes(wire.slice(1))).length !== 0)
throw new Error('channel-early-payload');
if (this.noise.read(wireBytes(wire)).length !== 0) throw new Error('channel-early-payload');
if (this.role === 'responder' && this.stage === 0) {
this.stage = 1;
return { reply: `h${bytesToHex(this.noise.write())}` };
Expand All @@ -94,7 +108,7 @@ export class ConnectChannel {
return { ...(reply === undefined ? {} : { reply }), ready: true };
}
if (wire[0] !== 'd') throw new Error('channel-frame-invalid');
const frame = hexToBytes(wire.slice(1));
const frame = wireBytes(wire);
if (frame.length < HEADER + 17) throw new Error('channel-length-invalid');
const header = frame.subarray(0, HEADER);
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
Expand Down