@xinghunm/ai-chat 1.2.0 → 1.2.1

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/dist/index.js CHANGED
@@ -47,7 +47,7 @@ module.exports = __toCommonJS(src_exports);
47
47
 
48
48
  // src/components/ai-chat/index.tsx
49
49
  var import_styled17 = __toESM(require("@emotion/styled"));
50
- var import_compass_ui5 = require("@xinghunm/compass-ui");
50
+ var import_compass_ui4 = require("@xinghunm/compass-ui");
51
51
 
52
52
  // src/components/ai-chat-provider/index.tsx
53
53
  var import_react2 = require("react");
@@ -96,6 +96,28 @@ var DEFAULT_AI_CHAT_LABELS = {
96
96
  questionnaireOtherPlaceholder: "Other"
97
97
  };
98
98
 
99
+ // src/lib/chat-session.ts
100
+ var DRAFT_CHAT_SESSION_ID_PREFIX = "draft-session-";
101
+ var draftChatSessionSequence = 0;
102
+ var createDraftChatSessionId = () => `${DRAFT_CHAT_SESSION_ID_PREFIX}${Date.now()}-${draftChatSessionSequence++}`;
103
+ var isDraftChatSessionId = (sessionId) => Boolean(sessionId?.startsWith(DRAFT_CHAT_SESSION_ID_PREFIX));
104
+ var createDraftChatSession = ({
105
+ model,
106
+ mode = DEFAULT_CHAT_AGENT_MODE,
107
+ nowIso: nowIso2,
108
+ createSessionId = createDraftChatSessionId
109
+ }) => {
110
+ const iso = nowIso2();
111
+ return {
112
+ sessionId: createSessionId(),
113
+ title: "New Chat",
114
+ createdAt: iso,
115
+ updatedAt: iso,
116
+ model,
117
+ mode
118
+ };
119
+ };
120
+
99
121
  // src/store/chat-store.ts
100
122
  var DEFAULT_CHAT_SESSION_TITLE = "New Chat";
101
123
  var IMAGE_MESSAGE_SESSION_TITLE = "Image message";
@@ -229,6 +251,15 @@ var createChatStore = (initialState) => (0, import_vanilla.createStore)((set, ge
229
251
  isStoppingBySession: nextIsStoppingBySession
230
252
  });
231
253
  },
254
+ startNewChat: () => {
255
+ const state = get();
256
+ const session = createDraftChatSession({
257
+ model: "",
258
+ mode: state.preferredMode,
259
+ nowIso: () => (/* @__PURE__ */ new Date()).toISOString()
260
+ });
261
+ get().createSession(session);
262
+ },
232
263
  setActiveSession: (sessionId) => {
233
264
  set({ activeSessionId: sessionId });
234
265
  },
@@ -746,6 +777,8 @@ var AiChatProvider = (props) => {
746
777
  });
747
778
  const retryRef = (0, import_react2.useRef)(async () => {
748
779
  });
780
+ const stopRef = (0, import_react2.useRef)(async (_sessionId) => {
781
+ });
749
782
  const defaultApiBaseUrl = "apiBaseUrl" in props ? props.apiBaseUrl : void 0;
750
783
  const defaultAuthToken = "authToken" in props ? props.authToken : void 0;
751
784
  const defaultTransformStreamPacket = "transformStreamPacket" in props ? props.transformStreamPacket : void 0;
@@ -789,6 +822,7 @@ var AiChatProvider = (props) => {
789
822
  labels: { ...DEFAULT_AI_CHAT_LABELS, ...labels },
790
823
  sendRef,
791
824
  retryRef,
825
+ stopRef,
792
826
  renderMessageBlock,
793
827
  handleQuestionnaireSubmit,
794
828
  handleConfirmationSubmit,
@@ -809,6 +843,7 @@ var AiChatProvider = (props) => {
809
843
  renderMessageBlock,
810
844
  sendRef,
811
845
  retryRef,
846
+ stopRef,
812
847
  store,
813
848
  transport
814
849
  ]
@@ -1609,7 +1644,6 @@ var Value = import_styled3.default.span`
1609
1644
  // src/components/chat-thread/components/questionnaire-card.tsx
1610
1645
  var import_react7 = require("react");
1611
1646
  var import_styled4 = __toESM(require("@emotion/styled"));
1612
- var import_compass_ui = require("@xinghunm/compass-ui");
1613
1647
 
1614
1648
  // src/components/chat-thread/components/questionnaire-card-helpers.ts
1615
1649
  var OTHER_OPTION_VALUE = "__other__";
@@ -2410,7 +2444,7 @@ var TextInput = import_styled4.default.input`
2410
2444
  color: rgba(255, 255, 255, 0.34);
2411
2445
  }
2412
2446
  `;
2413
- var InlineOtherInput = (0, import_styled4.default)(import_compass_ui.InputField)`
2447
+ var InlineOtherInput = import_styled4.default.input`
2414
2448
  width: 100%;
2415
2449
  margin-top: 0;
2416
2450
 
@@ -3798,19 +3832,20 @@ var ChatThread = () => {
3798
3832
  if (!activeSessionId)
3799
3833
  return;
3800
3834
  clearSessionError(activeSessionId);
3801
- void retryRef.current();
3835
+ void retryRef.current(activeSessionId);
3802
3836
  }, [activeSessionId, clearSessionError, retryRef]);
3803
3837
  const handleQuestionnaireSubmit = (0, import_react11.useCallback)(
3804
3838
  async (submission) => {
3839
+ const sourceSessionId = activeSessionId;
3805
3840
  if (customQuestionnaireSubmit) {
3806
3841
  const handled = await customQuestionnaireSubmit(submission, {
3807
- sessionId: activeSessionId ?? void 0,
3842
+ sessionId: sourceSessionId ?? void 0,
3808
3843
  mode: activeSessionMode
3809
3844
  });
3810
3845
  if (handled !== false) {
3811
- if (activeSessionId && submission.sourceMessageId) {
3846
+ if (sourceSessionId && submission.sourceMessageId) {
3812
3847
  updateQA(
3813
- activeSessionId,
3848
+ sourceSessionId,
3814
3849
  submission.sourceMessageId,
3815
3850
  submission.questionnaireId,
3816
3851
  submission.answers
@@ -3819,10 +3854,13 @@ var ChatThread = () => {
3819
3854
  return;
3820
3855
  }
3821
3856
  }
3822
- await sendRef.current(submission.content);
3823
- if (activeSessionId && submission.sourceMessageId) {
3857
+ await sendRef.current(submission.content, {
3858
+ sessionId: sourceSessionId ?? void 0,
3859
+ includeComposerAttachments: false
3860
+ });
3861
+ if (sourceSessionId && submission.sourceMessageId) {
3824
3862
  updateQA(
3825
- activeSessionId,
3863
+ sourceSessionId,
3826
3864
  submission.sourceMessageId,
3827
3865
  submission.questionnaireId,
3828
3866
  submission.answers
@@ -3833,16 +3871,20 @@ var ChatThread = () => {
3833
3871
  );
3834
3872
  const handleConfirmation = (0, import_react11.useCallback)(
3835
3873
  async (submission) => {
3874
+ const sourceSessionId = activeSessionId;
3836
3875
  if (customConfirmationSubmit) {
3837
3876
  const handled = await customConfirmationSubmit(submission, {
3838
- sessionId: activeSessionId ?? void 0,
3877
+ sessionId: sourceSessionId ?? void 0,
3839
3878
  mode: activeSessionMode
3840
3879
  });
3841
3880
  if (handled !== false) {
3842
3881
  return;
3843
3882
  }
3844
3883
  }
3845
- await sendRef.current(submission.content);
3884
+ await sendRef.current(submission.content, {
3885
+ sessionId: sourceSessionId ?? void 0,
3886
+ includeComposerAttachments: false
3887
+ });
3846
3888
  },
3847
3889
  [activeSessionId, activeSessionMode, sendRef, customConfirmationSubmit]
3848
3890
  );
@@ -3979,25 +4021,6 @@ var import_react15 = require("react");
3979
4021
  var import_styled14 = __toESM(require("@emotion/styled"));
3980
4022
 
3981
4023
  // src/components/chat-composer/lib/chat-composer.ts
3982
- var DRAFT_CHAT_SESSION_ID_PREFIX = "draft-session-";
3983
- var createDraftChatSessionId = () => `${DRAFT_CHAT_SESSION_ID_PREFIX}${Date.now()}`;
3984
- var isDraftChatSessionId = (sessionId) => Boolean(sessionId?.startsWith(DRAFT_CHAT_SESSION_ID_PREFIX));
3985
- var createDraftChatSession = ({
3986
- model,
3987
- mode = DEFAULT_CHAT_AGENT_MODE,
3988
- nowIso: nowIso2,
3989
- createSessionId
3990
- }) => {
3991
- const iso = nowIso2();
3992
- return {
3993
- sessionId: createSessionId(),
3994
- title: "New Chat",
3995
- createdAt: iso,
3996
- updatedAt: iso,
3997
- model,
3998
- mode
3999
- };
4000
- };
4001
4024
  var createUserMessage = ({
4002
4025
  sessionId,
4003
4026
  content,
@@ -4198,19 +4221,20 @@ var normalizeChatErrorMessage = (message, labels) => {
4198
4221
  return trimmedMessage;
4199
4222
  };
4200
4223
  var useChatComposer = () => {
4201
- const { transport, enableImageAttachments, labels } = useChatContext();
4224
+ const { transport, enableImageAttachments, labels, store } = useChatContext();
4202
4225
  const activeSessionId = useChatStore((s) => s.activeSessionId);
4203
4226
  const activeSession = useChatStore(
4204
4227
  (s) => s.sessions.find((x) => x.sessionId === s.activeSessionId) ?? null
4205
4228
  );
4206
4229
  const preferredMode = useChatStore((s) => s.preferredMode);
4207
4230
  const streamingSessionId = useChatStore(
4208
- (s) => Object.entries(s.isStreamingBySession).find(([, v]) => v)?.[0] ?? null
4231
+ (s) => s.activeSessionId && s.isStreamingBySession[s.activeSessionId] ? s.activeSessionId : null
4209
4232
  );
4210
4233
  const isStreaming = Boolean(streamingSessionId);
4211
- const isStopping = useChatStore(
4212
- (s) => streamingSessionId ? s.isStoppingBySession[streamingSessionId] ?? false : false
4213
- );
4234
+ const isStopping = useChatStore((s) => {
4235
+ const currentStreamingSessionId = s.activeSessionId && s.isStreamingBySession[s.activeSessionId] ? s.activeSessionId : null;
4236
+ return currentStreamingSessionId ? s.isStoppingBySession[currentStreamingSessionId] ?? false : false;
4237
+ });
4214
4238
  const createSession = useChatStore((s) => s.createSession);
4215
4239
  const replaceSessionId = useChatStore((s) => s.replaceSessionId);
4216
4240
  const appendMessage = useChatStore((s) => s.appendMessage);
@@ -4247,9 +4271,9 @@ var useChatComposer = () => {
4247
4271
  const [selectedMode, setSelectedModeLocal] = (0, import_react13.useState)(DEFAULT_CHAT_AGENT_MODE);
4248
4272
  const [attachmentNotice, setAttachmentNotice] = (0, import_react13.useState)(null);
4249
4273
  const { attachments, appendFiles, removeAttachment, takeMessageAttachments } = useComposerAttachments();
4250
- const abortControllerRef = (0, import_react13.useRef)(null);
4251
- const stopRequestRef = (0, import_react13.useRef)(null);
4252
- const lastRequestRef = (0, import_react13.useRef)(null);
4274
+ const abortControllerBySessionRef = (0, import_react13.useRef)(/* @__PURE__ */ new Map());
4275
+ const stopRequestBySessionRef = (0, import_react13.useRef)(/* @__PURE__ */ new Map());
4276
+ const lastRequestBySessionRef = (0, import_react13.useRef)(/* @__PURE__ */ new Map());
4253
4277
  (0, import_react13.useEffect)(() => {
4254
4278
  setSelectedModel(
4255
4279
  (current) => resolveSelectedChatModel({ currentModel: current, availableModels, isModelsLoading })
@@ -4272,33 +4296,47 @@ var useChatComposer = () => {
4272
4296
  return () => window.clearTimeout(timeoutId);
4273
4297
  }, [attachmentNotice]);
4274
4298
  const clearStopTimeout = (sessionId) => {
4275
- if (!stopRequestRef.current)
4299
+ const stopRequest = stopRequestBySessionRef.current.get(sessionId);
4300
+ if (!stopRequest || stopRequest.timeoutId === null) {
4276
4301
  return;
4277
- if (sessionId && stopRequestRef.current.sessionId !== sessionId)
4278
- return;
4279
- if (stopRequestRef.current.timeoutId !== null) {
4280
- window.clearTimeout(stopRequestRef.current.timeoutId);
4281
- stopRequestRef.current.timeoutId = null;
4282
4302
  }
4303
+ window.clearTimeout(stopRequest.timeoutId);
4304
+ stopRequest.timeoutId = null;
4283
4305
  };
4284
4306
  const clearStopRequest = (0, import_react13.useCallback)((sessionId) => {
4285
- if (!stopRequestRef.current)
4286
- return;
4287
- if (sessionId && stopRequestRef.current.sessionId !== sessionId)
4288
- return;
4289
4307
  clearStopTimeout(sessionId);
4290
- stopRequestRef.current = null;
4308
+ stopRequestBySessionRef.current.delete(sessionId);
4291
4309
  }, []);
4310
+ const moveSessionRuntimeState = (0, import_react13.useCallback)(
4311
+ (previousSessionId, nextSessionId) => {
4312
+ if (previousSessionId === nextSessionId) {
4313
+ return;
4314
+ }
4315
+ const abortController = abortControllerBySessionRef.current.get(previousSessionId);
4316
+ if (abortController) {
4317
+ abortControllerBySessionRef.current.set(nextSessionId, abortController);
4318
+ abortControllerBySessionRef.current.delete(previousSessionId);
4319
+ }
4320
+ const stopRequest = stopRequestBySessionRef.current.get(previousSessionId);
4321
+ if (stopRequest) {
4322
+ stopRequestBySessionRef.current.set(nextSessionId, stopRequest);
4323
+ stopRequestBySessionRef.current.delete(previousSessionId);
4324
+ }
4325
+ },
4326
+ []
4327
+ );
4292
4328
  const finalizeStop = (0, import_react13.useCallback)(
4293
4329
  (sessionId) => {
4294
- if (stopRequestRef.current?.sessionId === sessionId) {
4295
- if (stopRequestRef.current.finalized)
4330
+ const stopRequest = stopRequestBySessionRef.current.get(sessionId);
4331
+ if (stopRequest) {
4332
+ if (stopRequest.finalized) {
4296
4333
  return;
4297
- stopRequestRef.current.finalized = true;
4334
+ }
4335
+ stopRequest.finalized = true;
4298
4336
  }
4299
4337
  clearStopTimeout(sessionId);
4300
- abortControllerRef.current?.abort();
4301
- abortControllerRef.current = null;
4338
+ abortControllerBySessionRef.current.get(sessionId)?.abort();
4339
+ abortControllerBySessionRef.current.delete(sessionId);
4302
4340
  finalizeStoppedStreamingMessage(sessionId);
4303
4341
  clearStopRequest(sessionId);
4304
4342
  },
@@ -4313,7 +4351,7 @@ var useChatComposer = () => {
4313
4351
  model,
4314
4352
  mode
4315
4353
  }) => {
4316
- clearStopRequest();
4354
+ clearStopRequest(localSessionId);
4317
4355
  let currentSessionId = localSessionId;
4318
4356
  clearSessionError(currentSessionId);
4319
4357
  const assistantMessage = createAssistantStreamingMessage({
@@ -4322,10 +4360,19 @@ var useChatComposer = () => {
4322
4360
  createMessageId: () => `assistant-${Date.now()}`
4323
4361
  });
4324
4362
  startStreamingMessage(currentSessionId, assistantMessage);
4325
- abortControllerRef.current?.abort();
4326
- abortControllerRef.current = new AbortController();
4327
- lastRequestRef.current = { localSessionId, sessionId, content, attachments: attachments2, model, mode };
4363
+ abortControllerBySessionRef.current.get(currentSessionId)?.abort();
4364
+ const abortController = new AbortController();
4365
+ abortControllerBySessionRef.current.set(currentSessionId, abortController);
4366
+ lastRequestBySessionRef.current.set(currentSessionId, {
4367
+ localSessionId,
4368
+ sessionId,
4369
+ content,
4370
+ attachments: attachments2,
4371
+ model,
4372
+ mode
4373
+ });
4328
4374
  let accumulated = "";
4375
+ let streamSettled = false;
4329
4376
  try {
4330
4377
  await transport.startStream({
4331
4378
  sessionId,
@@ -4333,13 +4380,15 @@ var useChatComposer = () => {
4333
4380
  mode,
4334
4381
  content,
4335
4382
  attachments: attachments2,
4336
- signal: abortControllerRef.current.signal,
4383
+ signal: abortController.signal,
4337
4384
  onSessionId: (nextSessionId) => {
4338
4385
  if (!nextSessionId || nextSessionId === currentSessionId)
4339
4386
  return;
4340
- replaceSessionId(currentSessionId, nextSessionId);
4387
+ const previousSessionId = currentSessionId;
4388
+ replaceSessionId(previousSessionId, nextSessionId);
4389
+ moveSessionRuntimeState(previousSessionId, nextSessionId);
4341
4390
  currentSessionId = nextSessionId;
4342
- lastRequestRef.current = {
4391
+ const nextRequest = {
4343
4392
  localSessionId: nextSessionId,
4344
4393
  sessionId: nextSessionId,
4345
4394
  content,
@@ -4347,6 +4396,8 @@ var useChatComposer = () => {
4347
4396
  model,
4348
4397
  mode
4349
4398
  };
4399
+ lastRequestBySessionRef.current.delete(previousSessionId);
4400
+ lastRequestBySessionRef.current.set(nextSessionId, nextRequest);
4350
4401
  },
4351
4402
  onUpdate: (update) => {
4352
4403
  accumulated = resolveAccumulatedContent(accumulated, update);
@@ -4356,16 +4407,18 @@ var useChatComposer = () => {
4356
4407
  });
4357
4408
  },
4358
4409
  onDone: () => {
4359
- if (stopRequestRef.current?.sessionId === currentSessionId) {
4410
+ streamSettled = true;
4411
+ if (stopRequestBySessionRef.current.has(currentSessionId)) {
4360
4412
  finalizeStop(currentSessionId);
4361
4413
  return;
4362
4414
  }
4363
4415
  completeStreamingMessage(currentSessionId);
4364
- abortControllerRef.current = null;
4416
+ abortControllerBySessionRef.current.delete(currentSessionId);
4365
4417
  clearStopRequest(currentSessionId);
4366
4418
  },
4367
4419
  onError: (streamError) => {
4368
- if (stopRequestRef.current?.sessionId === currentSessionId) {
4420
+ streamSettled = true;
4421
+ if (stopRequestBySessionRef.current.has(currentSessionId)) {
4369
4422
  finalizeStop(currentSessionId);
4370
4423
  return;
4371
4424
  }
@@ -4374,12 +4427,24 @@ var useChatComposer = () => {
4374
4427
  currentSessionId,
4375
4428
  normalizeChatErrorMessage(streamError.message, labels)
4376
4429
  );
4377
- abortControllerRef.current = null;
4430
+ abortControllerBySessionRef.current.delete(currentSessionId);
4378
4431
  clearStopRequest(currentSessionId);
4379
4432
  }
4380
4433
  });
4381
- } catch {
4382
- abortControllerRef.current = null;
4434
+ } catch (streamError) {
4435
+ abortControllerBySessionRef.current.delete(currentSessionId);
4436
+ if (streamSettled || abortController.signal.aborted || !store.getState().isStreamingBySession[currentSessionId]) {
4437
+ return;
4438
+ }
4439
+ finalizeStoppedStreamingMessage(currentSessionId);
4440
+ setSessionError(
4441
+ currentSessionId,
4442
+ normalizeChatErrorMessage(
4443
+ streamError instanceof Error ? streamError.message : void 0,
4444
+ labels
4445
+ )
4446
+ );
4447
+ clearStopRequest(currentSessionId);
4383
4448
  }
4384
4449
  },
4385
4450
  [
@@ -4388,40 +4453,48 @@ var useChatComposer = () => {
4388
4453
  clearStopRequest,
4389
4454
  finalizeStop,
4390
4455
  labels,
4456
+ moveSessionRuntimeState,
4391
4457
  startStreamingMessage,
4392
4458
  replaceSessionId,
4393
4459
  patchStreamingMessage,
4394
4460
  completeStreamingMessage,
4395
4461
  finalizeStoppedStreamingMessage,
4396
- setSessionError
4462
+ setSessionError,
4463
+ store
4397
4464
  ]
4398
4465
  );
4399
4466
  const send = (0, import_react13.useCallback)(
4400
- async (contentOverride) => {
4467
+ async (contentOverride, options) => {
4401
4468
  const content = (contentOverride ?? value).trim();
4469
+ const includeComposerAttachments = options?.includeComposerAttachments ?? true;
4470
+ const composerAttachmentCount = includeComposerAttachments ? attachments.length : 0;
4402
4471
  if (!canSendChatMessage({
4403
4472
  value: content,
4404
- attachmentCount: attachments.length,
4473
+ attachmentCount: composerAttachmentCount,
4405
4474
  isModelsLoading,
4406
4475
  isModelsError,
4407
4476
  hasModels
4408
4477
  })) {
4409
4478
  return;
4410
4479
  }
4411
- if (!(selectedModel || activeSession?.model || availableModels[0]?.id)) {
4480
+ const storeState = store.getState();
4481
+ const currentActiveSessionId = options?.sessionId ?? storeState.activeSessionId;
4482
+ const currentActiveSession = storeState.sessions.find((session2) => session2.sessionId === currentActiveSessionId) ?? null;
4483
+ const currentMode = currentActiveSession?.mode ?? selectedMode;
4484
+ if (!(selectedModel || currentActiveSession?.model || availableModels[0]?.id)) {
4412
4485
  return;
4413
4486
  }
4414
- const resolvedModel = selectedModel || activeSession?.model || availableModels[0]?.id || "local-image";
4487
+ const resolvedModel = selectedModel || currentActiveSession?.model || availableModels[0]?.id || "local-image";
4415
4488
  const { localSessionId, sessionId, session } = resolveSendSession({
4416
- activeSessionId,
4489
+ activeSessionId: currentActiveSessionId,
4417
4490
  selectedModel: resolvedModel,
4418
- selectedMode,
4491
+ selectedMode: currentMode,
4419
4492
  nowIso,
4420
4493
  createSessionId: createDraftChatSessionId
4421
4494
  });
4422
4495
  if (session)
4423
4496
  createSession(session);
4424
- const messageAttachments = takeMessageAttachments();
4497
+ const messageAttachments = includeComposerAttachments ? takeMessageAttachments() : void 0;
4425
4498
  const userMessage = createUserMessage({
4426
4499
  sessionId: localSessionId,
4427
4500
  content,
@@ -4431,15 +4504,17 @@ var useChatComposer = () => {
4431
4504
  createMessageId: () => `user-${Date.now()}`
4432
4505
  });
4433
4506
  appendMessage(localSessionId, userMessage);
4434
- setAttachmentNotice(null);
4435
- setValue("");
4507
+ if (includeComposerAttachments) {
4508
+ setAttachmentNotice(null);
4509
+ setValue("");
4510
+ }
4436
4511
  await runStream({
4437
4512
  localSessionId,
4438
4513
  sessionId,
4439
4514
  content,
4440
4515
  attachments: messageAttachments,
4441
4516
  model: resolvedModel,
4442
- mode: selectedMode
4517
+ mode: currentMode
4443
4518
  });
4444
4519
  },
4445
4520
  [
@@ -4449,16 +4524,47 @@ var useChatComposer = () => {
4449
4524
  isModelsError,
4450
4525
  hasModels,
4451
4526
  selectedModel,
4452
- activeSession,
4453
4527
  availableModels,
4454
- activeSessionId,
4455
4528
  selectedMode,
4456
4529
  createSession,
4457
4530
  takeMessageAttachments,
4458
4531
  appendMessage,
4459
- runStream
4532
+ runStream,
4533
+ store
4460
4534
  ]
4461
4535
  );
4536
+ const stopSession = (0, import_react13.useCallback)(
4537
+ async (sessionId) => {
4538
+ const storeState = store.getState();
4539
+ const isSessionStreaming = storeState.isStreamingBySession[sessionId] ?? false;
4540
+ const isSessionStopping = storeState.isStoppingBySession[sessionId] ?? false;
4541
+ if (!isSessionStreaming || isSessionStopping) {
4542
+ return;
4543
+ }
4544
+ if (isDraftChatSessionId(sessionId)) {
4545
+ finalizeStop(sessionId);
4546
+ return;
4547
+ }
4548
+ requestStopStreaming(sessionId);
4549
+ stopRequestBySessionRef.current.set(sessionId, {
4550
+ timeoutId: window.setTimeout(() => {
4551
+ finalizeStop(sessionId);
4552
+ }, STOP_WAIT_TIMEOUT_MS),
4553
+ finalized: false
4554
+ });
4555
+ try {
4556
+ const result = await transport.terminateStream(sessionId);
4557
+ if (!result.terminated) {
4558
+ console.error("Failed to terminate chat session: server returned not terminated");
4559
+ }
4560
+ finalizeStop(sessionId);
4561
+ } catch (err) {
4562
+ console.error("Failed to terminate chat session", err);
4563
+ finalizeStop(sessionId);
4564
+ }
4565
+ },
4566
+ [finalizeStop, requestStopStreaming, store, transport]
4567
+ );
4462
4568
  return {
4463
4569
  state: {
4464
4570
  value,
@@ -4498,43 +4604,27 @@ var useChatComposer = () => {
4498
4604
  setPreferredMode(mode);
4499
4605
  if (activeSessionId)
4500
4606
  setSessionMode(activeSessionId, mode);
4501
- if (lastRequestRef.current && activeSessionId && (lastRequestRef.current.localSessionId === activeSessionId || lastRequestRef.current.sessionId === activeSessionId)) {
4502
- lastRequestRef.current = { ...lastRequestRef.current, mode };
4607
+ if (activeSessionId) {
4608
+ const previousRequest = lastRequestBySessionRef.current.get(activeSessionId);
4609
+ if (previousRequest) {
4610
+ lastRequestBySessionRef.current.set(activeSessionId, { ...previousRequest, mode });
4611
+ }
4503
4612
  }
4504
4613
  },
4505
4614
  reloadModels: () => void fetchModels(),
4615
+ stopSession,
4506
4616
  stop: async () => {
4507
4617
  if (!streamingSessionId)
4508
4618
  return;
4509
- if (isStopping)
4510
- return;
4511
- if (isDraftChatSessionId(streamingSessionId)) {
4512
- finalizeStop(streamingSessionId);
4513
- return;
4514
- }
4515
- requestStopStreaming(streamingSessionId);
4516
- stopRequestRef.current = {
4517
- sessionId: streamingSessionId,
4518
- timeoutId: window.setTimeout(() => {
4519
- finalizeStop(streamingSessionId);
4520
- }, STOP_WAIT_TIMEOUT_MS),
4521
- finalized: false
4522
- };
4523
- try {
4524
- const result = await transport.terminateStream(streamingSessionId);
4525
- if (!result.terminated) {
4526
- console.error("Failed to terminate chat session: server returned not terminated");
4527
- }
4528
- finalizeStop(streamingSessionId);
4529
- } catch (err) {
4530
- console.error("Failed to terminate chat session", err);
4531
- finalizeStop(streamingSessionId);
4532
- }
4619
+ await stopSession(streamingSessionId);
4533
4620
  },
4534
- retry: () => {
4535
- if (!lastRequestRef.current)
4621
+ retry: (sessionId) => {
4622
+ if (!sessionId)
4536
4623
  return;
4537
- void runStream(lastRequestRef.current);
4624
+ const request = lastRequestBySessionRef.current.get(sessionId);
4625
+ if (!request)
4626
+ return;
4627
+ void runStream(request);
4538
4628
  }
4539
4629
  }
4540
4630
  };
@@ -4699,7 +4789,7 @@ var CloseGlyph = import_styled10.default.span`
4699
4789
 
4700
4790
  // src/components/chat-composer/components/chat-model-control.tsx
4701
4791
  var import_styled11 = __toESM(require("@emotion/styled"));
4702
- var import_compass_ui2 = require("@xinghunm/compass-ui");
4792
+ var import_compass_ui = require("@xinghunm/compass-ui");
4703
4793
  var import_jsx_runtime12 = require("@emotion/react/jsx-runtime");
4704
4794
  var ChatModelControl = ({
4705
4795
  selectedModel,
@@ -4800,7 +4890,7 @@ var ModelReloadButton = import_styled11.default.button`
4800
4890
  var ReloadIcon = import_styled11.default.svg`
4801
4891
  flex-shrink: 0;
4802
4892
  `;
4803
- var ModelSelect = (0, import_styled11.default)(import_compass_ui2.Select)`
4893
+ var ModelSelect = (0, import_styled11.default)(import_compass_ui.Select)`
4804
4894
  && {
4805
4895
  width: auto;
4806
4896
  min-width: 0;
@@ -4821,7 +4911,7 @@ var ModelSelect = (0, import_styled11.default)(import_compass_ui2.Select)`
4821
4911
 
4822
4912
  // src/components/chat-composer/components/chat-mode-control.tsx
4823
4913
  var import_styled12 = __toESM(require("@emotion/styled"));
4824
- var import_compass_ui3 = require("@xinghunm/compass-ui");
4914
+ var import_compass_ui2 = require("@xinghunm/compass-ui");
4825
4915
  var import_jsx_runtime13 = require("@emotion/react/jsx-runtime");
4826
4916
  var ChatModeControl = ({
4827
4917
  value,
@@ -4844,7 +4934,7 @@ var ChatModeControl = ({
4844
4934
  }
4845
4935
  );
4846
4936
  };
4847
- var ModeSelect = (0, import_styled12.default)(import_compass_ui3.Select)`
4937
+ var ModeSelect = (0, import_styled12.default)(import_compass_ui2.Select)`
4848
4938
  && {
4849
4939
  flex: 0 1 auto;
4850
4940
  width: auto;
@@ -4866,7 +4956,7 @@ var ModeSelect = (0, import_styled12.default)(import_compass_ui3.Select)`
4866
4956
 
4867
4957
  // src/components/chat-composer/components/chat-send-actions.tsx
4868
4958
  var import_styled13 = __toESM(require("@emotion/styled"));
4869
- var import_compass_ui4 = require("@xinghunm/compass-ui");
4959
+ var import_compass_ui3 = require("@xinghunm/compass-ui");
4870
4960
  var import_jsx_runtime14 = require("@emotion/react/jsx-runtime");
4871
4961
  var ArrowUpIcon = () => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
4872
4962
  "svg",
@@ -4920,7 +5010,7 @@ var ChatSendActions = ({
4920
5010
  onClick: () => void onSend()
4921
5011
  }
4922
5012
  ) });
4923
- var PrimaryButton = (0, import_styled13.default)(import_compass_ui4.Button)`
5013
+ var PrimaryButton = (0, import_styled13.default)(import_compass_ui3.Button)`
4924
5014
  && {
4925
5015
  min-width: 24px;
4926
5016
  width: 24px;
@@ -4946,7 +5036,7 @@ var PrimaryButton = (0, import_styled13.default)(import_compass_ui4.Button)`
4946
5036
  }
4947
5037
  }
4948
5038
  `;
4949
- var StopButton = (0, import_styled13.default)(import_compass_ui4.Button)`
5039
+ var StopButton = (0, import_styled13.default)(import_compass_ui3.Button)`
4950
5040
  && {
4951
5041
  min-width: 24px;
4952
5042
  width: 24px;
@@ -5181,7 +5271,7 @@ var ChatComposerView = ({
5181
5271
  }
5182
5272
  ) : null,
5183
5273
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
5184
- Input2,
5274
+ Input,
5185
5275
  {
5186
5276
  ref: inputRef,
5187
5277
  "data-testid": "chat-composer-input",
@@ -5242,15 +5332,16 @@ var ChatComposerView = ({
5242
5332
  ] }) });
5243
5333
  };
5244
5334
  var ChatComposer = () => {
5245
- const { labels, sendRef, retryRef, enableImageAttachments } = useChatContext();
5335
+ const { labels, sendRef, retryRef, stopRef, enableImageAttachments } = useChatContext();
5246
5336
  const { state, actions } = useChatComposer();
5247
5337
  const { send, retry } = actions;
5248
5338
  (0, import_react15.useEffect)(() => {
5249
5339
  sendRef.current = send;
5250
- retryRef.current = async () => {
5251
- retry();
5340
+ retryRef.current = async (sessionId) => {
5341
+ retry(sessionId);
5252
5342
  };
5253
- }, [retry, retryRef, send, sendRef]);
5343
+ stopRef.current = actions.stopSession;
5344
+ }, [actions.stopSession, retry, retryRef, send, sendRef, stopRef]);
5254
5345
  const modeLabels = {
5255
5346
  ask: labels.modeLabelAsk,
5256
5347
  plan: labels.modeLabelPlan,
@@ -5325,7 +5416,7 @@ var InputArea = import_styled14.default.div`
5325
5416
  grid-area: input;
5326
5417
  position: relative;
5327
5418
  `;
5328
- var Input2 = import_styled14.default.textarea`
5419
+ var Input = import_styled14.default.textarea`
5329
5420
  --textarea-line-height: ${CHAT_COMPOSER_LINE_HEIGHT_PX}px;
5330
5421
  --textarea-min-rows: ${CHAT_COMPOSER_MIN_ROWS};
5331
5422
  --textarea-max-rows: ${CHAT_COMPOSER_MAX_ROWS};
@@ -5502,28 +5593,17 @@ var ChatConversationList = () => {
5502
5593
  const { labels } = useChatContext();
5503
5594
  const sessions = useChatStore((s) => s.sessions);
5504
5595
  const activeSessionId = useChatStore((s) => s.activeSessionId);
5505
- const preferredMode = useChatStore((s) => s.preferredMode);
5506
- const createSession = useChatStore((s) => s.createSession);
5596
+ const startNewChat = useChatStore((s) => s.startNewChat);
5507
5597
  const setActiveSession = useChatStore((s) => s.setActiveSession);
5508
5598
  const modeLabels = {
5509
5599
  ask: labels.modeLabelAsk,
5510
5600
  plan: labels.modeLabelPlan,
5511
5601
  agent: labels.modeLabelAgent
5512
5602
  };
5513
- const handleCreateSession = () => {
5514
- const session = createDraftChatSession({
5515
- // Model is intentionally deferred: ChatComposer resolves selectedModel at send time.
5516
- model: "",
5517
- mode: preferredMode,
5518
- nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
5519
- createSessionId: createDraftChatSessionId
5520
- });
5521
- createSession(session);
5522
- };
5523
5603
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(Container3, { children: [
5524
5604
  /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(Toolbar, { children: [
5525
5605
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Title3, { children: "Sessions" }),
5526
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CreateButton, { type: "button", "data-testid": "chat-create-session", onClick: handleCreateSession, children: labels.newChat })
5606
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CreateButton, { type: "button", "data-testid": "chat-create-session", onClick: startNewChat, children: labels.newChat })
5527
5607
  ] }),
5528
5608
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(List2, { "data-testid": "chat-session-list", children: sessions.map((session) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5529
5609
  ChatSessionItem,
@@ -5575,8 +5655,70 @@ var List2 = import_styled16.default.div`
5575
5655
 
5576
5656
  // src/components/ai-chat/index.tsx
5577
5657
  var import_jsx_runtime18 = require("@emotion/react/jsx-runtime");
5578
- var AiChat = ({ showConversationList = false, ...providerProps }) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5579
- import_compass_ui5.ConfigProvider,
5658
+ var QuickActions = ({ renderNewChatTrigger }) => {
5659
+ const { labels, stopRef, store } = useChatContext();
5660
+ const startNewChat = useChatStore((state) => state.startNewChat);
5661
+ const activeSessionId = useChatStore((state) => state.activeSessionId);
5662
+ const isActiveSessionStreaming = useChatStore(
5663
+ (state) => state.activeSessionId ? state.isStreamingBySession[state.activeSessionId] ?? false : false
5664
+ );
5665
+ const isActiveSessionStopping = useChatStore(
5666
+ (state) => state.activeSessionId ? state.isStoppingBySession[state.activeSessionId] ?? false : false
5667
+ );
5668
+ const createNewSession = () => {
5669
+ startNewChat();
5670
+ };
5671
+ const stopActiveSession = async () => {
5672
+ const currentState = store.getState();
5673
+ const currentSessionId = currentState.activeSessionId;
5674
+ const isCurrentSessionStreaming = currentSessionId ? currentState.isStreamingBySession[currentSessionId] ?? false : false;
5675
+ if (!currentSessionId || !isCurrentSessionStreaming) {
5676
+ return;
5677
+ }
5678
+ await stopRef.current(currentSessionId);
5679
+ };
5680
+ const handleStartNewChat = async () => {
5681
+ const currentState = store.getState();
5682
+ const currentSessionId = currentState.activeSessionId;
5683
+ const isCurrentSessionStreaming = currentSessionId ? currentState.isStreamingBySession[currentSessionId] ?? false : false;
5684
+ if (currentSessionId && isCurrentSessionStreaming) {
5685
+ void stopRef.current(currentSessionId);
5686
+ }
5687
+ createNewSession();
5688
+ };
5689
+ const triggerProps = {
5690
+ activeSessionId,
5691
+ isStreaming: isActiveSessionStreaming,
5692
+ isStopping: isActiveSessionStopping,
5693
+ createNewSession,
5694
+ stopActiveSession,
5695
+ startNewChat: handleStartNewChat
5696
+ };
5697
+ if (renderNewChatTrigger) {
5698
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(QuickActionsRow, { children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(NewChatTriggerRenderer, { renderNewChatTrigger, triggerProps }) });
5699
+ }
5700
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(QuickActionsRow, { children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5701
+ QuickActionButton,
5702
+ {
5703
+ type: "button",
5704
+ "data-testid": "chat-start-new-session",
5705
+ onClick: () => void handleStartNewChat(),
5706
+ disabled: isActiveSessionStopping,
5707
+ children: labels.newChat
5708
+ }
5709
+ ) });
5710
+ };
5711
+ var NewChatTriggerRenderer = ({
5712
+ renderNewChatTrigger,
5713
+ triggerProps
5714
+ }) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_jsx_runtime18.Fragment, { children: renderNewChatTrigger(triggerProps) });
5715
+ var AiChat = ({
5716
+ showConversationList = false,
5717
+ showNewChatButton = false,
5718
+ renderNewChatTrigger,
5719
+ ...providerProps
5720
+ }) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5721
+ import_compass_ui4.ConfigProvider,
5580
5722
  {
5581
5723
  theme: {
5582
5724
  token: {
@@ -5613,6 +5755,7 @@ var AiChat = ({ showConversationList = false, ...providerProps }) => /* @__PURE_
5613
5755
  children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(AiChatProvider, { ...providerProps, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(Root, { "data-testid": "ai-chat", children: [
5614
5756
  showConversationList ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ChatConversationList, {}) : null,
5615
5757
  /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(Workspace, { children: [
5758
+ showNewChatButton && !showConversationList ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(QuickActions, { renderNewChatTrigger }) : null,
5616
5759
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ChatThread, {}),
5617
5760
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ChatComposer, {})
5618
5761
  ] })
@@ -5634,6 +5777,24 @@ var Workspace = import_styled17.default.section`
5634
5777
  min-height: 0;
5635
5778
  overflow: hidden;
5636
5779
  `;
5780
+ var QuickActionsRow = import_styled17.default.div`
5781
+ display: flex;
5782
+ justify-content: flex-end;
5783
+ padding: 12px 12px 0;
5784
+ `;
5785
+ var QuickActionButton = import_styled17.default.button`
5786
+ border: none;
5787
+ border-radius: 12px;
5788
+ padding: 10px 14px;
5789
+ background: rgba(255, 255, 255, 0.08);
5790
+ color: var(--text-primary, #fcfbf8);
5791
+ cursor: pointer;
5792
+
5793
+ &:disabled {
5794
+ opacity: 0.5;
5795
+ cursor: not-allowed;
5796
+ }
5797
+ `;
5637
5798
  // Annotate the CommonJS export names for ESM import in node:
5638
5799
  0 && (module.exports = {
5639
5800
  AiChat,