|
@@ -338,45 +338,58 @@ export class ChatService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 开启一张**空白的待建会话**(不落任何数据)。
|
|
|
|
|
|
|
+ * 新建对话:**立刻创建一条会话**并在界面选中它。
|
|
|
*
|
|
*
|
|
|
- * 2026-11-01 用户裁决「首次发消息才建会话」后的入口:
|
|
|
|
|
- * 点「新建对话」只是把界面切到「该容器、还没选会话」的状态,
|
|
|
|
|
- * 标题栏显示「新对话」,用户输入并发送时才真正 `create()`。
|
|
|
|
|
|
|
+ * 2026-11-01 用户裁决变更(原为「首次发消息才建」):
|
|
|
|
|
+ * 点「新建对话」之后,左侧对应区域(工作区文件夹 / 日常对话)必须**立刻**
|
|
|
|
|
+ * 出现一条「新对话」并且处于选中态 —— 否则用户点了按钮看不到任何反馈,
|
|
|
|
|
+ * 不知道操作有没有生效。
|
|
|
*
|
|
*
|
|
|
- * 🔴 因此**不会出现 0 消息的空壳**:用户点了又切走、关页面,什么都不留下。
|
|
|
|
|
- * 这也是 `pruneEmpty` 这一整套事后清理可以整体删除的前提。
|
|
|
|
|
|
|
+ * 代价与对策:用户点了却不发消息就切走,会留下 0 条消息的空壳。
|
|
|
|
|
+ * 由 `sessions.pruneEmpty(runningId)` 在**打开容器**和**切走会话**时清理,
|
|
|
|
|
+ * 只保留当前正在看的那条。
|
|
|
*
|
|
*
|
|
|
- * @param agentId 目标容器
|
|
|
|
|
- * @returns 是否成功切换到该容器的空白态(容器连接失败时为 false)
|
|
|
|
|
|
|
+ * @param agentId 目标容器
|
|
|
|
|
+ * @param workspaceId 归属工作区(`null` = 日常对话)
|
|
|
|
|
+ * @returns 创建出的会话;容器未就绪时返回 null
|
|
|
*/
|
|
*/
|
|
|
- /**
|
|
|
|
|
- * 是否处于「待建态」:用户点了新建对话、选好容器,但还没发出第一条消息。
|
|
|
|
|
- *
|
|
|
|
|
- * 🔴 与「该容器没有会话」是**两种不同状态**,界面提示必须分开:
|
|
|
|
|
- * · 待建态 → 应该给输入框和欢迎引导,让用户直接开始;
|
|
|
|
|
- * · 无会话 → 才提示「点击左侧新建对话」。
|
|
|
|
|
- * 混为一谈会导致:用户点了新建,页面却还在提示「去点新建对话」(死循环)。
|
|
|
|
|
- */
|
|
|
|
|
- private readonly _pendingNew = signal(false);
|
|
|
|
|
- readonly pendingNewSession = this._pendingNew.asReadonly();
|
|
|
|
|
|
|
+ async startBlank(agentId: string, workspaceId: string | null = null): Promise<ChatSession | null> {
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 🔴 不调用 `open()`(2026-11-01 修正)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * `open()` 会顺带做「打开该容器最近的会话」这一步,把 `_sessionId` 指向已存在的
|
|
|
|
|
+ * 会话 —— 紧接着我们又把它改成本次新建的那条,来回横跳;
|
|
|
|
|
+ * 更糟的是当容器里已有空会话时,`open()` 会先选中它,导致复用判断和
|
|
|
|
|
+ * 「当前会话」指针不一致(测试实测:连点新建变成两条)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 这里只做「确保容器已连接」这件事,选会话完全由本方法自己决定。
|
|
|
|
|
+ */
|
|
|
|
|
+ if (this._agentId() !== agentId || !this.roomId) {
|
|
|
|
|
+ await this.open(agentId);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this._agentId() !== agentId) return null;
|
|
|
|
|
+ if (this._state() === 'error') return null;
|
|
|
|
|
+ const roomId = this.roomId;
|
|
|
|
|
+ if (!roomId) return null;
|
|
|
|
|
|
|
|
- async startBlank(agentId: string, workspaceId: string | null = null): Promise<boolean> {
|
|
|
|
|
- await this.open(agentId);
|
|
|
|
|
- if (this._agentId() !== agentId) return false;
|
|
|
|
|
- if (this._state() === 'error') return false;
|
|
|
|
|
/*
|
|
/*
|
|
|
- * 只清「当前会话」指针,不动列表:写一条空消息会触发提示词与 UI 抖动,
|
|
|
|
|
- * 这里直接复位即可。后续 sendText → ensureSessionForSend 会把会话建出来。
|
|
|
|
|
|
|
+ * 🔴 复用规则:该容器 + 该归属下已经有空会话时,直接打开它而不是再建一条。
|
|
|
|
|
+ * 否则用户连点几次「新建对话」就会攒出一排同名的「新对话」。
|
|
|
*/
|
|
*/
|
|
|
- this._sessionId.set('');
|
|
|
|
|
|
|
+ const existEmpty = this.sessions
|
|
|
|
|
+ .listFor(agentId)
|
|
|
|
|
+ .find((s) => !s.legacy && (s.messageCount ?? 0) === 0 && (s.workspaceId ?? null) === (workspaceId ?? null));
|
|
|
|
|
+ const s = existEmpty ?? this.sessions.create(roomId, agentId);
|
|
|
|
|
+ if (!existEmpty && workspaceId) this.sessions.moveTo(s.id, workspaceId);
|
|
|
|
|
+
|
|
|
|
|
+ // 切到这条新会话(左侧随之高亮,见 session-tree 的 activeId)
|
|
|
|
|
+ this._sessionId.set(s.id);
|
|
|
this._messages.set([]);
|
|
this._messages.set([]);
|
|
|
this._state.set('empty');
|
|
this._state.set('empty');
|
|
|
this._error.set(null);
|
|
this._error.set(null);
|
|
|
this._hasMoreHistory.set(false);
|
|
this._hasMoreHistory.set(false);
|
|
|
this._blankWorkspace = workspaceId;
|
|
this._blankWorkspace = workspaceId;
|
|
|
- this._pendingNew.set(true);
|
|
|
|
|
- return true;
|
|
|
|
|
|
|
+ return this.sessions.byId(s.id) ?? s;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -542,8 +555,6 @@ export class ChatService {
|
|
|
}
|
|
}
|
|
|
if (my !== this.seq) return;
|
|
if (my !== this.seq) return;
|
|
|
this._sessionId.set(sid);
|
|
this._sessionId.set(sid);
|
|
|
- // 打开了真实会话 → 退出待建态(否则界面会一直按「还没开始」渲染)
|
|
|
|
|
- this._pendingNew.set(false);
|
|
|
|
|
|
|
|
|
|
const all = this.mapTimeline(
|
|
const all = this.mapTimeline(
|
|
|
rawTimeline.filter((m) => this.belongsToSession(m, sid)),
|
|
rawTimeline.filter((m) => this.belongsToSession(m, sid)),
|
|
@@ -666,8 +677,6 @@ export class ChatService {
|
|
|
this._peerTyping.set(false);
|
|
this._peerTyping.set(false);
|
|
|
this._paginating.set(false);
|
|
this._paginating.set(false);
|
|
|
this._hasMoreHistory.set(false);
|
|
this._hasMoreHistory.set(false);
|
|
|
- // 离开聊天页 → 待建态作废(下次进来是新的一次,不该继承上次的空白态)
|
|
|
|
|
- this._pendingNew.set(false);
|
|
|
|
|
this._blankWorkspace = null;
|
|
this._blankWorkspace = null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -731,8 +740,6 @@ export class ChatService {
|
|
|
* 建出会话 = 这已经是正常会话了,状态必须跟着走。
|
|
* 建出会话 = 这已经是正常会话了,状态必须跟着走。
|
|
|
*/
|
|
*/
|
|
|
this._state.set('ready');
|
|
this._state.set('ready');
|
|
|
- // 会话已建出 → 不再是待建态
|
|
|
|
|
- this._pendingNew.set(false);
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
/*
|
|
/*
|
|
@@ -749,6 +756,13 @@ export class ChatService {
|
|
|
ts: Date.parse(msg.ts) || Date.now(),
|
|
ts: Date.parse(msg.ts) || Date.now(),
|
|
|
fromMe: true,
|
|
fromMe: true,
|
|
|
});
|
|
});
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 发出消息 = 这已是正常对话(2026-11-01)。
|
|
|
|
|
+ * 新建对话是「点击即建」,建出来时 state 是 empty(还没有消息),
|
|
|
|
|
+ * 用户一发消息就该切到 ready —— 否则消息区继续按空态渲染,
|
|
|
|
|
+ * 发出去的内容要等下一次刷新才看得见。
|
|
|
|
|
+ */
|
|
|
|
|
+ if (this._state() === 'empty') this._state.set('ready');
|
|
|
}
|
|
}
|
|
|
/*
|
|
/*
|
|
|
* 🔴 用户一发出消息就进入「忙」——**这是事实,不是推测**(容器必定会收到)。
|
|
* 🔴 用户一发出消息就进入「忙」——**这是事实,不是推测**(容器必定会收到)。
|