@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.mjs CHANGED
@@ -49,6 +49,28 @@ var DEFAULT_AI_CHAT_LABELS = {
49
49
  questionnaireOtherPlaceholder: "Other"
50
50
  };
51
51
 
52
+ // src/lib/chat-session.ts
53
+ var DRAFT_CHAT_SESSION_ID_PREFIX = "draft-session-";
54
+ var draftChatSessionSequence = 0;
55
+ var createDraftChatSessionId = () => `${DRAFT_CHAT_SESSION_ID_PREFIX}${Date.now()}-${draftChatSessionSequence++}`;
56
+ var isDraftChatSessionId = (sessionId) => Boolean(sessionId?.startsWith(DRAFT_CHAT_SESSION_ID_PREFIX));
57
+ var createDraftChatSession = ({
58
+ model,
59
+ mode = DEFAULT_CHAT_AGENT_MODE,
60
+ nowIso: nowIso2,
61
+ createSessionId = createDraftChatSessionId
62
+ }) => {
63
+ const iso = nowIso2();
64
+ return {
65
+ sessionId: createSessionId(),
66
+ title: "New Chat",
67
+ createdAt: iso,
68
+ updatedAt: iso,
69
+ model,
70
+ mode
71
+ };
72
+ };
73
+
52
74
  // src/store/chat-store.ts
53
75
  var DEFAULT_CHAT_SESSION_TITLE = "New Chat";
54
76
  var IMAGE_MESSAGE_SESSION_TITLE = "Image message";
@@ -182,6 +204,15 @@ var createChatStore = (initialState) => createStore((set, get) => ({
182
204
  isStoppingBySession: nextIsStoppingBySession
183
205
  });
184
206
  },
207
+ startNewChat: () => {
208
+ const state = get();
209
+ const session = createDraftChatSession({
210
+ model: "",
211
+ mode: state.preferredMode,
212
+ nowIso: () => (/* @__PURE__ */ new Date()).toISOString()
213
+ });
214
+ get().createSession(session);
215
+ },
185
216
  setActiveSession: (sessionId) => {
186
217
  set({ activeSessionId: sessionId });
187
218
  },
@@ -699,6 +730,8 @@ var AiChatProvider = (props) => {
699
730
  });
700
731
  const retryRef = useRef(async () => {
701
732
  });
733
+ const stopRef = useRef(async (_sessionId) => {
734
+ });
702
735
  const defaultApiBaseUrl = "apiBaseUrl" in props ? props.apiBaseUrl : void 0;
703
736
  const defaultAuthToken = "authToken" in props ? props.authToken : void 0;
704
737
  const defaultTransformStreamPacket = "transformStreamPacket" in props ? props.transformStreamPacket : void 0;
@@ -742,6 +775,7 @@ var AiChatProvider = (props) => {
742
775
  labels: { ...DEFAULT_AI_CHAT_LABELS, ...labels },
743
776
  sendRef,
744
777
  retryRef,
778
+ stopRef,
745
779
  renderMessageBlock,
746
780
  handleQuestionnaireSubmit,
747
781
  handleConfirmationSubmit,
@@ -762,6 +796,7 @@ var AiChatProvider = (props) => {
762
796
  renderMessageBlock,
763
797
  sendRef,
764
798
  retryRef,
799
+ stopRef,
765
800
  store,
766
801
  transport
767
802
  ]
@@ -1566,7 +1601,6 @@ import {
1566
1601
  useState as useState2
1567
1602
  } from "react";
1568
1603
  import styled4 from "@emotion/styled";
1569
- import { InputField as Input } from "@xinghunm/compass-ui";
1570
1604
 
1571
1605
  // src/components/chat-thread/components/questionnaire-card-helpers.ts
1572
1606
  var OTHER_OPTION_VALUE = "__other__";
@@ -2367,7 +2401,7 @@ var TextInput = styled4.input`
2367
2401
  color: rgba(255, 255, 255, 0.34);
2368
2402
  }
2369
2403
  `;
2370
- var InlineOtherInput = styled4(Input)`
2404
+ var InlineOtherInput = styled4.input`
2371
2405
  width: 100%;
2372
2406
  margin-top: 0;
2373
2407
 
@@ -3755,19 +3789,20 @@ var ChatThread = () => {
3755
3789
  if (!activeSessionId)
3756
3790
  return;
3757
3791
  clearSessionError(activeSessionId);
3758
- void retryRef.current();
3792
+ void retryRef.current(activeSessionId);
3759
3793
  }, [activeSessionId, clearSessionError, retryRef]);
3760
3794
  const handleQuestionnaireSubmit = useCallback3(
3761
3795
  async (submission) => {
3796
+ const sourceSessionId = activeSessionId;
3762
3797
  if (customQuestionnaireSubmit) {
3763
3798
  const handled = await customQuestionnaireSubmit(submission, {
3764
- sessionId: activeSessionId ?? void 0,
3799
+ sessionId: sourceSessionId ?? void 0,
3765
3800
  mode: activeSessionMode
3766
3801
  });
3767
3802
  if (handled !== false) {
3768
- if (activeSessionId && submission.sourceMessageId) {
3803
+ if (sourceSessionId && submission.sourceMessageId) {
3769
3804
  updateQA(
3770
- activeSessionId,
3805
+ sourceSessionId,
3771
3806
  submission.sourceMessageId,
3772
3807
  submission.questionnaireId,
3773
3808
  submission.answers
@@ -3776,10 +3811,13 @@ var ChatThread = () => {
3776
3811
  return;
3777
3812
  }
3778
3813
  }
3779
- await sendRef.current(submission.content);
3780
- if (activeSessionId && submission.sourceMessageId) {
3814
+ await sendRef.current(submission.content, {
3815
+ sessionId: sourceSessionId ?? void 0,
3816
+ includeComposerAttachments: false
3817
+ });
3818
+ if (sourceSessionId && submission.sourceMessageId) {
3781
3819
  updateQA(
3782
- activeSessionId,
3820
+ sourceSessionId,
3783
3821
  submission.sourceMessageId,
3784
3822
  submission.questionnaireId,
3785
3823
  submission.answers
@@ -3790,16 +3828,20 @@ var ChatThread = () => {
3790
3828
  );
3791
3829
  const handleConfirmation = useCallback3(
3792
3830
  async (submission) => {
3831
+ const sourceSessionId = activeSessionId;
3793
3832
  if (customConfirmationSubmit) {
3794
3833
  const handled = await customConfirmationSubmit(submission, {
3795
- sessionId: activeSessionId ?? void 0,
3834
+ sessionId: sourceSessionId ?? void 0,
3796
3835
  mode: activeSessionMode
3797
3836
  });
3798
3837
  if (handled !== false) {
3799
3838
  return;
3800
3839
  }
3801
3840
  }
3802
- await sendRef.current(submission.content);
3841
+ await sendRef.current(submission.content, {
3842
+ sessionId: sourceSessionId ?? void 0,
3843
+ includeComposerAttachments: false
3844
+ });
3803
3845
  },
3804
3846
  [activeSessionId, activeSessionMode, sendRef, customConfirmationSubmit]
3805
3847
  );
@@ -3936,25 +3978,6 @@ import { useEffect as useEffect7, useLayoutEffect as useLayoutEffect3, useRef as
3936
3978
  import styled14 from "@emotion/styled";
3937
3979
 
3938
3980
  // src/components/chat-composer/lib/chat-composer.ts
3939
- var DRAFT_CHAT_SESSION_ID_PREFIX = "draft-session-";
3940
- var createDraftChatSessionId = () => `${DRAFT_CHAT_SESSION_ID_PREFIX}${Date.now()}`;
3941
- var isDraftChatSessionId = (sessionId) => Boolean(sessionId?.startsWith(DRAFT_CHAT_SESSION_ID_PREFIX));
3942
- var createDraftChatSession = ({
3943
- model,
3944
- mode = DEFAULT_CHAT_AGENT_MODE,
3945
- nowIso: nowIso2,
3946
- createSessionId
3947
- }) => {
3948
- const iso = nowIso2();
3949
- return {
3950
- sessionId: createSessionId(),
3951
- title: "New Chat",
3952
- createdAt: iso,
3953
- updatedAt: iso,
3954
- model,
3955
- mode
3956
- };
3957
- };
3958
3981
  var createUserMessage = ({
3959
3982
  sessionId,
3960
3983
  content,
@@ -4155,19 +4178,20 @@ var normalizeChatErrorMessage = (message, labels) => {
4155
4178
  return trimmedMessage;
4156
4179
  };
4157
4180
  var useChatComposer = () => {
4158
- const { transport, enableImageAttachments, labels } = useChatContext();
4181
+ const { transport, enableImageAttachments, labels, store } = useChatContext();
4159
4182
  const activeSessionId = useChatStore((s) => s.activeSessionId);
4160
4183
  const activeSession = useChatStore(
4161
4184
  (s) => s.sessions.find((x) => x.sessionId === s.activeSessionId) ?? null
4162
4185
  );
4163
4186
  const preferredMode = useChatStore((s) => s.preferredMode);
4164
4187
  const streamingSessionId = useChatStore(
4165
- (s) => Object.entries(s.isStreamingBySession).find(([, v]) => v)?.[0] ?? null
4188
+ (s) => s.activeSessionId && s.isStreamingBySession[s.activeSessionId] ? s.activeSessionId : null
4166
4189
  );
4167
4190
  const isStreaming = Boolean(streamingSessionId);
4168
- const isStopping = useChatStore(
4169
- (s) => streamingSessionId ? s.isStoppingBySession[streamingSessionId] ?? false : false
4170
- );
4191
+ const isStopping = useChatStore((s) => {
4192
+ const currentStreamingSessionId = s.activeSessionId && s.isStreamingBySession[s.activeSessionId] ? s.activeSessionId : null;
4193
+ return currentStreamingSessionId ? s.isStoppingBySession[currentStreamingSessionId] ?? false : false;
4194
+ });
4171
4195
  const createSession = useChatStore((s) => s.createSession);
4172
4196
  const replaceSessionId = useChatStore((s) => s.replaceSessionId);
4173
4197
  const appendMessage = useChatStore((s) => s.appendMessage);
@@ -4204,9 +4228,9 @@ var useChatComposer = () => {
4204
4228
  const [selectedMode, setSelectedModeLocal] = useState6(DEFAULT_CHAT_AGENT_MODE);
4205
4229
  const [attachmentNotice, setAttachmentNotice] = useState6(null);
4206
4230
  const { attachments, appendFiles, removeAttachment, takeMessageAttachments } = useComposerAttachments();
4207
- const abortControllerRef = useRef7(null);
4208
- const stopRequestRef = useRef7(null);
4209
- const lastRequestRef = useRef7(null);
4231
+ const abortControllerBySessionRef = useRef7(/* @__PURE__ */ new Map());
4232
+ const stopRequestBySessionRef = useRef7(/* @__PURE__ */ new Map());
4233
+ const lastRequestBySessionRef = useRef7(/* @__PURE__ */ new Map());
4210
4234
  useEffect6(() => {
4211
4235
  setSelectedModel(
4212
4236
  (current) => resolveSelectedChatModel({ currentModel: current, availableModels, isModelsLoading })
@@ -4229,33 +4253,47 @@ var useChatComposer = () => {
4229
4253
  return () => window.clearTimeout(timeoutId);
4230
4254
  }, [attachmentNotice]);
4231
4255
  const clearStopTimeout = (sessionId) => {
4232
- if (!stopRequestRef.current)
4233
- return;
4234
- if (sessionId && stopRequestRef.current.sessionId !== sessionId)
4256
+ const stopRequest = stopRequestBySessionRef.current.get(sessionId);
4257
+ if (!stopRequest || stopRequest.timeoutId === null) {
4235
4258
  return;
4236
- if (stopRequestRef.current.timeoutId !== null) {
4237
- window.clearTimeout(stopRequestRef.current.timeoutId);
4238
- stopRequestRef.current.timeoutId = null;
4239
4259
  }
4260
+ window.clearTimeout(stopRequest.timeoutId);
4261
+ stopRequest.timeoutId = null;
4240
4262
  };
4241
4263
  const clearStopRequest = useCallback4((sessionId) => {
4242
- if (!stopRequestRef.current)
4243
- return;
4244
- if (sessionId && stopRequestRef.current.sessionId !== sessionId)
4245
- return;
4246
4264
  clearStopTimeout(sessionId);
4247
- stopRequestRef.current = null;
4265
+ stopRequestBySessionRef.current.delete(sessionId);
4248
4266
  }, []);
4267
+ const moveSessionRuntimeState = useCallback4(
4268
+ (previousSessionId, nextSessionId) => {
4269
+ if (previousSessionId === nextSessionId) {
4270
+ return;
4271
+ }
4272
+ const abortController = abortControllerBySessionRef.current.get(previousSessionId);
4273
+ if (abortController) {
4274
+ abortControllerBySessionRef.current.set(nextSessionId, abortController);
4275
+ abortControllerBySessionRef.current.delete(previousSessionId);
4276
+ }
4277
+ const stopRequest = stopRequestBySessionRef.current.get(previousSessionId);
4278
+ if (stopRequest) {
4279
+ stopRequestBySessionRef.current.set(nextSessionId, stopRequest);
4280
+ stopRequestBySessionRef.current.delete(previousSessionId);
4281
+ }
4282
+ },
4283
+ []
4284
+ );
4249
4285
  const finalizeStop = useCallback4(
4250
4286
  (sessionId) => {
4251
- if (stopRequestRef.current?.sessionId === sessionId) {
4252
- if (stopRequestRef.current.finalized)
4287
+ const stopRequest = stopRequestBySessionRef.current.get(sessionId);
4288
+ if (stopRequest) {
4289
+ if (stopRequest.finalized) {
4253
4290
  return;
4254
- stopRequestRef.current.finalized = true;
4291
+ }
4292
+ stopRequest.finalized = true;
4255
4293
  }
4256
4294
  clearStopTimeout(sessionId);
4257
- abortControllerRef.current?.abort();
4258
- abortControllerRef.current = null;
4295
+ abortControllerBySessionRef.current.get(sessionId)?.abort();
4296
+ abortControllerBySessionRef.current.delete(sessionId);
4259
4297
  finalizeStoppedStreamingMessage(sessionId);
4260
4298
  clearStopRequest(sessionId);
4261
4299
  },
@@ -4270,7 +4308,7 @@ var useChatComposer = () => {
4270
4308
  model,
4271
4309
  mode
4272
4310
  }) => {
4273
- clearStopRequest();
4311
+ clearStopRequest(localSessionId);
4274
4312
  let currentSessionId = localSessionId;
4275
4313
  clearSessionError(currentSessionId);
4276
4314
  const assistantMessage = createAssistantStreamingMessage({
@@ -4279,10 +4317,19 @@ var useChatComposer = () => {
4279
4317
  createMessageId: () => `assistant-${Date.now()}`
4280
4318
  });
4281
4319
  startStreamingMessage(currentSessionId, assistantMessage);
4282
- abortControllerRef.current?.abort();
4283
- abortControllerRef.current = new AbortController();
4284
- lastRequestRef.current = { localSessionId, sessionId, content, attachments: attachments2, model, mode };
4320
+ abortControllerBySessionRef.current.get(currentSessionId)?.abort();
4321
+ const abortController = new AbortController();
4322
+ abortControllerBySessionRef.current.set(currentSessionId, abortController);
4323
+ lastRequestBySessionRef.current.set(currentSessionId, {
4324
+ localSessionId,
4325
+ sessionId,
4326
+ content,
4327
+ attachments: attachments2,
4328
+ model,
4329
+ mode
4330
+ });
4285
4331
  let accumulated = "";
4332
+ let streamSettled = false;
4286
4333
  try {
4287
4334
  await transport.startStream({
4288
4335
  sessionId,
@@ -4290,13 +4337,15 @@ var useChatComposer = () => {
4290
4337
  mode,
4291
4338
  content,
4292
4339
  attachments: attachments2,
4293
- signal: abortControllerRef.current.signal,
4340
+ signal: abortController.signal,
4294
4341
  onSessionId: (nextSessionId) => {
4295
4342
  if (!nextSessionId || nextSessionId === currentSessionId)
4296
4343
  return;
4297
- replaceSessionId(currentSessionId, nextSessionId);
4344
+ const previousSessionId = currentSessionId;
4345
+ replaceSessionId(previousSessionId, nextSessionId);
4346
+ moveSessionRuntimeState(previousSessionId, nextSessionId);
4298
4347
  currentSessionId = nextSessionId;
4299
- lastRequestRef.current = {
4348
+ const nextRequest = {
4300
4349
  localSessionId: nextSessionId,
4301
4350
  sessionId: nextSessionId,
4302
4351
  content,
@@ -4304,6 +4353,8 @@ var useChatComposer = () => {
4304
4353
  model,
4305
4354
  mode
4306
4355
  };
4356
+ lastRequestBySessionRef.current.delete(previousSessionId);
4357
+ lastRequestBySessionRef.current.set(nextSessionId, nextRequest);
4307
4358
  },
4308
4359
  onUpdate: (update) => {
4309
4360
  accumulated = resolveAccumulatedContent(accumulated, update);
@@ -4313,16 +4364,18 @@ var useChatComposer = () => {
4313
4364
  });
4314
4365
  },
4315
4366
  onDone: () => {
4316
- if (stopRequestRef.current?.sessionId === currentSessionId) {
4367
+ streamSettled = true;
4368
+ if (stopRequestBySessionRef.current.has(currentSessionId)) {
4317
4369
  finalizeStop(currentSessionId);
4318
4370
  return;
4319
4371
  }
4320
4372
  completeStreamingMessage(currentSessionId);
4321
- abortControllerRef.current = null;
4373
+ abortControllerBySessionRef.current.delete(currentSessionId);
4322
4374
  clearStopRequest(currentSessionId);
4323
4375
  },
4324
4376
  onError: (streamError) => {
4325
- if (stopRequestRef.current?.sessionId === currentSessionId) {
4377
+ streamSettled = true;
4378
+ if (stopRequestBySessionRef.current.has(currentSessionId)) {
4326
4379
  finalizeStop(currentSessionId);
4327
4380
  return;
4328
4381
  }
@@ -4331,12 +4384,24 @@ var useChatComposer = () => {
4331
4384
  currentSessionId,
4332
4385
  normalizeChatErrorMessage(streamError.message, labels)
4333
4386
  );
4334
- abortControllerRef.current = null;
4387
+ abortControllerBySessionRef.current.delete(currentSessionId);
4335
4388
  clearStopRequest(currentSessionId);
4336
4389
  }
4337
4390
  });
4338
- } catch {
4339
- abortControllerRef.current = null;
4391
+ } catch (streamError) {
4392
+ abortControllerBySessionRef.current.delete(currentSessionId);
4393
+ if (streamSettled || abortController.signal.aborted || !store.getState().isStreamingBySession[currentSessionId]) {
4394
+ return;
4395
+ }
4396
+ finalizeStoppedStreamingMessage(currentSessionId);
4397
+ setSessionError(
4398
+ currentSessionId,
4399
+ normalizeChatErrorMessage(
4400
+ streamError instanceof Error ? streamError.message : void 0,
4401
+ labels
4402
+ )
4403
+ );
4404
+ clearStopRequest(currentSessionId);
4340
4405
  }
4341
4406
  },
4342
4407
  [
@@ -4345,40 +4410,48 @@ var useChatComposer = () => {
4345
4410
  clearStopRequest,
4346
4411
  finalizeStop,
4347
4412
  labels,
4413
+ moveSessionRuntimeState,
4348
4414
  startStreamingMessage,
4349
4415
  replaceSessionId,
4350
4416
  patchStreamingMessage,
4351
4417
  completeStreamingMessage,
4352
4418
  finalizeStoppedStreamingMessage,
4353
- setSessionError
4419
+ setSessionError,
4420
+ store
4354
4421
  ]
4355
4422
  );
4356
4423
  const send = useCallback4(
4357
- async (contentOverride) => {
4424
+ async (contentOverride, options) => {
4358
4425
  const content = (contentOverride ?? value).trim();
4426
+ const includeComposerAttachments = options?.includeComposerAttachments ?? true;
4427
+ const composerAttachmentCount = includeComposerAttachments ? attachments.length : 0;
4359
4428
  if (!canSendChatMessage({
4360
4429
  value: content,
4361
- attachmentCount: attachments.length,
4430
+ attachmentCount: composerAttachmentCount,
4362
4431
  isModelsLoading,
4363
4432
  isModelsError,
4364
4433
  hasModels
4365
4434
  })) {
4366
4435
  return;
4367
4436
  }
4368
- if (!(selectedModel || activeSession?.model || availableModels[0]?.id)) {
4437
+ const storeState = store.getState();
4438
+ const currentActiveSessionId = options?.sessionId ?? storeState.activeSessionId;
4439
+ const currentActiveSession = storeState.sessions.find((session2) => session2.sessionId === currentActiveSessionId) ?? null;
4440
+ const currentMode = currentActiveSession?.mode ?? selectedMode;
4441
+ if (!(selectedModel || currentActiveSession?.model || availableModels[0]?.id)) {
4369
4442
  return;
4370
4443
  }
4371
- const resolvedModel = selectedModel || activeSession?.model || availableModels[0]?.id || "local-image";
4444
+ const resolvedModel = selectedModel || currentActiveSession?.model || availableModels[0]?.id || "local-image";
4372
4445
  const { localSessionId, sessionId, session } = resolveSendSession({
4373
- activeSessionId,
4446
+ activeSessionId: currentActiveSessionId,
4374
4447
  selectedModel: resolvedModel,
4375
- selectedMode,
4448
+ selectedMode: currentMode,
4376
4449
  nowIso,
4377
4450
  createSessionId: createDraftChatSessionId
4378
4451
  });
4379
4452
  if (session)
4380
4453
  createSession(session);
4381
- const messageAttachments = takeMessageAttachments();
4454
+ const messageAttachments = includeComposerAttachments ? takeMessageAttachments() : void 0;
4382
4455
  const userMessage = createUserMessage({
4383
4456
  sessionId: localSessionId,
4384
4457
  content,
@@ -4388,15 +4461,17 @@ var useChatComposer = () => {
4388
4461
  createMessageId: () => `user-${Date.now()}`
4389
4462
  });
4390
4463
  appendMessage(localSessionId, userMessage);
4391
- setAttachmentNotice(null);
4392
- setValue("");
4464
+ if (includeComposerAttachments) {
4465
+ setAttachmentNotice(null);
4466
+ setValue("");
4467
+ }
4393
4468
  await runStream({
4394
4469
  localSessionId,
4395
4470
  sessionId,
4396
4471
  content,
4397
4472
  attachments: messageAttachments,
4398
4473
  model: resolvedModel,
4399
- mode: selectedMode
4474
+ mode: currentMode
4400
4475
  });
4401
4476
  },
4402
4477
  [
@@ -4406,16 +4481,47 @@ var useChatComposer = () => {
4406
4481
  isModelsError,
4407
4482
  hasModels,
4408
4483
  selectedModel,
4409
- activeSession,
4410
4484
  availableModels,
4411
- activeSessionId,
4412
4485
  selectedMode,
4413
4486
  createSession,
4414
4487
  takeMessageAttachments,
4415
4488
  appendMessage,
4416
- runStream
4489
+ runStream,
4490
+ store
4417
4491
  ]
4418
4492
  );
4493
+ const stopSession = useCallback4(
4494
+ async (sessionId) => {
4495
+ const storeState = store.getState();
4496
+ const isSessionStreaming = storeState.isStreamingBySession[sessionId] ?? false;
4497
+ const isSessionStopping = storeState.isStoppingBySession[sessionId] ?? false;
4498
+ if (!isSessionStreaming || isSessionStopping) {
4499
+ return;
4500
+ }
4501
+ if (isDraftChatSessionId(sessionId)) {
4502
+ finalizeStop(sessionId);
4503
+ return;
4504
+ }
4505
+ requestStopStreaming(sessionId);
4506
+ stopRequestBySessionRef.current.set(sessionId, {
4507
+ timeoutId: window.setTimeout(() => {
4508
+ finalizeStop(sessionId);
4509
+ }, STOP_WAIT_TIMEOUT_MS),
4510
+ finalized: false
4511
+ });
4512
+ try {
4513
+ const result = await transport.terminateStream(sessionId);
4514
+ if (!result.terminated) {
4515
+ console.error("Failed to terminate chat session: server returned not terminated");
4516
+ }
4517
+ finalizeStop(sessionId);
4518
+ } catch (err) {
4519
+ console.error("Failed to terminate chat session", err);
4520
+ finalizeStop(sessionId);
4521
+ }
4522
+ },
4523
+ [finalizeStop, requestStopStreaming, store, transport]
4524
+ );
4419
4525
  return {
4420
4526
  state: {
4421
4527
  value,
@@ -4455,43 +4561,27 @@ var useChatComposer = () => {
4455
4561
  setPreferredMode(mode);
4456
4562
  if (activeSessionId)
4457
4563
  setSessionMode(activeSessionId, mode);
4458
- if (lastRequestRef.current && activeSessionId && (lastRequestRef.current.localSessionId === activeSessionId || lastRequestRef.current.sessionId === activeSessionId)) {
4459
- lastRequestRef.current = { ...lastRequestRef.current, mode };
4564
+ if (activeSessionId) {
4565
+ const previousRequest = lastRequestBySessionRef.current.get(activeSessionId);
4566
+ if (previousRequest) {
4567
+ lastRequestBySessionRef.current.set(activeSessionId, { ...previousRequest, mode });
4568
+ }
4460
4569
  }
4461
4570
  },
4462
4571
  reloadModels: () => void fetchModels(),
4572
+ stopSession,
4463
4573
  stop: async () => {
4464
4574
  if (!streamingSessionId)
4465
4575
  return;
4466
- if (isStopping)
4467
- return;
4468
- if (isDraftChatSessionId(streamingSessionId)) {
4469
- finalizeStop(streamingSessionId);
4470
- return;
4471
- }
4472
- requestStopStreaming(streamingSessionId);
4473
- stopRequestRef.current = {
4474
- sessionId: streamingSessionId,
4475
- timeoutId: window.setTimeout(() => {
4476
- finalizeStop(streamingSessionId);
4477
- }, STOP_WAIT_TIMEOUT_MS),
4478
- finalized: false
4479
- };
4480
- try {
4481
- const result = await transport.terminateStream(streamingSessionId);
4482
- if (!result.terminated) {
4483
- console.error("Failed to terminate chat session: server returned not terminated");
4484
- }
4485
- finalizeStop(streamingSessionId);
4486
- } catch (err) {
4487
- console.error("Failed to terminate chat session", err);
4488
- finalizeStop(streamingSessionId);
4489
- }
4576
+ await stopSession(streamingSessionId);
4490
4577
  },
4491
- retry: () => {
4492
- if (!lastRequestRef.current)
4578
+ retry: (sessionId) => {
4579
+ if (!sessionId)
4580
+ return;
4581
+ const request = lastRequestBySessionRef.current.get(sessionId);
4582
+ if (!request)
4493
4583
  return;
4494
- void runStream(lastRequestRef.current);
4584
+ void runStream(request);
4495
4585
  }
4496
4586
  }
4497
4587
  };
@@ -5138,7 +5228,7 @@ var ChatComposerView = ({
5138
5228
  }
5139
5229
  ) : null,
5140
5230
  /* @__PURE__ */ jsx15(
5141
- Input2,
5231
+ Input,
5142
5232
  {
5143
5233
  ref: inputRef,
5144
5234
  "data-testid": "chat-composer-input",
@@ -5199,15 +5289,16 @@ var ChatComposerView = ({
5199
5289
  ] }) });
5200
5290
  };
5201
5291
  var ChatComposer = () => {
5202
- const { labels, sendRef, retryRef, enableImageAttachments } = useChatContext();
5292
+ const { labels, sendRef, retryRef, stopRef, enableImageAttachments } = useChatContext();
5203
5293
  const { state, actions } = useChatComposer();
5204
5294
  const { send, retry } = actions;
5205
5295
  useEffect7(() => {
5206
5296
  sendRef.current = send;
5207
- retryRef.current = async () => {
5208
- retry();
5297
+ retryRef.current = async (sessionId) => {
5298
+ retry(sessionId);
5209
5299
  };
5210
- }, [retry, retryRef, send, sendRef]);
5300
+ stopRef.current = actions.stopSession;
5301
+ }, [actions.stopSession, retry, retryRef, send, sendRef, stopRef]);
5211
5302
  const modeLabels = {
5212
5303
  ask: labels.modeLabelAsk,
5213
5304
  plan: labels.modeLabelPlan,
@@ -5282,7 +5373,7 @@ var InputArea = styled14.div`
5282
5373
  grid-area: input;
5283
5374
  position: relative;
5284
5375
  `;
5285
- var Input2 = styled14.textarea`
5376
+ var Input = styled14.textarea`
5286
5377
  --textarea-line-height: ${CHAT_COMPOSER_LINE_HEIGHT_PX}px;
5287
5378
  --textarea-min-rows: ${CHAT_COMPOSER_MIN_ROWS};
5288
5379
  --textarea-max-rows: ${CHAT_COMPOSER_MAX_ROWS};
@@ -5459,28 +5550,17 @@ var ChatConversationList = () => {
5459
5550
  const { labels } = useChatContext();
5460
5551
  const sessions = useChatStore((s) => s.sessions);
5461
5552
  const activeSessionId = useChatStore((s) => s.activeSessionId);
5462
- const preferredMode = useChatStore((s) => s.preferredMode);
5463
- const createSession = useChatStore((s) => s.createSession);
5553
+ const startNewChat = useChatStore((s) => s.startNewChat);
5464
5554
  const setActiveSession = useChatStore((s) => s.setActiveSession);
5465
5555
  const modeLabels = {
5466
5556
  ask: labels.modeLabelAsk,
5467
5557
  plan: labels.modeLabelPlan,
5468
5558
  agent: labels.modeLabelAgent
5469
5559
  };
5470
- const handleCreateSession = () => {
5471
- const session = createDraftChatSession({
5472
- // Model is intentionally deferred: ChatComposer resolves selectedModel at send time.
5473
- model: "",
5474
- mode: preferredMode,
5475
- nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
5476
- createSessionId: createDraftChatSessionId
5477
- });
5478
- createSession(session);
5479
- };
5480
5560
  return /* @__PURE__ */ jsxs12(Container3, { children: [
5481
5561
  /* @__PURE__ */ jsxs12(Toolbar, { children: [
5482
5562
  /* @__PURE__ */ jsx17(Title3, { children: "Sessions" }),
5483
- /* @__PURE__ */ jsx17(CreateButton, { type: "button", "data-testid": "chat-create-session", onClick: handleCreateSession, children: labels.newChat })
5563
+ /* @__PURE__ */ jsx17(CreateButton, { type: "button", "data-testid": "chat-create-session", onClick: startNewChat, children: labels.newChat })
5484
5564
  ] }),
5485
5565
  /* @__PURE__ */ jsx17(List2, { "data-testid": "chat-session-list", children: sessions.map((session) => /* @__PURE__ */ jsx17(
5486
5566
  ChatSessionItem,
@@ -5531,8 +5611,70 @@ var List2 = styled16.div`
5531
5611
  `;
5532
5612
 
5533
5613
  // src/components/ai-chat/index.tsx
5534
- import { jsx as jsx18, jsxs as jsxs13 } from "@emotion/react/jsx-runtime";
5535
- var AiChat = ({ showConversationList = false, ...providerProps }) => /* @__PURE__ */ jsx18(
5614
+ import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs13 } from "@emotion/react/jsx-runtime";
5615
+ var QuickActions = ({ renderNewChatTrigger }) => {
5616
+ const { labels, stopRef, store } = useChatContext();
5617
+ const startNewChat = useChatStore((state) => state.startNewChat);
5618
+ const activeSessionId = useChatStore((state) => state.activeSessionId);
5619
+ const isActiveSessionStreaming = useChatStore(
5620
+ (state) => state.activeSessionId ? state.isStreamingBySession[state.activeSessionId] ?? false : false
5621
+ );
5622
+ const isActiveSessionStopping = useChatStore(
5623
+ (state) => state.activeSessionId ? state.isStoppingBySession[state.activeSessionId] ?? false : false
5624
+ );
5625
+ const createNewSession = () => {
5626
+ startNewChat();
5627
+ };
5628
+ const stopActiveSession = async () => {
5629
+ const currentState = store.getState();
5630
+ const currentSessionId = currentState.activeSessionId;
5631
+ const isCurrentSessionStreaming = currentSessionId ? currentState.isStreamingBySession[currentSessionId] ?? false : false;
5632
+ if (!currentSessionId || !isCurrentSessionStreaming) {
5633
+ return;
5634
+ }
5635
+ await stopRef.current(currentSessionId);
5636
+ };
5637
+ const handleStartNewChat = async () => {
5638
+ const currentState = store.getState();
5639
+ const currentSessionId = currentState.activeSessionId;
5640
+ const isCurrentSessionStreaming = currentSessionId ? currentState.isStreamingBySession[currentSessionId] ?? false : false;
5641
+ if (currentSessionId && isCurrentSessionStreaming) {
5642
+ void stopRef.current(currentSessionId);
5643
+ }
5644
+ createNewSession();
5645
+ };
5646
+ const triggerProps = {
5647
+ activeSessionId,
5648
+ isStreaming: isActiveSessionStreaming,
5649
+ isStopping: isActiveSessionStopping,
5650
+ createNewSession,
5651
+ stopActiveSession,
5652
+ startNewChat: handleStartNewChat
5653
+ };
5654
+ if (renderNewChatTrigger) {
5655
+ return /* @__PURE__ */ jsx18(QuickActionsRow, { children: /* @__PURE__ */ jsx18(NewChatTriggerRenderer, { renderNewChatTrigger, triggerProps }) });
5656
+ }
5657
+ return /* @__PURE__ */ jsx18(QuickActionsRow, { children: /* @__PURE__ */ jsx18(
5658
+ QuickActionButton,
5659
+ {
5660
+ type: "button",
5661
+ "data-testid": "chat-start-new-session",
5662
+ onClick: () => void handleStartNewChat(),
5663
+ disabled: isActiveSessionStopping,
5664
+ children: labels.newChat
5665
+ }
5666
+ ) });
5667
+ };
5668
+ var NewChatTriggerRenderer = ({
5669
+ renderNewChatTrigger,
5670
+ triggerProps
5671
+ }) => /* @__PURE__ */ jsx18(Fragment5, { children: renderNewChatTrigger(triggerProps) });
5672
+ var AiChat = ({
5673
+ showConversationList = false,
5674
+ showNewChatButton = false,
5675
+ renderNewChatTrigger,
5676
+ ...providerProps
5677
+ }) => /* @__PURE__ */ jsx18(
5536
5678
  ConfigProvider,
5537
5679
  {
5538
5680
  theme: {
@@ -5570,6 +5712,7 @@ var AiChat = ({ showConversationList = false, ...providerProps }) => /* @__PURE_
5570
5712
  children: /* @__PURE__ */ jsx18(AiChatProvider, { ...providerProps, children: /* @__PURE__ */ jsxs13(Root, { "data-testid": "ai-chat", children: [
5571
5713
  showConversationList ? /* @__PURE__ */ jsx18(ChatConversationList, {}) : null,
5572
5714
  /* @__PURE__ */ jsxs13(Workspace, { children: [
5715
+ showNewChatButton && !showConversationList ? /* @__PURE__ */ jsx18(QuickActions, { renderNewChatTrigger }) : null,
5573
5716
  /* @__PURE__ */ jsx18(ChatThread, {}),
5574
5717
  /* @__PURE__ */ jsx18(ChatComposer, {})
5575
5718
  ] })
@@ -5591,6 +5734,24 @@ var Workspace = styled17.section`
5591
5734
  min-height: 0;
5592
5735
  overflow: hidden;
5593
5736
  `;
5737
+ var QuickActionsRow = styled17.div`
5738
+ display: flex;
5739
+ justify-content: flex-end;
5740
+ padding: 12px 12px 0;
5741
+ `;
5742
+ var QuickActionButton = styled17.button`
5743
+ border: none;
5744
+ border-radius: 12px;
5745
+ padding: 10px 14px;
5746
+ background: rgba(255, 255, 255, 0.08);
5747
+ color: var(--text-primary, #fcfbf8);
5748
+ cursor: pointer;
5749
+
5750
+ &:disabled {
5751
+ opacity: 0.5;
5752
+ cursor: not-allowed;
5753
+ }
5754
+ `;
5594
5755
  export {
5595
5756
  AiChat,
5596
5757
  AiChatProvider,