@agent-native/toolkit 0.22.0 → 0.22.1-nightly-20260926172134

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.
Files changed (39) hide show
  1. package/dist/composer/ComposerPlusMenu.d.ts +2 -1
  2. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
  3. package/dist/composer/ComposerPlusMenu.js +24 -18
  4. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  5. package/dist/composer/PromptComposer.context.spec.js +40 -25
  6. package/dist/composer/PromptComposer.context.spec.js.map +1 -1
  7. package/dist/composer/PromptComposer.d.ts +15 -7
  8. package/dist/composer/PromptComposer.d.ts.map +1 -1
  9. package/dist/composer/PromptComposer.js +59 -30
  10. package/dist/composer/PromptComposer.js.map +1 -1
  11. package/dist/composer/PromptComposer.spec.js +29 -18
  12. package/dist/composer/PromptComposer.spec.js.map +1 -1
  13. package/dist/composer/TiptapComposer.d.ts +5 -2
  14. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  15. package/dist/composer/TiptapComposer.js +35 -5
  16. package/dist/composer/TiptapComposer.js.map +1 -1
  17. package/dist/composer/TiptapComposer.spec.js +96 -0
  18. package/dist/composer/TiptapComposer.spec.js.map +1 -1
  19. package/dist/composer/index.d.ts +1 -1
  20. package/dist/composer/index.d.ts.map +1 -1
  21. package/dist/composer/index.js +1 -1
  22. package/dist/composer/index.js.map +1 -1
  23. package/dist/composer/runtime-adapters.d.ts +3 -2
  24. package/dist/composer/runtime-adapters.d.ts.map +1 -1
  25. package/dist/composer/runtime-adapters.js +4 -1
  26. package/dist/composer/runtime-adapters.js.map +1 -1
  27. package/dist/icons/ResourceIconPicker.d.ts.map +1 -1
  28. package/dist/icons/ResourceIconPicker.js +6 -1
  29. package/dist/icons/ResourceIconPicker.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/composer/ComposerPlusMenu.tsx +25 -14
  32. package/src/composer/PromptComposer.context.spec.tsx +60 -34
  33. package/src/composer/PromptComposer.spec.ts +39 -20
  34. package/src/composer/PromptComposer.tsx +106 -46
  35. package/src/composer/TiptapComposer.spec.ts +130 -0
  36. package/src/composer/TiptapComposer.tsx +61 -26
  37. package/src/composer/index.ts +1 -0
  38. package/src/composer/runtime-adapters.tsx +13 -4
  39. package/src/icons/ResourceIconPicker.tsx +13 -2
@@ -45,6 +45,7 @@ export interface ComposerTerminalModeControl {
45
45
  interface ComposerPlusMenuProps {
46
46
  onSelectMode?: (mode: ComposerMode) => void;
47
47
  addAttachment?: (file: File) => Promise<unknown>;
48
+ attachmentsEnabled?: boolean;
48
49
  onAttachmentError?: (message: string) => void;
49
50
  attachmentAccept?: string;
50
51
  /**
@@ -309,6 +310,7 @@ function UploadOnlyAttachButton({
309
310
  export function ComposerPlusMenu({
310
311
  onSelectMode,
311
312
  addAttachment,
313
+ attachmentsEnabled = true,
312
314
  onAttachmentError,
313
315
  attachmentAccept,
314
316
  extensionTools = false,
@@ -316,6 +318,7 @@ export function ComposerPlusMenu({
316
318
  terminalModeControl,
317
319
  }: ComposerPlusMenuProps) {
318
320
  if (mode === "upload-only") {
321
+ if (!attachmentsEnabled) return null;
319
322
  return (
320
323
  <UploadOnlyAttachButton
321
324
  addAttachment={addAttachment}
@@ -333,6 +336,7 @@ export function ComposerPlusMenu({
333
336
  <ComposerPlusMenuFull
334
337
  onSelectMode={onSelectMode}
335
338
  addAttachment={addAttachment}
339
+ attachmentsEnabled={attachmentsEnabled}
336
340
  onAttachmentError={onAttachmentError}
337
341
  attachmentAccept={attachmentAccept}
338
342
  extensionTools={extensionTools}
@@ -410,12 +414,14 @@ function ComposerPlusMenuTerminal({
410
414
  function ComposerPlusMenuFull({
411
415
  onSelectMode,
412
416
  addAttachment,
417
+ attachmentsEnabled,
413
418
  onAttachmentError,
414
419
  attachmentAccept,
415
420
  extensionTools,
416
421
  }: Pick<
417
422
  ComposerPlusMenuProps,
418
423
  | "addAttachment"
424
+ | "attachmentsEnabled"
419
425
  | "onSelectMode"
420
426
  | "onAttachmentError"
421
427
  | "attachmentAccept"
@@ -510,7 +516,7 @@ function ComposerPlusMenuFull({
510
516
  };
511
517
 
512
518
  const handleFilesSelected = async (files: FileList | null) => {
513
- if (!files || files.length === 0) return;
519
+ if (!attachmentsEnabled || !files || files.length === 0) return;
514
520
  try {
515
521
  await Promise.all(
516
522
  Array.from(files).map((file) =>
@@ -589,19 +595,23 @@ function ComposerPlusMenuFull({
589
595
  hoverAction?: () => void;
590
596
  isSkill?: boolean;
591
597
  }[] = [
592
- {
593
- icon: <IconUpload className="h-3.5 w-3.5" />,
594
- label: t("agentChat.composer.menu.uploadFile", {
595
- defaultValue: "Upload File",
596
- }),
597
- desc: t("agentChat.composer.menu.uploadFileDescription", {
598
- defaultValue: "Images, PDFs, text/code, JSON, CSV",
599
- }),
600
- action: () => {
601
- setOpen(false);
602
- setTimeout(() => fileUploadRef.current?.click(), 0);
603
- },
604
- },
598
+ ...(attachmentsEnabled
599
+ ? [
600
+ {
601
+ icon: <IconUpload className="h-3.5 w-3.5" />,
602
+ label: t("agentChat.composer.menu.uploadFile", {
603
+ defaultValue: "Upload File",
604
+ }),
605
+ desc: t("agentChat.composer.menu.uploadFileDescription", {
606
+ defaultValue: "Images, PDFs, text/code, JSON, CSV",
607
+ }),
608
+ action: () => {
609
+ setOpen(false);
610
+ setTimeout(() => fileUploadRef.current?.click(), 0);
611
+ },
612
+ },
613
+ ]
614
+ : []),
605
615
  {
606
616
  icon: <IconPhotoPlus className="h-3.5 w-3.5" />,
607
617
  label: t("agentChat.composer.menu.generateImage", {
@@ -698,6 +708,7 @@ function ComposerPlusMenuFull({
698
708
  multiple
699
709
  accept={attachmentAccept}
700
710
  className="hidden"
711
+ disabled={!attachmentsEnabled}
701
712
  onChange={(event) => {
702
713
  void handleFilesSelected(event.target.files);
703
714
  event.target.value = "";
@@ -94,7 +94,7 @@ describe("controlled composer context", () => {
94
94
  }
95
95
 
96
96
  it.each(["host", "provider"] as const)(
97
- "%s submission gating allows staging and blocks every send path until ready",
97
+ "%s submission gating prevents sends until ready",
98
98
  async (gate) => {
99
99
  const composerRef = React.createRef<TiptapComposerHandle>();
100
100
  const onSubmit = vi.fn();
@@ -152,6 +152,29 @@ describe("controlled composer context", () => {
152
152
  });
153
153
  };
154
154
  await render();
155
+ if (gate === "provider") {
156
+ expect(
157
+ container.querySelector('[data-testid="provider-setup"]'),
158
+ ).not.toBeNull();
159
+ expect(container.querySelector('[contenteditable="true"]')).toBeNull();
160
+ expect(
161
+ container.querySelector('button[aria-label="Add context"]'),
162
+ ).toBeNull();
163
+ expect(
164
+ container.querySelector<HTMLButtonElement>(
165
+ 'button[aria-label="Send message"]',
166
+ )?.disabled,
167
+ ).toBe(true);
168
+ await act(async () =>
169
+ expect(
170
+ await composerRef.current!.submitWithText("Blocked prompt"),
171
+ ).toBe(false),
172
+ );
173
+ expect(onSubmit).not.toHaveBeenCalled();
174
+ expect(onDisabledClick).not.toHaveBeenCalled();
175
+ blocked = false;
176
+ await render();
177
+ }
155
178
  const editor = container.querySelector<HTMLElement>(
156
179
  '[contenteditable="true"]',
157
180
  )!;
@@ -196,33 +219,37 @@ describe("controlled composer context", () => {
196
219
  const send = container.querySelector<HTMLButtonElement>(
197
220
  'button[aria-label="Send message"]',
198
221
  )!;
199
- expect(send.disabled).toBe(true);
200
- await act(async () => {
201
- send.click();
202
- editor.dispatchEvent(
203
- new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
204
- );
205
- editor.dispatchEvent(
206
- new KeyboardEvent("keydown", {
207
- key: "Enter",
208
- metaKey: true,
209
- bubbles: true,
210
- }),
211
- );
212
- editor.dispatchEvent(
213
- new KeyboardEvent("keydown", {
214
- key: "Enter",
215
- ctrlKey: true,
216
- bubbles: true,
217
- }),
218
- );
219
- expect(await composerRef.current!.submitWithText("Quick start")).toBe(
220
- false,
221
- );
222
- });
223
- expect(onSubmit).not.toHaveBeenCalled();
224
- expect(editor.textContent).toBe("Staged draft");
225
- expect(files).toEqual([file]);
222
+ if (gate === "host") {
223
+ expect(send.disabled).toBe(true);
224
+ await act(async () => {
225
+ send.click();
226
+ editor.dispatchEvent(
227
+ new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
228
+ );
229
+ editor.dispatchEvent(
230
+ new KeyboardEvent("keydown", {
231
+ key: "Enter",
232
+ metaKey: true,
233
+ bubbles: true,
234
+ }),
235
+ );
236
+ editor.dispatchEvent(
237
+ new KeyboardEvent("keydown", {
238
+ key: "Enter",
239
+ ctrlKey: true,
240
+ bubbles: true,
241
+ }),
242
+ );
243
+ expect(await composerRef.current!.submitWithText("Quick start")).toBe(
244
+ false,
245
+ );
246
+ });
247
+ expect(onSubmit).not.toHaveBeenCalled();
248
+ expect(editor.textContent).toBe("Staged draft");
249
+ expect(files).toEqual([file]);
250
+ } else {
251
+ expect(send.disabled).toBe(false);
252
+ }
226
253
  blocked = false;
227
254
  await render();
228
255
  await act(async () => {
@@ -378,10 +405,9 @@ describe("controlled composer context", () => {
378
405
  ).toBe("Keep the editable draft");
379
406
  });
380
407
 
381
- it("a failed provider preflight leaves the draft untouched and returns false", async () => {
408
+ it("keeps chat locked while provider status is unresolved", async () => {
382
409
  const composerRef = React.createRef<TiptapComposerHandle>();
383
410
  const onSubmit = vi.fn();
384
- const preflight = vi.fn().mockResolvedValue("missing");
385
411
  await act(async () =>
386
412
  root.render(
387
413
  <ComposerRuntimeAdaptersProvider
@@ -391,7 +417,6 @@ describe("controlled composer context", () => {
391
417
  state: "unknown",
392
418
  missing: false,
393
419
  }),
394
- fetchAgentEngineConfiguredState: preflight,
395
420
  },
396
421
  }}
397
422
  >
@@ -411,11 +436,12 @@ describe("controlled composer context", () => {
411
436
  false,
412
437
  ),
413
438
  );
414
- expect(preflight).toHaveBeenCalledOnce();
415
439
  expect(onSubmit).not.toHaveBeenCalled();
440
+ expect(container.querySelector('[contenteditable="true"]')).toBeNull();
441
+ expect(container.querySelector('[role="status"]')).not.toBeNull();
416
442
  expect(
417
- container.querySelector('[contenteditable="true"]')?.textContent,
418
- ).toBe("Keep my draft");
443
+ container.querySelector('[contenteditable="false"]')?.textContent,
444
+ ).toContain("Keep my draft");
419
445
  });
420
446
 
421
447
  it.each(["click", "enter"])(
@@ -7,7 +7,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
7
  import {
8
8
  buildPromptComposerSubmission,
9
9
  PromptComposer,
10
- shouldGateComposerForMissingEngine,
10
+ resolveComposerModelStatusChecksEnabled,
11
+ shouldGateComposerForEngine,
12
+ shouldCheckModelStatus,
11
13
  type PromptComposerFile,
12
14
  } from "./PromptComposer.js";
13
15
 
@@ -25,35 +27,52 @@ afterEach(() => {
25
27
  container.remove();
26
28
  });
27
29
 
28
- describe("shouldGateComposerForMissingEngine", () => {
29
- it("never disables the composer while the status check is unresolved", () => {
30
- for (const state of ["unknown", "unavailable"]) {
31
- expect(
32
- shouldGateComposerForMissingEngine({ state, hasSetupComponent: true }),
33
- ).toBe(false);
30
+ describe("shouldGateComposerForEngine", () => {
31
+ it("blocks typing until provider status confirms the engine is configured", () => {
32
+ for (const state of ["unknown", "unavailable", "missing"] as const) {
33
+ expect(shouldGateComposerForEngine(state)).toBe(true);
34
34
  }
35
35
  });
36
36
 
37
- it("gates only when a connect affordance can be rendered", () => {
37
+ it("leaves the composer usable once an engine is configured", () => {
38
+ expect(shouldGateComposerForEngine("configured")).toBe(false);
39
+ });
40
+ });
41
+
42
+ describe("shouldCheckModelStatus", () => {
43
+ it("checks hosted engines and skips local runtimes", () => {
44
+ expect(shouldCheckModelStatus({ selectedEngine: "openai" })).toBe(true);
45
+ expect(shouldCheckModelStatus({ selectedEngine: "codex-cli" })).toBe(false);
38
46
  expect(
39
- shouldGateComposerForMissingEngine({
40
- state: "missing",
41
- hasSetupComponent: true,
42
- }),
47
+ shouldCheckModelStatus({ enabled: true, selectedEngine: "codex-cli" }),
43
48
  ).toBe(true);
44
49
  expect(
45
- shouldGateComposerForMissingEngine({
46
- state: "missing",
47
- hasSetupComponent: false,
48
- }),
50
+ shouldCheckModelStatus({ enabled: false, selectedEngine: "openai" }),
49
51
  ).toBe(false);
50
52
  });
53
+ });
51
54
 
52
- it("leaves the composer usable once an engine is configured", () => {
55
+ describe("resolveComposerModelStatusChecksEnabled", () => {
56
+ it("uses the persisted engine when no picker selection is provided", () => {
57
+ expect(
58
+ resolveComposerModelStatusChecksEnabled({ defaultEngine: "codex-cli" }),
59
+ ).toBe(false);
60
+ expect(
61
+ resolveComposerModelStatusChecksEnabled({ defaultEngine: "openai" }),
62
+ ).toBe(true);
63
+ });
64
+
65
+ it("prefers an explicit engine and honors an explicit host override", () => {
66
+ expect(
67
+ resolveComposerModelStatusChecksEnabled({
68
+ selectedEngine: "codex-cli",
69
+ defaultEngine: "openai",
70
+ }),
71
+ ).toBe(false);
53
72
  expect(
54
- shouldGateComposerForMissingEngine({
55
- state: "configured",
56
- hasSetupComponent: true,
73
+ resolveComposerModelStatusChecksEnabled({
74
+ enabled: false,
75
+ selectedEngine: "openai",
57
76
  }),
58
77
  ).toBe(false);
59
78
  });
@@ -44,12 +44,14 @@ import { PastedTextChip } from "./PastedTextChip.js";
44
44
  import { escapePromptAttachmentAttribute } from "./prompt-attachments.js";
45
45
  import {
46
46
  type EngineModelGroup,
47
+ type ComposerAgentEngineState,
47
48
  type AgentChatContextItem,
48
49
  type ReasoningEffort,
49
50
  useComposerRuntimeAdapters,
50
51
  } from "./runtime-adapters.js";
51
52
  import {
52
53
  DEFAULT_VOICE_DICTATION_ENABLED,
54
+ isLocalRuntimeEngine,
53
55
  TiptapComposer,
54
56
  type ComposerAgentOption,
55
57
  type ComposerSubmitIntent,
@@ -193,10 +195,7 @@ export interface PromptComposerProps {
193
195
  onAgentChange?: (agent: string) => void;
194
196
  /** Called when the shared model picker opens or closes. */
195
197
  onModelSelectorOpenChange?: (open: boolean) => void;
196
- /**
197
- * Enable server-backed model/provider status checks. Defaults off when the
198
- * host supplies model state and callbacks, otherwise on.
199
- */
198
+ /** Enable server-backed model/provider status checks. Defaults on, except for a selected local runtime. */
200
199
  modelStatusChecksEnabled?: boolean;
201
200
  /** Called whenever the plain editor text changes. */
202
201
  onTextChange?: (text: string) => void;
@@ -294,9 +293,16 @@ function formatInlineTextFile(name: string, text: string): string {
294
293
  .join("\n");
295
294
  }
296
295
 
296
+ /** Chat stays closed until the provider check confirms it can run. */
297
+ export function shouldGateComposerForEngine(
298
+ state: ComposerAgentEngineState,
299
+ ): boolean {
300
+ return state !== "configured";
301
+ }
302
+
297
303
  /**
298
- * Show the attached setup treatment only for a confirmed-missing engine with
299
- * a setup component. Unresolved status does not mean a provider is missing.
304
+ * Show setup treatment only for a confirmed-missing engine with a setup
305
+ * component. Unresolved status uses the retry treatment instead.
300
306
  */
301
307
  export function shouldGateComposerForMissingEngine(input: {
302
308
  state: string;
@@ -305,6 +311,24 @@ export function shouldGateComposerForMissingEngine(input: {
305
311
  return input.state === "missing" && input.hasSetupComponent;
306
312
  }
307
313
 
314
+ export function shouldCheckModelStatus(input: {
315
+ enabled?: boolean;
316
+ selectedEngine?: string;
317
+ }): boolean {
318
+ return input.enabled ?? !isLocalRuntimeEngine(input.selectedEngine);
319
+ }
320
+
321
+ export function resolveComposerModelStatusChecksEnabled(input: {
322
+ enabled?: boolean;
323
+ selectedEngine?: string;
324
+ defaultEngine?: string;
325
+ }): boolean {
326
+ return shouldCheckModelStatus({
327
+ enabled: input.enabled,
328
+ selectedEngine: input.selectedEngine ?? input.defaultEngine,
329
+ });
330
+ }
331
+
308
332
  export async function buildPromptComposerSubmission(options: {
309
333
  text: string;
310
334
  attachments?: ReadonlyArray<unknown>;
@@ -602,6 +626,7 @@ function PromptComposerInner({
602
626
  composerRef,
603
627
  }: PromptComposerProps) {
604
628
  const adapters = useComposerRuntimeAdapters();
629
+ const t = adapters.translate!;
605
630
  const modelsAdapter = adapters.models!;
606
631
  const BuilderSetupCard = modelsAdapter.BuilderSetupCard;
607
632
  const BuilderSetupContent = modelsAdapter.BuilderSetupContent;
@@ -624,13 +649,12 @@ function PromptComposerInner({
624
649
  useEffect(() => {
625
650
  onAttachmentsChangeRef.current?.(attachmentFiles);
626
651
  }, [attachmentFiles]);
627
- const hostManagedModels = Boolean(
628
- availableModels && selectedModel && onModelChange,
629
- );
630
- const resolvedModelStatusChecksEnabled =
631
- modelStatusChecksEnabled ?? !hostManagedModels;
652
+ const requestedModelStatusChecksEnabled = shouldCheckModelStatus({
653
+ enabled: modelStatusChecksEnabled,
654
+ selectedEngine,
655
+ });
632
656
  const models = modelsAdapter.useChatModels!({
633
- enabled: showModelSelector && resolvedModelStatusChecksEnabled,
657
+ enabled: showModelSelector && requestedModelStatusChecksEnabled,
634
658
  });
635
659
  const composerModel = showModelSelector
636
660
  ? (selectedModel ?? models.selectedModel)
@@ -638,6 +662,12 @@ function PromptComposerInner({
638
662
  const composerEngine = showModelSelector
639
663
  ? (selectedEngine ?? models.selectedEngine)
640
664
  : undefined;
665
+ const resolvedModelStatusChecksEnabled =
666
+ resolveComposerModelStatusChecksEnabled({
667
+ enabled: modelStatusChecksEnabled,
668
+ selectedEngine,
669
+ defaultEngine: models.selectedEngine,
670
+ });
641
671
  const composerEffort = showModelSelector
642
672
  ? (selectedEffort ?? models.selectedEffort)
643
673
  : undefined;
@@ -668,7 +698,14 @@ function PromptComposerInner({
668
698
  const agentEngineConfigured = modelsAdapter.useAgentEngineConfigured!(
669
699
  resolvedModelStatusChecksEnabled,
670
700
  );
671
- const missingApiKey = agentEngineConfigured.missing;
701
+ const engineState = resolvedModelStatusChecksEnabled
702
+ ? agentEngineConfigured.state
703
+ : "configured";
704
+ const missingApiKey =
705
+ resolvedModelStatusChecksEnabled && engineState === "missing";
706
+ const engineStatusUnresolved =
707
+ resolvedModelStatusChecksEnabled &&
708
+ (engineState === "unknown" || engineState === "unavailable");
672
709
  const [missingKeyBouncePulse, setMissingKeyBouncePulse] = useState(0);
673
710
  const bounceMissingKeySetup = useCallback(() => {
674
711
  setMissingKeyBouncePulse((pulse) => pulse + 1);
@@ -682,30 +719,14 @@ function PromptComposerInner({
682
719
  }
683
720
  }, []);
684
721
  const useInlineMissingKeySetup = layoutVariant === "compact";
685
- const gateComposer = shouldGateComposerForMissingEngine({
686
- state: agentEngineConfigured.state,
687
- hasSetupComponent: Boolean(
688
- useInlineMissingKeySetup ? BuilderSetupContent : BuilderSetupCard,
689
- ),
690
- });
691
- const ensureEngineReadyBeforeSubmit = useCallback(async () => {
692
- if (agentEngineConfigured.state === "missing") {
693
- bounceMissingKeySetup();
694
- return false;
695
- }
696
- if (agentEngineConfigured.state !== "unknown") return true;
697
- const state = await modelsAdapter.fetchAgentEngineConfiguredState?.(true, {
698
- timeoutMs: 5_000,
699
- });
700
- if (state === "missing") {
701
- bounceMissingKeySetup();
702
- return false;
722
+ const gateComposer = shouldGateComposerForEngine(engineState);
723
+ const retryEngineStatus = useCallback(() => {
724
+ if (typeof window !== "undefined") {
725
+ window.dispatchEvent(new Event("agent-engine:configured-changed"));
703
726
  }
704
- return true;
705
- }, [agentEngineConfigured.state, bounceMissingKeySetup, modelsAdapter]);
706
-
727
+ }, []);
707
728
  useEffect(() => {
708
- if (!autoFocus || disabled) return;
729
+ if (!autoFocus || disabled || gateComposer) return;
709
730
  const id = window.setTimeout(() => {
710
731
  const target =
711
732
  typeof handleRef === "object" && handleRef && "current" in handleRef
@@ -714,7 +735,7 @@ function PromptComposerInner({
714
735
  target?.focus();
715
736
  }, 50);
716
737
  return () => window.clearTimeout(id);
717
- }, [autoFocus, disabled, handleRef]);
738
+ }, [autoFocus, disabled, gateComposer, handleRef]);
718
739
 
719
740
  const handleSubmit = useCallback(
720
741
  async (
@@ -772,6 +793,27 @@ function PromptComposerInner({
772
793
  />
773
794
  </div>
774
795
  ) : null}
796
+ {engineStatusUnresolved ? (
797
+ <div
798
+ className="mb-2 flex items-center justify-between gap-3 rounded-md border border-border bg-muted/50 px-3 py-2 text-sm text-muted-foreground"
799
+ role="status"
800
+ >
801
+ <span>
802
+ {engineState === "unknown"
803
+ ? t("agentChat.setup.checkingProvider")
804
+ : t("agentChat.setup.providerStatusUnavailable")}
805
+ </span>
806
+ {engineState === "unavailable" ? (
807
+ <button
808
+ type="button"
809
+ className="shrink-0 font-medium text-foreground underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
810
+ onClick={retryEngineStatus}
811
+ >
812
+ {t("agentChat.common.retry")}
813
+ </button>
814
+ ) : null}
815
+ </div>
816
+ ) : null}
775
817
  <AgentComposerFrame
776
818
  className={cn(
777
819
  "text-start",
@@ -783,38 +825,56 @@ function PromptComposerInner({
783
825
  style={style}
784
826
  rootStyle={rootStyle}
785
827
  layoutVariant={layoutVariant}
786
- onClick={disabled ? onDisabledClick : undefined}
828
+ onClick={
829
+ gateComposer
830
+ ? missingApiKey
831
+ ? bounceMissingKeySetup
832
+ : retryEngineStatus
833
+ : disabled && onDisabledClick
834
+ ? () => onDisabledClick()
835
+ : undefined
836
+ }
787
837
  >
788
838
  <PromptAttachmentStrip />
789
839
  <TiptapComposer
790
840
  contextItems={contextItems}
791
- contextMenuItems={contextMenuItems}
841
+ contextMenuItems={gateComposer ? undefined : contextMenuItems}
792
842
  onRemoveContextItem={onRemoveContextItem}
793
843
  onInspectContextItem={onInspectContextItem}
794
844
  onRetryContextItem={onRetryContextItem}
795
845
  attachmentsEnabled={attachmentsEnabled}
796
846
  ariaLabel={ariaLabel}
797
847
  focusRef={handleRef}
798
- disabled={disabled}
799
- submissionDisabled={
800
- submissionDisabled || agentEngineConfigured.state === "missing"
801
- }
848
+ disabled={disabled || gateComposer}
849
+ submissionDisabled={submissionDisabled || gateComposer}
802
850
  submitting={submitting}
803
851
  willQueue={willQueue}
804
852
  maxDocumentAttachmentBytes={maxDocumentAttachmentBytes}
805
853
  documentAttachmentLimitLabel={documentAttachmentLimitLabel}
806
- placeholder={placeholder}
854
+ placeholder={
855
+ gateComposer
856
+ ? engineStatusUnresolved
857
+ ? t("agentChat.setup.checkingProvider")
858
+ : t("agentChat.composer.connectAbove", {
859
+ defaultValue: "Connect AI above to continue...",
860
+ })
861
+ : placeholder
862
+ }
807
863
  initialText={initialText}
808
864
  initialTextKey={initialTextKey}
809
865
  onSubmit={handleSubmit}
810
- onBeforeSubmit={ensureEngineReadyBeforeSubmit}
811
866
  clearOnSubmit={!preserveDraftOnSubmit}
812
867
  plusMenuMode={
813
- plusMenuMode ?? (attachmentsEnabled ? "upload-only" : "hidden")
868
+ gateComposer
869
+ ? "hidden"
870
+ : (plusMenuMode ??
871
+ (attachmentsEnabled ? "upload-only" : "hidden"))
814
872
  }
815
873
  terminalModeControl={terminalModeControl}
816
874
  extensionTools={extensionTools}
817
- attachButton={attachButton}
875
+ attachButton={
876
+ gateComposer || !attachmentsEnabled ? null : attachButton
877
+ }
818
878
  modeControl={modeControl}
819
879
  execMode={execMode}
820
880
  onExecModeChange={onExecModeChange}