Commit Graph

1800 Commits

Author SHA1 Message Date
Benjamin Taylor e36a7c953c feat(vue): drawer-awareness state + mobile mutual-exclusion in chat config provider
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:01:34 -05:00
Benjamin Taylor 0254da874e feat(vue): add drawer-awareness fields to CopilotChatConfigurationValue type
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:00:03 -05:00
Benjamin Taylor 1def05e500 test(vue): clear-on-fresh clears messages on new-thread switch, not on mount
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:57:50 -05:00
Benjamin Taylor 0fea7a8ccb feat(vue): clear chat messages on fresh non-explicit thread switch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:53:58 -05:00
Benjamin Taylor 60c01eef1d test(vue): cover active-thread setActiveThreadId/startNewThread + authoritative guard
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:51:28 -05:00
Benjamin Taylor daf26f22c1 feat(vue): active-thread override + setActiveThreadId/startNewThread in chat config provider
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:50:18 -05:00
Benjamin Taylor 6ba86c4027 feat(vue): add active-thread setters to CopilotChatConfigurationValue type
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:48:37 -05:00
MikeRyanDev ca836ff920 chore: release monorepo v1.62.0 2026-07-01 15:45:54 +00:00
Benjamin Taylor 259276d795 chore(vue): depend on @copilotkit/web-components for the threads drawer element
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:44:32 -05:00
Benjamin Taylor 89837c50fb feat(vue): expose unarchive/refetch/startNewThread/listError/isMutating + enabled gate on useThreads
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:43:09 -05:00
Benjamin Taylor 2f85aa7bc1 feat(vue): add isMobileViewport helper for drawer mobile mutual-exclusion
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:40:07 -05:00
Ran Shemtov d0f4f27458 Merge branch 'main' into claude/happy-dirac-f6ed5e 2026-07-01 16:48:12 +02:00
Lukas Moschitz e481011efa feat(web-inspector): rename the Memories tab to Learning
Rename the inspector's user-facing feature name from Memories to Learning:
the menu tab label and the enabled-state panel heading. The internal
"memories" menu key, the Brain icon, the REST wiring, and entry-level copy
(search box, empty states) are unchanged. Update the tab-presence test.
2026-07-01 14:26:11 +02:00
Ran Shemtov 55e0a884b3 Merge branch 'main' into fix/inspector-subscribe-per-thread-clone-events 2026-07-01 10:10:12 +02:00
Ran Shemtov e1321406bc fix(vue): enable A2UI via catalog-on-provider path (#5777) 2026-07-01 08:33:53 +02:00
serhiizghama f9bba02c20 fix(web-inspector): subscribe to the running clone via onAgentRunStarted
The inspector subscribed to AG-UI events only through onAgentsChanged, which
skips per-thread clones, leaving the event timeline empty during runs
(regression of #3872). Subscribe to the run instance reported by
onAgentRunStarted so its events are recorded.
2026-07-01 08:23:53 +07:00
serhiizghama 7f0ca7eabf fix(core): notify subscribers of the agent instance at run start
Add an onAgentRunStarted subscriber event fired from RunHandler.runAgent and
connectAgent with the instance that actually runs. Per-thread clones (from
useAgent) are not in the agent registry, so onAgentsChanged never fires for
them and subscribers that only track registry agents can't observe their run.
2026-07-01 08:23:53 +07:00
Jordan Ritter 7d6a2b2a9a docs(runtime): correct forwardHeaders allowlist JSDoc accuracy
Empty/whitespace-only allow/deny/denyPrefixes entries are trimmed and
dropped before the allowlist-mode decision, so allow: [""] stays in
denylist mode rather than switching on exclusive allowlist mode. Also
document that allowlist mode bypasses the built-in default denylist, so
integrators must not allow-list protected/platform headers unintentionally.
2026-06-30 17:23:05 -07:00
Jordan Ritter dde79d1c89 fix(runtime): make forwardHeaders deny authoritative in allowlist mode
`shouldForwardHeader` returned early on `policy.allow` and silently ignored
the integrator's `deny`/`denyPrefixes`, so a header listed in BOTH `allow`
and `deny` still forwarded — a footgun on a security feature.

Rework the predicate so the integrator's own `deny`/`denyPrefixes` (exact,
case-insensitive, and prefix) always strip, including in allowlist mode:
`allow` selects the candidate set, `deny` subtracts from it. The built-in
default denylist is unchanged and still applies only in denylist mode (an
explicit `allow` is a deliberate opt-in), so only the integrator's OWN deny
subtracts from an allowlist.

Also harden `resolveForwardHeadersPolicy`: trim and drop empty/whitespace-only
entries from `deny`/`denyPrefixes`/`allow`. A stray `denyPrefixes:[""]` made
`startsWith("")` true for every header (silently denying ALL forwarding), and
`allow:[""]`/`allow:[" "]` seeded the exclusive allowlist with an entry that
could never match — both integrator typos that now can't silently break
forwarding. Entries are lowercased consistently with existing handling.
2026-06-30 17:12:55 -07:00
Martha Schumann 5df64923cc fix: stabilize inspector live event caches 2026-06-30 17:03:25 -07:00
Jordan Ritter cf951e04f2 test(runtime): harden header-forwarding coverage — clone isolation, case-collision, allowlist boundary
- agent-utils: assert configureAgentForRequest does not mutate the shared
  registered agent; only the per-request clone carries merged inbound headers
  (guards against a cross-request bearer-token leak, #5712).
- header-utils: direct mergeForwardableHeaders unit test for server Authorization
  vs inbound lowercase authorization with a different value — exactly one
  authorization-family key survives carrying the server value (#5712).
- header-utils: shouldForwardHeader boundary tests for the bare 'x' name and the
  empty-string name under both denylist and allowlist policies.
2026-06-30 16:58:11 -07:00
Jordan Ritter 4cdc1e16f5 fix(runtime): make forwardHeadersPolicy optional on CopilotRuntimeLike
The published CopilotRuntimeLike interface (v2 export surface) added
forwardHeadersPolicy as a REQUIRED field, which breaks any external
implementor of the interface — inconsistent with the minor-release
classification. The /run (agent-utils) and /connect (sse/connect) read
sites dereferenced runtime.forwardHeadersPolicy with no coalesce, so a
policy-less object crashed with "Cannot read properties of undefined
(reading 'allow')".

Make the field optional on the interface and coalesce both read sites to
the default resolved policy (resolveForwardHeadersPolicy(undefined),
default-on denylist) when absent. Concrete runtimes (BaseCopilotRuntime)
still always resolve and set it, so behavior is identical for all real
runtimes; the interface is now non-breaking and crash-proof.

Adds a red-green test driving configureAgentForRequest with a runtime
whose forwardHeadersPolicy is undefined: asserts no throw and that the
default denylist applies (x-forwarded-for dropped, custom x-* and
authorization forwarded).
2026-06-30 16:58:06 -07:00
Jordan Ritter 62ed6fa045 test(runtime): cover header-forwarding denylist + config policy on both paths (#5712)
- header-utils.test.ts: new coverage for the default denylist (exact names +
  prefix families, case-insensitive), custom x-* still forwarding, config
  overrides (useDefaultDenylist:false, deny, denyPrefixes, allow allowlist mode),
  and the breadth/precedence interaction. Migrate the inverting assertions
  (x-request-id / X-Forwarded-For now stripped; extract result drops x-request-id)
  and add the new required policy arg to all call sites.
- agent-utils-header-forwarding.test.ts + sse-connect-agent-id.test.ts: /run and
  /connect integration coverage — denylisted infra/platform headers dropped,
  custom x-* + authorization still forward, and a runtime-supplied forwardHeaders
  policy is actually applied (plumb-through). Swap denylisted filler headers for
  non-denylisted custom headers in the precedence regression tests.
- handle-run / handle-connect / intelligence-run-telemetry / get-runtime-info:
  add the resolved forwardHeadersPolicy to mock runtimes that route through the
  header merge so they satisfy the now-required policy.
2026-06-30 16:40:51 -07:00
Jordan Ritter 462fa7ad58 feat(runtime): configurable inbound-header forwarding policy with default infra/platform denylist
Tighten which inbound HTTP headers the v2 runtime forwards onto the outgoing
agent call. The old `authorization` + `x-*` allowlist leaked infrastructure,
proxy, and platform headers (x-forwarded-*, x-real-ip, x-amzn-trace-id,
x-vercel-*, and the Copilot Cloud platform key x-copilotcloud-public-api-key)
to arbitrary configured agent URLs (#5712, breadth half).

- header-utils.ts: add DEFAULT_DENY_HEADER_NAMES + DEFAULT_DENY_HEADER_PREFIXES
  constants and a policy-aware shouldForwardHeader; thread ResolvedForwardHeadersPolicy
  through extractForwardableHeaders and mergeForwardableHeaders. Add the public
  ForwardHeadersConfig and resolveForwardHeadersPolicy (useDefaultDenylist defaults
  to true; deny/denyPrefixes extend the default; allow switches to allowlist mode).
  Server-wins precedence and server-self case-dedup are unchanged.
- runtime.ts: add forwardHeaders?: ForwardHeadersConfig to BaseCopilotRuntimeOptions,
  resolve it once in the constructor into forwardHeadersPolicy (mirroring the
  debug -> ResolvedDebugConfig resolve-once), expose it on CopilotRuntimeLike /
  BaseCopilotRuntime, and add a passthrough getter on the CopilotRuntime shim.
- Apply the resolved policy at both call sites: /run (configureAgentForRequest)
  and /connect (handleSseConnect), so the two paths can never diverge.

Default-on in a minor with { useDefaultDenylist: false } as the documented opt-out.
2026-06-30 16:40:27 -07:00
Martha Kelly Schumann f0eb837dad Merge branch 'main' into codex/unified-thread-debugger 2026-06-30 15:53:41 -07:00
Benjamin Taylor 74b8a1981f feat(drawer): default Upgrade CTA URL + Angular license gate
Add a `licenseUrl` property to the shared element (default
https://docs.copilotkit.ai/intelligence). The locked view's Upgrade CTA now
dispatches a cancelable `licensed` event carrying the url and, unless the
host calls preventDefault(), opens it in a new tab; a blank url suppresses
navigation. React and Angular wrappers expose an optional `licenseUrl` prop.

License-gate the Angular drawer like React: surface `licenseStatus` as a
signal on the CopilotKit service and gate the wrapper on
status valid|expiring && checkFeature("threads"), skipping the thread fetch
while unlicensed and showing the loading state (not the locked view) until
the status resolves. Reverses the earlier always-licensed Angular call.

Closes ENT-1027.
2026-06-30 17:34:39 -05:00
Benjamin Taylor f53a70c691 refactor(drawer): rename to CopilotThreadsDrawer / copilotkit-threads-drawer
Final naming decision. Renames the public component + element across the
board: React/Angular CopilotDrawer -> CopilotThreadsDrawer, the Lit element
copilotkit-drawer -> copilotkit-threads-drawer (tag, CopilotKitThreadsDrawer
class, COPILOTKIT_THREADS_DRAWER_TAG, defineCopilotKitThreadsDrawer), the
@copilotkit/web-components/drawer subpath -> /threads-drawer (+ src dir),
the CopilotThreadsDrawerRow directive / copilotThreadsDrawerRow input, and
all prose/test references. Generic types (DrawerThread, DrawerFilter),
--cpk-drawer-* tokens, and ::part names are unchanged. Behavior unchanged.
2026-06-30 17:34:39 -05:00
Benjamin Taylor eb3c20e865 refactor(drawer): rename unlicensed -> licensed across the public surface
Per review, settle the locked-view affordance on a neutral 'license'
name. Renames the event (unlicensed -> licensed), React prop
(onUnlicensed -> onLicensed), slot/part/class (licensed, licensed-cta),
LicensedDetail type, data-testid, render method, and identifier-referencing
comments/test names. The existing 'licensed' boolean gate is unchanged;
prose describing the not-licensed state still reads 'unlicensed'/'locked
view'. Behavior unchanged; Angular unaffected.
2026-06-30 17:34:39 -05:00
Benjamin Taylor d7cb0fded1 refactor(drawer): rename upsell -> unlicensed across the public surface
Per review, drop the 'upsell' monetization jargon. Renames the event
(upsell -> unlicensed), the React prop (onUpsell -> onUnlicensed), the
slot/part/class (upsell -> unlicensed, upsell-cta -> unlicensed-cta),
the UnlicensedDetail type, the data-testid, and all comments/test names.
Behavior unchanged. Angular is unaffected (always-licensed, no gate).
2026-06-30 17:34:39 -05:00
Benjamin Taylor cf4f00ecc1 feat(drawer): activate thread pagination (Load more + limit prop)
Wires up the previously-dormant pagination plumbing (ENT-1016):

- Element: render a 'Load more' button at the list bottom when hasMore
  (and not fetching / not errored), emitting a new load-more event
  (LoadMoreDetail). Distinct from retry{scope:'fetch-more'} (error
  recovery); both advance pagination.
- React CopilotDrawer: add a limit prop (forwarded to useThreads) and
  route load-more to fetchMoreThreads.
- Angular CopilotDrawer: add a limit input (forwarded to injectThreads)
  and route load-more to fetchMoreThreads.

Tests: element load-more render + emit + precedence; React limit
forwarding + load-more routing; Angular load-more routing.
2026-06-30 17:34:38 -05:00
Benjamin Taylor f1cc30cda0 feat(react): add configurable label prop to CopilotDrawer
Mirrors the Angular wrapper + the copilotkit-drawer element's label
property: sets the drawer region aria-label and default header text,
defaulting to the element's built-in "Threads" when omitted.
2026-06-30 17:34:38 -05:00
Benjamin Taylor 0183a30369 fix(web-components): scroll the drawer thread list when it overflows (:host height + .list min-height:0) 2026-06-30 17:34:38 -05:00
github-actions[bot] 3c8e330fcb style: auto-fix formatting 2026-06-30 17:34:38 -05:00
Benjamin Taylor 10a5315c10 feat(angular): forward a label input to the drawer element 2026-06-30 17:34:38 -05:00
Benjamin Taylor 787b9ad382 feat(web-components): make the drawer label/aria-label configurable (label property, defaults to Threads) 2026-06-30 17:34:37 -05:00
github-actions[bot] b5580e32b8 style: auto-fix formatting 2026-06-30 17:34:37 -05:00
Benjamin Taylor d8a8dd4fe6 feat(angular): add <CopilotDrawer> wrapper for the copilotkit-drawer element
Standalone OnPush Angular component wrapping the framework-agnostic
copilotkit-drawer Lit element. Binds live thread state from injectThreads onto
the element (imperative viewChild+effect, per the a2ui-activity-renderer
precedent) and routes the element's DOM events to the ambient
CopilotChatConfiguration + thread mutations. Ships a CopilotDrawerRow directive
for per-row slot projection, host onThreadSelect/onNewThread escape-hatch
callbacks, and <ng-content> slot passthrough (launcher-icon/memories). Uses the
filtered listError for the error panel. Always-licensed (no licensed/upsell
wiring); inline-chat-only (no Layer-2 open-state coordination). Exported from
public-api.
2026-06-30 17:34:37 -05:00
Benjamin Taylor a7c25d9800 feat(angular): expose filtered listError from injectThreads
Adds a listError signal that returns only genuine list/mutation errors,
excluding developer/config errors (missing runtime URL, runtime without thread
endpoints) so consumer error UIs do not surface dev strings to end users.
Mirrors react-core's useThreads listError; `error` is unchanged (additive).
2026-06-30 17:34:37 -05:00
Benjamin Taylor 7a08fa2cc0 chore(angular): add @copilotkit/web-components dependency
Adds the workspace dep (consumed by the <CopilotDrawer> wrapper) and allows it
as a non-peer dependency in ng-package.json so ng-packagr packages cleanly.
2026-06-30 17:34:37 -05:00
Benjamin Taylor 01cd9eeabc feat(angular): CopilotChat consumes ambient CopilotChatConfiguration
CopilotChat reads the ambient config when a provideCopilotChatConfiguration
provider is in scope: input-first precedence ([agentId]/[threadId] win over the
config, matching React), seeding the config from a set [threadId], and driving
the connector with loading-cursor hooks. Standalone <copilot-chat [threadId]>
usage is unchanged when no provider is present. Exports CopilotChatConfiguration
from the package entry point.
2026-06-30 17:34:36 -05:00
Benjamin Taylor 4400a7c081 feat(angular): active-thread connector wiring config to the live agent
connectActiveThread reactively pins the resolved thread onto agent.threadId.
On an explicit switch it connects the agent, owning the loading-cursor + abort
+ detach lifecycle of the standalone connectToAgent path (per-run AbortController
on HttpAgents, a single caught connect chain so a rejecting connect never leaks
an unhandled rejection, and a staleness-guarded cursor settle). On a fresh /
non-explicit switch it clears messages only on a genuine new-thread transition,
never on mount or a same-thread agent swap. Injection-context-only; not exported
on the public surface.
2026-06-30 17:34:36 -05:00
Benjamin Taylor 35671a688f feat(angular): CopilotChatConfiguration service for active-thread resolution
Injectable service mirroring React's CopilotChatConfigurationProvider. Owns
agentId/threadId resolution (controlled prop > override > options > minted
fallback) and the hasExplicitThreadId welcome-screen flag, exposes
setActiveThreadId/startNewThread setters that no-op when the config is
host-controlled, and reserves drawerOpen/registerDrawer hooks for a future
popup/sidebar layer. provideCopilotChatConfiguration aliases the token to a
single instance via useExisting.
2026-06-30 17:34:36 -05:00
Jordan Ritter 636bcad058 fix(runtime): de-duplicate server-vs-server case-collision headers in mergeForwardableHeaders
When an agent is configured with both case-variants of the same header
in agent.headers (e.g. Authorization and authorization), the prior
{ ...base } spread kept both keys — the exact undici comma-join hazard
the function guards against for inbound collisions. Collapse server-self
case-collisions to a single first-occurrence-wins entry; server-wins-over
-inbound and case-insensitive inbound suppression are unchanged.
2026-06-30 15:00:16 -07:00
Jordan Ritter 8bdb3a1c3f test(runtime): cover header precedence — /run + /connect collision, x-* uniqueness, agent-undefined forwarding
Cover the #5712 header-precedence behavior across both paths:

- agent-header-precedence.test.ts: server-configured agent.headers win
  over forwarded inbound headers on collision (case-insensitive), with
  single-key uniqueness assertions for both authorization and the x-*
  family (exactly one surviving key carrying the SERVER value).
- agent-utils-header-forwarding.test.ts: the /run path merges via
  mergeForwardableHeaders so server values are authoritative and inbound
  headers fill only unset keys.
- sse/__tests__/sse-connect-agent-id.test.ts: the /connect path applies
  the same merge, plus the agent-undefined case (no server agent.headers)
  degrades to forwarding allowlisted inbound headers only and does not
  crash.
2026-06-30 14:39:51 -07:00
Jordan Ritter abb85c727d refactor(runtime): thread merged headers into runner.connect() as forward-looking plumbing
The /connect path now builds the same server-wins merged headers as the
/run path and passes them into runner.connect(). This does NOT fix
connect-path auth: no shipped runner consumes the headers field of
AgentRunnerConnectRequest today. The in-memory, intelligence, telemetry,
and sqlite runners all read only threadId from the connect request and
ignore headers entirely. The real outbound header forwarding lives on the
/run path, where agent.headers is mutated before the agent runs.

Passing merged headers here is the correct argument shape for a future
outbound-connecting runner, and keeps the connect path's merge semantics
consistent with /run. The comments and JSDoc are rewritten to state this
plainly rather than implying an active auth fix: the connect-site
cloneAgentForRequest call is documented as the sole agentId-existence
guard (the intelligence branch never re-validates the id), and
cloneAgentForRequest's AbstractAgent | Response (404) dual-return contract
that both callers depend on is now documented.
2026-06-30 14:39:38 -07:00
Jordan Ritter bc5e56a295 fix(runtime): server-configured agent headers take precedence over forwarded inbound headers
When a request hits the /run path, inbound headers are forwarded to the
agent. Previously, forwarded inbound headers could clobber the
server-configured agent.headers on a key collision, letting a client
override server-set values (e.g. authorization). This is the #5712 bug.

Introduce mergeForwardableHeaders (header-utils.ts): a case-insensitive
merge where server-configured agent.headers always win on collision,
regardless of header-name casing. agent-utils.ts now uses this helper on
the /run path so server-configured values are authoritative and inbound
headers only fill keys the server did not set.

Fixes #5712
2026-06-30 14:39:21 -07:00
Martha Kelly Schumann 08ed19f96f Merge branch 'main' into codex/unified-thread-debugger 2026-06-30 13:06:42 -07:00
github-actions[bot] 097d734bab style: auto-fix formatting 2026-06-30 14:26:15 -05:00
Benjamin Taylor 79276a2d4f fix(chat): keep message padding in sub-viewport panes via container query
CopilotChat message wrappers used viewport-keyed `cpk:sm:px-0`, collapsing
horizontal padding to 0 at any viewport >=640px. The message column is
`max-w-3xl` (768px) centered; the design assumes the chat fills the viewport,
so at >=640px the column has side gutters and inner padding can drop to 0.

But when the chat lives in a sub-viewport-width pane (e.g. the threads drawer
rail beside the chat, ~580px on an 820px iPad-portrait viewport), `sm:px-0`
still fires on viewport width while the 768px column overflows the narrow
pane and sits flush against both edges. The input wrapper looked fine because
it is visually inset by its own pill, so only message text appeared broken.

Make the padding container-relative instead of viewport-relative:
- add `cpk:@container` (container-type: inline-size) to the chat root, and
- switch the message/input/suggestion wrappers from `cpk:sm:px-0` to the
  container variant `cpk:@3xl:px-0`.

Padding now tracks the chat's own width and drops to 0 only once the container
is at least as wide as the column's own max-width, so the column has real
gutters; in any narrower pane the `px-4` inner padding is retained. React,
Angular, and Vue kept in lockstep.

Note on the breakpoint: Tailwind v4 container-query breakpoints differ from
viewport breakpoints (`@sm` = 24rem/384px, not 640px). A mechanical
`sm:` -> `@sm:` swap would still collapse the ~580px repro pane. `@3xl`
(48rem/768px) is used because it exactly matches the column's `max-w-3xl`,
which is the width at which side gutters first appear.

Verified: full-width desktop chat unchanged (container >=768px -> px-0);
580px pane retains 16px padding; render-prop layouts without a container
ancestor degrade safely to `px-4`; sidebar/popup `data-*` padding overrides
are unaffected.

ENT-1020

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:26:15 -05:00
Austin Merrick 6f49f69725 fix(angular): skip chat input submit during IME composition (#5764)
### What

The Angular `CopilotChatInput.handleKeyDown` submits the message when
Enter is pressed without Shift, but it never checks whether an IME
composition is in progress. When typing CJK text (Japanese, Chinese,
Korean), the Enter that confirms an IME candidate also fires a
`keydown`, so the half-composed text gets sent instead of the candidate
being committed.

The React and Vue bindings of the same v2 `CopilotChatInput` already
guard against this; Angular was the one binding still missing it:

- `packages/react-core/src/v2/components/chat/CopilotChatInput.tsx` —
`handleKeyDown` returns early on `e.nativeEvent.isComposing || e.keyCode
=== 229`
- `packages/vue/src/v2/components/chat/CopilotChatInput.vue` —
`handleKeydown` returns early on `isComposing.value || event.isComposing
|| event.keyCode === 229`, and has a test asserting it does not submit
while composing

### Change

Add the same early return to the Angular `handleKeyDown`, using the
native `KeyboardEvent` (`event.isComposing || event.keyCode === 229`),
which matches the Vue binding's idiom.

### Notes

When composition is not active `isComposing` is `false`, so Enter
submits exactly as before and Shift+Enter still inserts a newline. The
most visible case is Safari with a Japanese IME, where the confirming
Enter reports `key === "Enter"` with `isComposing === true`; the
`keyCode === 229` arm mirrors the sibling guards for browsers that
report the composing key that way.

I verified the handler logic in isolation (Enter while composing no
longer submits; plain Enter and Shift+Enter are unchanged). I did not
run the full Angular suite locally.
2026-06-30 11:23:18 -07:00