@xinghunm/ai-chat 1.1.2 → 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
  },
@@ -444,6 +475,7 @@ var startChatStream = async ({
444
475
  sessionId,
445
476
  authToken,
446
477
  requestHeaders,
478
+ requestBody,
447
479
  model,
448
480
  mode,
449
481
  content,
@@ -465,12 +497,14 @@ var startChatStream = async ({
465
497
  const response = await fetch(`${apiBaseUrl}${endpointPath}`, {
466
498
  method: "POST",
467
499
  headers,
468
- body: JSON.stringify({
469
- model,
470
- mode,
471
- stream: true,
472
- messages: [{ role: "user", content }]
473
- }),
500
+ body: JSON.stringify(
501
+ requestBody ?? {
502
+ model,
503
+ mode,
504
+ stream: true,
505
+ messages: [{ role: "user", content }]
506
+ }
507
+ ),
474
508
  signal
475
509
  });
476
510
  const contentType = response.headers.get("content-type") ?? "";
@@ -546,11 +580,70 @@ var createModeDefaultHeaders = (mode) => {
546
580
  }
547
581
  return {};
548
582
  };
583
+ var readFileAsDataUrl = (file) => new Promise((resolve, reject) => {
584
+ const reader = new FileReader();
585
+ reader.onload = () => {
586
+ if (typeof reader.result === "string") {
587
+ resolve(reader.result);
588
+ return;
589
+ }
590
+ reject(new Error(`Failed to read image attachment: ${file.name}`));
591
+ };
592
+ reader.onerror = () => {
593
+ reject(new Error(`Failed to read image attachment: ${file.name}`));
594
+ };
595
+ reader.readAsDataURL(file);
596
+ });
597
+ var resolveAttachmentDataUrl = async (attachment) => {
598
+ if (attachment.file) {
599
+ return readFileAsDataUrl(attachment.file);
600
+ }
601
+ if (attachment.previewUrl.startsWith("data:image/")) {
602
+ return attachment.previewUrl;
603
+ }
604
+ throw new Error(`Attachment is missing file data: ${attachment.name}`);
605
+ };
606
+ var createDefaultRequestBody = async ({
607
+ model,
608
+ mode,
609
+ content,
610
+ attachments
611
+ }) => {
612
+ const hasAttachments = Boolean(attachments?.length);
613
+ if (!hasAttachments) {
614
+ return {
615
+ model,
616
+ mode,
617
+ stream: true,
618
+ messages: [{ role: "user", content }]
619
+ };
620
+ }
621
+ const imageParts = await Promise.all(
622
+ (attachments ?? []).map(async (attachment) => ({
623
+ type: "image_url",
624
+ image_url: {
625
+ url: await resolveAttachmentDataUrl(attachment)
626
+ }
627
+ }))
628
+ );
629
+ const messageContent = [
630
+ ...content ? [{ type: "text", text: content }] : [],
631
+ ...imageParts
632
+ ];
633
+ return {
634
+ model,
635
+ mode,
636
+ stream: true,
637
+ messages: [{ role: "user", content: messageContent }]
638
+ };
639
+ };
549
640
  var createDefaultChatTransport = ({
550
641
  apiBaseUrl,
551
642
  authToken,
552
643
  toolExecutionPolicy,
553
644
  streamHeaders,
645
+ resolveModels,
646
+ buildRequestBody,
554
647
  transformStreamPacket,
555
648
  endpoints,
556
649
  axiosInstance
@@ -565,12 +658,13 @@ var createDefaultChatTransport = ({
565
658
  ...streamHeaders
566
659
  };
567
660
  return {
568
- getModels: () => getChatModels(client, resolvedEndpoints.models),
661
+ getModels: () => resolveModels?.() ?? getChatModels(client, resolvedEndpoints.models),
569
662
  startStream: async ({
570
663
  sessionId,
571
664
  model,
572
665
  mode,
573
666
  content,
667
+ attachments,
574
668
  signal,
575
669
  onUpdate,
576
670
  onSessionId,
@@ -581,12 +675,20 @@ var createDefaultChatTransport = ({
581
675
  ...createModeDefaultHeaders(mode),
582
676
  ...resolvedStreamHeaders
583
677
  };
678
+ const requestBody = buildRequestBody ? buildRequestBody({
679
+ sessionId,
680
+ model,
681
+ mode,
682
+ content,
683
+ attachments
684
+ }) : await createDefaultRequestBody({ model, mode, content, attachments });
584
685
  await startChatStream({
585
686
  apiBaseUrl,
586
687
  endpointPath: resolvedEndpoints.completions,
587
688
  sessionId,
588
689
  authToken,
589
690
  requestHeaders,
691
+ requestBody,
590
692
  model,
591
693
  mode,
592
694
  content,
@@ -628,6 +730,8 @@ var AiChatProvider = (props) => {
628
730
  });
629
731
  const retryRef = useRef(async () => {
630
732
  });
733
+ const stopRef = useRef(async (_sessionId) => {
734
+ });
631
735
  const defaultApiBaseUrl = "apiBaseUrl" in props ? props.apiBaseUrl : void 0;
632
736
  const defaultAuthToken = "authToken" in props ? props.authToken : void 0;
633
737
  const defaultTransformStreamPacket = "transformStreamPacket" in props ? props.transformStreamPacket : void 0;
@@ -671,6 +775,7 @@ var AiChatProvider = (props) => {
671
775
  labels: { ...DEFAULT_AI_CHAT_LABELS, ...labels },
672
776
  sendRef,
673
777
  retryRef,
778
+ stopRef,
674
779
  renderMessageBlock,
675
780
  handleQuestionnaireSubmit,
676
781
  handleConfirmationSubmit,
@@ -691,6 +796,7 @@ var AiChatProvider = (props) => {
691
796
  renderMessageBlock,
692
797
  sendRef,
693
798
  retryRef,
799
+ stopRef,
694
800
  store,
695
801
  transport
696
802
  ]
@@ -1495,7 +1601,6 @@ import {
1495
1601
  useState as useState2
1496
1602
  } from "react";
1497
1603
  import styled4 from "@emotion/styled";
1498
- import { Input } from "@xinghunm/compass-ui";
1499
1604
 
1500
1605
  // src/components/chat-thread/components/questionnaire-card-helpers.ts
1501
1606
  var OTHER_OPTION_VALUE = "__other__";
@@ -2296,7 +2401,7 @@ var TextInput = styled4.input`
2296
2401
  color: rgba(255, 255, 255, 0.34);
2297
2402
  }
2298
2403
  `;
2299
- var InlineOtherInput = styled4(Input)`
2404
+ var InlineOtherInput = styled4.input`
2300
2405
  width: 100%;
2301
2406
  margin-top: 0;
2302
2407
 
@@ -3684,19 +3789,20 @@ var ChatThread = () => {
3684
3789
  if (!activeSessionId)
3685
3790
  return;
3686
3791
  clearSessionError(activeSessionId);
3687
- void retryRef.current();
3792
+ void retryRef.current(activeSessionId);
3688
3793
  }, [activeSessionId, clearSessionError, retryRef]);
3689
3794
  const handleQuestionnaireSubmit = useCallback3(
3690
3795
  async (submission) => {
3796
+ const sourceSessionId = activeSessionId;
3691
3797
  if (customQuestionnaireSubmit) {
3692
3798
  const handled = await customQuestionnaireSubmit(submission, {
3693
- sessionId: activeSessionId ?? void 0,
3799
+ sessionId: sourceSessionId ?? void 0,
3694
3800
  mode: activeSessionMode
3695
3801
  });
3696
3802
  if (handled !== false) {
3697
- if (activeSessionId && submission.sourceMessageId) {
3803
+ if (sourceSessionId && submission.sourceMessageId) {
3698
3804
  updateQA(
3699
- activeSessionId,
3805
+ sourceSessionId,
3700
3806
  submission.sourceMessageId,
3701
3807
  submission.questionnaireId,
3702
3808
  submission.answers
@@ -3705,10 +3811,13 @@ var ChatThread = () => {
3705
3811
  return;
3706
3812
  }
3707
3813
  }
3708
- await sendRef.current(submission.content);
3709
- if (activeSessionId && submission.sourceMessageId) {
3814
+ await sendRef.current(submission.content, {
3815
+ sessionId: sourceSessionId ?? void 0,
3816
+ includeComposerAttachments: false
3817
+ });
3818
+ if (sourceSessionId && submission.sourceMessageId) {
3710
3819
  updateQA(
3711
- activeSessionId,
3820
+ sourceSessionId,
3712
3821
  submission.sourceMessageId,
3713
3822
  submission.questionnaireId,
3714
3823
  submission.answers
@@ -3719,16 +3828,20 @@ var ChatThread = () => {
3719
3828
  );
3720
3829
  const handleConfirmation = useCallback3(
3721
3830
  async (submission) => {
3831
+ const sourceSessionId = activeSessionId;
3722
3832
  if (customConfirmationSubmit) {
3723
3833
  const handled = await customConfirmationSubmit(submission, {
3724
- sessionId: activeSessionId ?? void 0,
3834
+ sessionId: sourceSessionId ?? void 0,
3725
3835
  mode: activeSessionMode
3726
3836
  });
3727
3837
  if (handled !== false) {
3728
3838
  return;
3729
3839
  }
3730
3840
  }
3731
- await sendRef.current(submission.content);
3841
+ await sendRef.current(submission.content, {
3842
+ sessionId: sourceSessionId ?? void 0,
3843
+ includeComposerAttachments: false
3844
+ });
3732
3845
  },
3733
3846
  [activeSessionId, activeSessionMode, sendRef, customConfirmationSubmit]
3734
3847
  );
@@ -3865,25 +3978,6 @@ import { useEffect as useEffect7, useLayoutEffect as useLayoutEffect3, useRef as
3865
3978
  import styled14 from "@emotion/styled";
3866
3979
 
3867
3980
  // src/components/chat-composer/lib/chat-composer.ts
3868
- var DRAFT_CHAT_SESSION_ID_PREFIX = "draft-session-";
3869
- var createDraftChatSessionId = () => `${DRAFT_CHAT_SESSION_ID_PREFIX}${Date.now()}`;
3870
- var isDraftChatSessionId = (sessionId) => Boolean(sessionId?.startsWith(DRAFT_CHAT_SESSION_ID_PREFIX));
3871
- var createDraftChatSession = ({
3872
- model,
3873
- mode = DEFAULT_CHAT_AGENT_MODE,
3874
- nowIso: nowIso2,
3875
- createSessionId
3876
- }) => {
3877
- const iso = nowIso2();
3878
- return {
3879
- sessionId: createSessionId(),
3880
- title: "New Chat",
3881
- createdAt: iso,
3882
- updatedAt: iso,
3883
- model,
3884
- mode
3885
- };
3886
- };
3887
3981
  var createUserMessage = ({
3888
3982
  sessionId,
3889
3983
  content,
@@ -4044,7 +4138,8 @@ var useComposerAttachments = () => {
4044
4138
  return [];
4045
4139
  }
4046
4140
  const nextMessageAttachments = currentAttachments.map(({ file: _file, ...attachment }) => ({
4047
- ...attachment
4141
+ ...attachment,
4142
+ file: _file
4048
4143
  }));
4049
4144
  attachmentsRef.current = [];
4050
4145
  setAttachments([]);
@@ -4083,19 +4178,20 @@ var normalizeChatErrorMessage = (message, labels) => {
4083
4178
  return trimmedMessage;
4084
4179
  };
4085
4180
  var useChatComposer = () => {
4086
- const { transport, enableImageAttachments, labels } = useChatContext();
4181
+ const { transport, enableImageAttachments, labels, store } = useChatContext();
4087
4182
  const activeSessionId = useChatStore((s) => s.activeSessionId);
4088
4183
  const activeSession = useChatStore(
4089
4184
  (s) => s.sessions.find((x) => x.sessionId === s.activeSessionId) ?? null
4090
4185
  );
4091
4186
  const preferredMode = useChatStore((s) => s.preferredMode);
4092
4187
  const streamingSessionId = useChatStore(
4093
- (s) => Object.entries(s.isStreamingBySession).find(([, v]) => v)?.[0] ?? null
4188
+ (s) => s.activeSessionId && s.isStreamingBySession[s.activeSessionId] ? s.activeSessionId : null
4094
4189
  );
4095
4190
  const isStreaming = Boolean(streamingSessionId);
4096
- const isStopping = useChatStore(
4097
- (s) => streamingSessionId ? s.isStoppingBySession[streamingSessionId] ?? false : false
4098
- );
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
+ });
4099
4195
  const createSession = useChatStore((s) => s.createSession);
4100
4196
  const replaceSessionId = useChatStore((s) => s.replaceSessionId);
4101
4197
  const appendMessage = useChatStore((s) => s.appendMessage);
@@ -4132,9 +4228,9 @@ var useChatComposer = () => {
4132
4228
  const [selectedMode, setSelectedModeLocal] = useState6(DEFAULT_CHAT_AGENT_MODE);
4133
4229
  const [attachmentNotice, setAttachmentNotice] = useState6(null);
4134
4230
  const { attachments, appendFiles, removeAttachment, takeMessageAttachments } = useComposerAttachments();
4135
- const abortControllerRef = useRef7(null);
4136
- const stopRequestRef = useRef7(null);
4137
- 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());
4138
4234
  useEffect6(() => {
4139
4235
  setSelectedModel(
4140
4236
  (current) => resolveSelectedChatModel({ currentModel: current, availableModels, isModelsLoading })
@@ -4157,33 +4253,47 @@ var useChatComposer = () => {
4157
4253
  return () => window.clearTimeout(timeoutId);
4158
4254
  }, [attachmentNotice]);
4159
4255
  const clearStopTimeout = (sessionId) => {
4160
- if (!stopRequestRef.current)
4161
- return;
4162
- if (sessionId && stopRequestRef.current.sessionId !== sessionId)
4256
+ const stopRequest = stopRequestBySessionRef.current.get(sessionId);
4257
+ if (!stopRequest || stopRequest.timeoutId === null) {
4163
4258
  return;
4164
- if (stopRequestRef.current.timeoutId !== null) {
4165
- window.clearTimeout(stopRequestRef.current.timeoutId);
4166
- stopRequestRef.current.timeoutId = null;
4167
4259
  }
4260
+ window.clearTimeout(stopRequest.timeoutId);
4261
+ stopRequest.timeoutId = null;
4168
4262
  };
4169
4263
  const clearStopRequest = useCallback4((sessionId) => {
4170
- if (!stopRequestRef.current)
4171
- return;
4172
- if (sessionId && stopRequestRef.current.sessionId !== sessionId)
4173
- return;
4174
4264
  clearStopTimeout(sessionId);
4175
- stopRequestRef.current = null;
4265
+ stopRequestBySessionRef.current.delete(sessionId);
4176
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
+ );
4177
4285
  const finalizeStop = useCallback4(
4178
4286
  (sessionId) => {
4179
- if (stopRequestRef.current?.sessionId === sessionId) {
4180
- if (stopRequestRef.current.finalized)
4287
+ const stopRequest = stopRequestBySessionRef.current.get(sessionId);
4288
+ if (stopRequest) {
4289
+ if (stopRequest.finalized) {
4181
4290
  return;
4182
- stopRequestRef.current.finalized = true;
4291
+ }
4292
+ stopRequest.finalized = true;
4183
4293
  }
4184
4294
  clearStopTimeout(sessionId);
4185
- abortControllerRef.current?.abort();
4186
- abortControllerRef.current = null;
4295
+ abortControllerBySessionRef.current.get(sessionId)?.abort();
4296
+ abortControllerBySessionRef.current.delete(sessionId);
4187
4297
  finalizeStoppedStreamingMessage(sessionId);
4188
4298
  clearStopRequest(sessionId);
4189
4299
  },
@@ -4194,10 +4304,11 @@ var useChatComposer = () => {
4194
4304
  localSessionId,
4195
4305
  sessionId,
4196
4306
  content,
4307
+ attachments: attachments2,
4197
4308
  model,
4198
4309
  mode
4199
4310
  }) => {
4200
- clearStopRequest();
4311
+ clearStopRequest(localSessionId);
4201
4312
  let currentSessionId = localSessionId;
4202
4313
  clearSessionError(currentSessionId);
4203
4314
  const assistantMessage = createAssistantStreamingMessage({
@@ -4206,29 +4317,44 @@ var useChatComposer = () => {
4206
4317
  createMessageId: () => `assistant-${Date.now()}`
4207
4318
  });
4208
4319
  startStreamingMessage(currentSessionId, assistantMessage);
4209
- abortControllerRef.current?.abort();
4210
- abortControllerRef.current = new AbortController();
4211
- lastRequestRef.current = { localSessionId, sessionId, content, 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
+ });
4212
4331
  let accumulated = "";
4332
+ let streamSettled = false;
4213
4333
  try {
4214
4334
  await transport.startStream({
4215
4335
  sessionId,
4216
4336
  model,
4217
4337
  mode,
4218
4338
  content,
4219
- signal: abortControllerRef.current.signal,
4339
+ attachments: attachments2,
4340
+ signal: abortController.signal,
4220
4341
  onSessionId: (nextSessionId) => {
4221
4342
  if (!nextSessionId || nextSessionId === currentSessionId)
4222
4343
  return;
4223
- replaceSessionId(currentSessionId, nextSessionId);
4344
+ const previousSessionId = currentSessionId;
4345
+ replaceSessionId(previousSessionId, nextSessionId);
4346
+ moveSessionRuntimeState(previousSessionId, nextSessionId);
4224
4347
  currentSessionId = nextSessionId;
4225
- lastRequestRef.current = {
4348
+ const nextRequest = {
4226
4349
  localSessionId: nextSessionId,
4227
4350
  sessionId: nextSessionId,
4228
4351
  content,
4352
+ attachments: attachments2,
4229
4353
  model,
4230
4354
  mode
4231
4355
  };
4356
+ lastRequestBySessionRef.current.delete(previousSessionId);
4357
+ lastRequestBySessionRef.current.set(nextSessionId, nextRequest);
4232
4358
  },
4233
4359
  onUpdate: (update) => {
4234
4360
  accumulated = resolveAccumulatedContent(accumulated, update);
@@ -4238,16 +4364,18 @@ var useChatComposer = () => {
4238
4364
  });
4239
4365
  },
4240
4366
  onDone: () => {
4241
- if (stopRequestRef.current?.sessionId === currentSessionId) {
4367
+ streamSettled = true;
4368
+ if (stopRequestBySessionRef.current.has(currentSessionId)) {
4242
4369
  finalizeStop(currentSessionId);
4243
4370
  return;
4244
4371
  }
4245
4372
  completeStreamingMessage(currentSessionId);
4246
- abortControllerRef.current = null;
4373
+ abortControllerBySessionRef.current.delete(currentSessionId);
4247
4374
  clearStopRequest(currentSessionId);
4248
4375
  },
4249
4376
  onError: (streamError) => {
4250
- if (stopRequestRef.current?.sessionId === currentSessionId) {
4377
+ streamSettled = true;
4378
+ if (stopRequestBySessionRef.current.has(currentSessionId)) {
4251
4379
  finalizeStop(currentSessionId);
4252
4380
  return;
4253
4381
  }
@@ -4256,12 +4384,24 @@ var useChatComposer = () => {
4256
4384
  currentSessionId,
4257
4385
  normalizeChatErrorMessage(streamError.message, labels)
4258
4386
  );
4259
- abortControllerRef.current = null;
4387
+ abortControllerBySessionRef.current.delete(currentSessionId);
4260
4388
  clearStopRequest(currentSessionId);
4261
4389
  }
4262
4390
  });
4263
- } catch {
4264
- 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);
4265
4405
  }
4266
4406
  },
4267
4407
  [
@@ -4270,61 +4410,68 @@ var useChatComposer = () => {
4270
4410
  clearStopRequest,
4271
4411
  finalizeStop,
4272
4412
  labels,
4413
+ moveSessionRuntimeState,
4273
4414
  startStreamingMessage,
4274
4415
  replaceSessionId,
4275
4416
  patchStreamingMessage,
4276
4417
  completeStreamingMessage,
4277
4418
  finalizeStoppedStreamingMessage,
4278
- setSessionError
4419
+ setSessionError,
4420
+ store
4279
4421
  ]
4280
4422
  );
4281
4423
  const send = useCallback4(
4282
- async (contentOverride) => {
4424
+ async (contentOverride, options) => {
4283
4425
  const content = (contentOverride ?? value).trim();
4284
- const hasText = Boolean(content);
4285
- const hasAttachments = attachments.length > 0;
4426
+ const includeComposerAttachments = options?.includeComposerAttachments ?? true;
4427
+ const composerAttachmentCount = includeComposerAttachments ? attachments.length : 0;
4286
4428
  if (!canSendChatMessage({
4287
4429
  value: content,
4288
- attachmentCount: attachments.length,
4430
+ attachmentCount: composerAttachmentCount,
4289
4431
  isModelsLoading,
4290
4432
  isModelsError,
4291
4433
  hasModels
4292
4434
  })) {
4293
4435
  return;
4294
4436
  }
4295
- if (hasText && !(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)) {
4296
4442
  return;
4297
4443
  }
4298
- const resolvedModel = selectedModel || activeSession?.model || availableModels[0]?.id || "local-image";
4444
+ const resolvedModel = selectedModel || currentActiveSession?.model || availableModels[0]?.id || "local-image";
4299
4445
  const { localSessionId, sessionId, session } = resolveSendSession({
4300
- activeSessionId,
4446
+ activeSessionId: currentActiveSessionId,
4301
4447
  selectedModel: resolvedModel,
4302
- selectedMode,
4448
+ selectedMode: currentMode,
4303
4449
  nowIso,
4304
4450
  createSessionId: createDraftChatSessionId
4305
4451
  });
4306
4452
  if (session)
4307
4453
  createSession(session);
4308
- const messageAttachments = takeMessageAttachments();
4454
+ const messageAttachments = includeComposerAttachments ? takeMessageAttachments() : void 0;
4309
4455
  const userMessage = createUserMessage({
4310
4456
  sessionId: localSessionId,
4311
4457
  content,
4312
4458
  attachments: messageAttachments,
4313
- localOnly: hasAttachments,
4459
+ localOnly: false,
4314
4460
  createdAt: nowIso(),
4315
4461
  createMessageId: () => `user-${Date.now()}`
4316
4462
  });
4317
4463
  appendMessage(localSessionId, userMessage);
4318
- setAttachmentNotice(null);
4319
- setValue("");
4320
- if (!hasText)
4321
- return;
4464
+ if (includeComposerAttachments) {
4465
+ setAttachmentNotice(null);
4466
+ setValue("");
4467
+ }
4322
4468
  await runStream({
4323
4469
  localSessionId,
4324
4470
  sessionId,
4325
4471
  content,
4472
+ attachments: messageAttachments,
4326
4473
  model: resolvedModel,
4327
- mode: selectedMode
4474
+ mode: currentMode
4328
4475
  });
4329
4476
  },
4330
4477
  [
@@ -4334,16 +4481,47 @@ var useChatComposer = () => {
4334
4481
  isModelsError,
4335
4482
  hasModels,
4336
4483
  selectedModel,
4337
- activeSession,
4338
4484
  availableModels,
4339
- activeSessionId,
4340
4485
  selectedMode,
4341
4486
  createSession,
4342
4487
  takeMessageAttachments,
4343
4488
  appendMessage,
4344
- runStream
4489
+ runStream,
4490
+ store
4345
4491
  ]
4346
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
+ );
4347
4525
  return {
4348
4526
  state: {
4349
4527
  value,
@@ -4383,43 +4561,27 @@ var useChatComposer = () => {
4383
4561
  setPreferredMode(mode);
4384
4562
  if (activeSessionId)
4385
4563
  setSessionMode(activeSessionId, mode);
4386
- if (lastRequestRef.current && activeSessionId && (lastRequestRef.current.localSessionId === activeSessionId || lastRequestRef.current.sessionId === activeSessionId)) {
4387
- 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
+ }
4388
4569
  }
4389
4570
  },
4390
4571
  reloadModels: () => void fetchModels(),
4572
+ stopSession,
4391
4573
  stop: async () => {
4392
4574
  if (!streamingSessionId)
4393
4575
  return;
4394
- if (isStopping)
4395
- return;
4396
- if (isDraftChatSessionId(streamingSessionId)) {
4397
- finalizeStop(streamingSessionId);
4398
- return;
4399
- }
4400
- requestStopStreaming(streamingSessionId);
4401
- stopRequestRef.current = {
4402
- sessionId: streamingSessionId,
4403
- timeoutId: window.setTimeout(() => {
4404
- finalizeStop(streamingSessionId);
4405
- }, STOP_WAIT_TIMEOUT_MS),
4406
- finalized: false
4407
- };
4408
- try {
4409
- const result = await transport.terminateStream(streamingSessionId);
4410
- if (!result.terminated) {
4411
- console.error("Failed to terminate chat session: server returned not terminated");
4412
- }
4413
- finalizeStop(streamingSessionId);
4414
- } catch (err) {
4415
- console.error("Failed to terminate chat session", err);
4416
- finalizeStop(streamingSessionId);
4417
- }
4576
+ await stopSession(streamingSessionId);
4418
4577
  },
4419
- retry: () => {
4420
- if (!lastRequestRef.current)
4578
+ retry: (sessionId) => {
4579
+ if (!sessionId)
4421
4580
  return;
4422
- void runStream(lastRequestRef.current);
4581
+ const request = lastRequestBySessionRef.current.get(sessionId);
4582
+ if (!request)
4583
+ return;
4584
+ void runStream(request);
4423
4585
  }
4424
4586
  }
4425
4587
  };
@@ -5066,7 +5228,7 @@ var ChatComposerView = ({
5066
5228
  }
5067
5229
  ) : null,
5068
5230
  /* @__PURE__ */ jsx15(
5069
- Input2,
5231
+ Input,
5070
5232
  {
5071
5233
  ref: inputRef,
5072
5234
  "data-testid": "chat-composer-input",
@@ -5127,15 +5289,16 @@ var ChatComposerView = ({
5127
5289
  ] }) });
5128
5290
  };
5129
5291
  var ChatComposer = () => {
5130
- const { labels, sendRef, retryRef, enableImageAttachments } = useChatContext();
5292
+ const { labels, sendRef, retryRef, stopRef, enableImageAttachments } = useChatContext();
5131
5293
  const { state, actions } = useChatComposer();
5132
5294
  const { send, retry } = actions;
5133
5295
  useEffect7(() => {
5134
5296
  sendRef.current = send;
5135
- retryRef.current = async () => {
5136
- retry();
5297
+ retryRef.current = async (sessionId) => {
5298
+ retry(sessionId);
5137
5299
  };
5138
- }, [retry, retryRef, send, sendRef]);
5300
+ stopRef.current = actions.stopSession;
5301
+ }, [actions.stopSession, retry, retryRef, send, sendRef, stopRef]);
5139
5302
  const modeLabels = {
5140
5303
  ask: labels.modeLabelAsk,
5141
5304
  plan: labels.modeLabelPlan,
@@ -5210,7 +5373,7 @@ var InputArea = styled14.div`
5210
5373
  grid-area: input;
5211
5374
  position: relative;
5212
5375
  `;
5213
- var Input2 = styled14.textarea`
5376
+ var Input = styled14.textarea`
5214
5377
  --textarea-line-height: ${CHAT_COMPOSER_LINE_HEIGHT_PX}px;
5215
5378
  --textarea-min-rows: ${CHAT_COMPOSER_MIN_ROWS};
5216
5379
  --textarea-max-rows: ${CHAT_COMPOSER_MAX_ROWS};
@@ -5281,9 +5444,9 @@ var ComposerExpandButton = styled14.button`
5281
5444
  var Footer = styled14.div`
5282
5445
  grid-area: footer;
5283
5446
  display: grid;
5284
- grid-template-columns: minmax(0, 1fr) auto;
5447
+ grid-template-columns: auto minmax(0, 1fr);
5285
5448
  align-items: flex-end;
5286
- gap: 16px;
5449
+ gap: 8px;
5287
5450
  padding: 0 14px 14px;
5288
5451
  `;
5289
5452
  var LeadingActions = styled14.div`
@@ -5298,6 +5461,9 @@ var TrailingActions = styled14.div`
5298
5461
  align-items: center;
5299
5462
  flex-wrap: wrap;
5300
5463
  min-width: 0;
5464
+ width: fit-content;
5465
+ max-width: 100%;
5466
+ justify-self: end;
5301
5467
  justify-content: flex-end;
5302
5468
  gap: 8px;
5303
5469
  `;
@@ -5384,28 +5550,17 @@ var ChatConversationList = () => {
5384
5550
  const { labels } = useChatContext();
5385
5551
  const sessions = useChatStore((s) => s.sessions);
5386
5552
  const activeSessionId = useChatStore((s) => s.activeSessionId);
5387
- const preferredMode = useChatStore((s) => s.preferredMode);
5388
- const createSession = useChatStore((s) => s.createSession);
5553
+ const startNewChat = useChatStore((s) => s.startNewChat);
5389
5554
  const setActiveSession = useChatStore((s) => s.setActiveSession);
5390
5555
  const modeLabels = {
5391
5556
  ask: labels.modeLabelAsk,
5392
5557
  plan: labels.modeLabelPlan,
5393
5558
  agent: labels.modeLabelAgent
5394
5559
  };
5395
- const handleCreateSession = () => {
5396
- const session = createDraftChatSession({
5397
- // Model is intentionally deferred: ChatComposer resolves selectedModel at send time.
5398
- model: "",
5399
- mode: preferredMode,
5400
- nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
5401
- createSessionId: createDraftChatSessionId
5402
- });
5403
- createSession(session);
5404
- };
5405
5560
  return /* @__PURE__ */ jsxs12(Container3, { children: [
5406
5561
  /* @__PURE__ */ jsxs12(Toolbar, { children: [
5407
5562
  /* @__PURE__ */ jsx17(Title3, { children: "Sessions" }),
5408
- /* @__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 })
5409
5564
  ] }),
5410
5565
  /* @__PURE__ */ jsx17(List2, { "data-testid": "chat-session-list", children: sessions.map((session) => /* @__PURE__ */ jsx17(
5411
5566
  ChatSessionItem,
@@ -5456,8 +5611,70 @@ var List2 = styled16.div`
5456
5611
  `;
5457
5612
 
5458
5613
  // src/components/ai-chat/index.tsx
5459
- import { jsx as jsx18, jsxs as jsxs13 } from "@emotion/react/jsx-runtime";
5460
- 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(
5461
5678
  ConfigProvider,
5462
5679
  {
5463
5680
  theme: {
@@ -5495,6 +5712,7 @@ var AiChat = ({ showConversationList = false, ...providerProps }) => /* @__PURE_
5495
5712
  children: /* @__PURE__ */ jsx18(AiChatProvider, { ...providerProps, children: /* @__PURE__ */ jsxs13(Root, { "data-testid": "ai-chat", children: [
5496
5713
  showConversationList ? /* @__PURE__ */ jsx18(ChatConversationList, {}) : null,
5497
5714
  /* @__PURE__ */ jsxs13(Workspace, { children: [
5715
+ showNewChatButton && !showConversationList ? /* @__PURE__ */ jsx18(QuickActions, { renderNewChatTrigger }) : null,
5498
5716
  /* @__PURE__ */ jsx18(ChatThread, {}),
5499
5717
  /* @__PURE__ */ jsx18(ChatComposer, {})
5500
5718
  ] })
@@ -5512,9 +5730,28 @@ var Workspace = styled17.section`
5512
5730
  flex: 1;
5513
5731
  display: flex;
5514
5732
  flex-direction: column;
5733
+ gap: 12px;
5515
5734
  min-height: 0;
5516
5735
  overflow: hidden;
5517
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
+ `;
5518
5755
  export {
5519
5756
  AiChat,
5520
5757
  AiChatProvider,