@tea-agent/loop-agent 0.43.0-next.17 → 0.43.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -140
- package/dist/application/task-lifecycle/advance.js +4 -17
- package/dist/application/task-lifecycle/observe.js +29 -43
- package/dist/application/task-lifecycle/plan-transitions.js +4 -5
- package/dist/build-stamp.json +3 -3
- package/dist/commands/client-recovery.js +55 -20
- package/dist/commands/new-task.js +5 -0
- package/dist/commands/status.js +57 -0
- package/dist/commands/task-contract.js +269 -0
- package/dist/executors/dag-pi-executor.js +230 -269
- package/dist/executors/pi-executor.js +0 -2
- package/dist/executors/pi-sdk-executor.js +39 -77
- package/dist/executors/shell-executor.js +2 -3
- package/dist/executors/shell-write-guard.js +32 -27
- package/dist/shared/pi-context-pressure/extension.js +16 -26
- package/dist/shared/pi-retry-settings.js +0 -40
- package/dist/shared/prompts.js +26 -0
- package/dist/task/config.js +2 -0
- package/dist/task/contract/validate-draft.js +106 -0
- package/dist/task/goal.js +3 -0
- package/dist/task/index.js +12 -0
- package/dist/task/lifecycle.js +1 -0
- package/dist/task/operator/capabilities.js +6 -0
- package/dist/task/operator/envelope.js +2 -0
- package/dist/task/operator/index.js +5 -0
- package/dist/task/operator/registry.js +2 -0
- package/dist/task/operator/types.js +1 -0
- package/dist/task/paths.js +1 -0
- package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
- package/dist/task/source-references.js +23 -48
- package/dist/task/source-state.js +1 -0
- package/dist/task/subagent-guidance.js +1 -0
- package/dist/worker/console/chat/assistant-content.js +2 -23
- package/dist/worker/console/chat/explore-tools.js +0 -13
- package/dist/worker/console/chat/pi-runtime.js +1182 -221
- package/dist/worker/console/chat/routes.js +71 -213
- package/dist/worker/console/chat/sdd-data-alignment.js +9 -65
- package/dist/worker/console/chat/session-catalog.js +0 -32
- package/dist/worker/console/chat/session-store.js +6 -22
- package/dist/worker/console/chat/shortcuts.js +0 -6
- package/dist/worker/console/chat/sift-bridge.js +1 -0
- package/dist/worker/console/chat/tool-preview.js +0 -11
- package/dist/worker/console/chat/tools.js +0 -6
- package/dist/worker/console/console-update-and-init.js +19 -1
- package/dist/worker/console/console-update-runtime.js +20 -5
- package/dist/worker/console/index.js +20 -0
- package/dist/worker/console/operator-actions.js +1 -13
- package/dist/worker/console/routes.js +0 -8
- package/dist/worker/console/server.js +29 -11
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4NXyedy.js → abnfDiagram-N423BO3Z-hUmM_GRf.js} +1 -1
- package/dist/worker/console/static/assets/{arc-NZ3qXvBI.js → arc-Bt-xuieW.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-COZSRvml.js → architectureDiagram-T3A2C74G-DTqkL4DJ.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Ck0Gxi9C.js → blockDiagram-VBNYF7ZC-Cy0MNnzV.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BX5HQ4Ox.js → c4Diagram-5PPSVZJV-DDdlT0Pd.js} +1 -1
- package/dist/worker/console/static/assets/channel-BpzRnZmU.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CoviemSL.js → chunk-2GRJ4B5K-3JawwNZ6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DbOdOkbZ.js → chunk-2Q5K7J3B-DoTp7Cri.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BsG2pmdQ.js → chunk-5RXB4S5H-CcirhZaS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DGdvrNm8.js → chunk-5VM5RSS4-VrG21gnP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAy7Yd6y.js → chunk-6Q2QTUOP-DDOgIS70.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D9UA7hUb.js → chunk-GF5L2VYU-CBjmgvJ_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DPz7SZ8V.js → chunk-JWPE2WC7-CCQSht8M.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BmuEOIh7.js → chunk-KBJHAD2P-BpGMkUUV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CIhBWnd8.js → chunk-RYQCIY6F-DNDT41QL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CScR6mDD.js → chunk-XXDRQBXY-euXnDFbn.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BjpfWaUr.js → cose-bilkent-JH36ORCC-BJ6NAeql.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BFQohJgT.js → cynefin-VYW2F7L2-F5JKIvIq.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D3gzRJnS.js → cynefinDiagram-MW4NZA55-Cwmkl_Gi.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-5eojkhoA.js → dagre-VZM6K2ZE-BmCZ8815.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CpD8LWu_.js → diagram-7IWD3JNH-zWA3hUnX.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B8_4qJ2a.js → diagram-B4RE2ZJO-BzA-888F.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-HOsXf0eu.js → diagram-LBJQPF4R-tPNheoch.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BqwRwiWP.js → diagram-Q27KOJAE-Cdi7Fyqp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BRVCNZe1.js → diagram-UB23O5K3-DFK9VBf1.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Cvb1TgGh.js → ebnfDiagram-BXEA7PRR-xCUr1kpg.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DS0O0qG9.js → erDiagram-JOGREHBK-BANQHY_A.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BHIRvRr3.js → flowDiagram-UKHOOZJN-VHJYEMkP.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DcCJ1JWx.js → ganttDiagram-PKOTCBZU-DiyZQ0C6.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-B9ia5ony.js → gitGraphDiagram-DS77QQ5N-CZtIeBBa.js} +1 -1
- package/dist/worker/console/static/assets/index-BlN6ytLr.css +1 -0
- package/dist/worker/console/static/assets/index-CA-jfWlE.js +449 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-3ogpgegn.js → infoDiagram-6WML65LV-BuSspC0X.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-4tgpQ5SL.js → ishikawaDiagram-WSZJBQD7-DRJZyPFy.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DNfgHqFx.js → journeyDiagram-NVQOT4AX-DIfXIVuE.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Dz7ngC8z.js → kanban-definition-27J2QSJJ-CT10_7Lt.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DI7Jv62E.js → linear-ufemj74H.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CQA4Nzmo.js → mermaid.core-DEQe8-_m.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CtsMc8Mf.js → mindmap-definition-FAOFIHXS-BRLUrbrC.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-ByppE9yH.js → pegDiagram-VL7TDLO6-BgibKtiF.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B5PtgFgt.js → pieDiagram-7S7Q4E2Y-D3eyfdV9.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq4p0k2K.js → quadrantDiagram-CIZ2JOQS-DXeTk5Px.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DjfkyljN.js → railroadDiagram-AXF67PYL-BgXdFEe7.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DzynNRSl.js → requirementDiagram-LRYGKXZP-CseC2A7R.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cj7TCpTJ.js → sankeyDiagram-W5VNT64P-D80PFzfJ.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C73C6ykA.js → sequenceDiagram-SI44F4Z6-BWzHW7LS.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BiNLgN2x.js → sizeCapture-X5ZJPWSS-xWQYTLAn.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-UwvjS64-.js → stateDiagram-OKZ733FA-CLxtpZeL.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CC7DH98r.js → swimlanes-SLNWSIFB-DwugtmWV.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-LSBXTR70.js → timeline-definition-Z64GVDOM-DHEOwHIe.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BAuxWwGP.js → vennDiagram-T6HMQDX7-pmbZKlBK.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y--E5_gjZx.js → wardleyDiagram-T6FBY63Y-CivHoM5q.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-dJWNmyzF.js → xychartDiagram-ELKLHX3M-BfHIE7o6.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +10 -122
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +0 -7
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +15 -35
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +62 -136
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +68 -159
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +56 -75
- package/dist/worker/console/static-src/operator-chat/useComposer.js +78 -206
- package/dist/worker/console/workspace-context.js +1 -21
- package/dist/worker/observe/static/router.js +1 -1
- package/dist/worker/observe/static/styles.css +4 -7
- package/dist/worker/observe/static/views/dag-inspector.js +19 -12
- package/dist/worker/observe/static/views/dag.js +2 -2
- package/dist/worker/scheduler/index.js +25 -0
- package/dist/workflows/dag/facts.js +4 -0
- package/dist/workflows/dag/frontend-closeout.js +1 -1
- package/dist/workflows/dag/frontend-design-policy.js +1 -2
- package/dist/workflows/dag/frontend-implementation-contract.js +4 -8
- package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
- package/dist/workflows/dag/frontend-repair.js +76 -55
- package/dist/workflows/dag/frontend-review-context.js +1 -2
- package/dist/workflows/dag/frontend-review-scopes.js +12 -34
- package/dist/workflows/dag/frontend-shape-facts.js +2 -12
- package/dist/workflows/dag/frontend-verification-trace.js +42 -8
- package/dist/workflows/dag/index.js +6 -0
- package/dist/workflows/dag/init-hybrid.js +160 -97
- package/dist/workflows/dynamic/index.js +7 -0
- package/docs/architecture/runtime-boundaries.md +3 -3
- package/docs/templates/frontend-implementation-dag.json +8 -9
- package/package.json +3 -7
- package/skills/frontend-bounded-implement/SKILL.md +3 -4
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/references/contract-protocol.md +1 -3
- package/skills/frontend-design-review/SKILL.md +7 -12
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +0 -19
- package/skills/frontend-plan/references/decision-contract.md +19 -26
- package/skills/frontend-review/SKILL.md +18 -12
- package/skills/frontend-review/references/review-findings.md +43 -24
- package/dist/shared/pi-provider-recovery.js +0 -182
- package/dist/worker/console/chat/browser-automation.js +0 -363
- package/dist/worker/console/chat/browser-policy.js +0 -113
- package/dist/worker/console/chat/browser-routes.js +0 -162
- package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +0 -44
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +0 -89
- package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +0 -202
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +0 -79
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +0 -86
- package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +0 -38
- package/dist/worker/console/chat/pi-runtime/inspection-types.js +0 -28
- package/dist/worker/console/chat/pi-runtime/inspection.js +0 -439
- package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +0 -153
- package/dist/worker/console/chat/pi-runtime/sdk-session.js +0 -5
- package/dist/worker/console/chat/scm-routes.js +0 -217
- package/dist/worker/console/chat/scm-service.js +0 -283
- package/dist/worker/console/chat/scm-tools.js +0 -111
- package/dist/worker/console/chat/session-mode-view.js +0 -49
- package/dist/worker/console/chat/session-mode.js +0 -220
- package/dist/worker/console/chat/terminal-routes.js +0 -216
- package/dist/worker/console/chat/terminal-sessions.js +0 -348
- package/dist/worker/console/chat/terminal-tools.js +0 -211
- package/dist/worker/console/static/assets/channel-BxdTlb_5.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-_ByA1Nf8.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-_ByA1Nf8.js +0 -1
- package/dist/worker/console/static/assets/index-B9goN3S5.css +0 -1
- package/dist/worker/console/static/assets/index-wXlQohNs.js +0 -469
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-nLgMZ0D3.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BcYqTady.js +0 -8
- package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +0 -10
- package/dist/worker/console/static-src/operator-chat/chat-link.js +0 -94
- package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +0 -24
- package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +0 -192
- package/dist/worker/console/static-src/operator-chat/process-label.js +0 -41
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +0 -13
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +0 -10
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +0 -51
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +0 -21
- package/dist/worker/console/workspace-initialization.js +0 -209
- package/dist/workflows/dag/frontend-capacity.js +0 -9
- package/dist/workflows/dag/frontend-plan-completeness.js +0 -186
- package/dist/workflows/dag/frontend-plan-recovery-policy.js +0 -18
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { useCallback, useEffect,
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, } from "../../chat/turn-process.js";
|
|
3
|
-
import { isToolFailure } from "../../chat/tool-preview.js";
|
|
4
3
|
import { mergeOperationCardState, } from "../../chat/operation-card.js";
|
|
5
4
|
import { createChatSseEventApplier, } from "./chat-sse-events.js";
|
|
6
5
|
import { mergePendingSubmissionOnHydration } from "./pending-user-message.js";
|
|
7
6
|
import { taskContextUnchanged } from "./format.js";
|
|
8
7
|
import { upsertCompactionMessage } from "./compaction-message.js";
|
|
9
|
-
import { sameTurnGroupInputs } from "./turn-group-equality.js";
|
|
10
8
|
const EMPTY_USAGE = {
|
|
11
9
|
inputTokens: 0,
|
|
12
10
|
outputTokens: 0,
|
|
@@ -117,12 +115,6 @@ export function useChatThread(params) {
|
|
|
117
115
|
// (UI-08); the SSE applier must not open the process panel.
|
|
118
116
|
void params.setProcessOpen;
|
|
119
117
|
const [messages, setMessages] = useState([]);
|
|
120
|
-
/** Rendered-messages mirror for the replay flush: it must resolve its target
|
|
121
|
-
* row synchronously, before React commits the queued updater (2026-09-08). */
|
|
122
|
-
const messagesRef = useRef([]);
|
|
123
|
-
useLayoutEffect(() => {
|
|
124
|
-
messagesRef.current = messages;
|
|
125
|
-
}, [messages]);
|
|
126
118
|
const [toolCalls, setToolCalls] = useState([]);
|
|
127
119
|
const [operationCards, setOperationCards] = useState([]);
|
|
128
120
|
const [artifactCards, setArtifactCards] = useState([]);
|
|
@@ -130,7 +122,7 @@ export function useChatThread(params) {
|
|
|
130
122
|
const [historyHasMore, setHistoryHasMore] = useState(false);
|
|
131
123
|
const [historyNextCursor, setHistoryNextCursor] = useState(null);
|
|
132
124
|
const [historyLoading, setHistoryLoading] = useState(false);
|
|
133
|
-
const historyLoadingRef = useRef(
|
|
125
|
+
const historyLoadingRef = useRef(false);
|
|
134
126
|
const [unread, setUnread] = useState(0);
|
|
135
127
|
const [humanGateCards, setHumanGateCards] = useState([]);
|
|
136
128
|
const [interview, setInterview] = useState(null);
|
|
@@ -150,6 +142,7 @@ export function useChatThread(params) {
|
|
|
150
142
|
const [streamingAssistantId, setStreamingAssistantId] = useState(null);
|
|
151
143
|
const [showSystemInMessages] = useState(false);
|
|
152
144
|
const completedGroupsRef = useRef([]);
|
|
145
|
+
const previousTimelineRef = useRef(null);
|
|
153
146
|
const refreshSessionTelemetry = useCallback(async () => {
|
|
154
147
|
const targetSessionId = refs.sessionRef.current?.sessionId;
|
|
155
148
|
if (!targetSessionId)
|
|
@@ -194,7 +187,6 @@ export function useChatThread(params) {
|
|
|
194
187
|
onTelemetryRefresh: () => void refreshSessionTelemetry(),
|
|
195
188
|
setMessages,
|
|
196
189
|
setToolCalls,
|
|
197
|
-
getMessages: () => messagesRef.current,
|
|
198
190
|
}), [refs, onReconcile, setError, refreshSessionTelemetry]);
|
|
199
191
|
/** Parse + apply one raw SSE block (frames separated by \n\n). Returns the
|
|
200
192
|
* eventId if the block carried an `id:` line (for Last-Event-ID tracking). */
|
|
@@ -293,7 +285,7 @@ export function useChatThread(params) {
|
|
|
293
285
|
if (toolCalls.length === 0)
|
|
294
286
|
return null;
|
|
295
287
|
const running = toolCalls.filter((t) => t.status === "running");
|
|
296
|
-
const errored = toolCalls.filter((t) =>
|
|
288
|
+
const errored = toolCalls.filter((t) => t.status === "error");
|
|
297
289
|
const current = running[running.length - 1];
|
|
298
290
|
let statusLabel = "完成";
|
|
299
291
|
let statusKind = "ok";
|
|
@@ -316,26 +308,55 @@ export function useChatThread(params) {
|
|
|
316
308
|
};
|
|
317
309
|
}, [toolCalls]);
|
|
318
310
|
const turnGroups = useMemo(() => {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
311
|
+
const previousTimeline = previousTimelineRef.current;
|
|
312
|
+
let built;
|
|
313
|
+
const activeIndex = streamingAssistantId
|
|
314
|
+
? timelineMessages.findIndex((message) => message.id === streamingAssistantId)
|
|
315
|
+
: -1;
|
|
316
|
+
const canBuildActiveTail = Boolean(previousTimeline && streaming && streamingAssistantId && activeIndex >= 0) &&
|
|
317
|
+
previousTimeline?.activeAssistantId === streamingAssistantId &&
|
|
318
|
+
(previousTimeline?.messages.every((message, index) => message.id === streamingAssistantId || timelineMessages[index]?.id === message.id) ?? false);
|
|
319
|
+
if (canBuildActiveTail && previousTimeline) {
|
|
320
|
+
let tailStart = activeIndex;
|
|
321
|
+
while (tailStart > 0 && timelineMessages[tailStart - 1]?.role !== "user")
|
|
322
|
+
tailStart -= 1;
|
|
323
|
+
const tailMessages = timelineMessages.slice(tailStart);
|
|
324
|
+
const tailIds = new Set(tailMessages.map((message) => message.id));
|
|
325
|
+
const tailTurnIds = new Set(tailMessages.map((message) => message.turnId).filter(Boolean));
|
|
326
|
+
const tailTools = toolCalls.filter((tool) => tailIds.has(tool.assistantMessageId ?? "") ||
|
|
327
|
+
(Boolean(tool.turnId) && tailTurnIds.has(tool.turnId)) ||
|
|
328
|
+
(tool.createdAt >= (tailMessages[0]?.createdAt ?? Number.POSITIVE_INFINITY)));
|
|
329
|
+
const activeTail = buildTurnProcessGroups({
|
|
330
|
+
messages: tailMessages,
|
|
331
|
+
toolCalls: tailTools,
|
|
332
|
+
streaming,
|
|
333
|
+
activeAssistantId: streamingAssistantId,
|
|
334
|
+
});
|
|
335
|
+
built = [...previousTimeline.groups.filter((group) => !group.live), ...activeTail];
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
built = buildTurnProcessGroups({
|
|
339
|
+
messages: timelineMessages,
|
|
340
|
+
toolCalls,
|
|
341
|
+
streaming,
|
|
342
|
+
activeAssistantId: streamingAssistantId,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
// Preserve completed group objects across streaming tokens. The active
|
|
346
|
+
// tail is rebuilt, while stable historical groups keep their references so
|
|
330
347
|
// memoized rows do not re-render or re-parse the full conversation.
|
|
331
348
|
const previous = new Map(completedGroupsRef.current.map((group) => [group.key, group]));
|
|
332
349
|
const all = built.map((group) => {
|
|
333
350
|
const cached = previous.get(group.key);
|
|
334
|
-
return cached && !group.live
|
|
335
|
-
? cached
|
|
336
|
-
: group;
|
|
351
|
+
return cached && !group.live ? cached : group;
|
|
337
352
|
});
|
|
338
353
|
completedGroupsRef.current = all.filter((group) => !group.live);
|
|
354
|
+
previousTimelineRef.current = {
|
|
355
|
+
messages: timelineMessages,
|
|
356
|
+
toolCalls,
|
|
357
|
+
activeAssistantId: streamingAssistantId,
|
|
358
|
+
groups: all,
|
|
359
|
+
};
|
|
339
360
|
return selectRecentTurnGroups(all, historyLimit);
|
|
340
361
|
}, [
|
|
341
362
|
timelineMessages,
|
|
@@ -344,14 +365,12 @@ export function useChatThread(params) {
|
|
|
344
365
|
streamingAssistantId,
|
|
345
366
|
historyLimit,
|
|
346
367
|
]);
|
|
347
|
-
const loadEarlierMessages = useCallback(async (
|
|
368
|
+
const loadEarlierMessages = useCallback(async () => {
|
|
348
369
|
const targetSessionId = refs.sessionRef.current?.sessionId;
|
|
349
370
|
const beforeId = historyNextCursor;
|
|
350
371
|
if (!targetSessionId || !historyHasMore || !beforeId || historyLoadingRef.current)
|
|
351
372
|
return false;
|
|
352
|
-
|
|
353
|
-
const request = Symbol();
|
|
354
|
-
historyLoadingRef.current = request;
|
|
373
|
+
historyLoadingRef.current = true;
|
|
355
374
|
setHistoryLoading(true);
|
|
356
375
|
try {
|
|
357
376
|
const query = new URLSearchParams({ limit: "50", beforeId });
|
|
@@ -359,12 +378,7 @@ export function useChatThread(params) {
|
|
|
359
378
|
if (!res.ok || refs.sessionRef.current?.sessionId !== targetSessionId)
|
|
360
379
|
return false;
|
|
361
380
|
const body = (await res.json());
|
|
362
|
-
if (refs.sessionRef.current?.sessionId !== targetSessionId ||
|
|
363
|
-
refs.sessionGenerationRef.current !== generation ||
|
|
364
|
-
historyLoadingRef.current !== request)
|
|
365
|
-
return false;
|
|
366
381
|
const page = hydrateSessionThread(body.messages ?? []);
|
|
367
|
-
beforePrepend?.();
|
|
368
382
|
setMessages((current) => {
|
|
369
383
|
const seen = new Set();
|
|
370
384
|
const merged = [...page.messages, ...current].filter((message) => {
|
|
@@ -390,19 +404,11 @@ export function useChatThread(params) {
|
|
|
390
404
|
setHistoryLimit((value) => value + 50);
|
|
391
405
|
return true;
|
|
392
406
|
}
|
|
393
|
-
catch (error) {
|
|
394
|
-
if (refs.sessionRef.current?.sessionId === targetSessionId && refs.sessionGenerationRef.current === generation) {
|
|
395
|
-
setError(`加载历史消息失败:${error instanceof Error ? error.message : String(error)}`);
|
|
396
|
-
}
|
|
397
|
-
return false;
|
|
398
|
-
}
|
|
399
407
|
finally {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
setHistoryLoading(false);
|
|
403
|
-
}
|
|
408
|
+
historyLoadingRef.current = false;
|
|
409
|
+
setHistoryLoading(false);
|
|
404
410
|
}
|
|
405
|
-
}, [historyHasMore, historyNextCursor, origin, refs
|
|
411
|
+
}, [historyHasMore, historyNextCursor, origin, refs]);
|
|
406
412
|
const visibleMessages = useMemo(() => {
|
|
407
413
|
const next = [];
|
|
408
414
|
for (const group of turnGroups) {
|
|
@@ -436,41 +442,15 @@ export function useChatThread(params) {
|
|
|
436
442
|
setHistoryLimit(50);
|
|
437
443
|
setHistoryHasMore(false);
|
|
438
444
|
setHistoryNextCursor(null);
|
|
439
|
-
historyLoadingRef.current =
|
|
440
|
-
setHistoryLoading(false);
|
|
445
|
+
historyLoadingRef.current = false;
|
|
441
446
|
completedGroupsRef.current = [];
|
|
447
|
+
previousTimelineRef.current = null;
|
|
442
448
|
refs.usageResponseKeysRef.current = new Set();
|
|
443
449
|
}, [refs]);
|
|
444
450
|
/** Adopt a freshly activated session's hydrated thread + streaming flags. */
|
|
445
451
|
const applyActivatedSession = useCallback((next) => {
|
|
446
|
-
historyLoadingRef.current = null;
|
|
447
|
-
setHistoryLoading(false);
|
|
448
452
|
const visuallyStopped = next.turnPhase === "stopping";
|
|
449
|
-
|
|
450
|
-
// across an activation snapshot (refresh / reopen); a lease owned by
|
|
451
|
-
// another session must never leak into this thread (2026-09-08).
|
|
452
|
-
const ownedPending = refs.pendingSubmissionRef.current?.sessionId ===
|
|
453
|
-
refs.sessionRef.current?.sessionId
|
|
454
|
-
? refs.pendingSubmissionRef.current
|
|
455
|
-
: undefined;
|
|
456
|
-
setMessages(mergePendingSubmissionOnHydration(next.messages, ownedPending
|
|
457
|
-
? {
|
|
458
|
-
clientRequestId: ownedPending.clientRequestId,
|
|
459
|
-
user: {
|
|
460
|
-
id: ownedPending.userMessageId,
|
|
461
|
-
role: "user",
|
|
462
|
-
text: ownedPending.text,
|
|
463
|
-
createdAt: Date.now(),
|
|
464
|
-
clientRequestId: ownedPending.clientRequestId,
|
|
465
|
-
},
|
|
466
|
-
assistant: {
|
|
467
|
-
id: ownedPending.assistantId,
|
|
468
|
-
role: "assistant",
|
|
469
|
-
text: "",
|
|
470
|
-
createdAt: Date.now(),
|
|
471
|
-
},
|
|
472
|
-
}
|
|
473
|
-
: undefined));
|
|
453
|
+
setMessages(next.messages);
|
|
474
454
|
setToolCalls(next.toolCalls);
|
|
475
455
|
setOperationCards([]);
|
|
476
456
|
setHumanGateCards([]);
|
|
@@ -496,6 +476,7 @@ export function useChatThread(params) {
|
|
|
496
476
|
setHistoryHasMore(next.history?.hasMore === true);
|
|
497
477
|
setHistoryNextCursor(next.history?.nextCursor ?? null);
|
|
498
478
|
completedGroupsRef.current = [];
|
|
479
|
+
previousTimelineRef.current = null;
|
|
499
480
|
}, [refs]);
|
|
500
481
|
/** Reconcile recovery (AC-001/AC-002/AC-003): reload the canonical session
|
|
501
482
|
* facts (session-store messages + event-ring tail cursor) and REPLACE the
|
|
@@ -1,103 +1,62 @@
|
|
|
1
|
-
import { useCallback, useEffect, useLayoutEffect, useMemo,
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
2
2
|
import { filterSlashCommands, mergeSlashCommands, } from "../../chat/shortcuts.js";
|
|
3
|
-
import {
|
|
4
|
-
import { confirmationToken, requestId } from "./format.js";
|
|
3
|
+
import { confirmationToken } from "./format.js";
|
|
5
4
|
import { clampShortcutIndex } from "./slash-palette-nav.js";
|
|
5
|
+
import { createInputHistoryState, navigateInputHistory, recordInputHistoryEntry, resetInputHistoryBrowsing, } from "./input-history.js";
|
|
6
6
|
import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
|
|
7
|
-
function sameComposerSession(left, right) {
|
|
8
|
-
if (!left || !right)
|
|
9
|
-
return left === right;
|
|
10
|
-
return composerSessionKey(left) === composerSessionKey(right);
|
|
11
|
-
}
|
|
12
|
-
function readPendingImage(file) {
|
|
13
|
-
return new Promise((resolve, reject) => {
|
|
14
|
-
const reader = new FileReader();
|
|
15
|
-
reader.onerror = () => reject(reader.error ?? new Error("图片读取失败"));
|
|
16
|
-
reader.onload = () => {
|
|
17
|
-
const value = String(reader.result ?? "");
|
|
18
|
-
resolve({
|
|
19
|
-
name: file.name,
|
|
20
|
-
type: "image",
|
|
21
|
-
mimeType: file.type,
|
|
22
|
-
data: value.slice(value.indexOf(",") + 1),
|
|
23
|
-
});
|
|
24
|
-
};
|
|
25
|
-
reader.readAsDataURL(file);
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
7
|
/** Composer state: input text, @file palette, image attachments, /shortcuts,
|
|
29
8
|
* IME composition guards, auto-grow and server-side draft sync. */
|
|
30
9
|
export function useComposer(params) {
|
|
31
|
-
const { origin,
|
|
32
|
-
const
|
|
33
|
-
const ledgerRef = useRef(null);
|
|
34
|
-
if (!ledgerRef.current)
|
|
35
|
-
ledgerRef.current = new ComposerSessionLedger();
|
|
36
|
-
const ledger = ledgerRef.current;
|
|
37
|
-
const activeIdentityRef = useRef(sessionIdentity);
|
|
38
|
-
activeIdentityRef.current = sessionIdentity;
|
|
39
|
-
const [, forceRender] = useReducer((version) => version + 1, 0);
|
|
40
|
-
const activeSnapshot = sessionIdentity
|
|
41
|
-
? ledger.snapshot(sessionIdentity)
|
|
42
|
-
: { draft: "", images: [] };
|
|
43
|
-
const input = activeSnapshot.draft;
|
|
44
|
-
const pendingImages = activeSnapshot.images;
|
|
10
|
+
const { origin, sessionId, refs, setError, onUnrecoverableSession } = params;
|
|
11
|
+
const [input, setInputState] = useState("");
|
|
45
12
|
const [fileOptions, setFileOptions] = useState([]);
|
|
13
|
+
const [pendingImages, setPendingImages] = useState([]);
|
|
46
14
|
const [shortcutOpen, setShortcutOpen] = useState(false);
|
|
47
15
|
/** Any composer text that starts with `/` should reveal the palette,
|
|
48
16
|
* including draft restore. Escape can still close it without clearing. */
|
|
49
17
|
const setInput = useCallback((value) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
forceRender();
|
|
57
|
-
setShortcutOpen(next.startsWith("/"));
|
|
58
|
-
}, [ledger]);
|
|
59
|
-
const setPendingImages = useCallback((value) => {
|
|
60
|
-
const identity = activeIdentityRef.current;
|
|
61
|
-
if (!identity)
|
|
18
|
+
if (typeof value === "function") {
|
|
19
|
+
setInputState((prev) => {
|
|
20
|
+
const next = value(prev);
|
|
21
|
+
setShortcutOpen(next.startsWith("/"));
|
|
22
|
+
return next;
|
|
23
|
+
});
|
|
62
24
|
return;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}, [ledger]);
|
|
25
|
+
}
|
|
26
|
+
setInputState(value);
|
|
27
|
+
setShortcutOpen(value.startsWith("/"));
|
|
28
|
+
}, []);
|
|
68
29
|
const [shortcutIndex, setShortcutIndex] = useState(0);
|
|
69
30
|
const [dynamicCommands, setDynamicCommands] = useState([]);
|
|
70
|
-
const [commandCapabilities, setCommandCapabilities] = useState({
|
|
71
|
-
sddBingo: false,
|
|
72
|
-
sessionMode: undefined,
|
|
73
|
-
});
|
|
31
|
+
const [commandCapabilities, setCommandCapabilities] = useState({ sddBingo: false });
|
|
74
32
|
const [dynamicCommandsLoading, setDynamicCommandsLoading] = useState(false);
|
|
75
33
|
const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
|
|
76
34
|
const [commandsEpoch, setCommandsEpoch] = useState(0);
|
|
77
35
|
const textareaRef = useRef(null);
|
|
36
|
+
/** Arrow-key input history; unsent drafts survive history browsing. */
|
|
37
|
+
const historyRef = useRef(createInputHistoryState());
|
|
78
38
|
/** Active session generation for /commands; bumped immediately on switch (AC-2). */
|
|
79
39
|
const commandsSessionRef = useRef(sessionId);
|
|
80
40
|
const commandsGenerationRef = useRef(0);
|
|
81
41
|
const handleInputChange = useCallback((value) => {
|
|
82
42
|
setInput(value);
|
|
43
|
+
setShortcutOpen(value.startsWith("/"));
|
|
83
44
|
setShortcutIndex(0);
|
|
84
|
-
|
|
45
|
+
// Manual typing edits a recalled entry in place → back to present draft.
|
|
46
|
+
historyRef.current = resetInputHistoryBrowsing(historyRef.current);
|
|
47
|
+
}, []);
|
|
85
48
|
/** Recall an earlier input via ↑/↓; returns true when the key was consumed. */
|
|
86
49
|
const navigateHistory = useCallback((direction) => {
|
|
87
|
-
const identity = activeIdentityRef.current;
|
|
88
|
-
if (!identity)
|
|
89
|
-
return false;
|
|
90
|
-
const current = ledger.snapshot(identity).draft;
|
|
91
50
|
const textarea = textareaRef.current;
|
|
92
|
-
const decision =
|
|
93
|
-
text:
|
|
94
|
-
caretStart: textarea?.selectionStart ??
|
|
95
|
-
caretEnd: textarea?.selectionEnd ??
|
|
51
|
+
const decision = navigateInputHistory(historyRef.current, direction, {
|
|
52
|
+
text: input,
|
|
53
|
+
caretStart: textarea?.selectionStart ?? input.length,
|
|
54
|
+
caretEnd: textarea?.selectionEnd ?? input.length,
|
|
96
55
|
});
|
|
97
56
|
if (!decision.handled)
|
|
98
57
|
return false;
|
|
99
|
-
|
|
100
|
-
|
|
58
|
+
historyRef.current = decision.state;
|
|
59
|
+
setInput(decision.text);
|
|
101
60
|
const caret = decision.text.length;
|
|
102
61
|
queueMicrotask(() => {
|
|
103
62
|
const el = textareaRef.current;
|
|
@@ -106,76 +65,22 @@ export function useComposer(params) {
|
|
|
106
65
|
}
|
|
107
66
|
});
|
|
108
67
|
return true;
|
|
109
|
-
}, [
|
|
110
|
-
/** Record a submitted text into
|
|
68
|
+
}, [input, setInput]);
|
|
69
|
+
/** Record a submitted text into history (consecutive duplicates collapse). */
|
|
111
70
|
const recordHistory = useCallback((text) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
ledger.recordHistory(identity, text);
|
|
115
|
-
}, [ledger]);
|
|
116
|
-
const claimSubmission = useCallback(() => {
|
|
117
|
-
const identity = activeIdentityRef.current;
|
|
118
|
-
if (!identity)
|
|
119
|
-
return null;
|
|
120
|
-
const attempt = ledger.beginSubmission(identity, refs.sessionGenerationRef.current, requestId("chat-submit"));
|
|
121
|
-
if (!attempt)
|
|
122
|
-
return null;
|
|
123
|
-
forceRender();
|
|
124
|
-
setShortcutOpen(false);
|
|
125
|
-
setShortcutIndex(0);
|
|
126
|
-
return attempt;
|
|
127
|
-
}, [ledger, refs]);
|
|
128
|
-
const claimSessionSubmission = useCallback((identity, text, images = []) => {
|
|
129
|
-
ledger.setDraft(identity, text);
|
|
130
|
-
ledger.setImages(identity, images);
|
|
131
|
-
const attempt = ledger.beginSubmission(identity, refs.sessionGenerationRef.current, requestId("chat-submit"));
|
|
132
|
-
if (sameComposerSession(activeIdentityRef.current, identity))
|
|
133
|
-
forceRender();
|
|
134
|
-
return attempt;
|
|
135
|
-
}, [ledger, refs]);
|
|
136
|
-
const settleSubmission = useCallback((attempt, outcome) => {
|
|
137
|
-
const result = ledger.settle(attempt, outcome);
|
|
138
|
-
if (sameComposerSession(activeIdentityRef.current, attempt.identity))
|
|
139
|
-
forceRender();
|
|
140
|
-
return result;
|
|
141
|
-
}, [ledger]);
|
|
142
|
-
const settleSubmissionByRequest = useCallback((identity, clientRequestId, outcome) => {
|
|
143
|
-
const result = ledger.settleByRequest(identity, clientRequestId, outcome);
|
|
144
|
-
if (sameComposerSession(activeIdentityRef.current, identity))
|
|
145
|
-
forceRender();
|
|
146
|
-
return result;
|
|
147
|
-
}, [ledger]);
|
|
148
|
-
const seedSession = useCallback((identity, draft) => {
|
|
149
|
-
const seeded = ledger.seedDraft(identity, draft);
|
|
150
|
-
if (seeded && sameComposerSession(activeIdentityRef.current, identity))
|
|
151
|
-
forceRender();
|
|
152
|
-
return seeded;
|
|
153
|
-
}, [ledger]);
|
|
154
|
-
const replaceSessionDraft = useCallback((identity, draft, images) => {
|
|
155
|
-
ledger.setDraft(identity, draft);
|
|
156
|
-
ledger.setImages(identity, images);
|
|
157
|
-
if (sameComposerSession(activeIdentityRef.current, identity))
|
|
158
|
-
forceRender();
|
|
159
|
-
}, [ledger]);
|
|
160
|
-
const releaseSession = useCallback((identity) => {
|
|
161
|
-
ledger.release(identity);
|
|
162
|
-
if (sameComposerSession(activeIdentityRef.current, identity))
|
|
163
|
-
forceRender();
|
|
164
|
-
}, [ledger]);
|
|
71
|
+
historyRef.current = recordInputHistoryEntry(historyRef.current, text);
|
|
72
|
+
}, []);
|
|
165
73
|
// Immediate invalidation on A→B even when B palette never opens (AC-2).
|
|
166
74
|
useLayoutEffect(() => {
|
|
167
75
|
commandsSessionRef.current = sessionId;
|
|
168
76
|
commandsGenerationRef.current += 1;
|
|
169
|
-
refs.isComposingRef.current = false;
|
|
170
|
-
setFileOptions([]);
|
|
171
|
-
setShortcutOpen(input.startsWith("/"));
|
|
172
77
|
setDynamicCommands([]);
|
|
173
|
-
setCommandCapabilities({ sddBingo: false
|
|
78
|
+
setCommandCapabilities({ sddBingo: false });
|
|
174
79
|
setDynamicCommandsError(null);
|
|
175
80
|
setDynamicCommandsLoading(false);
|
|
176
|
-
}, [sessionId
|
|
81
|
+
}, [sessionId]);
|
|
177
82
|
const loadDynamicCommands = useCallback(async () => {
|
|
178
|
-
if (!sessionId
|
|
83
|
+
if (!sessionId) {
|
|
179
84
|
setDynamicCommands([]);
|
|
180
85
|
setDynamicCommandsError(null);
|
|
181
86
|
setDynamicCommandsLoading(false);
|
|
@@ -219,10 +124,7 @@ export function useComposer(params) {
|
|
|
219
124
|
if (!isCurrent())
|
|
220
125
|
return;
|
|
221
126
|
setDynamicCommands(items);
|
|
222
|
-
setCommandCapabilities({
|
|
223
|
-
sddBingo: body?.capabilities?.sddBingo === true,
|
|
224
|
-
sessionMode: body?.capabilities?.sessionMode ?? sessionMode,
|
|
225
|
-
});
|
|
127
|
+
setCommandCapabilities({ sddBingo: body?.capabilities?.sddBingo === true });
|
|
226
128
|
// Compact server fail-soft / partial-source warning; local Operator/Web still merge.
|
|
227
129
|
const warning = typeof body?.warning === "string" ? body.warning.trim() : "";
|
|
228
130
|
setDynamicCommandsError(warning ? warning.slice(0, 160) : null);
|
|
@@ -232,28 +134,28 @@ export function useComposer(params) {
|
|
|
232
134
|
return;
|
|
233
135
|
setDynamicCommandsError("动态命令加载失败");
|
|
234
136
|
setDynamicCommands([]);
|
|
235
|
-
setCommandCapabilities({ sddBingo: false
|
|
137
|
+
setCommandCapabilities({ sddBingo: false });
|
|
236
138
|
}
|
|
237
139
|
finally {
|
|
238
140
|
if (isCurrent()) {
|
|
239
141
|
setDynamicCommandsLoading(false);
|
|
240
142
|
}
|
|
241
143
|
}
|
|
242
|
-
}, [origin, sessionId
|
|
144
|
+
}, [origin, sessionId]);
|
|
243
145
|
// Lazy-load dynamic commands when slash palette opens or session changes.
|
|
244
146
|
useEffect(() => {
|
|
245
|
-
if (!sessionId
|
|
147
|
+
if (!sessionId)
|
|
246
148
|
return;
|
|
247
149
|
if (!shortcutOpen && commandsEpoch === 0)
|
|
248
150
|
return;
|
|
249
151
|
void loadDynamicCommands();
|
|
250
|
-
}, [sessionId,
|
|
152
|
+
}, [sessionId, shortcutOpen, commandsEpoch, loadDynamicCommands]);
|
|
251
153
|
const retryDynamicCommands = useCallback(() => {
|
|
252
154
|
setCommandsEpoch((n) => n + 1);
|
|
253
155
|
void loadDynamicCommands();
|
|
254
156
|
}, [loadDynamicCommands]);
|
|
255
157
|
useEffect(() => {
|
|
256
|
-
if (!sessionId
|
|
158
|
+
if (!sessionId)
|
|
257
159
|
return;
|
|
258
160
|
const match = input
|
|
259
161
|
.slice(0, textareaRef.current?.selectionStart ?? input.length)
|
|
@@ -262,29 +164,13 @@ export function useComposer(params) {
|
|
|
262
164
|
setFileOptions([]);
|
|
263
165
|
return;
|
|
264
166
|
}
|
|
265
|
-
const owner = { ...sessionIdentity };
|
|
266
|
-
const ownerKey = composerSessionKey(owner);
|
|
267
|
-
const generation = refs.sessionGenerationRef.current;
|
|
268
|
-
const controller = new AbortController();
|
|
269
167
|
const timer = window.setTimeout(() => {
|
|
270
|
-
void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/files?q=${encodeURIComponent(match[1] ?? "")}
|
|
168
|
+
void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/files?q=${encodeURIComponent(match[1] ?? "")}`)
|
|
271
169
|
.then((r) => (r.ok ? r.json() : null))
|
|
272
|
-
.then((body) =>
|
|
273
|
-
const active = activeIdentityRef.current;
|
|
274
|
-
if (controller.signal.aborted ||
|
|
275
|
-
!active ||
|
|
276
|
-
composerSessionKey(active) !== ownerKey ||
|
|
277
|
-
refs.sessionGenerationRef.current !== generation)
|
|
278
|
-
return;
|
|
279
|
-
setFileOptions((body?.files ?? []).map((item) => item.path));
|
|
280
|
-
})
|
|
281
|
-
.catch(() => undefined);
|
|
170
|
+
.then((body) => setFileOptions((body?.files ?? []).map((item) => item.path)));
|
|
282
171
|
}, 150);
|
|
283
|
-
return () =>
|
|
284
|
-
|
|
285
|
-
controller.abort();
|
|
286
|
-
};
|
|
287
|
-
}, [origin, sessionId, sessionIdentity, sessionReady, input, refs]);
|
|
172
|
+
return () => window.clearTimeout(timer);
|
|
173
|
+
}, [origin, sessionId, input]);
|
|
288
174
|
const insertFileReference = useCallback((file) => {
|
|
289
175
|
const caret = textareaRef.current?.selectionStart ?? input.length;
|
|
290
176
|
const before = input.slice(0, caret);
|
|
@@ -293,51 +179,44 @@ export function useComposer(params) {
|
|
|
293
179
|
return;
|
|
294
180
|
setInput(`${input.slice(0, start)}@${file} ${input.slice(caret)}`);
|
|
295
181
|
setFileOptions([]);
|
|
296
|
-
}, [input
|
|
182
|
+
}, [input]);
|
|
297
183
|
const addImagesFromFiles = useCallback((files) => {
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
184
|
+
for (const file of Array.from(files).slice(0, 4)) {
|
|
185
|
+
if (!file.type.startsWith("image/") || file.size > 5 * 1024 * 1024) {
|
|
186
|
+
setError("图片仅支持 PNG/JPEG/GIF/WebP,单张不超过 5 MiB");
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const reader = new FileReader();
|
|
190
|
+
reader.onload = () => {
|
|
191
|
+
const value = String(reader.result ?? "");
|
|
192
|
+
const data = value.slice(value.indexOf(",") + 1);
|
|
193
|
+
setPendingImages((items) => [
|
|
194
|
+
...items,
|
|
195
|
+
{
|
|
196
|
+
name: file.name,
|
|
197
|
+
type: "image",
|
|
198
|
+
mimeType: file.type,
|
|
199
|
+
data,
|
|
200
|
+
},
|
|
201
|
+
].slice(0, 4));
|
|
202
|
+
};
|
|
203
|
+
reader.readAsDataURL(file);
|
|
204
|
+
}
|
|
205
|
+
}, [setError]);
|
|
318
206
|
const removeImage = useCallback((image) => {
|
|
319
207
|
setPendingImages((items) => items.filter((item) => item !== image));
|
|
320
|
-
}, [
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
useEffect(() => {
|
|
208
|
+
}, []);
|
|
209
|
+
// Auto-grow textarea height.
|
|
210
|
+
useLayoutEffect(() => {
|
|
324
211
|
const ta = textareaRef.current;
|
|
325
212
|
if (!ta)
|
|
326
213
|
return;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
ta.style.removeProperty("height");
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
const frame = window.requestAnimationFrame(() => {
|
|
333
|
-
ta.style.height = "auto";
|
|
334
|
-
ta.style.height = `${Math.min(ta.scrollHeight, 280)}px`;
|
|
335
|
-
});
|
|
336
|
-
return () => window.cancelAnimationFrame(frame);
|
|
214
|
+
ta.style.height = "auto";
|
|
215
|
+
ta.style.height = `${Math.min(ta.scrollHeight, 280)}px`;
|
|
337
216
|
}, [input]);
|
|
338
217
|
// Debounced server-side composer draft sync.
|
|
339
218
|
useEffect(() => {
|
|
340
|
-
if (!sessionId
|
|
219
|
+
if (!sessionId)
|
|
341
220
|
return;
|
|
342
221
|
const timer = window.setTimeout(() => {
|
|
343
222
|
void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/draft`, {
|
|
@@ -351,7 +230,7 @@ export function useComposer(params) {
|
|
|
351
230
|
});
|
|
352
231
|
}, 400);
|
|
353
232
|
return () => window.clearTimeout(timer);
|
|
354
|
-
}, [origin, sessionId,
|
|
233
|
+
}, [origin, sessionId, input]);
|
|
355
234
|
const mergedCommands = useMemo(() => mergeSlashCommands(dynamicCommands, commandCapabilities), [dynamicCommands, commandCapabilities]);
|
|
356
235
|
const filteredShortcuts = useMemo(() => input.startsWith("/")
|
|
357
236
|
? filterSlashCommands(mergedCommands, input)
|
|
@@ -385,24 +264,17 @@ export function useComposer(params) {
|
|
|
385
264
|
useEffect(() => {
|
|
386
265
|
attachRuntimeSnapshotController({
|
|
387
266
|
origin,
|
|
388
|
-
sessionId
|
|
267
|
+
sessionId,
|
|
389
268
|
refs,
|
|
390
269
|
onUnrecoverableSession,
|
|
391
270
|
});
|
|
392
|
-
}, [origin, sessionId,
|
|
271
|
+
}, [origin, sessionId, refs, onUnrecoverableSession]);
|
|
393
272
|
return {
|
|
394
273
|
input,
|
|
395
274
|
setInput,
|
|
396
275
|
handleInputChange,
|
|
397
276
|
navigateHistory,
|
|
398
277
|
recordHistory,
|
|
399
|
-
claimSubmission,
|
|
400
|
-
claimSessionSubmission,
|
|
401
|
-
settleSubmission,
|
|
402
|
-
settleSubmissionByRequest,
|
|
403
|
-
seedSession,
|
|
404
|
-
replaceSessionDraft,
|
|
405
|
-
releaseSession,
|
|
406
278
|
fileOptions,
|
|
407
279
|
pendingImages,
|
|
408
280
|
setPendingImages,
|