@ai-matrx/chat 0.4.21 → 0.4.22

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.4.22
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/chat/v0.4.21..npm/chat/v0.4.22 -- apps/shared/chat`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.4.21 — Queued lines show their wait and their delivery
10
+
11
+ - The waiting-messages strip shows how long each queued line has waited (a live 1 s timer), and
12
+ when the agent takes a line it flashes "Delivered · waited 12s" for 1.6 s before clearing.
13
+ New slice actions `deliverInboxItem` / `clearDeliveredInboxItem` and selector
14
+ `selectDeliveredInboxItems`; delivery still leaves the queue exactly like a removal, so
15
+ `selectInboxItemStatus` reads `null` on delivery as before. Ported from the browser extension.
16
+
3
17
  ## 0.4.21
4
18
 
5
19
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -20,6 +20,13 @@ import React from "react";
20
20
  interface InboxQueueStripProps {
21
21
  conversationId: string;
22
22
  }
23
+ /** How long a delivered line keeps its "Delivered" moment before it clears (ms). */
24
+ export declare const DELIVERED_LINGER_MS = 1600;
25
+ /**
26
+ * How long a line has waited — kit's `compact` elapsed voice, floored to whole
27
+ * seconds and rounded DOWN so the 1 s ticker never shows time that has not passed.
28
+ */
29
+ export declare function waitedLabel(fromIso: string, toMs: number): string;
23
30
  /**
24
31
  * Delivered collab notes are prefixed "[Collaboration note] Agent '<name>' …"
25
32
  * server-side; the queued text carries the same prefix. Pull the agent name
@@ -1,8 +1,10 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import React, { useState } from "react";
3
+ import React, { useEffect, useState } from "react";
4
+ import { formatDurationSeconds } from "@ai-matrx/kit/format";
4
5
  import {
5
6
  ArrowUp,
7
+ Check,
6
8
  CircleHelp,
7
9
  Clock,
8
10
  Handshake,
@@ -19,7 +21,10 @@ import {
19
21
  TooltipContent,
20
22
  TooltipTrigger
21
23
  } from "@ai-matrx/design-system";
22
- import { selectInboxItems } from "../../../redux/execution-system/inbox/inbox.selectors.js";
24
+ import {
25
+ selectDeliveredInboxItems,
26
+ selectInboxItems
27
+ } from "../../../redux/execution-system/inbox/inbox.selectors.js";
23
28
  import { TERMINAL_RUNTIME_STATUSES } from "../../../runtime-reconnect/types.js";
24
29
  import { selectIsExecuting } from "../../../redux/execution-system/selectors/aggregate.selectors.js";
25
30
  import {
@@ -29,11 +34,27 @@ import {
29
34
  editInboxItem
30
35
  } from "../../../redux/execution-system/inbox/inbox.thunks.js";
31
36
  import {
37
+ clearDeliveredInboxItem,
32
38
  removeInboxItem
33
39
  } from "../../../redux/execution-system/inbox/inbox.slice.js";
34
40
  import { Button } from "@ai-matrx/design-system/controls";
35
41
  import { useConversationAbilities } from "../../../redux/execution-system/host-conversations/conversation-abilities.js";
36
42
  const queueActionClassName = "h-11 w-11 min-h-11 min-w-11 shrink-0 rounded p-0 text-muted-foreground hover:bg-muted hover:text-foreground lg:h-8 lg:w-8 lg:min-h-8 lg:min-w-8";
43
+ const DELIVERED_LINGER_MS = 1600;
44
+ function waitedLabel(fromIso, toMs) {
45
+ const seconds = Math.max(0, Math.floor((toMs - Date.parse(fromIso)) / 1e3));
46
+ return formatDurationSeconds(seconds, { style: "compact", round: "down" });
47
+ }
48
+ function useSecondTicker(active) {
49
+ const [now, setNow] = useState(() => Date.now());
50
+ useEffect(() => {
51
+ if (!active) return;
52
+ setNow(Date.now());
53
+ const id = setInterval(() => setNow(Date.now()), 1e3);
54
+ return () => clearInterval(id);
55
+ }, [active]);
56
+ return now;
57
+ }
37
58
  function statusLabel(item, running) {
38
59
  if (item.status === "failed") return item.error ?? "Failed to send";
39
60
  if (item.status === "sending") return "Sending\u2026";
@@ -71,7 +92,22 @@ function InboxQueueStripBody({ conversationId }) {
71
92
  /* @__PURE__ */ new Set()
72
93
  );
73
94
  const visible = items.filter((i) => i.isVisibleToUser || isCollabNoteItem(i));
74
- if (visible.length === 0) return null;
95
+ const delivered = useAppSelector(selectDeliveredInboxItems(conversationId));
96
+ const hasWaiting = visible.some(
97
+ (i) => (i.status === "pending" || i.status === "sending") && !isCollabNoteItem(i)
98
+ );
99
+ const now = useSecondTicker(hasWaiting);
100
+ useEffect(() => {
101
+ if (delivered.length === 0) return;
102
+ const timers = delivered.map(
103
+ (d) => setTimeout(
104
+ () => dispatch(clearDeliveredInboxItem({ conversationId, injectionId: d.injectionId })),
105
+ Math.max(0, DELIVERED_LINGER_MS - (Date.now() - Date.parse(d.deliveredAt)))
106
+ )
107
+ );
108
+ return () => timers.forEach(clearTimeout);
109
+ }, [delivered, conversationId, dispatch]);
110
+ if (visible.length === 0 && delivered.length === 0) return null;
75
111
  const isDeparting = (item) => !isCollabNoteItem(item) && (item.mode === "steer" || promotingIds.has(item.injectionId));
76
112
  const departing = visible.filter(isDeparting);
77
113
  const waiting = visible.filter((item) => !isDeparting(item));
@@ -142,13 +178,16 @@ function InboxQueueStripBody({ conversationId }) {
142
178
  )
143
179
  ] }),
144
180
  /* @__PURE__ */ jsxs(Tooltip, { children: [
145
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
181
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
146
182
  "span",
147
183
  {
148
184
  tabIndex: 0,
149
185
  role: "status",
150
186
  className: `block truncate rounded-sm text-[11px] leading-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring lg:cursor-help ${failed ? "text-destructive" : "text-muted-foreground"}`,
151
- children: promoting ? "Sending\u2026" : statusLabel(item, running)
187
+ children: [
188
+ promoting ? "Sending\u2026" : statusLabel(item, running),
189
+ (item.status === "pending" || item.status === "sending") && !collabNote ? ` \xB7 ${waitedLabel(item.queuedAt, now)}` : null
190
+ ]
152
191
  }
153
192
  ) }),
154
193
  /* @__PURE__ */ jsx(TooltipContent, { side: "top", className: "max-w-[20rem]", children: statusHelp })
@@ -262,6 +301,20 @@ function InboxQueueStripBody({ conversationId }) {
262
301
  )
263
302
  ] }, item.injectionId);
264
303
  }),
304
+ delivered.map((d) => /* @__PURE__ */ jsxs(
305
+ "div",
306
+ {
307
+ "data-inbox-delivered": "",
308
+ role: "status",
309
+ className: "flex min-w-0 items-center gap-2 rounded-lg border border-emerald-500/30 bg-emerald-500/[0.08] px-3 py-2 text-xs motion-safe:animate-out motion-safe:fade-out motion-safe:delay-1000 motion-safe:duration-500",
310
+ children: [
311
+ /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" }),
312
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-foreground", children: d.text }),
313
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[11px] text-muted-foreground", children: `Delivered \xB7 waited ${waitedLabel(d.queuedAt, Date.parse(d.deliveredAt))}` })
314
+ ]
315
+ },
316
+ `delivered-${d.injectionId}`
317
+ )),
265
318
  /* @__PURE__ */ jsx(
266
319
  TextInputDialog,
267
320
  {
@@ -296,6 +349,8 @@ function InboxQueueStripBody({ conversationId }) {
296
349
  ] });
297
350
  }
298
351
  export {
352
+ DELIVERED_LINGER_MS,
299
353
  InboxQueueStrip,
300
- parseCollabNoteAgent
354
+ parseCollabNoteAgent,
355
+ waitedLabel
301
356
  };
@@ -2,7 +2,9 @@
2
2
  * conversationInbox selectors — queued-while-running message cards.
3
3
  */
4
4
  import type { ChatRootState } from "../../../../store/root-state.js";
5
- import type { ConversationInboxItem } from "./inbox.slice.js";
5
+ import type { ConversationInboxItem, DeliveredInboxItem } from "./inbox.slice.js";
6
+ /** Lines the agent just took, still showing their "Delivered" moment. Stable empty reference. */
7
+ export declare const selectDeliveredInboxItems: (conversationId: string) => (state: ChatRootState) => DeliveredInboxItem[];
6
8
  /** All queued items for a conversation, FIFO. Stable empty reference. */
7
9
  export declare const selectInboxItems: (conversationId: string) => (state: ChatRootState) => ConversationInboxItem[];
8
10
  /** Count of items still waiting (sending + pending). Primitive — no memo needed. */
@@ -1,4 +1,6 @@
1
1
  const EMPTY_ITEMS = [];
2
+ const EMPTY_DELIVERED = [];
3
+ const selectDeliveredInboxItems = (conversationId) => (state) => state.conversationInbox?.deliveredByConversationId?.[conversationId] ?? EMPTY_DELIVERED;
2
4
  const selectInboxItems = (conversationId) => (state) => state.conversationInbox?.byConversationId[conversationId] ?? EMPTY_ITEMS;
3
5
  const selectInboxWaitingCount = (conversationId) => (state) => {
4
6
  const items = state.conversationInbox?.byConversationId[conversationId];
@@ -17,6 +19,7 @@ function isStrandablePersonLine(item) {
17
19
  }
18
20
  export {
19
21
  isStrandablePersonLine,
22
+ selectDeliveredInboxItems,
20
23
  selectInboxItemStatus,
21
24
  selectInboxItems,
22
25
  selectInboxWaitingCount
@@ -49,9 +49,20 @@ export interface ConversationInboxItem {
49
49
  */
50
50
  source?: string | null;
51
51
  }
52
+ /** A queued line the agent just took — shown as "Delivered" for a moment, then cleared. */
53
+ export interface DeliveredInboxItem {
54
+ injectionId: string;
55
+ text: string;
56
+ mode: InboxItemMode;
57
+ queuedAt: string;
58
+ /** ISO timestamp — when `injection_consumed` named it. */
59
+ deliveredAt: string;
60
+ }
52
61
  export interface ConversationInboxState {
53
62
  /** FIFO per conversation — order mirrors server `enqueued_seq`. */
54
63
  byConversationId: Record<string, ConversationInboxItem[]>;
64
+ /** Items the stream just delivered, per conversation (the strip clears each after a beat). */
65
+ deliveredByConversationId?: Record<string, DeliveredInboxItem[]>;
55
66
  }
56
67
  export declare const addInboxItem: import("@reduxjs/toolkit").ActionCreatorWithPayload<ConversationInboxItem, "conversationInbox/addInboxItem">, confirmInboxItem: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
57
68
  conversationId: string;
@@ -68,7 +79,14 @@ export declare const addInboxItem: import("@reduxjs/toolkit").ActionCreatorWithP
68
79
  }, "conversationInbox/setInboxItemText">, removeInboxItem: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
69
80
  conversationId: string;
70
81
  injectionId: string;
71
- }, "conversationInbox/removeInboxItem">, hydrateInboxItems: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
82
+ }, "conversationInbox/removeInboxItem">, deliverInboxItem: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
83
+ conversationId: string;
84
+ injectionId: string;
85
+ deliveredAt?: string;
86
+ }, "conversationInbox/deliverInboxItem">, clearDeliveredInboxItem: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
87
+ conversationId: string;
88
+ injectionId: string;
89
+ }, "conversationInbox/clearDeliveredInboxItem">, hydrateInboxItems: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
72
90
  conversationId: string;
73
91
  items: ConversationInboxItem[];
74
92
  }, "conversationInbox/hydrateInboxItems">;
@@ -1,7 +1,8 @@
1
1
  import { createSlice } from "@reduxjs/toolkit";
2
2
  import { destroyInstance } from "../conversations/conversations.slice.js";
3
3
  const initialState = {
4
- byConversationId: {}
4
+ byConversationId: {},
5
+ deliveredByConversationId: {}
5
6
  };
6
7
  function bucket(state, conversationId) {
7
8
  if (!state.byConversationId[conversationId]) {
@@ -64,6 +65,35 @@ const conversationInboxSlice = createSlice({
64
65
  (i) => i.injectionId !== injectionId
65
66
  );
66
67
  },
68
+ /**
69
+ * The stream delivered an item (`injection_consumed`): it leaves the queue
70
+ * exactly like `removeInboxItem` (so `selectInboxItemStatus` reads null — the
71
+ * delivery ack), and a visible person line is remembered as just delivered.
72
+ */
73
+ deliverInboxItem(state, action) {
74
+ const { conversationId, injectionId } = action.payload;
75
+ const items = state.byConversationId[conversationId];
76
+ const item = items?.find((i) => i.injectionId === injectionId);
77
+ if (!items || !item) return;
78
+ state.byConversationId[conversationId] = items.filter((i) => i.injectionId !== injectionId);
79
+ if (!item.isVisibleToUser || item.source === "agent_collab") return;
80
+ const delivered = state.deliveredByConversationId ??= {};
81
+ (delivered[conversationId] ??= []).push({
82
+ injectionId,
83
+ text: item.text,
84
+ mode: item.mode,
85
+ queuedAt: item.queuedAt,
86
+ deliveredAt: action.payload.deliveredAt ?? (/* @__PURE__ */ new Date()).toISOString()
87
+ });
88
+ },
89
+ /** The "Delivered" moment is over — drop it. */
90
+ clearDeliveredInboxItem(state, action) {
91
+ const list = state.deliveredByConversationId?.[action.payload.conversationId];
92
+ if (!list) return;
93
+ state.deliveredByConversationId[action.payload.conversationId] = list.filter(
94
+ (i) => i.injectionId !== action.payload.injectionId
95
+ );
96
+ },
67
97
  /**
68
98
  * Replace the pending set from a server list (`GET …/inbox?status=pending`)
69
99
  * — reopen-mid-run rehydration. Local `sending`/`failed` items are kept
@@ -80,6 +110,7 @@ const conversationInboxSlice = createSlice({
80
110
  extraReducers: (builder) => {
81
111
  builder.addCase(destroyInstance, (state, action) => {
82
112
  delete state.byConversationId[action.payload];
113
+ delete state.deliveredByConversationId?.[action.payload];
83
114
  });
84
115
  }
85
116
  });
@@ -89,13 +120,17 @@ const {
89
120
  failInboxItem,
90
121
  setInboxItemText,
91
122
  removeInboxItem,
123
+ deliverInboxItem,
124
+ clearDeliveredInboxItem,
92
125
  hydrateInboxItems
93
126
  } = conversationInboxSlice.actions;
94
127
  var inbox_slice_default = conversationInboxSlice.reducer;
95
128
  export {
96
129
  addInboxItem,
130
+ clearDeliveredInboxItem,
97
131
  confirmInboxItem,
98
132
  inbox_slice_default as default,
133
+ deliverInboxItem,
99
134
  failInboxItem,
100
135
  hydrateInboxItems,
101
136
  removeInboxItem,
@@ -132,7 +132,7 @@ import {
132
132
  hasDisplayableMessageContent,
133
133
  refetchSingleMessage
134
134
  } from "../message-crud/refetch-single-message.thunk.js";
135
- import { removeInboxItem } from "../inbox/inbox.slice.js";
135
+ import { deliverInboxItem } from "../inbox/inbox.slice.js";
136
136
  import {
137
137
  selectConversationMessages,
138
138
  selectMessageCount,
@@ -2034,7 +2034,7 @@ async function processStream({
2034
2034
  }
2035
2035
  for (const item of event.data.items ?? []) {
2036
2036
  dispatch(
2037
- removeInboxItem({
2037
+ deliverInboxItem({
2038
2038
  conversationId,
2039
2039
  injectionId: item.injection_id
2040
2040
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.4.21",
3
+ "version": "0.4.22",
4
4
  "description": "THE one AI Matrx person-to-agent chat system: the full chat (running agents, conversations, tool calls, streaming, context, surfaces, voice) as per-module subpaths @ai-matrx/chat/<domain>/<module> over a host contract (/host, /host/react) whose only required value is a db; plus the kernel compatibility entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) until P28.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -227,17 +227,17 @@
227
227
  "uuid": "^14.0.2",
228
228
  "zod": "^4.6.5",
229
229
  "@ai-matrx/alchemy": "latest",
230
- "@ai-matrx/browser-audio": "latest",
231
230
  "@ai-matrx/associations": "latest",
231
+ "@ai-matrx/browser-audio": "latest",
232
+ "@ai-matrx/agents": "latest",
232
233
  "@ai-matrx/canvas": "latest",
233
234
  "@ai-matrx/content-ir": "latest",
234
235
  "@ai-matrx/content-ir-react": "latest",
235
236
  "@ai-matrx/data": "latest",
236
237
  "@ai-matrx/design-system": "latest",
237
238
  "@ai-matrx/diff": "latest",
238
- "@ai-matrx/icons": "latest",
239
239
  "@ai-matrx/kit": "latest",
240
- "@ai-matrx/media": "latest",
240
+ "@ai-matrx/icons": "latest",
241
241
  "@ai-matrx/print": "latest",
242
242
  "@ai-matrx/realtime": "latest",
243
243
  "@ai-matrx/records": "latest",
@@ -245,7 +245,7 @@
245
245
  "@ai-matrx/rich-content": "latest",
246
246
  "@ai-matrx/rich-editor": "latest",
247
247
  "@ai-matrx/tap-target": "latest",
248
- "@ai-matrx/agents": "latest"
248
+ "@ai-matrx/media": "latest"
249
249
  },
250
250
  "devDependencies": {
251
251
  "@arethetypeswrong/cli": "^0.18.5",