DEV Community

Cover image for Dual-Window Parallel Conversation: A Technical Implementation for Branching Ideas Without Blocking the Main Flow
EntropicRemainder
EntropicRemainder

Posted on

Dual-Window Parallel Conversation: A Technical Implementation for Branching Ideas Without Blocking the Main Flow

[ZSHX]双窗口并行对话:让思路分叉而不被阻断

当你正在对话,一个“不该出现在这里”的想法冒出来时,如何让它被立即接住,而不打断主线?

一、核心场景

你正在与AI深入讨论一个话题。突然,一个新想法涌现——它有价值,但属于另一个方向。此时有三个选择:

选择 后果
当场展开 主线被打断,节奏崩塌
忽略它 大概率再也回不来
记下来,等主线结束再处理 状态已凉,重新预热成本极高

真实需求是:让想法在另一个窗口中立即被“行走”,而不是被“记录”。

二、技术实现(localStorage方案,可直接运行)

将以下代码保存为branch.html,用浏览器打开两次(窗口A带?window=A,窗口B带?window=B):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <title>双窗口并行对话</title>
    <style>
        body { font-family: system-ui; max-width: 800px; margin: 2rem auto; padding: 0 1.5rem; }
        .panel { background: #f8f9fa; border-radius: 12px; padding: 1.5rem; margin: 1rem 0; border-left: 4px solid #2563eb; }
        .panel-b { border-left-color: #059669; }
        .window-id { font-size: 14px; color: #6b7280; }
        textarea { width: 100%; padding: 0.75rem; border-radius: 8px; border: 1px solid #d1d5db; box-sizing: border-box; }
        .btn { padding: 0.5rem 1.5rem; border-radius: 8px; border: none; background: #2563eb; color: white; cursor: pointer; }
        .btn-b { background: #059669; }
        .log { background: #1e293b; color: #e2e8f0; padding: 0.75rem 1rem; border-radius: 8px; max-height: 150px; overflow-y: auto; font-size: 13px; }
        .branch-display { background: #f1f5f9; padding: 0.75rem 1rem; border-radius: 8px; margin: 0.5rem 0; }
        .status { display: inline-block; padding: 0.2rem 0.8rem; border-radius: 20px; font-size: 12px; }
        .status-pending { background: #fef3c7; color: #92400e; }
        .status-processing { background: #dbeafe; color: #1e40af; }
        .status-done { background: #d1fae5; color: #065f46; }
        .row { display: flex; gap: 1rem; flex-wrap: wrap; }
        .row > * { flex: 1; min-width: 250px; }
    </style>
</head>
<body>
<h1>双窗口并行对话</h1>
<div class="row">
    <div class="panel">
        <div class="window-id">窗口A(发送端)</div>
        <textarea id="branchInput" rows="2" placeholder="输入你想分叉的想法..."></textarea>
        <button class="btn" onclick="sendBranch()">发送到窗口B</button>
        <div style="margin-top:0.75rem;font-size:14px;color:#6b7280;">状态:<span id="aStatus">就绪</span></div>
        <div class="log" id="aLog">等待操作...</div>
    </div>
    <div class="panel panel-b">
        <div class="window-id">窗口B(接收端)</div>
        <div id="currentBranchDisplay" class="branch-display" style="color:#6b7280;font-style:italic;">等待窗口A发送...</div>
        <div style="margin-top:0.75rem;font-size:14px;color:#6b7280;">状态:<span id="bStatus" class="status status-pending">等待中</span></div>
        <div class="log" id="bLog">等待接收...</div>
    </div>
</div>
<script>
const STORAGE_KEY = 'branch_queue';
const WINDOW_ID = location.search.includes('window=B') ? 'B' : 'A';

function getBranches() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch { return []; } }
function saveBranches(b) { localStorage.setItem(STORAGE_KEY, JSON.stringify(b)); }
function genId() { return Date.now().toString(36) + Math.random().toString(36).slice(2,6); }
function log(id, msg) {
    const el = document.getElementById(id);
    el.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
    el.scrollTop = el.scrollHeight;
}

function sendBranch() {
    const input = document.getElementById('branchInput');
    const content = input.value.trim();
    if (!content) return alert('请输入想法');
    const branch = { id: genId(), content, status: 'pending', createdAt: new Date().toISOString() };
    const branches = getBranches();
    branches.push(branch);
    saveBranches(branches);
    log('aLog', `已发送:「${content}」`);
    document.getElementById('aStatus').textContent = '已发送,等待窗口B承接...';
    input.value = '';
    localStorage.setItem('__branch_notify', Date.now().toString());
}

function processBranches() {
    const branches = getBranches();
    const pending = branches.find(b => b.status === 'pending');
    if (!pending) {
        document.getElementById('currentBranchDisplay').textContent = '暂无待处理的想法';
        document.getElementById('bStatus').textContent = '等待中';
        document.getElementById('bStatus').className = 'status status-pending';
        return;
    }
    document.getElementById('currentBranchDisplay').textContent = pending.content;
    document.getElementById('bStatus').textContent = '分析中...';
    document.getElementById('bStatus').className = 'status status-processing';
    log('bLog', `接收到:「${pending.content}」`);
    let progress = 0;
    const interval = setInterval(() => {
        progress += 10;
        if (progress <= 100) log('bLog', `分析中... ${progress}%`);
        if (progress >= 100) {
            clearInterval(interval);
            const updated = getBranches();
            const target = updated.find(b => b.id === pending.id);
            if (target) { target.status = 'done'; saveBranches(updated); }
            document.getElementById('bStatus').textContent = '分析完成,等待你切换过来';
            document.getElementById('bStatus').className = 'status status-done';
            log('bLog', '分析完成!切换到此窗口继续深入。');
        }
    }, 400);
}

window.addEventListener('storage', function(e) {
    if (e.key === STORAGE_KEY || e.key === '__branch_notify') {
        if (WINDOW_ID === 'B') processBranches();
        else if (WINDOW_ID === 'A') {
            const branches = getBranches();
            const pending = branches.find(b => b.status === 'pending');
            if (pending) document.getElementById('aStatus').textContent = '想法已发送,窗口B正在处理...';
            else if (branches.find(b => b.status === 'processing')) document.getElementById('aStatus').textContent = '窗口B正在分析中...';
            else if (branches.find(b => b.status === 'done')) document.getElementById('aStatus').textContent = '想法已在窗口B中完成分析,可切换查看';
        }
    }
});

if (WINDOW_ID === 'A') {
    document.querySelector('.panel .window-id').textContent = '窗口A(发送端)— 当前';
    log('aLog', '窗口A已就绪');
    const done = getBranches().find(b => b.status === 'done');
    if (done) document.getElementById('aStatus').textContent = '想法已在窗口B中完成分析,可切换查看';
}
if (WINDOW_ID === 'B') {
    document.querySelector('.panel-b .window-id').textContent = '窗口B(接收端)— 当前';
    log('bLog', '窗口B已就绪,监听中...');
    processBranches();
    setInterval(processBranches, 3000);
}
</script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

三、工作机制

步骤 窗口A 窗口B
1 输入想法,点击发送
2 写入localStorage,触发storage事件 监听到事件,自动接收
3 继续主线对话(不被打断) 自动开始“分析”
4 对话完成 分析完成,等待用户切换

四、方案对比

方案 适用场景 优缺点
localStorage + storage事件 快速验证、日常使用 ✅ 无外部依赖,保存HTML即可运行
❌ 秒级延迟,依赖页面焦点
SharedWorker 高频交互、稳定通信 ✅ 性能更好,不依赖焦点
❌ 浏览器支持有限
WebSocket + 服务端 跨设备/跨浏览器 ✅ 真正跨终端
❌ 需要自己搭服务端

五、能力边界

能做的:

  • 在对话中分叉思路,不打断主线
  • 想法被另一个窗口立即接住并开始处理
  • 两个窗口独立运行,互不干扰
  • 完全在浏览器本地完成,无服务端依赖

不能做的:

  • 不能“传送”AI的对话状态(上下文、记忆、会话状态无法跨窗口复制)
  • 不能在窗口B中自动继续窗口A的对话
  • 不能实现真正的实时同步(存在秒级延迟)

六、总结

双窗口并行对话的工程本质是:让思路在对话过程中分叉,在另一个空间中并行行走,不打断主线。

它解决的不是“跨窗口通信”,而是——让想法被立即接住并开始行走,而不是被记录后等待。

下一步:用上述代码本地打开两个标签页测试,然后将窗口B中的模拟分析替换为真实AI接口调用。

Top comments (1)

Collapse
 
entropicremainder profile image
EntropicRemainder

@hadil 是这种双窗并行会话,状态同步的效果吗?