From c21b9e28cd38e77e05c60d4f41a7ec9b90f4dddd Mon Sep 17 00:00:00 2001
From: Blue <3067670134@qq.com>
Date: Tue, 22 Sep 2026 11:27:54 +0800
Subject: [PATCH 1/2] feat(stability): throttle text_delta streaming, bound
file preview, add chromium smoke to CI, and preserve offline terminal states
---
.github/workflows/validate.yml | 19 +++
apps/electron/src/server/handlers.ts | 5 +-
package.json | 1 +
packages/shared/src/client/types.ts | 10 +-
packages/shared/src/sessions/index.ts | 1 +
packages/shared/src/sessions/preview.ts | 92 ++++++++++++++
packages/shared/tests/preview.test.ts | 126 ++++++++++++++++++++
packages/ui/src/components/Conversation.tsx | 31 ++++-
packages/ui/src/components/DetailsPanel.tsx | 10 +-
packages/ui/src/components/RunIndicator.tsx | 90 ++++++++++++--
packages/ui/src/streamBatcher.ts | 101 ++++++++++++++++
packages/ui/src/useWorkbench.ts | 98 ++++++++++++---
packages/ui/src/workbench.css | 6 +
packages/ui/tests/RunIndicator.test.tsx | 69 +++++++++++
packages/ui/tests/streamBatcher.test.ts | 113 ++++++++++++++++++
scripts/verify-ui.cjs | 81 +++++++++----
16 files changed, 791 insertions(+), 62 deletions(-)
create mode 100644 packages/shared/src/sessions/preview.ts
create mode 100644 packages/shared/tests/preview.test.ts
create mode 100644 packages/ui/src/streamBatcher.ts
create mode 100644 packages/ui/tests/RunIndicator.test.tsx
create mode 100644 packages/ui/tests/streamBatcher.test.ts
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index e426a27..5c05b01 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -47,3 +47,22 @@ jobs:
- name: Build WebUI and Electron
run: bun run build
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+
+ - name: Install Chromium and system dependencies
+ run: npx playwright install --with-deps chromium
+
+ - name: Run Chromium motion smoke (web-only)
+ run: node scripts/verify-ui.cjs --web-only
+
+ - name: Upload QA artifacts on failure
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: qa-artifacts
+ path: artifacts/qa/
+ if-no-files-found: ignore
diff --git a/apps/electron/src/server/handlers.ts b/apps/electron/src/server/handlers.ts
index c9d0fd1..66098ec 100644
--- a/apps/electron/src/server/handlers.ts
+++ b/apps/electron/src/server/handlers.ts
@@ -23,9 +23,10 @@ import {
resolveSessionFilePath,
updateSessionConfig,
validateSessionId,
+ readFilePreview,
} from '@threadcove/shared/sessions';
import { listSources } from '@threadcove/shared/sources';
-import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from 'fs';
+import { writeFileSync, readdirSync, existsSync, statSync } from 'fs';
import { join } from 'path';
import type { ModelProvider } from '@threadcove/shared/config';
import type { SessionManager } from './session-manager.ts';
@@ -229,7 +230,7 @@ export function registerHandlers(server: RpcServerLike, ctx: HandlerContext): vo
// getSessionPath imported at module top (defense-in-depth sanitize inside)
const file = resolveSessionFilePath(root, String(sessionId), String(subPath));
if (!existsSync(file)) throw new Error(`File not found: ${String(subPath)}`);
- return { content: readFileSync(file, 'utf-8') };
+ return readFilePreview(file);
});
server.handle(RPC_CHANNELS.files.WRITE, (...args: unknown[]) => {
diff --git a/package.json b/package.json
index 4cf0b38..fd5dec7 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"scripts": {
"test": "bun test",
"test:ui": "node scripts/verify-ui.cjs",
+ "test:ui:web": "node scripts/verify-ui.cjs --web-only",
"typecheck": "bun run typecheck:core && bun run typecheck:shared && bun run typecheck:pi-agent-server && bun run typecheck:electron && bun run typecheck:webui && bun run --cwd packages/ui typecheck",
"typecheck:core": "cd packages/core && bun run typecheck",
"typecheck:shared": "cd packages/shared && bun run typecheck",
diff --git a/packages/shared/src/client/types.ts b/packages/shared/src/client/types.ts
index db37f1c..c1103f4 100644
--- a/packages/shared/src/client/types.ts
+++ b/packages/shared/src/client/types.ts
@@ -50,7 +50,7 @@ export interface ElectronAPI {
// Files (workspace content)
listFiles(workspaceId: string, sessionId: string, subPath?: string): Promise<{ entries: unknown[] }>;
- readFile(workspaceId: string, sessionId: string, subPath: string): Promise<{ content: string }>;
+ readFile(workspaceId: string, sessionId: string, subPath: string): Promise;
writeFile(workspaceId: string, sessionId: string, subPath: string, content: string): Promise<{ success: boolean }>;
// LOCAL_ONLY — native dialog (Electron only; WebUI overrides with input[type=file])
@@ -71,3 +71,11 @@ export interface SessionEventPayload {
sessionId: string;
event: AgentEvent | { type: 'user_message'; message: unknown };
}
+
+/** Result shape for bounded file reading. */
+export interface FileReadResult {
+ content: string;
+ truncated?: boolean;
+ totalBytes?: number;
+ previewBytes?: number;
+}
diff --git a/packages/shared/src/sessions/index.ts b/packages/shared/src/sessions/index.ts
index 48fc3d0..29e389c 100644
--- a/packages/shared/src/sessions/index.ts
+++ b/packages/shared/src/sessions/index.ts
@@ -4,3 +4,4 @@ export * from './persistence-queue.ts';
export * from './validation.ts';
export * from './slug-generator.ts';
export * from './file-path.ts';
+export * from './preview.ts';
diff --git a/packages/shared/src/sessions/preview.ts b/packages/shared/src/sessions/preview.ts
new file mode 100644
index 0000000..5ccc45c
--- /dev/null
+++ b/packages/shared/src/sessions/preview.ts
@@ -0,0 +1,92 @@
+import { openSync, fstatSync, readSync, closeSync } from 'node:fs';
+
+export const MAX_FULL_PREVIEW_BYTES = 1024 * 1024; // 1 MiB
+export const TRUNCATED_PREVIEW_BYTES = 256 * 1024; // 256 KiB
+
+export interface FilePreviewResult {
+ content: string;
+ truncated?: boolean;
+ totalBytes: number;
+ previewBytes: number;
+}
+
+/**
+ * Truncates a buffer to end at a valid UTF-8 character boundary.
+ * Prevents mangled replacement characters (\uFFFD) when slicing multibyte sequences.
+ */
+export function truncateToValidUtf8(buf: Buffer): Buffer {
+ const len = buf.length;
+ if (len === 0) return buf;
+
+ let i = len - 1;
+ let continuations = 0;
+ while (i >= 0 && i >= len - 4) {
+ const byte = buf[i]!;
+ if ((byte & 0x80) === 0) {
+ // ASCII byte, sequence complete
+ break;
+ }
+ if ((byte & 0xc0) === 0x80) {
+ // Continuation byte (10xxxxxx)
+ continuations++;
+ i--;
+ continue;
+ }
+ // Found leading byte
+ let expected = 0;
+ if ((byte & 0xe0) === 0xc0) expected = 1;
+ else if ((byte & 0xf0) === 0xe0) expected = 2;
+ else if ((byte & 0xf8) === 0xf0) expected = 3;
+
+ if (expected > 0 && continuations < expected) {
+ // Incomplete trailing sequence: strip lead byte and trailing continuation bytes
+ return buf.subarray(0, i);
+ }
+ // Complete sequence
+ return buf;
+ }
+
+ // Trailing orphan continuation bytes without a leading byte
+ if (continuations > 0 && i < len - continuations) {
+ return buf.subarray(0, len - continuations);
+ }
+
+ return buf;
+}
+
+/**
+ * Reads a bounded file preview using a single opened file descriptor.
+ * - Rejects non-regular files
+ * - Bounded reading: <= 1 MiB full read, > 1 MiB reads at most 256 KiB
+ * - UTF-8 character truncation never forges or mangles trailing bytes
+ */
+export function readFilePreview(filePath: string): FilePreviewResult {
+ const fd = openSync(filePath, 'r');
+ try {
+ const stat = fstatSync(fd);
+ if (!stat.isFile()) {
+ throw new Error(`Not a regular file: ${filePath}`);
+ }
+ const totalBytes = stat.size;
+ const isTruncated = totalBytes > MAX_FULL_PREVIEW_BYTES;
+ const maxToRead = isTruncated ? TRUNCATED_PREVIEW_BYTES : Math.min(totalBytes, MAX_FULL_PREVIEW_BYTES);
+
+ const buf = Buffer.alloc(maxToRead);
+ let bytesRead = 0;
+ if (maxToRead > 0) {
+ bytesRead = readSync(fd, buf, 0, maxToRead, 0);
+ }
+
+ const rawSlice = buf.subarray(0, bytesRead);
+ const validSlice = isTruncated ? truncateToValidUtf8(rawSlice) : rawSlice;
+
+ return {
+ content: validSlice.toString('utf-8'),
+ truncated: isTruncated,
+ totalBytes,
+ previewBytes: validSlice.length,
+ };
+ } finally {
+ closeSync(fd);
+ }
+}
diff --git a/packages/shared/tests/preview.test.ts b/packages/shared/tests/preview.test.ts
new file mode 100644
index 0000000..f4b8845
--- /dev/null
+++ b/packages/shared/tests/preview.test.ts
@@ -0,0 +1,126 @@
+import { describe, it, expect } from 'bun:test';
+import { mkdtempSync, writeFileSync, statSync, readFileSync, mkdirSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import {
+ readFilePreview,
+ truncateToValidUtf8,
+ MAX_FULL_PREVIEW_BYTES,
+ TRUNCATED_PREVIEW_BYTES,
+} from '../src/sessions/preview.ts';
+import { resolveSessionFilePath } from '../src/sessions/file-path.ts';
+
+describe('readFilePreview', () => {
+ it('reads small files completely (<= threshold)', () => {
+ const dir = mkdtempSync(join(tmpdir(), 'tc-preview-test-'));
+ const file = join(dir, 'small.txt');
+ const text = 'Hello, ThreadCove!'.repeat(50);
+ writeFileSync(file, text, 'utf-8');
+
+ const result = readFilePreview(file);
+ expect(result.truncated).toBe(false);
+ expect(result.content).toBe(text);
+ expect(result.totalBytes).toBe(Buffer.byteLength(text));
+ expect(result.previewBytes).toBe(Buffer.byteLength(text));
+ });
+
+ it('bounds reading to at most 256KiB for files > 1MiB (> threshold)', () => {
+ const dir = mkdtempSync(join(tmpdir(), 'tc-preview-test-'));
+ const file = join(dir, 'large.txt');
+ const count = 1200; // 1200 KiB > 1024 KiB
+ const fd = Buffer.alloc(count * 1024, 65); // All 'A's
+ writeFileSync(file, fd);
+
+ const statBefore = statSync(file);
+ const result = readFilePreview(file);
+
+ expect(result.truncated).toBe(true);
+ expect(result.totalBytes).toBe(statBefore.size);
+ expect(result.previewBytes).toBe(TRUNCATED_PREVIEW_BYTES);
+ expect(result.content.length).toBe(TRUNCATED_PREVIEW_BYTES);
+ expect(Buffer.byteLength(result.content, 'utf-8')).toBe(TRUNCATED_PREVIEW_BYTES);
+ });
+
+ it('safely truncates multibyte UTF-8 characters without corruption', () => {
+ // Chinese character '中' is 3 bytes: 0xE4, 0xB8, 0xAD
+ // Emoji '🎉' is 4 bytes: 0xF0, 0x9F, 0x8E, 0x89
+ // Let's test truncateToValidUtf8 directly with partial sequences
+ const char3 = Buffer.from('中', 'utf-8'); // 3 bytes
+ expect(char3.length).toBe(3);
+
+ // Buffer ending with only 1 byte of a 3-byte sequence
+ const partial1 = Buffer.concat([Buffer.from('ASCII'), char3.subarray(0, 1)]);
+ const fixed1 = truncateToValidUtf8(partial1);
+ expect(fixed1.toString('utf-8')).toBe('ASCII');
+ expect(fixed1.toString('utf-8').includes('\uFFFD')).toBe(false);
+
+ // Buffer ending with 2 bytes of a 3-byte sequence
+ const partial2 = Buffer.concat([Buffer.from('ASCII'), char3.subarray(0, 2)]);
+ const fixed2 = truncateToValidUtf8(partial2);
+ expect(fixed2.toString('utf-8')).toBe('ASCII');
+ expect(fixed2.toString('utf-8').includes('\uFFFD')).toBe(false);
+
+ // Buffer ending with full 3-byte sequence
+ const complete3 = Buffer.concat([Buffer.from('ASCII'), char3]);
+ const fixed3 = truncateToValidUtf8(complete3);
+ expect(fixed3.toString('utf-8')).toBe('ASCII中');
+
+ // Emoji 4-byte partial
+ const emoji = Buffer.from('🎉', 'utf-8'); // 4 bytes
+ const partialEmoji = Buffer.concat([Buffer.from('Hello'), emoji.subarray(0, 3)]);
+ const fixedEmoji = truncateToValidUtf8(partialEmoji);
+ expect(fixedEmoji.toString('utf-8')).toBe('Hello');
+ expect(fixedEmoji.toString('utf-8').includes('\uFFFD')).toBe(false);
+
+ // File-level test: place a 3-byte character right at 256KiB boundary
+ const dir = mkdtempSync(join(tmpdir(), 'tc-preview-test-'));
+ const file = join(dir, 'multibyte-boundary.txt');
+ // 262143 bytes of 'A', then '中' (3 bytes), followed by padding up to 1.1 MiB
+ const prefix = Buffer.alloc(TRUNCATED_PREVIEW_BYTES - 1, 65); // 262143 bytes
+ const middle = Buffer.from('中', 'utf-8'); // 3 bytes, starts at byte 262143, ends at 262146
+ const rest = Buffer.alloc(MAX_FULL_PREVIEW_BYTES, 66);
+ writeFileSync(file, Buffer.concat([prefix, middle, rest]));
+
+ const res = readFilePreview(file);
+ expect(res.truncated).toBe(true);
+ expect(res.content.includes('\uFFFD')).toBe(false);
+ expect(res.previewBytes).toBe(TRUNCATED_PREVIEW_BYTES - 1);
+ expect(res.content).toBe(prefix.toString('utf-8'));
+ });
+
+ it('rejects directory paths', () => {
+ const dir = mkdtempSync(join(tmpdir(), 'tc-preview-test-'));
+ expect(() => readFilePreview(dir)).toThrow(/regular file/i);
+ });
+
+ it('ensures target file is never modified by preview read', () => {
+ const dir = mkdtempSync(join(tmpdir(), 'tc-preview-test-'));
+ const file = join(dir, 'read-only-test.txt');
+ const content = 'UNTOUCHED_CONTENT_TEST_'.repeat(100);
+ writeFileSync(file, content, 'utf-8');
+
+ const beforeStat = statSync(file);
+ const res = readFilePreview(file);
+ const afterStat = statSync(file);
+
+ expect(res.content).toBe(content);
+ expect(afterStat.size).toBe(beforeStat.size);
+ expect(afterStat.mtimeMs).toBe(beforeStat.mtimeMs);
+ expect(readFileSync(file, 'utf-8')).toBe(content);
+ });
+
+ it('preserves path containment and security via resolveSessionFilePath', () => {
+ const root = mkdtempSync(join(tmpdir(), 'tc-containment-test-'));
+ const sessionDir = join(root, 'sessions', 'sess-1', 'data');
+ mkdirSync(sessionDir, { recursive: true });
+ writeFileSync(join(sessionDir, 'artifact.txt'), 'clean artifact');
+
+ // Valid path resolves under data
+ const valid = resolveSessionFilePath(root, 'sess-1', 'artifact.txt');
+ expect(valid).toBe(join(sessionDir, 'artifact.txt'));
+
+ // Traversal attempts throw
+ expect(() => resolveSessionFilePath(root, 'sess-1', '../session.jsonl')).toThrow();
+ expect(() => resolveSessionFilePath(root, 'sess-1', '../../secret.txt')).toThrow();
+ });
+});
diff --git a/packages/ui/src/components/Conversation.tsx b/packages/ui/src/components/Conversation.tsx
index 73ad4ea..f001a51 100644
--- a/packages/ui/src/components/Conversation.tsx
+++ b/packages/ui/src/components/Conversation.tsx
@@ -1,3 +1,4 @@
+import { memo, useCallback } from 'react';
import type { StoredMessage } from '@threadcove/core/types';
import { RunIndicator } from './RunIndicator.tsx';
@@ -13,14 +14,34 @@ type Props = {
onCopyError: (error: unknown) => void;
};
+type MessageItemProps = {
+ message: StoredMessage;
+ onCopyError: (error: unknown) => void;
+};
+
+export const MessageItem = memo(function MessageItem({ message, onCopyError }: MessageItemProps) {
+ const handleCopy = useCallback(() => {
+ void navigator.clipboard.writeText(message.content).catch(onCopyError);
+ }, [message.content, onCopyError]);
+
+ return (
+
+
+ {message.type === 'user' ? '你 / 提问' : message.type === 'assistant' ? 'THREADCOVE / 研究回复' : message.type === 'tool' ? `${message.toolName || '工具'} / 结果` : '执行记录'}
+
+
+ {message.content}
+ {message.type === 'assistant' && }
+
+ );
+});
+
export function Conversation({ messages, status, online, waiting, completion, sessionId, scrollRef, onScroll, onCopyError }: Props) {
return
- {messages.map(message =>
- {message.type === 'user' ? '你 / 提问' : message.type === 'assistant' ? 'THREADCOVE / 研究回复' : message.type === 'tool' ? `${message.toolName || '工具'} / 结果` : '执行记录'}
- {message.content}
- {message.type === 'assistant' && }
- )}
+ {messages.map(message => (
+
+ ))}
;
diff --git a/packages/ui/src/components/DetailsPanel.tsx b/packages/ui/src/components/DetailsPanel.tsx
index 4de2bea..9c77755 100644
--- a/packages/ui/src/components/DetailsPanel.tsx
+++ b/packages/ui/src/components/DetailsPanel.tsx
@@ -7,7 +7,13 @@ type Props = {
active?: SessionDto;
sources: SourceDto[];
files: Array<{ name: string; type: string }>;
- filePreview?: { name: string; content: string };
+ filePreview?: {
+ name: string;
+ content: string;
+ truncated?: boolean;
+ totalBytes?: number;
+ previewBytes?: number;
+ };
activity: string[];
permission?: Permission;
onClose: () => void;
@@ -20,6 +26,6 @@ export function DetailsPanel({ active, sources, files, filePreview, activity, pe
{permission && 等待你的允许
{permission.description}
}
执行记录 01
{active?.isProcessing ? '正在研究' : runNames[active?.lastRun?.status ?? ''] ?? '等待问题'}
{activity.map((item, index) => {item}
)}{!activity.length && 工具调用发生时,会在这里显示。
}
资料源 {String(sources.length).padStart(2, '0')}
{sources.map(source => {source.name}{source.enabled ? '已配置' : '已停用'}
)}{!sources.length && 尚未配置资料源。
}
- 任务文件 {String(files.length).padStart(2, '0')}
{files.map(file => )}{!files.length && 任务资料目录中还没有文件。
}{filePreview && {filePreview.name}{filePreview.content} }
+ 任务文件 {String(files.length).padStart(2, '0')}
{files.map(file => )}{!files.length && 任务资料目录中还没有文件。
}{filePreview && {filePreview.name}{filePreview.truncated &&
文件较大,仅显示前 {Math.round((filePreview.previewBytes ?? 262144) / 1024)} KiB
}
{filePreview.content} }
;
}
diff --git a/packages/ui/src/components/RunIndicator.tsx b/packages/ui/src/components/RunIndicator.tsx
index 012046a..e0af13a 100644
--- a/packages/ui/src/components/RunIndicator.tsx
+++ b/packages/ui/src/components/RunIndicator.tsx
@@ -3,25 +3,89 @@ import './conversation-motion.css';
export type RunFeedback = { sessionId: string; runId: string; status: string; completion?: string };
type Props = { status?: string; online: boolean; waiting: boolean; completion?: string };
-const labels: Record = { running: '正在研究…', completed: '研究已完成', failed: '研究失败', cancelled: '研究已取消', interrupted: '研究已中断' };
-/** Only a new live completion token can animate; mounting historical state stays quiet. */
+export const runIndicatorLabels: Record = {
+ running: '正在研究…',
+ completed: '研究已完成',
+ failed: '研究失败',
+ cancelled: '研究已取消',
+ interrupted: '研究已中断',
+};
+
+export const TERMINAL_STATUSES = new Set(['completed', 'cancelled', 'failed', 'interrupted']);
+
+/**
+ * Pure state calculation for RunIndicator:
+ * - Terminal states (completed, cancelled, failed, interrupted) remain preserved when offline.
+ * - Only unfinished active research shows 'disconnected' ("连接中断,执行状态待同步") when offline.
+ * - Idle/empty tasks without active runs do not show disconnect banners.
+ */
+export function computeRunIndicatorState(
+ status?: string,
+ online = true,
+ waiting = false,
+): { state?: string; label?: string } {
+ let state: string | undefined;
+ if (status && TERMINAL_STATUSES.has(status)) {
+ state = status;
+ } else if (!online) {
+ if (status === 'running' || waiting) {
+ state = 'disconnected';
+ } else {
+ state = undefined;
+ }
+ } else if (waiting) {
+ state = 'waiting';
+ } else {
+ state = status;
+ }
+
+ if (!state || (state !== 'disconnected' && state !== 'waiting' && !runIndicatorLabels[state])) {
+ return {};
+ }
+
+ const label = state === 'disconnected'
+ ? '连接中断,执行状态待同步'
+ : state === 'waiting'
+ ? '等待你的确认'
+ : runIndicatorLabels[state];
+
+ return { state, label };
+}
+
+/** Only a new live completion token can animate; mounting historical state stays quiet; reconnecting does not celebrate. */
export function RunIndicator({ status, online, waiting, completion }: Props) {
- const previous = useRef(completion);
+ const animatedCompletions = useRef>(new Set());
const [finishing, setFinishing] = useState(false);
- const state = !online ? 'disconnected' : waiting ? 'waiting' : status;
+
+ const { state, label } = computeRunIndicatorState(status, online, waiting);
+
useEffect(() => {
- const fresh = Boolean(completion && completion !== previous.current);
- previous.current = completion;
- if (state !== 'completed' || !fresh) { setFinishing(false); return; }
+ if (state !== 'completed' || !completion || animatedCompletions.current.has(completion)) {
+ setFinishing(false);
+ return;
+ }
+ animatedCompletions.current.add(completion);
setFinishing(true);
const timer = setTimeout(() => setFinishing(false), 260);
return () => clearTimeout(timer);
}, [completion, state]);
- if (!state || (online && !waiting && !labels[state])) return null;
- const label = state === 'disconnected' ? '连接中断,执行状态待同步' : state === 'waiting' ? '等待你的确认' : labels[state];
- return
-
- {label}
-
;
+
+ if (!state || !label) return null;
+
+ return (
+
+
+
+
+
+ {label}
+
+ );
}
diff --git a/packages/ui/src/streamBatcher.ts b/packages/ui/src/streamBatcher.ts
new file mode 100644
index 0000000..4e85cd0
--- /dev/null
+++ b/packages/ui/src/streamBatcher.ts
@@ -0,0 +1,101 @@
+import type { StoredMessage } from '@threadcove/core/types';
+
+export const BATCH_INTERVAL_MS = 40;
+
+export interface PendingStreamDelta {
+ messageId: string;
+ turnId?: string;
+ timestamp?: number;
+ snapshot?: string;
+ deltaText: string;
+ completeText?: string;
+}
+
+export interface StreamEventPayload {
+ type: 'text_delta' | 'text_complete';
+ messageId?: string;
+ text: string;
+ textSnapshot?: string;
+ turnId?: string;
+}
+
+/**
+ * Accumulates a text_delta or text_complete event into the pending batch map.
+ * Returns the resolved message ID.
+ */
+export function accumulateStreamDelta(
+ pending: Map,
+ event: StreamEventPayload,
+ fallbackId: string,
+): string {
+ const id = event.messageId ?? fallbackId;
+ const existing = pending.get(id) ?? {
+ messageId: id,
+ turnId: event.turnId,
+ timestamp: Date.now(),
+ deltaText: '',
+ };
+ if (event.turnId) existing.turnId = event.turnId;
+
+ if (event.type === 'text_complete') {
+ existing.completeText = event.text;
+ } else if (event.textSnapshot !== undefined) {
+ existing.snapshot = event.textSnapshot;
+ existing.deltaText = '';
+ } else {
+ existing.deltaText += event.text;
+ }
+
+ pending.set(id, existing);
+ return id;
+}
+
+/**
+ * Applies all pending message updates to the current message list.
+ * Preserves object identity for unchanged messages so React.memo works optimally.
+ * Returns next message array (or current if no changes).
+ */
+export function applyPendingBatch(
+ current: StoredMessage[],
+ pending: Map,
+): StoredMessage[] {
+ if (pending.size === 0) return current;
+
+ let changed = false;
+
+ const next = current.map(message => {
+ const update = pending.get(message.id);
+ if (!update) return message;
+
+ const base = update.snapshot !== undefined ? update.snapshot : message.content;
+ const nextContent = update.completeText !== undefined ? update.completeText : (base + update.deltaText);
+
+ if (nextContent === message.content && (update.turnId === undefined || update.turnId === message.turnId)) {
+ return message;
+ }
+
+ changed = true;
+ return {
+ ...message,
+ content: nextContent,
+ turnId: update.turnId ?? message.turnId,
+ };
+ });
+
+ for (const [id, update] of pending.entries()) {
+ if (!current.some(m => m.id === id)) {
+ changed = true;
+ const base = update.snapshot !== undefined ? update.snapshot : '';
+ const content = update.completeText !== undefined ? update.completeText : (base + update.deltaText);
+ next.push({
+ id,
+ type: 'assistant',
+ content,
+ timestamp: update.timestamp ?? Date.now(),
+ turnId: update.turnId,
+ });
+ }
+ }
+
+ return changed ? next : current;
+}
diff --git a/packages/ui/src/useWorkbench.ts b/packages/ui/src/useWorkbench.ts
index 40f2bbf..3cdcf5d 100644
--- a/packages/ui/src/useWorkbench.ts
+++ b/packages/ui/src/useWorkbench.ts
@@ -3,10 +3,24 @@ import type { AgentEvent, StoredMessage } from '@threadcove/core/types';
import type { SessionDto, SourceDto } from '@threadcove/shared/protocol';
import type { ElectronAPI, TransportConnectionState } from '@threadcove/shared/client';
import type { RunFeedback } from './components/RunIndicator.tsx';
+import {
+ accumulateStreamDelta,
+ applyPendingBatch,
+ BATCH_INTERVAL_MS,
+ type PendingStreamDelta,
+} from './streamBatcher.ts';
export const connectionNames: Record = { idle: '等待连接', connecting: '正在连接', connected: '已连接', reconnecting: '正在重连', disconnected: '已断开', failed: '连接失败' };
export const runNames: Record = { running: '研究中', completed: '已完成', failed: '执行失败', cancelled: '已停止', interrupted: '执行已中断' };
+export interface FilePreviewState {
+ name: string;
+ content: string;
+ truncated?: boolean;
+ totalBytes?: number;
+ previewBytes?: number;
+}
+
type RunEvent = { type: 'run_status'; status: string; error?: string; runId: string };
type Incoming = { workspaceId?: string; sessionId: string; event: AgentEvent | RunEvent | { type: 'user_message'; message: StoredMessage } };
@@ -28,7 +42,7 @@ export function useWorkbench(api: ElectronAPI) {
const [submitting, setSubmitting] = useState(false);
const [sources, setSources] = useState([]);
const [files, setFiles] = useState>([]);
- const [filePreview, setFilePreview] = useState<{ name: string; content: string }>();
+ const [filePreview, setFilePreview] = useState();
const [activity, setActivity] = useState([]);
const [permission, setPermission] = useState['request']>();
const [runFeedback, setRunFeedback] = useState();
@@ -40,6 +54,8 @@ export function useWorkbench(api: ElectronAPI) {
const selectedRef = useRef(selected);
const workspaceRef = useRef(workspace);
const revision = useRef(0);
+ const pendingDeltas = useRef
-
-
-
-
-
-
-
-
+
----
+# ThreadCove
-ThreadCove 把复杂研究问题拆成多个**并行任务**(搜索 → 阅读 → 分析 → 汇总),每个任务一个独立 Session 与工作区;按任务阶段选模型(抓取整理求快省、分析综合求强推理);外部能力(搜索 API / 网页抓取 / 本地文档 / MCP 工具)统一接入。Electron 桌面端是主力工作台,WebUI 是同一套逻辑的第二个载体——**传输层只写一遍**。
+### 让零散线索,长成自己的洞见。
-> 个人项目 · 工程实践风格:类型严格、测试护住核心机制、CI 常驻、提交历史干净。不是企业级系统——没有数据库/RBAC/监控/容灾。
+从一个问题出发,连接资料、追问与推演,留下一份可以继续生长的研究档案。
-## ✨ 核心能力
+*A personal AI research workbench. Follow the question. Keep the thread.*
-| | 能力 | 说明 |
-|---|---|---|
-| 🔌 | **模型后端解耦** | `AgentBackend` 统一 `chat()` 接口,DeepSeek / Claude / Pi 三后端可换,切换 = 改一行配置 |
-| 🗂 | **任务隔离** | 一个研究任务一个 Session + 独立工作目录,并行任务的产物物理分家,互不污染 |
-| 🌊 | **统一事件流** | `AgentEvent` 可辨识联合类型——文本增量/工具/权限/结构化错误/完成,双后端出同一种事件,UI 按 type 分流渲染 |
-| 🔑 | **统一能力接入** | MCP / API / 本地数据源统一转成 Agent 可调用工具,凭据加密集中管理,调用时自动注入 |
-| 🖥 | **多端复用** | 每条 RPC 通道显式分类 LOCAL_ONLY / REMOTE_ELIGIBLE(穷举测试强制),桌面直连与 Web 代理共用一套协议 |
+[](https://github.com/Bluuok/ThreadCove/actions/workflows/validate.yml)
+[](https://bun.sh)
+[](packages/ui)
+[](apps/electron)
+[](LICENSE)
-## 🏗 架构
+[产品界面](#产品界面) · [研究流程](#研究流程) · [快速开始](#快速开始) · [项目文档](#项目文档)
-```text
-研究任务 = Session(会话上下文 + 工具上下文 + 独立工作目录)
-└── Workspace(rootPath:偏好默认值 / sources / skills / sessions 存储)
- └── AgentBackend(deepseek=OpenAI兼容流式 / anthropic=进程内 SDK / pi=进程外 JSONL 子进程)
- └── AgentEvent 统一词表 × 多后端适配 × EventQueue 桥 × typed_error 恢复
- └── Session/Workspace 隔离 · Source→Credential→Tool 能力接入(运行时汇合)
- └── 传输层 CHANNEL_MAP + WsRpc + 路由穷举 → 多端 UI
-```
+

-### 包结构
+**FIELDNOTES LAB / 个人研究现场**
-```text
-packages/
-├── core/ 纯类型层:AgentEvent / Message / Session / Workspace + win32 路径可移植工具
-└── shared/
- ├── protocol/ channels · dto · events · routing(穷举测试) · types · codec
- ├── agent/ backend 抽象 / factory / claude / pi / deepseek / EventQueue / 事件适配器
- ├── sessions/ session.jsonl 存储 · 持久化队列 · 路径可移植 · 防穿越
- ├── workspaces/ Workspace CRUD + defaults
- ├── sources/ SourceServerBuilder(三层header优先级) · api-tools · storage
- ├── credentials/ AES-256-GCM 机器绑定加密库
- ├── mcp/ McpClientPool 中央池 + 代理工具命名
- ├── skills/ SKILL.md 加载 + requiredSources 校验
- └── config/ MODEL_REGISTRY(能力字段) · 思考档 · 权限模式
-packages/pi-agent-server/ Pi 子进程:stdin/stdout JSONL 协议循环
-apps/
-├── electron/ 主进程(内嵌 WS server + handlers + SessionManager) · transport · preload · React renderer
-└── webui/ 复用同一 transport + CHANNEL_MAP,只覆写 LOCAL_ONLY 方法
-```
+
-### 一条消息的完整旅程
+## 为什么做 ThreadCove
-```text
-UI 调用 api.sendMessage()
- → buildClientApi 生成的代理 → RoutedClient(REMOTE_ELIGIBLE → WsRpcClient)
- → WS envelope(codec JSON+base64) → WsRpcServer → handlers → SessionManager
- → AgentBackend.chat() 产出 AsyncGenerator
- ├─ deepseek:OpenAI 兼容 /chat/completions SSE 流
- ├─ anthropic:Claude Agent SDK query() 流式消息 → 事件适配器
- └─ pi:spawn 子进程 → JSONL init/prompt → 原生事件 → PiEventAdapter → EventQueue 桥
- → 每个事件实时广播 session:event 到所有已连接客户端 + 按 parity 契约落盘 session.jsonl
+一个值得深入的问题,往往会带来许多网页、几段对话和零散的笔记。材料越来越多,思路却容易在窗口切换中断掉。
+
+ThreadCove 把这些过程放回同一个工作台:提出问题,让 AI 协助探索资料,在对话中持续追问,再把结论和文件留在当前任务里。下次回来,仍然能找到上次思考的起点。
+
+> 一个研究任务,一段连续的对话,一处独立的资料空间。
+
+## 研究流程
+
+```mermaid
+flowchart LR
+ A[提出问题] --> B[搜索与阅读]
+ B --> C[追问与整理]
+ C --> D[形成结论]
+ D --> E[保存与归档]
+ E -. 继续探索 .-> A
+ classDef paper fill:#edf2f5,stroke:#7893a6,color:#294356;
+ class A,B,C,D,E paper;
```
-## 🚀 快速开始
+- **开始**:记录一个疑问,或从首页的线索节点获得提问灵感。
+- **深入**:由模型按需调用搜索、网页读取和子任务工具,在同一段对话里综合资料。
+- **留下**:查看研究回复与任务文件,归档阶段性成果,随时重新打开并继续追问。
+
+这是使用路径,不是固定的执行阶段或进度条;具体工具调用由模型与所选后端决定。
+
+## 产品界面
-**环境要求**:[Bun](https://bun.sh) ≥ 1.3(Windows/macOS/Linux 均可,本仓库在 win32 上开发)
+### 从一个好问题开始
+
+雾蓝纸面、档案索引与节点连线,组成一张可以回应你的研究手记。首页保持安静,聚焦线索时,再用轻微的描线反馈引导探索。
+
+### 让思考有上下文,让成果有去处
+
+| 研究对话 | 资料与成果 |
+| :---: | :---: |
+| [](docs/images/research-conversation.png) | [](docs/images/research-notes.png) |
+| **沿着线索,让思考逐渐清晰。**
提问、流式回复与运行反馈,留在同一条研究线上。 | **将探索留下,成为下一次研究的起点。**
打开任务文件,在侧栏查看与当前研究关联的内容。 |
+
+截图来自实际运行的界面;对话和文件为人工准备的演示内容,不代表真实在线研究结果。点击双栏图片可查看原图。
+
+## 核心能力
+
+| 能力 | 在工作台里做什么 |
+| --- | --- |
+| **连续研究对话** | 提问、追问、查看流式回复;需要时停止当前运行,保留已收到的内容。 |
+| **搜索与网页阅读** | Pi 后端可按需检索资料、读取公开网页,并将来源产物保存在任务目录。 |
+| **有界子任务协作** | 将独立问题交给子任务研究,由主任务汇总;宿主管理并发、次数、超时和取消。 |
+| **独立研究档案** | 每个任务有独立会话与文件目录,支持历史恢复、归档和继续研究。 |
+| **资料与执行侧栏** | 查看工具活动、权限请求和任务文件;大文件提供有上限的内容预览。 |
+| **桌面与浏览器共用** | Electron 与 Web 共用工作台界面和事件协议,连接同一套会话服务。 |
+| **模型与工具可接入** | 提供 Pi、DeepSeek、Claude 后端适配,以及 MCP/API 资料源接入层。具体能力因后端而异。 |
+
+## 快速开始
+
+需要 [Bun](https://bun.sh) **≥ 1.3.10**。浏览器验收脚本另需 Node.js。
+
+### 1. 获取项目并配置模型
```bash
git clone https://github.com/Bluuok/ThreadCove.git
cd ThreadCove
bun install
+```
-# 配置 LLM 密钥(任选其一,写入 .env 或环境变量)
-cp .env.example .env # 编辑 .env,填写自己的密钥
-# ANTHROPIC_API_KEY=sk-ant-... # Claude
+将 `.env.example` 复制为 `.env`(PowerShell 可使用 `Copy-Item .env.example .env`),再填写自己的凭据。仓库示例使用 Pi + OpenCode Go:
-# 验证工程链路
-bun run typecheck # 全仓 strict tsc 零错误
-bun run lint
-bun test # 无密钥时自动跳过 5 个 live 测试
-bun run build # WebUI 和 Electron 生产构建
+```dotenv
+THREADCOVE_PROVIDER=pi
+THREADCOVE_PI_PROVIDER=opencode-go
+THREADCOVE_MODEL=deepseek-v4.1-flash
+OPENCODE_GO_API_KEY=你的密钥
+```
-# 桌面端
-bun run dev:electron
+也可使用本机加密配置,步骤见 [OpenCode Go 接入说明](docs/OPENCODE-GO.md)。模型请求使用所配置服务的额度;密钥不要提交到仓库。
-# WebUI + headless 服务(演示同一套逻辑服务两种载体)
-bun run server:headless 8787
-bun run dev:webui # 打开 http://localhost:5173
-```
+### 2. 打开桌面工作台
-WebUI 启动后,使用 headless 终端输出的带 fragment 引导链接连接;随机 token 只保存在当前浏览器会话。服务默认监听本机,数据位于 `.threadcove-workspace/`。模型可通过 `THREADCOVE_PROVIDER` / `THREADCOVE_MODEL` 配置,任务设置支持持久化模型覆盖。
+```bash
+bun run dev:electron
+```
-本次评审整改、兼容边界和验证步骤见 [整改说明](docs/IMPLEMENTATION-REVIEW.md);OpenDesign 规范与实际前端移植见 [前端设计](docs/FRONTEND-DESIGN.md)。[OpenCode Go 接入说明](docs/OPENCODE-GO.md) 提供本机加密配置与真实 Pi SDK 验证方法。OpenCode Go / DeepSeek V4.1 Flash 已通过真实 SDK、宿主工具往返及 Web/Electron 联调;DeepSeek 直连接口和 Claude API 本次未验证。
+### 3. 或在浏览器中使用
-**验证流式对话**(需要真实密钥,无需 UI):
+分别在两个终端运行:
```bash
-DEEPSEEK_API_KEY=sk-... bun run scripts/smoke-deepseek.ts
+# 终端一:启动本地会话服务
+bun run server:headless 8787
```
-## 🎨 三个值得看的设计
+```bash
+# 终端二:启动 Web 界面
+bun run dev:webui
+```
-### 1. redirect 双分支——统一接口不假装所有后端一样
+使用 headless 终端输出的带 fragment 引导链接连接。服务默认监听本机,数据默认保存在 `.threadcove-workspace/`;连接 token 保存在当前浏览器会话中。
-```typescript
-// pi-agent.ts — 有原生 steering:消息注入当前流,事件继续走
-redirect(message: string): boolean {
- if (!this.isProcessing()) { this.forceAbort(AbortReason.Redirect); return false; }
- this.send({ type: 'steer', message });
- return true; // ← true:会话层什么都不用做
-}
+## 项目结构
-// deepseek-agent.ts — 无原生 steering:内部排队,会话层决定重发时机
+```text
+ThreadCove/
+├── apps/
+│ ├── electron/ 桌面宿主、会话服务与 RPC 处理
+│ └── webui/ 浏览器入口与传输适配
+├── packages/
+│ ├── ui/ 两端共用的研究工作台
+│ ├── core/ 会话、消息与事件类型
+│ ├── shared/ 后端适配、工具、存储与通信协议
+│ └── pi-agent-server/ Pi SDK 子进程与宿主工具桥接
+├── scripts/ 开发与验收脚本
+└── docs/ 接入说明、设计文档与界面截图
```
-接口签名一个 `boolean`,把「各后端能力不同」这个事实诚实地暴露给上层,而不是用一个统一假象掩盖它。
+界面通过统一 RPC 调用会话服务;模型后端产出统一事件,由宿主保存并推送到客户端。任务存储、模型适配和界面展示各自独立,便于更换后端或调整交互。
-### 2. EventQueue——不是 async generator 换皮
+## 开发与验证
-Claude 是同步 `for-await` 消费 SDK 流;Pi 子进程事件是**异步回调**到达。`EventQueue`(`enqueue` 推入唤醒 / `drain` 等待产出 / `complete` 收流 / `reset` 翻新)把 push 变 pull,让两种截然不同的喂法产出**同一个** `AsyncGenerator` 消费面。六个测试锁住:顺序保证、空队列等待、提前 complete 缓冲投递、reset 复用、交错生产。
+```bash
+bun run typecheck
+bun run lint
+bun test
+bun run build
-### 3. 路由穷举——每条通道都被显式决策过
+# 安装浏览器后,运行 Web 界面验收
+bunx playwright install chromium
+bun run test:ui:web
-```typescript
-// packages/shared/tests/routing.test.ts
-test('every channel is classified exactly once', () => {
- for (const ch of all) {
- const inLocal = LOCAL_ONLY_CHANNELS.has(ch);
- const inRemote = REMOTE_ELIGIBLE_CHANNELS.has(ch);
- if (!inLocal && !inRemote) throw new Error(`Channel "${ch}" 未分类`);
- if (inLocal && inRemote) throw new Error(`Channel "${ch}" 双重分类`);
- }
-});
+# 本地完整验收,包含 Electron 窗口
+bun run test:ui
```
-新增通道不分类 → CI 直接红。「文件对话框只能在桌面走,会话内容两端都要走」不是约定俗成,是测试强制的架构约束。
+CI 执行类型检查、lint、自动测试、双端构建与 Chromium 界面验收。真实模型调用需单独配置凭据;自动界面验收使用本地测试服务,不证明真实上游服务可用。
-## 📡 传输协议(摘要)
+## 使用边界
-```text
-client → { type:'handshake', protocolVersion:'1.0', token?, workspaceId? }
-server → { type:'handshake_ack', clientId, registeredChannels }
+- 面向个人、本地优先的研究场景,目前没有多用户权限系统、向量知识库或跨任务长期记忆。
+- 搜索与子任务编排属于 Pi 研究工具链;其他后端不能视为具备相同能力。
+- 网页读取面向公开 HTML、文本和 JSON;登录页面、PDF 和依赖 JavaScript 渲染的内容不在当前读取范围内。
+- 中断的子任务会记录状态,不会自动恢复其 SDK 内存上下文或重新执行。来源内容与模型结论仍需核对。
-request { id, type:'request', channel:'sessions:sendMessage', args:[...] }
-response { id, type:'response', result:{...} } | error { code:'CHANNEL_NOT_FOUND', message }
-event { id, type:'event', channel:'session:event', args:[{ sessionId, event:AgentEvent }] }
-```
+## 项目文档
-- 二进制走 `{"__tcRpcType":"u8","base64":"..."}` 标注编码,两端无损还原
-- 错误码:`HANDLER_ERROR` · `CHANNEL_NOT_FOUND` · `AUTH_FAILED` · `PROTOCOL_VERSION_UNSUPPORTED` · `REQUEST_TIMEOUT`(类标识不跨线,接收端按 `err.code` 分支)
-- 非 localhost 明文 `ws://` 一律拒绝(token 明文防护)
-- 完整通道分类表与 envelope 细节见 **[docs/PROTOCOL.md](docs/PROTOCOL.md)**
-
-## 🔒 安全模型
-
-- **凭据**:AES-256-GCM 加密落盘(`credentials.enc`,64B 头 + PBKDF2(机器硬件 ID ∧ 随机盐, 100k 轮))——文件拷到别的机器解不开。机器标识是**密钥派生输入**,不是密钥本身
-- **进程边界**:MCP/API 源凭据只在宿主进程;Pi 子进程执行源工具时发 `tool_execute_request` 回宿主,**凭据永不进入子进程环境**
-- **渲染进程**:preload 只有引导逻辑,无手工 IPC 分发表;`contextIsolation: true`;未分类通道过不了路由穷举测试
-- 细节与已知边界见 **[docs/SECURITY.md](docs/SECURITY.md)**
-
-## 🧪 测试地图
-
-| 套件 | 护住什么 |
-|---|---|
-| `routing.test` | 通道分类穷举:完备/总数相等/零交集/非空 |
-| `codec.test` | envelope 往返含二进制标注编码、畸形包拒绝 |
-| `event-queue.test` | push→pull 桥:顺序/等待/收流/复位/交错 |
-| `factory.test` | provider→backend 路由、未知 provider 抛错、生命周期面 |
-| `claude/pi-event-adapter.test` | 双后端原生事件 → 统一词表快照 + 未知事件容错 |
-| `pi-protocol.test` | 真实子进程:init→ready 握手、事件流、tool_execute 往返、坏 JSONL 容错、abort、redirect 双分支 |
-| `session-event-message-parity.test` | 事件字段 ↔ 持久化字段双端一致(防字段漂移) |
-| `sessions.test` | 双会话物理隔离、路径穿越、归档/删除、并发持久化、`{{SESSION_PATH}}` 往返(含 Windows 转义) |
-| `sources.test` | 三层 header 优先级、缺 token→null、Authorization 三态、AES 加解密往返+换机失败、代理命名映射、SKILL.md 校验 |
-| `transport/bootstrap.test` | 真实 WS 收发、token 鉴权、广播、buildClientApi、明文 ws:// 拒绝 |
-| `integration.test` | 端到端:建会话→发消息→收流→落盘、多会话隔离、后端切换、typed error code |
-| `deepseek.test` | 无密钥 typed_error;**有密钥时跑 live**:流式/用量/中断/标题生成 |
-
-## 📚 文档
-
-- **[docs/REPRODUCTION_SPEC.md](docs/REPRODUCTION_SPEC.md)** — 完整复现规格:范围/深度/边界/不做清单
-- **[docs/PROTOCOL.md](docs/PROTOCOL.md)** — 传输协议:envelope 格式 + 通道分类表
-- **[docs/SECURITY.md](docs/SECURITY.md)** — 凭据加密模型 + 安全设计动机
-
-## 🗺 边界(诚实交底)
-
-- **不做**:RAG/向量库(主打实时抓取+多源核对)、多 Agent 编排(多任务=多 Session)、Memory/跨任务知识复用、完整 OAuth(凭据手动粘贴,只留 `getToken` 钩子)
-- **MCP 只到统一转换层**:官方 SDK Client + 代理工具组装,不自研 JSON-RPC/握手
-- WebUI 定位是「验证同一套逻辑能否复用」的接续查看面,不是全功能第二产品
-- Pi 后端已接入真实 `createAgentSession`,默认注册免密钥搜索、公开网页读取及最多 3 个并发子任务;详见[研究工具与预算](docs/RESEARCH-TOOLS.md)
-- 已有真实 OpenCode Go API / Pi SDK / Web / Electron 验收,以及两个真实子任务搜索、读取、来源落盘和刷新恢复验证。自动测试不使用个人 API 密钥,DeepSeek 直连 live 测试默认跳过;1M 上下文尚未实测满窗口
+| 文档 | 内容 |
+| --- | --- |
+| [研究工具与子任务](docs/RESEARCH-TOOLS.md) | 搜索、网页读取、编排约束与来源保存 |
+| [OpenCode Go 接入](docs/OPENCODE-GO.md) | API 配置、本机凭据与 Pi SDK 验证 |
+| [前端设计](docs/FRONTEND-DESIGN.md) | 设计方向与共享界面实现 |
+| [通信协议](docs/PROTOCOL.md) | RPC、事件与多端路由 |
+| [安全模型](docs/SECURITY.md) | 凭据管理、进程边界与已知限制 |
+| [实现与验收记录](docs/IMPLEMENTATION-REVIEW.md) | 实现调整与验证步骤 |
+| [项目范围与复现规格](docs/REPRODUCTION_SPEC.md) | 设计范围、实现深度与边界 |
+
+## 参与项目
+
+欢迎通过 [Issues](https://github.com/Bluuok/ThreadCove/issues) 反馈问题或提出想法。提交改动时,请附上复现步骤与相关验证,并保持一个改动对应一个明确范围。
## License
-MIT
+[MIT](LICENSE)
+
+---
+
+
+
+**不只获得一个回答,也留下通往答案的线索。**
+
+ThreadCove · FIELDNOTES LAB
+
+
diff --git a/docs/images/research-conversation.png b/docs/images/research-conversation.png
new file mode 100644
index 0000000000000000000000000000000000000000..77f93f685f4e1f4e9bb8d9fa6e75d8d43c8598fc
GIT binary patch
literal 95386
zcmd>lQ+Omy)Mh6$NoHaj6T4$;V%s)5=ERwaJ#jLzZQHhO+t%jO|LorH)oxw%Q(av+
zr%u(WgSUg_WJKYhv7rF~0Gzm(kOBYzkpTdFI`{(q(UOlfmIDAF0>p**m42k0tiot1
zIlb|{jpuDZC;_RCeBc{?C*&v*2?;zv=1mk8B-V>*XX{P?Cm@Z!9)}ozZ|<<3;G9_;U;2
zxvVVtxOxs3_1NGyz_%#xtCgNf_|G3C0RS6zSD(TDt^oiCdx#tVR(-4yz~KLDzWe{K
zxtzQagX#S(DIrB#>HYjFk`?j4eF;2V1IJ~2v`yk+BPIX05rBZzRPb;8dwn1P>fgGf
zJKj;&R>IpTGKRw;?qb-dWm7Ty1cF4iePS>Bi5cT^U@E7IweehN`sC8paIRuHS>6c^
z@qd%14v1ml%COrg6CGvO`n(hTT|_?8v?1G
zhg(A{0Phh^eDJ0HipNO5X$C!eG+g9%SBvWdj&!7s+srj#x5S
z;XQhrd_;=N+9olp7O=W}#duoLt(=5Q4c
zwvVYi4sm{P%qUM-#h<0_0SjCWYR1Qqa3@`GMX6v@e77F|0+afEZ9mJ^6n
z3hC~Sx=d*w3b~~7c|Gra;k43G$=~bWF9VJ%_O6rpo+u mLs}9ir|LTuZVvtJQ5k
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