;
onPrefill?: (prompt: string, skillId?: string) => void;
}) {
@@ -50,7 +52,7 @@ export function ChatSkillPicker({
{open && (
diff --git a/src/components/ChatView.tsx b/src/components/ChatView.tsx
index b63d097..b7179f4 100644
--- a/src/components/ChatView.tsx
+++ b/src/components/ChatView.tsx
@@ -547,6 +547,7 @@ export function ChatView({
{
messageId: options?.messageId,
forceDeepReasoning: options?.forceDeepReasoning,
+ pgRequired: options?.pgRequired,
selectedChatSkill: options?.selectedChatSkill,
fileAttachments: options?.fileAttachments,
},
diff --git a/src/components/SpaceChatPanel.tsx b/src/components/SpaceChatPanel.tsx
index 3c3f590..9b5b37e 100644
--- a/src/components/SpaceChatPanel.tsx
+++ b/src/components/SpaceChatPanel.tsx
@@ -147,6 +147,7 @@ export function SpaceChatPanel({
...context,
messageId: options?.messageId,
forceDeepReasoning: options?.forceDeepReasoning,
+ pgRequired: options?.pgRequired,
fileAttachments: options?.fileAttachments,
},
imageUrls,
@@ -158,6 +159,7 @@ export function SpaceChatPanel({
mindspaceContext: context,
messageId: options?.messageId,
forceDeepReasoning: options?.forceDeepReasoning,
+ pgRequired: options?.pgRequired,
fileAttachments: options?.fileAttachments,
},
imageUrls,
diff --git a/src/hooks/useTKMindChat.ts b/src/hooks/useTKMindChat.ts
index 55c18c1..e88be94 100644
--- a/src/hooks/useTKMindChat.ts
+++ b/src/hooks/useTKMindChat.ts
@@ -279,6 +279,10 @@ function isTransientConnectError(err: unknown) {
return /超时|timeout/i.test(err.message);
}
+function isMissingAgentSessionError(err: unknown) {
+ return err instanceof ApiError && /session not found/i.test(err.message);
+}
+
async function withTransientConnectRetry(fn: () => Promise, attempts = 3): Promise {
let lastErr: unknown;
for (let attempt = 0; attempt < attempts; attempt += 1) {
@@ -329,6 +333,7 @@ export function useTKMindChat(
const subscribedSessionIdRef = useRef(null);
const unsubscribeRef = useRef<(() => void) | null>(null);
const connectTokenRef = useRef(0);
+ const unavailableAgentSessionIdsRef = useRef(new Set());
const messagesRef = useRef([]);
const sessionRef = useRef(null);
const sessionsRef = useRef([]);
@@ -1139,7 +1144,9 @@ export function useTKMindChat(
}),
);
const resumedPromise =
- options?.skipResume || isDirectChatSessionId(sessionId)
+ options?.skipResume ||
+ isDirectChatSessionId(sessionId) ||
+ unavailableAgentSessionIdsRef.current.has(sessionId)
? Promise.resolve(options.seedSession ?? null)
: withTransientConnectRetry(() =>
resumeSession(sessionId, {
@@ -1160,7 +1167,16 @@ export function useTKMindChat(
setMessageHistoryHasMore(messageHistoryHasMoreRef.current);
setMessageHistoryTotal(messageHistoryTotalRef.current);
setMessages(history);
- const resumed = await resumedPromise;
+ let resumed: Session | null = null;
+ try {
+ resumed = await resumedPromise;
+ } catch (err) {
+ if (!isMissingAgentSessionError(err)) throw err;
+ // Persisted conversation is still readable even when its transient
+ // Agent runtime has been reclaimed. Do not leave a completed chat
+ // in the connecting state or retry resume on future selections.
+ unavailableAgentSessionIdsRef.current.add(sessionId);
+ }
if (token !== connectTokenRef.current) return;
setSession({ ...detail, ...(resumed ?? {}), id: sessionId });
setChatState('idle');
@@ -1383,6 +1399,7 @@ export function useTKMindChat(
mindspaceContext?: MindSpaceChatContext;
messageId?: string;
forceDeepReasoning?: boolean;
+ pgRequired?: boolean;
selectedChatSkill?: string;
fileAttachments?: ChatFileAttachment[];
},
@@ -1412,7 +1429,10 @@ export function useTKMindChat(
: '';
const userPrefix = buildUserAddressPrefix(userRef.current);
const skillPrefix = buildAutoChatSkillPrefix(trimmed, grantedSkills ?? []);
- const agentPrefix = `${userPrefix}${mindspacePrefix}${skillPrefix}`;
+ const pgContractPrefix = options?.pgRequired
+ ? '[交付约束:用户已明确要求使用专属 PostgreSQL 数据空间。若生成页面,必须按 page-data-collect 完成建表、dataset、policy 和 workspace page 绑定;禁止 localStorage、SQLite、静态 JSON 或内存持久化。所有验证通过前不得回复已发布或给出页面链接。]\n'
+ : '';
+ const agentPrefix = `${userPrefix}${mindspacePrefix}${skillPrefix}${pgContractPrefix}`;
const priorMessageCount = messagesRef.current.length;
const userMessage = buildUserMessage(trimmed, {
id: options?.messageId,
@@ -1429,6 +1449,7 @@ export function useTKMindChat(
? userMessage.metadata.memindRun
: {}),
sessionMessageCount: priorMessageCount,
+ ...(options?.pgRequired ? { pgRequired: true } : {}),
...(options?.selectedChatSkill ? { selectedChatSkill: options.selectedChatSkill } : {}),
},
};
@@ -1582,6 +1603,11 @@ export function useTKMindChat(
const nextChatState = resolvePostAgentRunChatState({
chatState: chatStateRef.current,
finishedViaPortalDirectChat,
+ // The agent-run result is authoritative even when the immediate
+ // session snapshot has not yet carried portal-direct metadata.
+ // Without this, a completed Page Data task can re-enter streaming
+ // and leave the Stop button attached to no active request.
+ agentRunSucceeded: finishedRun.status === 'succeeded',
});
if (nextChatState === 'idle') {
clearActiveRequestMissingTimer();
diff --git a/src/index.css b/src/index.css
index 7df14ed..fffe923 100644
--- a/src/index.css
+++ b/src/index.css
@@ -717,20 +717,25 @@ body,
justify-content: center;
width: auto;
min-width: 0;
- height: 30px;
- padding: 2px 4px;
+ height: 24px;
+ padding: 2px 3px;
border-radius: 6px;
font-size: 10px;
flex-direction: row;
- gap: 3px;
+ gap: 2px;
font-weight: 600;
line-height: 1;
white-space: normal;
}
.app-shell-h5 .chat-deep-reasoning-toggle input {
- width: 11px;
- height: 11px;
+ width: 9px;
+ height: 9px;
+}
+
+.app-shell-h5 .chat-deep-reasoning-toggle-icon {
+ width: 13px;
+ height: 13px;
}
.app-shell-h5 .chat-deep-reasoning-toggle-label {
@@ -1843,7 +1848,7 @@ body,
.page-save-preview-hint {
margin: 0;
color: #7a8680;
- font-size: 11px;
+ font-size: 10px;
line-height: 1.4;
}
@@ -3061,6 +3066,7 @@ body,
}
.chat-deep-reasoning-toggle {
+ position: relative;
display: inline-flex;
flex: 0 0 auto;
align-items: center;
@@ -3101,6 +3107,70 @@ body,
line-height: 1.1;
}
+.chat-deep-reasoning-toggle-icon {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+}
+
+.chat-control-onboarding-active {
+ position: relative;
+ z-index: 31;
+ animation: chat-control-onboarding-pulse 1s ease-in-out infinite;
+}
+
+.chat-control-onboarding-tip {
+ position: absolute;
+ bottom: calc(100% + 7px);
+ left: 50%;
+ z-index: 2;
+ width: max-content;
+ max-width: min(165px, calc(100vw - 20px));
+ padding: 4px 6px;
+ border: 1px solid rgba(218, 229, 255, 0.6);
+ border-radius: 9px;
+ background: linear-gradient(135deg, rgba(73, 117, 239, 0.98), rgba(152, 91, 230, 0.98));
+ box-shadow: 0 6px 14px rgba(103, 89, 221, 0.28), 0 1px 3px rgba(28, 38, 100, 0.2);
+ color: #fff;
+ font-size: 8px;
+ font-weight: 600;
+ line-height: 1.35;
+ letter-spacing: 0.01em;
+ pointer-events: none;
+ transform: translateX(-50%);
+ animation: chat-control-onboarding-tip-in 220ms ease-out both;
+}
+
+.chat-control-onboarding-tip::after {
+ position: absolute;
+ bottom: -4px;
+ left: 50%;
+ width: 6px;
+ height: 6px;
+ border-right: 1px solid rgba(218, 229, 255, 0.6);
+ border-bottom: 1px solid rgba(218, 229, 255, 0.6);
+ background: #835fdb;
+ content: '';
+ transform: translateX(-50%) rotate(45deg);
+}
+
+@keyframes chat-control-onboarding-pulse {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-2px) scale(1.07); }
+}
+
+@keyframes chat-control-onboarding-tip-in {
+ from { opacity: 0; transform: translate(-50%, 4px); }
+ to { opacity: 1; transform: translate(-50%, 0); }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .chat-control-onboarding-active,
+ .chat-control-onboarding-tip {
+ animation: none;
+ }
+}
+
.chat-deep-reasoning-toggle.is-active {
color: #eff6ff;
border-color: rgba(121, 183, 255, 0.6);
@@ -9938,12 +10008,12 @@ body,
justify-content: center;
width: auto;
min-width: 0;
- height: 30px;
- padding: 2px 4px;
+ height: 24px;
+ padding: 2px 3px;
border-radius: 6px;
font-size: 10px;
flex-direction: row;
- gap: 3px;
+ gap: 2px;
font-weight: 600;
white-space: normal;
}
@@ -9953,8 +10023,13 @@ body,
}
.chat-deep-reasoning-toggle input {
- width: 11px;
- height: 11px;
+ width: 9px;
+ height: 9px;
+ }
+
+ .chat-deep-reasoning-toggle-icon {
+ width: 13px;
+ height: 13px;
}
.chat-deep-reasoning-toggle-label {