@llui/agent 0.0.50 → 0.0.51

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.
@@ -36,7 +36,7 @@ export type AgentChatInitOpts = {
36
36
  initialInput?: string;
37
37
  };
38
38
  export type AgentChatMsg = {
39
- /** Bound to the input's `oninput` event. Round-trip stays in the slice. */
39
+ /** Bound to the input's `onInput` event. Round-trip stays in the slice. */
40
40
  type: 'SetInput';
41
41
  value: string;
42
42
  } | {
@@ -65,11 +65,14 @@ import { type Send } from '@llui/dom';
65
65
  * element helpers; matches the convention of the other agent
66
66
  * namespaces.
67
67
  *
68
- * The `input` bag carries both `oninput` and `onkeydown` because
68
+ * The `input` bag carries both `onInput` and `onKeyDown` because
69
69
  * the canonical chat-composer affordance is "type, press Enter"
70
70
  * (Shift+Enter for newline if the host wraps it themselves). Hosts
71
- * that want a multiline textarea can spread `input.oninput` and
72
- * skip `onkeydown`, wiring submit to the button only.
71
+ * that want a multiline textarea can spread `input.onInput` and
72
+ * skip `onKeyDown`, wiring submit to the button only. Handler keys
73
+ * are camelCase because LLui's element runtime only attaches keys
74
+ * matching `/^on[A-Z]/` as event listeners — lowercase `oninput`
75
+ * would silently degrade to a string attribute.
73
76
  */
74
77
  export type ConnectBag<S> = {
75
78
  root: {
@@ -80,8 +83,8 @@ export type ConnectBag<S> = {
80
83
  'data-part': 'input';
81
84
  value: (s: S) => string;
82
85
  disabled: (s: S) => boolean;
83
- oninput: (e: Event) => void;
84
- onkeydown: (e: KeyboardEvent) => void;
86
+ onInput: (e: Event) => void;
87
+ onKeyDown: (e: KeyboardEvent) => void;
85
88
  };
86
89
  submitButton: {
87
90
  'data-part': 'submit';
@@ -1 +1 @@
1
- {"version":3,"file":"agentChat.d.ts","sourceRoot":"","sources":["../../src/client/agentChat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,YAAY,GACpB;IACE,2EAA2E;IAC3E,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;CACd,GACD;IACE;;;;;;;OAOG;IACH,IAAI,EAAE,QAAQ,CAAA;CACf,GACD;IACE;;;;OAIG;IACH,IAAI,EAAE,gBAAgB,CAAA;CACvB,CAAA;AAEL,wBAAgB,IAAI,CAAC,IAAI,GAAE,iBAAsB,GAAG,CAAC,cAAc,EAAE,WAAW,EAAE,CAAC,CAQlF;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,YAAY,GAAG,CAAC,cAAc,EAAE,WAAW,EAAE,CAAC,CAyBhG;AAED,OAAO,EAAW,KAAK,IAAI,EAAE,MAAM,WAAW,CAAA;AAE9C;;;;;;;;;;GAUG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAC1B,IAAI,EAAE;QAAE,YAAY,EAAE,YAAY,CAAC;QAAC,iBAAiB,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;KAAE,CAAA;IAC1E,KAAK,EAAE;QACL,WAAW,EAAE,OAAO,CAAA;QACpB,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;QACvB,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;QAC3B,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,CAAA;QAC3B,SAAS,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;KACtC,CAAA;IACD,YAAY,EAAE;QACZ,WAAW,EAAE,QAAQ,CAAA;QACrB,OAAO,EAAE,MAAM,IAAI,CAAA;QACnB,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;KAC5B,CAAA;IACD;;;;;OAKG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;CAC7B,CAAA;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,cAAc,EAAE,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAyCjG"}
1
+ {"version":3,"file":"agentChat.d.ts","sourceRoot":"","sources":["../../src/client/agentChat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,YAAY,GACpB;IACE,2EAA2E;IAC3E,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;CACd,GACD;IACE;;;;;;;OAOG;IACH,IAAI,EAAE,QAAQ,CAAA;CACf,GACD;IACE;;;;OAIG;IACH,IAAI,EAAE,gBAAgB,CAAA;CACvB,CAAA;AAEL,wBAAgB,IAAI,CAAC,IAAI,GAAE,iBAAsB,GAAG,CAAC,cAAc,EAAE,WAAW,EAAE,CAAC,CAQlF;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,YAAY,GAAG,CAAC,cAAc,EAAE,WAAW,EAAE,CAAC,CAyBhG;AAED,OAAO,EAAW,KAAK,IAAI,EAAE,MAAM,WAAW,CAAA;AAE9C;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAC1B,IAAI,EAAE;QAAE,YAAY,EAAE,YAAY,CAAC;QAAC,iBAAiB,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;KAAE,CAAA;IAC1E,KAAK,EAAE;QACL,WAAW,EAAE,OAAO,CAAA;QACpB,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;QACvB,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;QAC3B,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,CAAA;QAC3B,SAAS,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;KACtC,CAAA;IACD,YAAY,EAAE;QACZ,WAAW,EAAE,QAAQ,CAAA;QACrB,OAAO,EAAE,MAAM,IAAI,CAAA;QACnB,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;KAC5B,CAAA;IACD;;;;;OAKG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;CAC7B,CAAA;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,cAAc,EAAE,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CA+CjG"}
@@ -50,7 +50,13 @@ export function connect(get, send) {
50
50
  'data-part': 'input',
51
51
  value: (s) => get(s).pendingInput,
52
52
  disabled: (s) => get(s).submitting,
53
- oninput: (e) => {
53
+ // Event handler keys MUST be camelCase: LLui's elements pass only
54
+ // keys matching `/^on[A-Z]/` to `addEventListener` (see
55
+ // packages/dom/src/elements.ts). Lowercase `oninput` would silently
56
+ // be set as a string attribute and never fire — the input field
57
+ // would freeze with no SetInput dispatches and the submit button
58
+ // would stay disabled forever.
59
+ onInput: (e) => {
54
60
  // The cast is the standard "input event target is an
55
61
  // HTMLInputElement / HTMLTextAreaElement" assumption; we
56
62
  // coerce via `.value`. Hosts using non-DOM inputs (custom
@@ -60,7 +66,7 @@ export function connect(get, send) {
60
66
  if (target && typeof target.value === 'string')
61
67
  setInput(target.value);
62
68
  },
63
- onkeydown: (e) => {
69
+ onKeyDown: (e) => {
64
70
  if (e.key === 'Enter' && !e.shiftKey) {
65
71
  e.preventDefault();
66
72
  submit();
@@ -1 +1 @@
1
- {"version":3,"file":"agentChat.js","sourceRoot":"","sources":["../../src/client/agentChat.ts"],"names":[],"mappings":"AAkEA,MAAM,UAAU,IAAI,CAAC,OAA0B,EAAE;IAC/C,OAAO;QACL;YACE,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,EAAE;YACrC,UAAU,EAAE,KAAK;SAClB;QACD,EAAE;KACH,CAAA;AACH,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAqB,EAAE,GAAiB;IAC7D,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;QACjB,KAAK,UAAU;YACb,6DAA6D;YAC7D,oCAAoC;YACpC,IAAI,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,YAAY;gBAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACxD,OAAO,CAAC,EAAE,GAAG,KAAK,EAAE,YAAY,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,CAAC,CAAA;QACpD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,8DAA8D;YAC9D,6DAA6D;YAC7D,yDAAyD;YACzD,2DAA2D;YAC3D,IAAI,KAAK,CAAC,UAAU;gBAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACxC,MAAM,OAAO,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,CAAA;YACzC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YACrB,OAAO;gBACL,EAAE,GAAG,KAAK,EAAE,YAAY,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;gBAChD,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;aACpD,CAAA;QACH,CAAC;QACD,KAAK,gBAAgB;YACnB,IAAI,CAAC,KAAK,CAAC,UAAU;gBAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACzC,OAAO,CAAC,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAA;IAChD,CAAC;AACH,CAAC;AAED,OAAO,EAAE,OAAO,EAAa,MAAM,WAAW,CAAA;AAoC9C,MAAM,UAAU,OAAO,CAAI,GAA6B,EAAE,IAAwB;IAChF,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAA;IACxE,MAAM,QAAQ,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;IACrE,OAAO;QACL,IAAI,EAAE;YACJ,YAAY,EAAE,YAAY;YAC1B,iBAAiB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU;SAC5C;QACD,KAAK,EAAE;YACL,WAAW,EAAE,OAAO;YACpB,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY;YACjC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU;YAClC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;gBACb,qDAAqD;gBACrD,yDAAyD;gBACzD,0DAA0D;gBAC1D,0DAA0D;gBAC1D,qBAAqB;gBACrB,MAAM,MAAM,GAAG,CAAC,CAAC,MAAmC,CAAA;gBACpD,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;oBAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACxE,CAAC;YACD,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;gBACf,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;oBACrC,CAAC,CAAC,cAAc,EAAE,CAAA;oBAClB,MAAM,EAAE,CAAA;gBACV,CAAC;YACH,CAAC;SACF;QACD,YAAY,EAAE;YACZ,WAAW,EAAE,QAAQ;YACrB,OAAO,EAAE,MAAM;YACf,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;gBACd,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;gBACjB,OAAO,EAAE,CAAC,UAAU,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,CAAA;YAC7D,CAAC;SACF;QACD,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;YACf,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;YACjB,OAAO,CAAC,EAAE,CAAC,UAAU,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAA;QAC5D,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["import type { AgentEffect } from './effects.js'\n\n/**\n * In-app chat composer slice. Owns the editor-state half of the\n * conversational surface (`pendingInput`, `submitting`); the\n * timeline half lives in `agentLog` (which renders the user's\n * submission as a `LogEntry { kind: 'user-input' }` alongside agent\n * actions, so the conversation reads chronologically).\n *\n * Pattern: host wires the slice into its app state via\n * `sliceHandler`, spreads `connect()`'s prop bag into the host's\n * own input/button layout, and the `Submit` Msg fires an effect\n * that the framework's effect handler turns into a WS\n * `user-input-submitted` frame + a synthesized log entry. The\n * agent's `wait_for_user_input` LAP tool picks up the frame at the\n * server.\n *\n * The composer is NOT an LLM. It's a relay surface: the user's text\n * is delivered to the user's own LLM (Claude desktop / IDE / wherever\n * is mounted on the MCP bridge), which already has cross-app context.\n * The framework just provides the in-app rendezvous so the user\n * doesn't have to alt-tab to a different window to talk to their\n * agent about the app they're looking at.\n */\nexport type AgentChatState = {\n /** Current contents of the input field. Bound to the input's `value`. */\n pendingInput: string\n /**\n * True between `Submit` and the effect handler completing the\n * frame send. Disables the submit button and (typically) the\n * input itself; reducer-driven so the UI never disagrees.\n */\n submitting: boolean\n}\n\nexport type AgentChatInitOpts = {\n /** Pre-fill the input on mount — e.g. session-restore. */\n initialInput?: string\n}\n\nexport type AgentChatMsg =\n | {\n /** Bound to the input's `oninput` event. Round-trip stays in the slice. */\n type: 'SetInput'\n value: string\n }\n | {\n /**\n * Bound to the submit button's `onClick` and the input's\n * `onKeyDown` (Enter, no shift). Reducer:\n *\n * - Empty/whitespace → no-op (no effect, no state change).\n * - Has content → clear `pendingInput`, set `submitting: true`,\n * emit `AgentChatSendInput { text, at }` effect.\n */\n type: 'Submit'\n }\n | {\n /**\n * Fired by the effect handler after the frame sends so the\n * UI re-enables the input. Always paired 1:1 with each\n * dispatched `AgentChatSendInput` effect.\n */\n type: 'SubmitComplete'\n }\n\nexport function init(opts: AgentChatInitOpts = {}): [AgentChatState, AgentEffect[]] {\n return [\n {\n pendingInput: opts.initialInput ?? '',\n submitting: false,\n },\n [],\n ]\n}\n\nexport function update(state: AgentChatState, msg: AgentChatMsg): [AgentChatState, AgentEffect[]] {\n switch (msg.type) {\n case 'SetInput':\n // No-op when the value is identical — keeps state ref stable\n // for memoization above this slice.\n if (msg.value === state.pendingInput) return [state, []]\n return [{ ...state, pendingInput: msg.value }, []]\n case 'Submit': {\n // Don't double-submit while a previous send is in flight, and\n // don't send empty/whitespace messages — neither is a useful\n // signal for the agent and both are common bounce events\n // (keyboard auto-repeat on Enter, click-then-click-again).\n if (state.submitting) return [state, []]\n const trimmed = state.pendingInput.trim()\n if (trimmed.length === 0) return [state, []]\n const at = Date.now()\n return [\n { ...state, pendingInput: '', submitting: true },\n [{ type: 'AgentChatSendInput', text: trimmed, at }],\n ]\n }\n case 'SubmitComplete':\n if (!state.submitting) return [state, []]\n return [{ ...state, submitting: false }, []]\n }\n}\n\nimport { tagSend, type Send } from '@llui/dom'\n\n/**\n * Static-prop-bag-with-reactive-accessors. Spread directly into\n * element helpers; matches the convention of the other agent\n * namespaces.\n *\n * The `input` bag carries both `oninput` and `onkeydown` because\n * the canonical chat-composer affordance is \"type, press Enter\"\n * (Shift+Enter for newline if the host wraps it themselves). Hosts\n * that want a multiline textarea can spread `input.oninput` and\n * skip `onkeydown`, wiring submit to the button only.\n */\nexport type ConnectBag<S> = {\n root: { 'data-scope': 'agent-chat'; 'data-submitting': (s: S) => boolean }\n input: {\n 'data-part': 'input'\n value: (s: S) => string\n disabled: (s: S) => boolean\n oninput: (e: Event) => void\n onkeydown: (e: KeyboardEvent) => void\n }\n submitButton: {\n 'data-part': 'submit'\n onClick: () => void\n disabled: (s: S) => boolean\n }\n /**\n * True iff the input has non-whitespace content AND we're not\n * mid-submit. Useful as the predicate for a \"send\" affordance\n * separate from the button's own `disabled` field (e.g. a\n * keyboard-shortcut hint).\n */\n canSubmit: (s: S) => boolean\n}\n\nexport function connect<S>(get: (s: S) => AgentChatState, send: Send<AgentChatMsg>): ConnectBag<S> {\n const submit = tagSend(send, ['Submit'], () => send({ type: 'Submit' }))\n const setInput = (value: string) => send({ type: 'SetInput', value })\n return {\n root: {\n 'data-scope': 'agent-chat',\n 'data-submitting': (s) => get(s).submitting,\n },\n input: {\n 'data-part': 'input',\n value: (s) => get(s).pendingInput,\n disabled: (s) => get(s).submitting,\n oninput: (e) => {\n // The cast is the standard \"input event target is an\n // HTMLInputElement / HTMLTextAreaElement\" assumption; we\n // coerce via `.value`. Hosts using non-DOM inputs (custom\n // contenteditable etc.) bypass this prop bag and dispatch\n // SetInput directly.\n const target = e.target as { value?: string } | null\n if (target && typeof target.value === 'string') setInput(target.value)\n },\n onkeydown: (e) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n submit()\n }\n },\n },\n submitButton: {\n 'data-part': 'submit',\n onClick: submit,\n disabled: (s) => {\n const cs = get(s)\n return cs.submitting || cs.pendingInput.trim().length === 0\n },\n },\n canSubmit: (s) => {\n const cs = get(s)\n return !cs.submitting && cs.pendingInput.trim().length > 0\n },\n }\n}\n"]}
1
+ {"version":3,"file":"agentChat.js","sourceRoot":"","sources":["../../src/client/agentChat.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentEffect } from './effects.js'\n\n/**\n * In-app chat composer slice. Owns the editor-state half of the\n * conversational surface (`pendingInput`, `submitting`); the\n * timeline half lives in `agentLog` (which renders the user's\n * submission as a `LogEntry { kind: 'user-input' }` alongside agent\n * actions, so the conversation reads chronologically).\n *\n * Pattern: host wires the slice into its app state via\n * `sliceHandler`, spreads `connect()`'s prop bag into the host's\n * own input/button layout, and the `Submit` Msg fires an effect\n * that the framework's effect handler turns into a WS\n * `user-input-submitted` frame + a synthesized log entry. The\n * agent's `wait_for_user_input` LAP tool picks up the frame at the\n * server.\n *\n * The composer is NOT an LLM. It's a relay surface: the user's text\n * is delivered to the user's own LLM (Claude desktop / IDE / wherever\n * is mounted on the MCP bridge), which already has cross-app context.\n * The framework just provides the in-app rendezvous so the user\n * doesn't have to alt-tab to a different window to talk to their\n * agent about the app they're looking at.\n */\nexport type AgentChatState = {\n /** Current contents of the input field. Bound to the input's `value`. */\n pendingInput: string\n /**\n * True between `Submit` and the effect handler completing the\n * frame send. Disables the submit button and (typically) the\n * input itself; reducer-driven so the UI never disagrees.\n */\n submitting: boolean\n}\n\nexport type AgentChatInitOpts = {\n /** Pre-fill the input on mount — e.g. session-restore. */\n initialInput?: string\n}\n\nexport type AgentChatMsg =\n | {\n /** Bound to the input's `onInput` event. Round-trip stays in the slice. */\n type: 'SetInput'\n value: string\n }\n | {\n /**\n * Bound to the submit button's `onClick` and the input's\n * `onKeyDown` (Enter, no shift). Reducer:\n *\n * - Empty/whitespace → no-op (no effect, no state change).\n * - Has content → clear `pendingInput`, set `submitting: true`,\n * emit `AgentChatSendInput { text, at }` effect.\n */\n type: 'Submit'\n }\n | {\n /**\n * Fired by the effect handler after the frame sends so the\n * UI re-enables the input. Always paired 1:1 with each\n * dispatched `AgentChatSendInput` effect.\n */\n type: 'SubmitComplete'\n }\n\nexport function init(opts: AgentChatInitOpts = {}): [AgentChatState, AgentEffect[]] {\n return [\n {\n pendingInput: opts.initialInput ?? '',\n submitting: false,\n },\n [],\n ]\n}\n\nexport function update(state: AgentChatState, msg: AgentChatMsg): [AgentChatState, AgentEffect[]] {\n switch (msg.type) {\n case 'SetInput':\n // No-op when the value is identical — keeps state ref stable\n // for memoization above this slice.\n if (msg.value === state.pendingInput) return [state, []]\n return [{ ...state, pendingInput: msg.value }, []]\n case 'Submit': {\n // Don't double-submit while a previous send is in flight, and\n // don't send empty/whitespace messages — neither is a useful\n // signal for the agent and both are common bounce events\n // (keyboard auto-repeat on Enter, click-then-click-again).\n if (state.submitting) return [state, []]\n const trimmed = state.pendingInput.trim()\n if (trimmed.length === 0) return [state, []]\n const at = Date.now()\n return [\n { ...state, pendingInput: '', submitting: true },\n [{ type: 'AgentChatSendInput', text: trimmed, at }],\n ]\n }\n case 'SubmitComplete':\n if (!state.submitting) return [state, []]\n return [{ ...state, submitting: false }, []]\n }\n}\n\nimport { tagSend, type Send } from '@llui/dom'\n\n/**\n * Static-prop-bag-with-reactive-accessors. Spread directly into\n * element helpers; matches the convention of the other agent\n * namespaces.\n *\n * The `input` bag carries both `onInput` and `onKeyDown` because\n * the canonical chat-composer affordance is \"type, press Enter\"\n * (Shift+Enter for newline if the host wraps it themselves). Hosts\n * that want a multiline textarea can spread `input.onInput` and\n * skip `onKeyDown`, wiring submit to the button only. Handler keys\n * are camelCase because LLui's element runtime only attaches keys\n * matching `/^on[A-Z]/` as event listeners — lowercase `oninput`\n * would silently degrade to a string attribute.\n */\nexport type ConnectBag<S> = {\n root: { 'data-scope': 'agent-chat'; 'data-submitting': (s: S) => boolean }\n input: {\n 'data-part': 'input'\n value: (s: S) => string\n disabled: (s: S) => boolean\n onInput: (e: Event) => void\n onKeyDown: (e: KeyboardEvent) => void\n }\n submitButton: {\n 'data-part': 'submit'\n onClick: () => void\n disabled: (s: S) => boolean\n }\n /**\n * True iff the input has non-whitespace content AND we're not\n * mid-submit. Useful as the predicate for a \"send\" affordance\n * separate from the button's own `disabled` field (e.g. a\n * keyboard-shortcut hint).\n */\n canSubmit: (s: S) => boolean\n}\n\nexport function connect<S>(get: (s: S) => AgentChatState, send: Send<AgentChatMsg>): ConnectBag<S> {\n const submit = tagSend(send, ['Submit'], () => send({ type: 'Submit' }))\n const setInput = (value: string) => send({ type: 'SetInput', value })\n return {\n root: {\n 'data-scope': 'agent-chat',\n 'data-submitting': (s) => get(s).submitting,\n },\n input: {\n 'data-part': 'input',\n value: (s) => get(s).pendingInput,\n disabled: (s) => get(s).submitting,\n // Event handler keys MUST be camelCase: LLui's elements pass only\n // keys matching `/^on[A-Z]/` to `addEventListener` (see\n // packages/dom/src/elements.ts). Lowercase `oninput` would silently\n // be set as a string attribute and never fire — the input field\n // would freeze with no SetInput dispatches and the submit button\n // would stay disabled forever.\n onInput: (e) => {\n // The cast is the standard \"input event target is an\n // HTMLInputElement / HTMLTextAreaElement\" assumption; we\n // coerce via `.value`. Hosts using non-DOM inputs (custom\n // contenteditable etc.) bypass this prop bag and dispatch\n // SetInput directly.\n const target = e.target as { value?: string } | null\n if (target && typeof target.value === 'string') setInput(target.value)\n },\n onKeyDown: (e) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n submit()\n }\n },\n },\n submitButton: {\n 'data-part': 'submit',\n onClick: submit,\n disabled: (s) => {\n const cs = get(s)\n return cs.submitting || cs.pendingInput.trim().length === 0\n },\n },\n canSubmit: (s) => {\n const cs = get(s)\n return !cs.submitting && cs.pendingInput.trim().length > 0\n },\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llui/agent",
3
- "version": "0.0.50",
3
+ "version": "0.0.51",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -42,12 +42,12 @@
42
42
  "ws": "^8.18.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@llui/dom": "^0.0.36"
45
+ "@llui/dom": "^0.0.37"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^22.0.0",
49
49
  "@types/ws": "^8.5.13",
50
- "@llui/dom": "0.0.36"
50
+ "@llui/dom": "0.0.37"
51
51
  },
52
52
  "description": "LLui Agent — LAP server + browser client runtime for driving LLui apps from LLM clients",
53
53
  "keywords": [