@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.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
  },
@@ -491,6 +522,7 @@ var startChatStream = async ({
491
522
  sessionId,
492
523
  authToken,
493
524
  requestHeaders,
525
+ requestBody,
494
526
  model,
495
527
  mode,
496
528
  content,
@@ -512,12 +544,14 @@ var startChatStream = async ({
512
544
  const response = await fetch(`${apiBaseUrl}${endpointPath}`, {
513
545
  method: "POST",
514
546
  headers,
515
- body: JSON.stringify({
516
- model,
517
- mode,
518
- stream: true,
519
- messages: [{ role: "user", content }]
520
- }),
547
+ body: JSON.stringify(
548
+ requestBody ?? {
549
+ model,
550
+ mode,
551
+ stream: true,
552
+ messages: [{ role: "user", content }]
553
+ }
554
+ ),
521
555
  signal
522
556
  });
523
557
  const contentType = response.headers.get("content-type") ?? "";
@@ -593,11 +627,70 @@ var createModeDefaultHeaders = (mode) => {
593
627
  }
594
628
  return {};
595
629
  };
630
+ var readFileAsDataUrl = (file) => new Promise((resolve, reject) => {
631
+ const reader = new FileReader();
632
+ reader.onload = () => {
633
+ if (typeof reader.result === "string") {
634
+ resolve(reader.result);
635
+ return;
636
+ }
637
+ reject(new Error(`Failed to read image attachment: ${file.name}`));
638
+ };
639
+ reader.onerror = () => {
640
+ reject(new Error(`Failed to read image attachment: ${file.name}`));
641
+ };
642
+ reader.readAsDataURL(file);
643
+ });
644
+ var resolveAttachmentDataUrl = async (attachment) => {
645
+ if (attachment.file) {
646
+ return readFileAsDataUrl(attachment.file);
647
+ }
648
+ if (attachment.previewUrl.startsWith("data:image/")) {
649
+ return attachment.previewUrl;
650
+ }
651
+ throw new Error(`Attachment is missing file data: ${attachment.name}`);
652
+ };
653
+ var createDefaultRequestBody = async ({
654
+ model,
655
+ mode,
656
+ content,
657
+ attachments
658
+ }) => {
659
+ const hasAttachments = Boolean(attachments?.length);
660
+ if (!hasAttachments) {
661
+ return {
662
+ model,
663
+ mode,
664
+ stream: true,
665
+ messages: [{ role: "user", content }]
666
+ };
667
+ }
668
+ const imageParts = await Promise.all(
669
+ (attachments ?? []).map(async (attachment) => ({
670
+ type: "image_url",
671
+ image_url: {
672
+ url: await resolveAttachmentDataUrl(attachment)
673
+ }
674
+ }))
675
+ );
676
+ const messageContent = [
677
+ ...content ? [{ type: "text", text: content }] : [],
678
+ ...imageParts
679
+ ];
680
+ return {
681
+ model,
682
+ mode,
683
+ stream: true,
684
+ messages: [{ role: "user", content: messageContent }]
685
+ };
686
+ };
596
687
  var createDefaultChatTransport = ({
597
688
  apiBaseUrl,
598
689
  authToken,
599
690
  toolExecutionPolicy,
600
691
  streamHeaders,
692
+ resolveModels,
693
+ buildRequestBody,
601
694
  transformStreamPacket,
602
695
  endpoints,
603
696
  axiosInstance
@@ -612,12 +705,13 @@ var createDefaultChatTransport = ({
612
705
  ...streamHeaders
613
706
  };
614
707
  return {
615
- getModels: () => getChatModels(client, resolvedEndpoints.models),
708
+ getModels: () => resolveModels?.() ?? getChatModels(client, resolvedEndpoints.models),
616
709
  startStream: async ({
617
710
  sessionId,
618
711
  model,
619
712
  mode,
620
713
  content,
714
+ attachments,
621
715
  signal,
622
716
  onUpdate,
623
717
  onSessionId,
@@ -628,12 +722,20 @@ var createDefaultChatTransport = ({
628
722
  ...createModeDefaultHeaders(mode),
629
723
  ...resolvedStreamHeaders
630
724
  };
725
+ const requestBody = buildRequestBody ? buildRequestBody({
726
+ sessionId,
727
+ model,
728
+ mode,
729
+ content,
730
+ attachments
731
+ }) : await createDefaultRequestBody({ model, mode, content, attachments });
631
732
  await startChatStream({
632
733
  apiBaseUrl,
633
734
  endpointPath: resolvedEndpoints.completions,
634
735
  sessionId,
635
736
  authToken,
636
737
  requestHeaders,
738
+ requestBody,
637
739
  model,
638
740
  mode,
639
741
  content,
@@ -675,6 +777,8 @@ var AiChatProvider = (props) => {
675
777
  });
676
778
  const retryRef = (0, import_react2.useRef)(async () => {
677
779
  });
780
+ const stopRef = (0, import_react2.useRef)(async (_sessionId) => {
781
+ });
678
782
  const defaultApiBaseUrl = "apiBaseUrl" in props ? props.apiBaseUrl : void 0;
679
783
  const defaultAuthToken = "authToken" in props ? props.authToken : void 0;
680
784
  const defaultTransformStreamPacket = "transformStreamPacket" in props ? props.transformStreamPacket : void 0;
@@ -718,6 +822,7 @@ var AiChatProvider = (props) => {
718
822
  labels: { ...DEFAULT_AI_CHAT_LABELS, ...labels },
719
823
  sendRef,
720
824
  retryRef,
825
+ stopRef,
721
826
  renderMessageBlock,
722
827
  handleQuestionnaireSubmit,
723
828
  handleConfirmationSubmit,
@@ -738,6 +843,7 @@ var AiChatProvider = (props) => {
738
843
  renderMessageBlock,
739
844
  sendRef,
740
845
  retryRef,
846
+ stopRef,
741
847
  store,
742
848
  transport
743
849
  ]
@@ -1538,7 +1644,6 @@ var Value = import_styled3.default.span`
1538
1644
  // src/components/chat-thread/components/questionnaire-card.tsx
1539
1645
  var import_react7 = require("react");
1540
1646
  var import_styled4 = __toESM(require("@emotion/styled"));
1541
- var import_compass_ui = require("@xinghunm/compass-ui");
1542
1647
 
1543
1648
  // src/components/chat-thread/components/questionnaire-card-helpers.ts
1544
1649
  var OTHER_OPTION_VALUE = "__other__";
@@ -2339,7 +2444,7 @@ var TextInput = import_styled4.default.input`
2339
2444
  color: rgba(255, 255, 255, 0.34);
2340
2445
  }
2341
2446
  `;
2342
- var InlineOtherInput = (0, import_styled4.default)(import_compass_ui.Input)`
2447
+ var InlineOtherInput = import_styled4.default.input`
2343
2448
  width: 100%;
2344
2449
  margin-top: 0;
2345
2450
 
@@ -3727,19 +3832,20 @@ var ChatThread = () => {
3727
3832
  if (!activeSessionId)
3728
3833
  return;
3729
3834
  clearSessionError(activeSessionId);
3730
- void retryRef.current();
3835
+ void retryRef.current(activeSessionId);
3731
3836
  }, [activeSessionId, clearSessionError, retryRef]);
3732
3837
  const handleQuestionnaireSubmit = (0, import_react11.useCallback)(
3733
3838
  async (submission) => {
3839
+ const sourceSessionId = activeSessionId;
3734
3840
  if (customQuestionnaireSubmit) {
3735
3841
  const handled = await customQuestionnaireSubmit(submission, {
3736
- sessionId: activeSessionId ?? void 0,
3842
+ sessionId: sourceSessionId ?? void 0,
3737
3843
  mode: activeSessionMode
3738
3844
  });
3739
3845
  if (handled !== false) {
3740
- if (activeSessionId && submission.sourceMessageId) {
3846
+ if (sourceSessionId && submission.sourceMessageId) {
3741
3847
  updateQA(
3742
- activeSessionId,
3848
+ sourceSessionId,
3743
3849
  submission.sourceMessageId,
3744
3850
  submission.questionnaireId,
3745
3851
  submission.answers
@@ -3748,10 +3854,13 @@ var ChatThread = () => {
3748
3854
  return;
3749
3855
  }
3750
3856
  }
3751
- await sendRef.current(submission.content);
3752
- if (activeSessionId && submission.sourceMessageId) {
3857
+ await sendRef.current(submission.content, {
3858
+ sessionId: sourceSessionId ?? void 0,
3859
+ includeComposerAttachments: false
3860
+ });
3861
+ if (sourceSessionId && submission.sourceMessageId) {
3753
3862
  updateQA(
3754
- activeSessionId,
3863
+ sourceSessionId,
3755
3864
  submission.sourceMessageId,
3756
3865
  submission.questionnaireId,
3757
3866
  submission.answers
@@ -3762,16 +3871,20 @@ var ChatThread = () => {
3762
3871
  );
3763
3872
  const handleConfirmation = (0, import_react11.useCallback)(
3764
3873
  async (submission) => {
3874
+ const sourceSessionId = activeSessionId;
3765
3875
  if (customConfirmationSubmit) {
3766
3876
  const handled = await customConfirmationSubmit(submission, {
3767
- sessionId: activeSessionId ?? void 0,
3877
+ sessionId: sourceSessionId ?? void 0,
3768
3878
  mode: activeSessionMode
3769
3879
  });
3770
3880
  if (handled !== false) {
3771
3881
  return;
3772
3882
  }
3773
3883
  }
3774
- await sendRef.current(submission.content);
3884
+ await sendRef.current(submission.content, {
3885
+ sessionId: sourceSessionId ?? void 0,
3886
+ includeComposerAttachments: false
3887
+ });
3775
3888
  },
3776
3889
  [activeSessionId, activeSessionMode, sendRef, customConfirmationSubmit]
3777
3890
  );
@@ -3908,25 +4021,6 @@ var import_react15 = require("react");
3908
4021
  var import_styled14 = __toESM(require("@emotion/styled"));
3909
4022
 
3910
4023
  // src/components/chat-composer/lib/chat-composer.ts
3911
- var DRAFT_CHAT_SESSION_ID_PREFIX = "draft-session-";
3912
- var createDraftChatSessionId = () => `${DRAFT_CHAT_SESSION_ID_PREFIX}${Date.now()}`;
3913
- var isDraftChatSessionId = (sessionId) => Boolean(sessionId?.startsWith(DRAFT_CHAT_SESSION_ID_PREFIX));
3914
- var createDraftChatSession = ({
3915
- model,
3916
- mode = DEFAULT_CHAT_AGENT_MODE,
3917
- nowIso: nowIso2,
3918
- createSessionId
3919
- }) => {
3920
- const iso = nowIso2();
3921
- return {
3922
- sessionId: createSessionId(),
3923
- title: "New Chat",
3924
- createdAt: iso,
3925
- updatedAt: iso,
3926
- model,
3927
- mode
3928
- };
3929
- };
3930
4024
  var createUserMessage = ({
3931
4025
  sessionId,
3932
4026
  content,
@@ -4087,7 +4181,8 @@ var useComposerAttachments = () => {
4087
4181
  return [];
4088
4182
  }
4089
4183
  const nextMessageAttachments = currentAttachments.map(({ file: _file, ...attachment }) => ({
4090
- ...attachment
4184
+ ...attachment,
4185
+ file: _file
4091
4186
  }));
4092
4187
  attachmentsRef.current = [];
4093
4188
  setAttachments([]);
@@ -4126,19 +4221,20 @@ var normalizeChatErrorMessage = (message, labels) => {
4126
4221
  return trimmedMessage;
4127
4222
  };
4128
4223
  var useChatComposer = () => {
4129
- const { transport, enableImageAttachments, labels } = useChatContext();
4224
+ const { transport, enableImageAttachments, labels, store } = useChatContext();
4130
4225
  const activeSessionId = useChatStore((s) => s.activeSessionId);
4131
4226
  const activeSession = useChatStore(
4132
4227
  (s) => s.sessions.find((x) => x.sessionId === s.activeSessionId) ?? null
4133
4228
  );
4134
4229
  const preferredMode = useChatStore((s) => s.preferredMode);
4135
4230
  const streamingSessionId = useChatStore(
4136
- (s) => Object.entries(s.isStreamingBySession).find(([, v]) => v)?.[0] ?? null
4231
+ (s) => s.activeSessionId && s.isStreamingBySession[s.activeSessionId] ? s.activeSessionId : null
4137
4232
  );
4138
4233
  const isStreaming = Boolean(streamingSessionId);
4139
- const isStopping = useChatStore(
4140
- (s) => streamingSessionId ? s.isStoppingBySession[streamingSessionId] ?? false : false
4141
- );
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
+ });
4142
4238
  const createSession = useChatStore((s) => s.createSession);
4143
4239
  const replaceSessionId = useChatStore((s) => s.replaceSessionId);
4144
4240
  const appendMessage = useChatStore((s) => s.appendMessage);
@@ -4175,9 +4271,9 @@ var useChatComposer = () => {
4175
4271
  const [selectedMode, setSelectedModeLocal] = (0, import_react13.useState)(DEFAULT_CHAT_AGENT_MODE);
4176
4272
  const [attachmentNotice, setAttachmentNotice] = (0, import_react13.useState)(null);
4177
4273
  const { attachments, appendFiles, removeAttachment, takeMessageAttachments } = useComposerAttachments();
4178
- const abortControllerRef = (0, import_react13.useRef)(null);
4179
- const stopRequestRef = (0, import_react13.useRef)(null);
4180
- 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());
4181
4277
  (0, import_react13.useEffect)(() => {
4182
4278
  setSelectedModel(
4183
4279
  (current) => resolveSelectedChatModel({ currentModel: current, availableModels, isModelsLoading })
@@ -4200,33 +4296,47 @@ var useChatComposer = () => {
4200
4296
  return () => window.clearTimeout(timeoutId);
4201
4297
  }, [attachmentNotice]);
4202
4298
  const clearStopTimeout = (sessionId) => {
4203
- if (!stopRequestRef.current)
4299
+ const stopRequest = stopRequestBySessionRef.current.get(sessionId);
4300
+ if (!stopRequest || stopRequest.timeoutId === null) {
4204
4301
  return;
4205
- if (sessionId && stopRequestRef.current.sessionId !== sessionId)
4206
- return;
4207
- if (stopRequestRef.current.timeoutId !== null) {
4208
- window.clearTimeout(stopRequestRef.current.timeoutId);
4209
- stopRequestRef.current.timeoutId = null;
4210
4302
  }
4303
+ window.clearTimeout(stopRequest.timeoutId);
4304
+ stopRequest.timeoutId = null;
4211
4305
  };
4212
4306
  const clearStopRequest = (0, import_react13.useCallback)((sessionId) => {
4213
- if (!stopRequestRef.current)
4214
- return;
4215
- if (sessionId && stopRequestRef.current.sessionId !== sessionId)
4216
- return;
4217
4307
  clearStopTimeout(sessionId);
4218
- stopRequestRef.current = null;
4308
+ stopRequestBySessionRef.current.delete(sessionId);
4219
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
+ );
4220
4328
  const finalizeStop = (0, import_react13.useCallback)(
4221
4329
  (sessionId) => {
4222
- if (stopRequestRef.current?.sessionId === sessionId) {
4223
- if (stopRequestRef.current.finalized)
4330
+ const stopRequest = stopRequestBySessionRef.current.get(sessionId);
4331
+ if (stopRequest) {
4332
+ if (stopRequest.finalized) {
4224
4333
  return;
4225
- stopRequestRef.current.finalized = true;
4334
+ }
4335
+ stopRequest.finalized = true;
4226
4336
  }
4227
4337
  clearStopTimeout(sessionId);
4228
- abortControllerRef.current?.abort();
4229
- abortControllerRef.current = null;
4338
+ abortControllerBySessionRef.current.get(sessionId)?.abort();
4339
+ abortControllerBySessionRef.current.delete(sessionId);
4230
4340
  finalizeStoppedStreamingMessage(sessionId);
4231
4341
  clearStopRequest(sessionId);
4232
4342
  },
@@ -4237,10 +4347,11 @@ var useChatComposer = () => {
4237
4347
  localSessionId,
4238
4348
  sessionId,
4239
4349
  content,
4350
+ attachments: attachments2,
4240
4351
  model,
4241
4352
  mode
4242
4353
  }) => {
4243
- clearStopRequest();
4354
+ clearStopRequest(localSessionId);
4244
4355
  let currentSessionId = localSessionId;
4245
4356
  clearSessionError(currentSessionId);
4246
4357
  const assistantMessage = createAssistantStreamingMessage({
@@ -4249,29 +4360,44 @@ var useChatComposer = () => {
4249
4360
  createMessageId: () => `assistant-${Date.now()}`
4250
4361
  });
4251
4362
  startStreamingMessage(currentSessionId, assistantMessage);
4252
- abortControllerRef.current?.abort();
4253
- abortControllerRef.current = new AbortController();
4254
- lastRequestRef.current = { localSessionId, sessionId, content, 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
+ });
4255
4374
  let accumulated = "";
4375
+ let streamSettled = false;
4256
4376
  try {
4257
4377
  await transport.startStream({
4258
4378
  sessionId,
4259
4379
  model,
4260
4380
  mode,
4261
4381
  content,
4262
- signal: abortControllerRef.current.signal,
4382
+ attachments: attachments2,
4383
+ signal: abortController.signal,
4263
4384
  onSessionId: (nextSessionId) => {
4264
4385
  if (!nextSessionId || nextSessionId === currentSessionId)
4265
4386
  return;
4266
- replaceSessionId(currentSessionId, nextSessionId);
4387
+ const previousSessionId = currentSessionId;
4388
+ replaceSessionId(previousSessionId, nextSessionId);
4389
+ moveSessionRuntimeState(previousSessionId, nextSessionId);
4267
4390
  currentSessionId = nextSessionId;
4268
- lastRequestRef.current = {
4391
+ const nextRequest = {
4269
4392
  localSessionId: nextSessionId,
4270
4393
  sessionId: nextSessionId,
4271
4394
  content,
4395
+ attachments: attachments2,
4272
4396
  model,
4273
4397
  mode
4274
4398
  };
4399
+ lastRequestBySessionRef.current.delete(previousSessionId);
4400
+ lastRequestBySessionRef.current.set(nextSessionId, nextRequest);
4275
4401
  },
4276
4402
  onUpdate: (update) => {
4277
4403
  accumulated = resolveAccumulatedContent(accumulated, update);
@@ -4281,16 +4407,18 @@ var useChatComposer = () => {
4281
4407
  });
4282
4408
  },
4283
4409
  onDone: () => {
4284
- if (stopRequestRef.current?.sessionId === currentSessionId) {
4410
+ streamSettled = true;
4411
+ if (stopRequestBySessionRef.current.has(currentSessionId)) {
4285
4412
  finalizeStop(currentSessionId);
4286
4413
  return;
4287
4414
  }
4288
4415
  completeStreamingMessage(currentSessionId);
4289
- abortControllerRef.current = null;
4416
+ abortControllerBySessionRef.current.delete(currentSessionId);
4290
4417
  clearStopRequest(currentSessionId);
4291
4418
  },
4292
4419
  onError: (streamError) => {
4293
- if (stopRequestRef.current?.sessionId === currentSessionId) {
4420
+ streamSettled = true;
4421
+ if (stopRequestBySessionRef.current.has(currentSessionId)) {
4294
4422
  finalizeStop(currentSessionId);
4295
4423
  return;
4296
4424
  }
@@ -4299,12 +4427,24 @@ var useChatComposer = () => {
4299
4427
  currentSessionId,
4300
4428
  normalizeChatErrorMessage(streamError.message, labels)
4301
4429
  );
4302
- abortControllerRef.current = null;
4430
+ abortControllerBySessionRef.current.delete(currentSessionId);
4303
4431
  clearStopRequest(currentSessionId);
4304
4432
  }
4305
4433
  });
4306
- } catch {
4307
- 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);
4308
4448
  }
4309
4449
  },
4310
4450
  [
@@ -4313,61 +4453,68 @@ var useChatComposer = () => {
4313
4453
  clearStopRequest,
4314
4454
  finalizeStop,
4315
4455
  labels,
4456
+ moveSessionRuntimeState,
4316
4457
  startStreamingMessage,
4317
4458
  replaceSessionId,
4318
4459
  patchStreamingMessage,
4319
4460
  completeStreamingMessage,
4320
4461
  finalizeStoppedStreamingMessage,
4321
- setSessionError
4462
+ setSessionError,
4463
+ store
4322
4464
  ]
4323
4465
  );
4324
4466
  const send = (0, import_react13.useCallback)(
4325
- async (contentOverride) => {
4467
+ async (contentOverride, options) => {
4326
4468
  const content = (contentOverride ?? value).trim();
4327
- const hasText = Boolean(content);
4328
- const hasAttachments = attachments.length > 0;
4469
+ const includeComposerAttachments = options?.includeComposerAttachments ?? true;
4470
+ const composerAttachmentCount = includeComposerAttachments ? attachments.length : 0;
4329
4471
  if (!canSendChatMessage({
4330
4472
  value: content,
4331
- attachmentCount: attachments.length,
4473
+ attachmentCount: composerAttachmentCount,
4332
4474
  isModelsLoading,
4333
4475
  isModelsError,
4334
4476
  hasModels
4335
4477
  })) {
4336
4478
  return;
4337
4479
  }
4338
- if (hasText && !(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)) {
4339
4485
  return;
4340
4486
  }
4341
- const resolvedModel = selectedModel || activeSession?.model || availableModels[0]?.id || "local-image";
4487
+ const resolvedModel = selectedModel || currentActiveSession?.model || availableModels[0]?.id || "local-image";
4342
4488
  const { localSessionId, sessionId, session } = resolveSendSession({
4343
- activeSessionId,
4489
+ activeSessionId: currentActiveSessionId,
4344
4490
  selectedModel: resolvedModel,
4345
- selectedMode,
4491
+ selectedMode: currentMode,
4346
4492
  nowIso,
4347
4493
  createSessionId: createDraftChatSessionId
4348
4494
  });
4349
4495
  if (session)
4350
4496
  createSession(session);
4351
- const messageAttachments = takeMessageAttachments();
4497
+ const messageAttachments = includeComposerAttachments ? takeMessageAttachments() : void 0;
4352
4498
  const userMessage = createUserMessage({
4353
4499
  sessionId: localSessionId,
4354
4500
  content,
4355
4501
  attachments: messageAttachments,
4356
- localOnly: hasAttachments,
4502
+ localOnly: false,
4357
4503
  createdAt: nowIso(),
4358
4504
  createMessageId: () => `user-${Date.now()}`
4359
4505
  });
4360
4506
  appendMessage(localSessionId, userMessage);
4361
- setAttachmentNotice(null);
4362
- setValue("");
4363
- if (!hasText)
4364
- return;
4507
+ if (includeComposerAttachments) {
4508
+ setAttachmentNotice(null);
4509
+ setValue("");
4510
+ }
4365
4511
  await runStream({
4366
4512
  localSessionId,
4367
4513
  sessionId,
4368
4514
  content,
4515
+ attachments: messageAttachments,
4369
4516
  model: resolvedModel,
4370
- mode: selectedMode
4517
+ mode: currentMode
4371
4518
  });
4372
4519
  },
4373
4520
  [
@@ -4377,16 +4524,47 @@ var useChatComposer = () => {
4377
4524
  isModelsError,
4378
4525
  hasModels,
4379
4526
  selectedModel,
4380
- activeSession,
4381
4527
  availableModels,
4382
- activeSessionId,
4383
4528
  selectedMode,
4384
4529
  createSession,
4385
4530
  takeMessageAttachments,
4386
4531
  appendMessage,
4387
- runStream
4532
+ runStream,
4533
+ store
4388
4534
  ]
4389
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
+ );
4390
4568
  return {
4391
4569
  state: {
4392
4570
  value,
@@ -4426,43 +4604,27 @@ var useChatComposer = () => {
4426
4604
  setPreferredMode(mode);
4427
4605
  if (activeSessionId)
4428
4606
  setSessionMode(activeSessionId, mode);
4429
- if (lastRequestRef.current && activeSessionId && (lastRequestRef.current.localSessionId === activeSessionId || lastRequestRef.current.sessionId === activeSessionId)) {
4430
- 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
+ }
4431
4612
  }
4432
4613
  },
4433
4614
  reloadModels: () => void fetchModels(),
4615
+ stopSession,
4434
4616
  stop: async () => {
4435
4617
  if (!streamingSessionId)
4436
4618
  return;
4437
- if (isStopping)
4438
- return;
4439
- if (isDraftChatSessionId(streamingSessionId)) {
4440
- finalizeStop(streamingSessionId);
4441
- return;
4442
- }
4443
- requestStopStreaming(streamingSessionId);
4444
- stopRequestRef.current = {
4445
- sessionId: streamingSessionId,
4446
- timeoutId: window.setTimeout(() => {
4447
- finalizeStop(streamingSessionId);
4448
- }, STOP_WAIT_TIMEOUT_MS),
4449
- finalized: false
4450
- };
4451
- try {
4452
- const result = await transport.terminateStream(streamingSessionId);
4453
- if (!result.terminated) {
4454
- console.error("Failed to terminate chat session: server returned not terminated");
4455
- }
4456
- finalizeStop(streamingSessionId);
4457
- } catch (err) {
4458
- console.error("Failed to terminate chat session", err);
4459
- finalizeStop(streamingSessionId);
4460
- }
4619
+ await stopSession(streamingSessionId);
4461
4620
  },
4462
- retry: () => {
4463
- if (!lastRequestRef.current)
4621
+ retry: (sessionId) => {
4622
+ if (!sessionId)
4623
+ return;
4624
+ const request = lastRequestBySessionRef.current.get(sessionId);
4625
+ if (!request)
4464
4626
  return;
4465
- void runStream(lastRequestRef.current);
4627
+ void runStream(request);
4466
4628
  }
4467
4629
  }
4468
4630
  };
@@ -4627,7 +4789,7 @@ var CloseGlyph = import_styled10.default.span`
4627
4789
 
4628
4790
  // src/components/chat-composer/components/chat-model-control.tsx
4629
4791
  var import_styled11 = __toESM(require("@emotion/styled"));
4630
- var import_compass_ui2 = require("@xinghunm/compass-ui");
4792
+ var import_compass_ui = require("@xinghunm/compass-ui");
4631
4793
  var import_jsx_runtime12 = require("@emotion/react/jsx-runtime");
4632
4794
  var ChatModelControl = ({
4633
4795
  selectedModel,
@@ -4728,7 +4890,7 @@ var ModelReloadButton = import_styled11.default.button`
4728
4890
  var ReloadIcon = import_styled11.default.svg`
4729
4891
  flex-shrink: 0;
4730
4892
  `;
4731
- var ModelSelect = (0, import_styled11.default)(import_compass_ui2.Select)`
4893
+ var ModelSelect = (0, import_styled11.default)(import_compass_ui.Select)`
4732
4894
  && {
4733
4895
  width: auto;
4734
4896
  min-width: 0;
@@ -4749,7 +4911,7 @@ var ModelSelect = (0, import_styled11.default)(import_compass_ui2.Select)`
4749
4911
 
4750
4912
  // src/components/chat-composer/components/chat-mode-control.tsx
4751
4913
  var import_styled12 = __toESM(require("@emotion/styled"));
4752
- var import_compass_ui3 = require("@xinghunm/compass-ui");
4914
+ var import_compass_ui2 = require("@xinghunm/compass-ui");
4753
4915
  var import_jsx_runtime13 = require("@emotion/react/jsx-runtime");
4754
4916
  var ChatModeControl = ({
4755
4917
  value,
@@ -4772,7 +4934,7 @@ var ChatModeControl = ({
4772
4934
  }
4773
4935
  );
4774
4936
  };
4775
- var ModeSelect = (0, import_styled12.default)(import_compass_ui3.Select)`
4937
+ var ModeSelect = (0, import_styled12.default)(import_compass_ui2.Select)`
4776
4938
  && {
4777
4939
  flex: 0 1 auto;
4778
4940
  width: auto;
@@ -4794,7 +4956,7 @@ var ModeSelect = (0, import_styled12.default)(import_compass_ui3.Select)`
4794
4956
 
4795
4957
  // src/components/chat-composer/components/chat-send-actions.tsx
4796
4958
  var import_styled13 = __toESM(require("@emotion/styled"));
4797
- var import_compass_ui4 = require("@xinghunm/compass-ui");
4959
+ var import_compass_ui3 = require("@xinghunm/compass-ui");
4798
4960
  var import_jsx_runtime14 = require("@emotion/react/jsx-runtime");
4799
4961
  var ArrowUpIcon = () => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
4800
4962
  "svg",
@@ -4848,7 +5010,7 @@ var ChatSendActions = ({
4848
5010
  onClick: () => void onSend()
4849
5011
  }
4850
5012
  ) });
4851
- var PrimaryButton = (0, import_styled13.default)(import_compass_ui4.Button)`
5013
+ var PrimaryButton = (0, import_styled13.default)(import_compass_ui3.Button)`
4852
5014
  && {
4853
5015
  min-width: 24px;
4854
5016
  width: 24px;
@@ -4874,7 +5036,7 @@ var PrimaryButton = (0, import_styled13.default)(import_compass_ui4.Button)`
4874
5036
  }
4875
5037
  }
4876
5038
  `;
4877
- var StopButton = (0, import_styled13.default)(import_compass_ui4.Button)`
5039
+ var StopButton = (0, import_styled13.default)(import_compass_ui3.Button)`
4878
5040
  && {
4879
5041
  min-width: 24px;
4880
5042
  width: 24px;
@@ -5109,7 +5271,7 @@ var ChatComposerView = ({
5109
5271
  }
5110
5272
  ) : null,
5111
5273
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
5112
- Input2,
5274
+ Input,
5113
5275
  {
5114
5276
  ref: inputRef,
5115
5277
  "data-testid": "chat-composer-input",
@@ -5170,15 +5332,16 @@ var ChatComposerView = ({
5170
5332
  ] }) });
5171
5333
  };
5172
5334
  var ChatComposer = () => {
5173
- const { labels, sendRef, retryRef, enableImageAttachments } = useChatContext();
5335
+ const { labels, sendRef, retryRef, stopRef, enableImageAttachments } = useChatContext();
5174
5336
  const { state, actions } = useChatComposer();
5175
5337
  const { send, retry } = actions;
5176
5338
  (0, import_react15.useEffect)(() => {
5177
5339
  sendRef.current = send;
5178
- retryRef.current = async () => {
5179
- retry();
5340
+ retryRef.current = async (sessionId) => {
5341
+ retry(sessionId);
5180
5342
  };
5181
- }, [retry, retryRef, send, sendRef]);
5343
+ stopRef.current = actions.stopSession;
5344
+ }, [actions.stopSession, retry, retryRef, send, sendRef, stopRef]);
5182
5345
  const modeLabels = {
5183
5346
  ask: labels.modeLabelAsk,
5184
5347
  plan: labels.modeLabelPlan,
@@ -5253,7 +5416,7 @@ var InputArea = import_styled14.default.div`
5253
5416
  grid-area: input;
5254
5417
  position: relative;
5255
5418
  `;
5256
- var Input2 = import_styled14.default.textarea`
5419
+ var Input = import_styled14.default.textarea`
5257
5420
  --textarea-line-height: ${CHAT_COMPOSER_LINE_HEIGHT_PX}px;
5258
5421
  --textarea-min-rows: ${CHAT_COMPOSER_MIN_ROWS};
5259
5422
  --textarea-max-rows: ${CHAT_COMPOSER_MAX_ROWS};
@@ -5324,9 +5487,9 @@ var ComposerExpandButton = import_styled14.default.button`
5324
5487
  var Footer = import_styled14.default.div`
5325
5488
  grid-area: footer;
5326
5489
  display: grid;
5327
- grid-template-columns: minmax(0, 1fr) auto;
5490
+ grid-template-columns: auto minmax(0, 1fr);
5328
5491
  align-items: flex-end;
5329
- gap: 16px;
5492
+ gap: 8px;
5330
5493
  padding: 0 14px 14px;
5331
5494
  `;
5332
5495
  var LeadingActions = import_styled14.default.div`
@@ -5341,6 +5504,9 @@ var TrailingActions = import_styled14.default.div`
5341
5504
  align-items: center;
5342
5505
  flex-wrap: wrap;
5343
5506
  min-width: 0;
5507
+ width: fit-content;
5508
+ max-width: 100%;
5509
+ justify-self: end;
5344
5510
  justify-content: flex-end;
5345
5511
  gap: 8px;
5346
5512
  `;
@@ -5427,28 +5593,17 @@ var ChatConversationList = () => {
5427
5593
  const { labels } = useChatContext();
5428
5594
  const sessions = useChatStore((s) => s.sessions);
5429
5595
  const activeSessionId = useChatStore((s) => s.activeSessionId);
5430
- const preferredMode = useChatStore((s) => s.preferredMode);
5431
- const createSession = useChatStore((s) => s.createSession);
5596
+ const startNewChat = useChatStore((s) => s.startNewChat);
5432
5597
  const setActiveSession = useChatStore((s) => s.setActiveSession);
5433
5598
  const modeLabels = {
5434
5599
  ask: labels.modeLabelAsk,
5435
5600
  plan: labels.modeLabelPlan,
5436
5601
  agent: labels.modeLabelAgent
5437
5602
  };
5438
- const handleCreateSession = () => {
5439
- const session = createDraftChatSession({
5440
- // Model is intentionally deferred: ChatComposer resolves selectedModel at send time.
5441
- model: "",
5442
- mode: preferredMode,
5443
- nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
5444
- createSessionId: createDraftChatSessionId
5445
- });
5446
- createSession(session);
5447
- };
5448
5603
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(Container3, { children: [
5449
5604
  /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(Toolbar, { children: [
5450
5605
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Title3, { children: "Sessions" }),
5451
- /* @__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 })
5452
5607
  ] }),
5453
5608
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(List2, { "data-testid": "chat-session-list", children: sessions.map((session) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5454
5609
  ChatSessionItem,
@@ -5500,8 +5655,70 @@ var List2 = import_styled16.default.div`
5500
5655
 
5501
5656
  // src/components/ai-chat/index.tsx
5502
5657
  var import_jsx_runtime18 = require("@emotion/react/jsx-runtime");
5503
- var AiChat = ({ showConversationList = false, ...providerProps }) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5504
- 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,
5505
5722
  {
5506
5723
  theme: {
5507
5724
  token: {
@@ -5538,6 +5755,7 @@ var AiChat = ({ showConversationList = false, ...providerProps }) => /* @__PURE_
5538
5755
  children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(AiChatProvider, { ...providerProps, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(Root, { "data-testid": "ai-chat", children: [
5539
5756
  showConversationList ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ChatConversationList, {}) : null,
5540
5757
  /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(Workspace, { children: [
5758
+ showNewChatButton && !showConversationList ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(QuickActions, { renderNewChatTrigger }) : null,
5541
5759
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ChatThread, {}),
5542
5760
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ChatComposer, {})
5543
5761
  ] })
@@ -5555,9 +5773,28 @@ var Workspace = import_styled17.default.section`
5555
5773
  flex: 1;
5556
5774
  display: flex;
5557
5775
  flex-direction: column;
5776
+ gap: 12px;
5558
5777
  min-height: 0;
5559
5778
  overflow: hidden;
5560
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
+ `;
5561
5798
  // Annotate the CommonJS export names for ESM import in node:
5562
5799
  0 && (module.exports = {
5563
5800
  AiChat,