@ai-matrx/chat 0.6.4 → 0.6.6

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/CHANGELOG.md +55 -0
  2. package/dist/agents/components/chat/ChatHistorySidebar.d.ts +4 -4
  3. package/dist/agents/components/chat/ChatHistorySidebar.js +2 -2
  4. package/dist/agents/components/chat/ChatSidebarMenu.js +1 -1
  5. package/dist/agents/components/conversation-actions/conversationActionRegistry.d.ts +8 -0
  6. package/dist/agents/components/conversation-actions/conversationActionRegistry.js +11 -4
  7. package/dist/agents/components/conversation-history/ConversationArchiveSection.d.ts +7 -0
  8. package/dist/agents/components/conversation-history/{ConversationTrashSection.js → ConversationArchiveSection.js} +20 -20
  9. package/dist/agents/components/conversation-history/ConversationHistorySidebar.d.ts +29 -10
  10. package/dist/agents/components/conversation-history/ConversationHistorySidebar.js +75 -76
  11. package/dist/agents/components/conversation-history/ConversationPickerWindow.js +1 -2
  12. package/dist/agents/components/inputs/smart-input/RunControlsTabPanel.d.ts +2 -0
  13. package/dist/agents/components/inputs/smart-input/RunControlsTabPanel.js +8 -1
  14. package/dist/agents/components/inputs/smart-input/RunToolPicker.js +2 -1
  15. package/dist/agents/components/run/AgentRunHeader.js +11 -5
  16. package/dist/agents/components/shared/AgentHeaderMobile.js +1 -0
  17. package/dist/agents/components/shared/AgentModeController.js +90 -27
  18. package/dist/agents/components/shared/AgentSelectorIsland.js +2 -1
  19. package/dist/agents/redux/agent-definition/run-tier-record.js +1 -0
  20. package/dist/agents/redux/conversation-history/history-filters.d.ts +6 -0
  21. package/dist/agents/redux/conversation-history/history-filters.js +4 -1
  22. package/dist/agents/redux/conversation-history/selectors.js +2 -3
  23. package/dist/agents/redux/conversation-history/slice.d.ts +6 -9
  24. package/dist/agents/redux/conversation-history/slice.js +14 -23
  25. package/dist/agents/redux/conversation-history/thunks.js +1 -23
  26. package/dist/agents/redux/conversation-history/types.d.ts +12 -12
  27. package/dist/agents/redux/conversation-history/types.js +4 -6
  28. package/dist/agents/redux/conversation-list/conversation-list.selectors.d.ts +6 -0
  29. package/dist/agents/redux/conversation-list/conversation-list.selectors.js +10 -0
  30. package/dist/agents/redux/conversation-list/conversation-list.types.d.ts +6 -0
  31. package/dist/agents/redux/conversation-list/conversation-row-actions.thunks.js +11 -2
  32. package/dist/agents/redux/conversation-list/conversation-trash.thunks.d.ts +11 -4
  33. package/dist/agents/redux/conversation-list/conversation-trash.thunks.js +41 -20
  34. package/dist/agents/redux/execution-system/message-crud/soft-delete-conversation.thunk.js +5 -2
  35. package/dist/window-panels/windows/agents/AgentRunWindow.d.ts +1 -1
  36. package/dist/window-panels/windows/agents/AgentRunWindow.js +27 -21
  37. package/dist/window-panels/windows/agents/RunControlsWindow.js +1 -0
  38. package/package.json +6 -6
  39. package/dist/agents/components/conversation-history/ConversationTrashSection.d.ts +0 -7
@@ -153,6 +153,9 @@ function useRunControlsState(conversationId, includeAttach) {
153
153
  if (tabId === "tools" && addedCount > 0) {
154
154
  return /* @__PURE__ */ jsx(Badge, { tone: "primary", children: addedCount });
155
155
  }
156
+ if (tabId === "skills" && addedSkillsCount > 0) {
157
+ return /* @__PURE__ */ jsx(Badge, { tone: "primary", children: addedSkillsCount });
158
+ }
156
159
  if (tabId === "model" && hasModelOverride) {
157
160
  return /* @__PURE__ */ jsx(TabStatusDot, { label: "overridden" });
158
161
  }
@@ -169,7 +172,11 @@ function useRunControlsState(conversationId, includeAttach) {
169
172
  defaultTab,
170
173
  resolveTab,
171
174
  tabTrailing,
172
- addedCount,
175
+ // EVERYTHING the person added to this run (tools + skills): the + control's
176
+ // label and tint read this one number, so Skills count exactly like Tools.
177
+ addedCount: addedCount + addedSkillsCount,
178
+ addedToolsCount: addedCount,
179
+ addedSkillsCount,
173
180
  hasModelOverride,
174
181
  hasActiveContext,
175
182
  anyDocActive,
@@ -38,6 +38,7 @@ import { DEFAULT_BUILDER_ADVANCED_SETTINGS } from "../../../types/instance.types
38
38
  import { filterAndSortBySearch } from "@ai-matrx/kit/search-scoring";
39
39
  import { getToolDisplayName } from "../../../../tool-call-visualization/registry/registry.js";
40
40
  import { groupToolCatalog, toolCategoryLabel } from "./run-tool-catalog.js";
41
+ import { isBundleListerName } from "@ai-matrx/agents/tools";
41
42
  import { loadAvailableTools, selectAllTools, selectToolsError, selectToolsStatus, useToolCatalog } from "../../../identity/tool-catalog.js";
42
43
  function RunToolPicker({ conversationId }) {
43
44
  const dispatch = useAppDispatch();
@@ -123,7 +124,7 @@ function RunToolPicker({ conversationId }) {
123
124
  }
124
125
  })
125
126
  );
126
- const list = tools ?? [];
127
+ const list = (tools ?? []).filter((t) => !isBundleListerName(t.name));
127
128
  const toolMap = new Map(list.map((t) => [t.id, t]));
128
129
  const builtInIds = Array.isArray(agentToolIds) ? agentToolIds : [];
129
130
  const agentOwned = new Set(builtInIds);
@@ -15,7 +15,7 @@ function AgentRunHeader({
15
15
  currentPath
16
16
  }) {
17
17
  return /* @__PURE__ */ jsxs(Fragment, { children: [
18
- /* @__PURE__ */ jsx("div", { className: "lg:hidden w-full", children: /* @__PURE__ */ jsx(
18
+ /* @__PURE__ */ jsx("div", { className: "w-full @min-[44rem]/shell-header:hidden", children: /* @__PURE__ */ jsx(
19
19
  AgentHeaderMobile,
20
20
  {
21
21
  agentId,
@@ -24,8 +24,8 @@ function AgentRunHeader({
24
24
  extraActions: /* @__PURE__ */ jsx(AgentNewRunButton, { surfaceKey })
25
25
  }
26
26
  ) }),
27
- /* @__PURE__ */ jsxs("div", { className: "@container/agent-header hidden lg:flex items-center justify-between w-full gap-2 shrink-0", children: [
28
- /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
27
+ /* @__PURE__ */ jsxs("div", { className: "@container/agent-header hidden @min-[44rem]/shell-header:flex items-center justify-between w-full min-w-0 gap-2", children: [
28
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 shrink items-center", children: [
29
29
  /* @__PURE__ */ jsx(ChevronLeftTapButton, { variant: "transparent", href: backHref, "aria-label": "Back to Agents" }),
30
30
  /* @__PURE__ */ jsx("div", { className: "flex min-w-0 items-center ps-[calc(var(--matrx-tap-gap)/2)]", children: /* @__PURE__ */ jsx(
31
31
  AgentSelectorIsland,
@@ -42,7 +42,7 @@ function AgentRunHeader({
42
42
  /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "w-[calc(var(--matrx-tap-gap)/2)] shrink-0" }),
43
43
  /* @__PURE__ */ jsx(AgentNewRunButton, { surfaceKey })
44
44
  ] }),
45
- /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
45
+ /* @__PURE__ */ jsx("div", { className: "shrink-0", children: /* @__PURE__ */ jsx(
46
46
  AgentModeController,
47
47
  {
48
48
  agentId,
@@ -51,7 +51,13 @@ function AgentRunHeader({
51
51
  }
52
52
  ) }),
53
53
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 pt-0.5 shrink-0", children: [
54
- /* @__PURE__ */ jsx(AgentSaveStatus, { agentId }),
54
+ /* @__PURE__ */ jsx(
55
+ AgentSaveStatus,
56
+ {
57
+ agentId,
58
+ versionClassName: "hidden @min-[40rem]/agent-header:inline"
59
+ }
60
+ ),
55
61
  basePath === "/agents" && /* @__PURE__ */ jsx(ReviewAnswersLink, { agentId }),
56
62
  /* @__PURE__ */ jsx(AgentOptionsMenu, { agentId, basePath })
57
63
  ] })
@@ -196,6 +196,7 @@ function AgentHeaderMobile({
196
196
  }
197
197
  )
198
198
  ] }) }),
199
+ extraActions,
199
200
  /* @__PURE__ */ jsx(AgentSaveTapButton, { agentId }),
200
201
  /* @__PURE__ */ jsx(AgentOptionsMenu, { agentId, asTapTarget: true, basePath })
201
202
  ] }),
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useState, useTransition } from "react";
3
+ import { useRef, useState, useTransition } from "react";
4
4
  import { useRouter, usePathname, Link } from "../../../host/navigation.js";
5
5
  import {
6
6
  Activity,
@@ -9,6 +9,7 @@ import {
9
9
  History,
10
10
  Layers,
11
11
  LayoutGrid,
12
+ MoreHorizontal,
12
13
  Pencil,
13
14
  Play,
14
15
  Plus,
@@ -26,7 +27,11 @@ import {
26
27
  AlertDialogDescription,
27
28
  AlertDialogFooter,
28
29
  AlertDialogHeader,
29
- AlertDialogTitle
30
+ AlertDialogTitle,
31
+ DropdownMenu,
32
+ DropdownMenuContent,
33
+ DropdownMenuItem,
34
+ DropdownMenuTrigger
30
35
  } from "@ai-matrx/design-system";
31
36
  import {
32
37
  NavItemTooltip,
@@ -46,6 +51,20 @@ const MODES = [
46
51
  { id: "widgets", label: "Widgets", icon: LayoutGrid },
47
52
  { id: "new", label: "New", icon: Plus }
48
53
  ];
54
+ const INLINE_FROM = {
55
+ run: "",
56
+ edit: "hidden @min-[500px]/agent-header:flex",
57
+ view: "hidden @min-[540px]/agent-header:flex",
58
+ runs: "hidden @min-[580px]/agent-header:flex",
59
+ versions: "hidden @min-[620px]/agent-header:flex",
60
+ hindsight: "hidden @min-[660px]/agent-header:flex",
61
+ shortcuts: "hidden @min-[700px]/agent-header:flex",
62
+ apps: "hidden @min-[740px]/agent-header:flex",
63
+ surfaces: "hidden @min-[780px]/agent-header:flex",
64
+ widgets: "hidden @min-[820px]/agent-header:flex",
65
+ new: "hidden @min-[860px]/agent-header:flex"
66
+ };
67
+ const MORE_HIDDEN_FROM = "@min-[860px]/agent-header:hidden";
49
68
  function getAgentModeHref(mode, agentId, basePath) {
50
69
  if (mode === "new") return `${basePath}/new`;
51
70
  const map = {
@@ -112,32 +131,76 @@ function AgentModeController({
112
131
  navigateTo(getAgentModeHref(next, agentId, basePath));
113
132
  };
114
133
  const getModeHref = (id) => getAgentModeHref(id, agentId, basePath);
134
+ const inlineRefs = useRef({});
135
+ const [folded, setFolded] = useState([]);
136
+ const readFolded = () => setFolded(
137
+ MODES.map((m) => m.id).filter((id) => {
138
+ const el = inlineRefs.current[id];
139
+ return !el || el.getClientRects().length === 0;
140
+ })
141
+ );
115
142
  return /* @__PURE__ */ jsxs(Fragment, { children: [
116
- /* @__PURE__ */ jsx(NavTooltipProvider, { children: /* @__PURE__ */ jsx("div", { className: "matrx-glass-thin-border flex items-center gap-0 rounded-full p-0.5", children: MODES.map(({ id, label, icon: Icon }) => {
117
- const isActive = id === mode;
118
- return /* @__PURE__ */ jsx(NavItemTooltip, { label, children: /* @__PURE__ */ jsxs(
119
- Link,
120
- {
121
- href: getModeHref(id),
122
- onClick: (e) => {
123
- if (e.metaKey || e.ctrlKey) return;
124
- e.preventDefault();
125
- handleModeChange(id);
126
- },
127
- "aria-label": label,
128
- className: cn(
129
- "flex items-center justify-center gap-1 py-0.5 text-[0.6875rem] font-medium rounded-full transition-colors cursor-pointer",
130
- "px-2.5",
131
- "[&_svg]:w-3.5 [&_svg]:h-3.5",
132
- isActive ? NAV_ITEM_SELECTED : NAV_ITEM_UNSELECTED
133
- ),
134
- children: [
135
- /* @__PURE__ */ jsx(Icon, {}),
136
- /* @__PURE__ */ jsx("span", { className: "hidden @min-[1240px]/agent-header:inline", children: label })
137
- ]
138
- }
139
- ) }, id);
140
- }) }) }),
143
+ /* @__PURE__ */ jsx(NavTooltipProvider, { children: /* @__PURE__ */ jsxs("div", { className: "matrx-glass-thin-border flex items-center gap-0 rounded-full p-0.5", children: [
144
+ MODES.map(({ id, label, icon: Icon }) => {
145
+ const isActive = id === mode;
146
+ return /* @__PURE__ */ jsx(NavItemTooltip, { label, children: /* @__PURE__ */ jsxs(
147
+ Link,
148
+ {
149
+ ref: (el) => {
150
+ inlineRefs.current[id] = el;
151
+ },
152
+ href: getModeHref(id),
153
+ onClick: (e) => {
154
+ if (e.metaKey || e.ctrlKey) return;
155
+ e.preventDefault();
156
+ handleModeChange(id);
157
+ },
158
+ "aria-label": label,
159
+ className: cn(
160
+ "flex items-center justify-center gap-1 py-0.5 text-[0.6875rem] font-medium rounded-full transition-colors cursor-pointer",
161
+ "px-2.5",
162
+ "[&_svg]:w-3.5 [&_svg]:h-3.5",
163
+ isActive ? NAV_ITEM_SELECTED : NAV_ITEM_UNSELECTED,
164
+ !isActive && INLINE_FROM[id]
165
+ ),
166
+ children: [
167
+ /* @__PURE__ */ jsx(Icon, {}),
168
+ /* @__PURE__ */ jsx("span", { className: "hidden @min-[1240px]/agent-header:inline", children: label })
169
+ ]
170
+ }
171
+ ) }, id);
172
+ }),
173
+ /* @__PURE__ */ jsxs(DropdownMenu, { onOpenChange: (open) => open && readFolded(), children: [
174
+ /* @__PURE__ */ jsx(NavItemTooltip, { label: "More", children: /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
175
+ "button",
176
+ {
177
+ type: "button",
178
+ "aria-label": "More modes",
179
+ className: cn(
180
+ "flex items-center justify-center py-0.5 px-2.5 rounded-full transition-colors cursor-pointer",
181
+ "[&_svg]:w-3.5 [&_svg]:h-3.5",
182
+ NAV_ITEM_UNSELECTED,
183
+ MORE_HIDDEN_FROM
184
+ ),
185
+ children: /* @__PURE__ */ jsx(MoreHorizontal, {})
186
+ }
187
+ ) }) }),
188
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", className: "min-w-40", children: MODES.filter((m) => folded.includes(m.id)).map(
189
+ ({ id, label, icon: Icon }) => /* @__PURE__ */ jsxs(
190
+ DropdownMenuItem,
191
+ {
192
+ onSelect: () => handleModeChange(id),
193
+ className: "gap-2",
194
+ children: [
195
+ /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 text-muted-foreground" }),
196
+ label
197
+ ]
198
+ },
199
+ id
200
+ )
201
+ ) })
202
+ ] })
203
+ ] }) }),
141
204
  /* @__PURE__ */ jsx(AlertDialog, { open: showDirtyDialog, onOpenChange: setShowDirtyDialog, children: /* @__PURE__ */ jsxs(AlertDialogContent, { children: [
142
205
  /* @__PURE__ */ jsxs(AlertDialogHeader, { children: [
143
206
  /* @__PURE__ */ jsx(AlertDialogTitle, { children: "Unsaved Changes" }),
@@ -44,7 +44,8 @@ function AgentSelectorIsland({
44
44
  label: displayName,
45
45
  activeAgentId: agentId,
46
46
  triggerSlot,
47
- noBorder: true
47
+ noBorder: true,
48
+ className: "min-w-0 shrink"
48
49
  }
49
50
  ),
50
51
  showVersion && /* @__PURE__ */ jsxs("span", { className: "text-[0.625rem] font-medium text-[var(--shell-nav-text)] tabular-nums shrink-0", children: [
@@ -45,6 +45,7 @@ function completeAgentRecord(record, state) {
45
45
  if (unread("contextPolicies")) fill("contextPolicies", rt.context_policies ?? []);
46
46
  if (unread("autoContextDisabled")) fill("autoContextDisabled", rt.auto_context_disabled === true);
47
47
  if (rt.model_id && unread("modelId")) fill("modelId", rt.model_id);
48
+ if (rt.settings && unread("settings")) fill("settings", rt.settings);
48
49
  if (unread("uiGates")) fill("uiGates", rt.ui_gates ?? {});
49
50
  if (rt.tool_ids && unread("tools")) fill("tools", rt.tool_ids);
50
51
  if (!out.name && unread("name")) fill("name", rt.name);
@@ -15,6 +15,12 @@ export interface HistoryFilterInput {
15
15
  includeOriginClasses: string[];
16
16
  /** Lane gate. `null` = no gate; `[]` = every lane off. */
17
17
  includeLanes: ConversationLane[] | null;
18
+ /**
19
+ * Only these conversations (`id in (...)`): a surface whose list is "the
20
+ * conversations that belong to THIS thing" (a board's). `null`/absent = no
21
+ * restriction; `[]` = none belong, so nothing is fetched.
22
+ */
23
+ onlyConversationIds?: string[] | null;
18
24
  }
19
25
  /** The slice of the PostgREST filter builder the history filters use. */
20
26
  export interface HistoryFilterable<Q> {
@@ -4,7 +4,9 @@ function excludeHiddenLane(query) {
4
4
  }
5
5
  function applyHistoryFilters(query, f) {
6
6
  if (f.includeLanes !== null && f.includeLanes.length === 0) return null;
7
+ if (f.onlyConversationIds && f.onlyConversationIds.length === 0) return null;
7
8
  let q = query;
9
+ if (f.onlyConversationIds) q = q.in("id", f.onlyConversationIds);
8
10
  if (f.includeLanes !== null) {
9
11
  q = q.in("lane", f.includeLanes);
10
12
  } else {
@@ -39,7 +41,8 @@ function historyFilterInputFromScope(scope, overrides = {}) {
39
41
  includeSourceApps: overrides.includeSourceApps ?? scope.includeSourceApps,
40
42
  includeEmptySource: overrides.includeEmptySource ?? scope.includeEmptySource,
41
43
  includeOriginClasses: scope.includeOriginClasses ?? [],
42
- includeLanes: scope.includeLanes ?? null
44
+ includeLanes: scope.includeLanes ?? null,
45
+ onlyConversationIds: scope.onlyConversationIds ?? null
43
46
  };
44
47
  }
45
48
  export {
@@ -2,7 +2,7 @@ import { createSelector } from "@reduxjs/toolkit";
2
2
  import { idMatchesQuery } from "@ai-matrx/kit/search-scoring";
3
3
  import {
4
4
  defaultScopeState,
5
- rowMatchesArchiveView
5
+ isLiveConversationRow
6
6
  } from "./types.js";
7
7
  import {
8
8
  CONVERSATION_LANES,
@@ -35,9 +35,8 @@ const makeSelectConversationHistoryScope = (scopeId) => createSelector(selectCon
35
35
  const makeSelectConversationHistoryItems = (scopeId) => {
36
36
  const selectScope = makeSelectConversationHistoryScope(scopeId);
37
37
  return createSelector(selectScope, (scope) => {
38
- const view = scope.archiveView ?? "active";
39
38
  const inView = scope.items.filter(
40
- (item) => rowMatchesArchiveView(item.status, view)
39
+ (item) => isLiveConversationRow(item.status)
41
40
  );
42
41
  const term = scope.searchTerm.trim().toLowerCase();
43
42
  if (!term) return inView;
@@ -1,5 +1,5 @@
1
1
  import type { ConversationListItem } from "../conversation-list/conversation-list.types.js";
2
- import { type ConversationHistoryState, type HistoryGrouping, type HistoryArchiveView, type HistoryStatus, type SourceFacet, type SourceFacetsStatus } from "./types.js";
2
+ import { type ConversationHistoryState, type HistoryGrouping, type HistoryStatus, type SourceFacet, type SourceFacetsStatus } from "./types.js";
3
3
  import { type ConversationLane } from "./lanes.js";
4
4
  export declare const configureScope: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
5
5
  scopeId: string;
@@ -21,7 +21,10 @@ export declare const configureScope: import("@reduxjs/toolkit").ActionCreatorWit
21
21
  }, "conversationHistory/configureScope">, setScopeAgentIds: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
22
22
  scopeId: string;
23
23
  agentIds: string[];
24
- }, "conversationHistory/setScopeAgentIds">, setScopeSourceFilter: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
24
+ }, "conversationHistory/setScopeAgentIds">, setScopeOnlyConversationIds: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
25
+ scopeId: string;
26
+ ids: string[] | null;
27
+ }, "conversationHistory/setScopeOnlyConversationIds">, setScopeSourceFilter: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
25
28
  scopeId: string;
26
29
  includeSourceFeatures: string[];
27
30
  includeSourceApps: string[];
@@ -58,13 +61,7 @@ export declare const configureScope: import("@reduxjs/toolkit").ActionCreatorWit
58
61
  }, "conversationHistory/setScopePageSuccess">, patchConversationInScopes: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
59
62
  conversationId: string;
60
63
  patch: Partial<ConversationListItem>;
61
- }, "conversationHistory/patchConversationInScopes">, setScopeArchiveView: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
62
- scopeId: string;
63
- view: HistoryArchiveView;
64
- }, "conversationHistory/setScopeArchiveView">, setScopeArchivedCount: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
65
- scopeId: string;
66
- count: number | null;
67
- }, "conversationHistory/setScopeArchivedCount">, removeConversationFromScopes: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
64
+ }, "conversationHistory/patchConversationInScopes">, removeConversationFromScopes: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
68
65
  conversationId: string;
69
66
  }, "conversationHistory/removeConversationFromScopes">, upsertConversationIntoScopes: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
70
67
  row: ConversationListItem;
@@ -32,8 +32,6 @@ function ensureScope(state, scopeId) {
32
32
  }
33
33
  if (scope.includeOriginClasses === void 0) scope.includeOriginClasses = [];
34
34
  if (scope.includeLanes === void 0) scope.includeLanes = null;
35
- if (scope.archiveView === void 0) scope.archiveView = "active";
36
- if (scope.archivedCount === void 0) scope.archivedCount = null;
37
35
  return scope;
38
36
  }
39
37
  function sortByUpdated(items) {
@@ -152,6 +150,15 @@ const slice = createSlice({
152
150
  scope.includeLanes = lanes === null ? null : [...lanes];
153
151
  invalidateScopeWindow(scope);
154
152
  },
153
+ setScopeOnlyConversationIds(state, action) {
154
+ const { scopeId, ids } = action.payload;
155
+ const scope = ensureScope(state, scopeId);
156
+ const current = scope.onlyConversationIds ?? null;
157
+ const unchanged = current === ids || current !== null && ids !== null && sameStringArray(current, ids);
158
+ if (unchanged) return;
159
+ scope.onlyConversationIds = ids === null ? null : [...ids];
160
+ invalidateScopeWindow(scope);
161
+ },
155
162
  setScopeAgentIds(state, action) {
156
163
  const scope = ensureScope(state, action.payload.scopeId);
157
164
  const sameLen = scope.agentIds.length === action.payload.agentIds.length;
@@ -168,16 +175,6 @@ const slice = createSlice({
168
175
  setScopeSearch(state, action) {
169
176
  ensureScope(state, action.payload.scopeId).searchTerm = action.payload.searchTerm;
170
177
  },
171
- /** "Archived (N)" ⇄ back: switches which half the list shows and refetches. */
172
- setScopeArchiveView(state, action) {
173
- const scope = ensureScope(state, action.payload.scopeId);
174
- if (scope.archiveView === action.payload.view) return;
175
- scope.archiveView = action.payload.view;
176
- invalidateScopeWindow(scope);
177
- },
178
- setScopeArchivedCount(state, action) {
179
- ensureScope(state, action.payload.scopeId).archivedCount = action.payload.count;
180
- },
181
178
  setScopeGrouping(state, action) {
182
179
  ensureScope(state, action.payload.scopeId).grouping = action.payload.grouping;
183
180
  },
@@ -222,14 +219,7 @@ const slice = createSlice({
222
219
  (i) => i.conversationId === conversationId
223
220
  );
224
221
  if (idx === -1) continue;
225
- const before = scope.items[idx].status;
226
222
  scope.items[idx] = { ...scope.items[idx], ...patch };
227
- if (patch.status !== void 0 && scope.archivedCount !== null && scope.archivedCount !== void 0 && before === "archived" !== (patch.status === "archived")) {
228
- scope.archivedCount = Math.max(
229
- 0,
230
- scope.archivedCount + (patch.status === "archived" ? 1 : -1)
231
- );
232
- }
233
223
  }
234
224
  },
235
225
  /**
@@ -247,6 +237,9 @@ const slice = createSlice({
247
237
  if (scope.agentIds.length > 0 && !scope.agentIds.includes(agentId)) {
248
238
  continue;
249
239
  }
240
+ if (scope.onlyConversationIds && !scope.onlyConversationIds.includes(row.conversationId)) {
241
+ continue;
242
+ }
250
243
  const feature = row.sourceFeature ?? "";
251
244
  if (scope.excludeSourceFeatures.includes(feature)) continue;
252
245
  const hasAllowList = scope.includeSourceFeatures.length > 0 || scope.includeSourceApps.length > 0 || scope.includeEmptySource;
@@ -305,6 +298,7 @@ const slice = createSlice({
305
298
  const {
306
299
  configureScope,
307
300
  setScopeAgentIds,
301
+ setScopeOnlyConversationIds,
308
302
  setScopeSourceFilter,
309
303
  seedScopeSourceFilter,
310
304
  setScopeLanes,
@@ -313,8 +307,6 @@ const {
313
307
  setScopeStatus,
314
308
  setScopePageSuccess,
315
309
  patchConversationInScopes,
316
- setScopeArchiveView,
317
- setScopeArchivedCount,
318
310
  removeConversationFromScopes,
319
311
  upsertConversationIntoScopes,
320
312
  clearScope,
@@ -332,10 +324,9 @@ export {
332
324
  removeConversationFromScopes,
333
325
  seedScopeSourceFilter,
334
326
  setScopeAgentIds,
335
- setScopeArchiveView,
336
- setScopeArchivedCount,
337
327
  setScopeGrouping,
338
328
  setScopeLanes,
329
+ setScopeOnlyConversationIds,
339
330
  setScopePageSuccess,
340
331
  setScopeSearch,
341
332
  setScopeSourceFilter,
@@ -2,7 +2,6 @@ import { createAsyncThunk } from "@reduxjs/toolkit";
2
2
  import { supabase } from "../../../host/db.js";
3
3
  import { applyFavoritesFromUes } from "../conversation-list/conversation-list.thunks.js";
4
4
  import {
5
- setScopeArchivedCount,
6
5
  setScopePageSuccess,
7
6
  setScopeStatus,
8
7
  configureScope,
@@ -70,8 +69,7 @@ const fetchConversationHistory = createAsyncThunk(
70
69
  if (agentIds.length > 0) {
71
70
  query = query.in("initial_agent_id", agentIds);
72
71
  }
73
- const archiveView = scope.archiveView ?? "active";
74
- query = archiveView === "archived" ? query.eq("status", "archived") : query.neq("status", "archived");
72
+ query = query.neq("status", "archived");
75
73
  const filterInput = historyFilterInputFromScope(scope, {
76
74
  excludeSourceFeatures,
77
75
  includeSourceFeatures,
@@ -79,26 +77,6 @@ const fetchConversationHistory = createAsyncThunk(
79
77
  includeEmptySource
80
78
  });
81
79
  const filtered = applyHistoryFilters(query, filterInput);
82
- if (replace) {
83
- let countQuery = supabase.schema("chat").from("conversation").select("id", { count: "exact", head: true }).is("deleted_at", null).eq("is_ephemeral", false).eq("status", "archived");
84
- if (viewerId) countQuery = countQuery.eq("created_by", viewerId);
85
- if (agentIds.length > 0) {
86
- countQuery = countQuery.in("initial_agent_id", agentIds);
87
- }
88
- const countFiltered = applyHistoryFilters(countQuery, filterInput);
89
- if (countFiltered === null) {
90
- dispatch(setScopeArchivedCount({ scopeId: args.scopeId, count: 0 }));
91
- } else {
92
- void Promise.resolve(countFiltered).then(({ count, error: countError }) => {
93
- dispatch(
94
- setScopeArchivedCount({
95
- scopeId: args.scopeId,
96
- count: countError ? null : count ?? 0
97
- })
98
- );
99
- });
100
- }
101
- }
102
80
  if (filtered === null) {
103
81
  dispatch(
104
82
  setScopePageSuccess({
@@ -33,6 +33,11 @@ export type HistoryStatus = "idle" | "loading" | "loading-more" | "succeeded" |
33
33
  export interface ConversationHistoryScopeState {
34
34
  /** Agent ids whose conversations should appear in this scope. Empty = all. */
35
35
  agentIds: string[];
36
+ /**
37
+ * Restrict the list to these conversations (a board's own). `null` = no
38
+ * restriction (the default); `[]` = the list is empty without a read.
39
+ */
40
+ onlyConversationIds?: string[] | null;
36
41
  /**
37
42
  * `cx_conversation.source_feature` values to hide from this scope.
38
43
  * Empty = no filtering. Used by `/chat` to hide voice-agent transcripts
@@ -102,19 +107,14 @@ export interface ConversationHistoryScopeState {
102
107
  error: string | null;
103
108
  /** Last `Date.now()` at which a fresh fetch completed. */
104
109
  lastFetchedAt: number | null;
105
- /**
106
- * THE ARCHIVED-ITEMS LAW (common-docs/policies/archived-items.md): the list
107
- * hides archived conversations by default; "Archived (N)" switches this to
108
- * `"archived"` in one click. The server query and the client view both obey
109
- * it, so a conversation archived from any ⋯ leaves the list at once.
110
- */
111
- archiveView: HistoryArchiveView;
112
- /** Archived conversations under this scope's filters; null = not known yet. */
113
- archivedCount: number | null;
114
110
  }
115
- export type HistoryArchiveView = "active" | "archived";
116
- /** Does this row belong in the list for `view`? The one test, shared. */
117
- export declare function rowMatchesArchiveView(status: string | null | undefined, view: HistoryArchiveView): boolean;
111
+ /**
112
+ * Does this row belong in a live conversation list? Archived conversations
113
+ * never sit beside live ones: they live in the ONE "Archived" section at the
114
+ * foot of every conversation list, together with deleted ones (Arman,
115
+ * 2026-10-08: one place, called Archived). The one test, shared.
116
+ */
117
+ export declare function isLiveConversationRow(status: string | null | undefined): boolean;
118
118
  /**
119
119
  * A distinct (lane, source_app, source_feature) grouping with the user's
120
120
  * conversation count. Returned by `get_cx_conversation_lane_facets` and
@@ -1,5 +1,5 @@
1
- function rowMatchesArchiveView(status, view) {
2
- return status === "archived" === (view === "archived");
1
+ function isLiveConversationRow(status) {
2
+ return status !== "archived";
3
3
  }
4
4
  const defaultScopeState = {
5
5
  agentIds: [],
@@ -18,9 +18,7 @@ const defaultScopeState = {
18
18
  hasMore: false,
19
19
  status: "idle",
20
20
  error: null,
21
- lastFetchedAt: null,
22
- archiveView: "active",
23
- archivedCount: null
21
+ lastFetchedAt: null
24
22
  };
25
23
  const CONVERSATION_HISTORY_TTL_MS = 6e4;
26
24
  const SOURCE_FACETS_TTL_MS = 5 * 6e4;
@@ -28,5 +26,5 @@ export {
28
26
  CONVERSATION_HISTORY_TTL_MS,
29
27
  SOURCE_FACETS_TTL_MS,
30
28
  defaultScopeState,
31
- rowMatchesArchiveView
29
+ isLiveConversationRow
32
30
  };
@@ -190,4 +190,10 @@ export declare function selectAgentConversationsEntry(state: ChatRootState, agen
190
190
  * instance → agent map key → canonical agent id, then reads the cache.
191
191
  */
192
192
  export declare function selectAgentConversationsEntryForInstance(state: ChatRootState, instanceId: string, versionFilter: number | null): ConversationListAgentCacheEntry | undefined;
193
+ /**
194
+ * The row for a conversation from whichever store holds it — the global list
195
+ * or any mounted sidebar scope. The Archived section needs a title for a row
196
+ * that was archived or deleted from a list only one of them had loaded.
197
+ */
198
+ export declare function findConversationRow(state: ChatRootState, conversationId: string): ConversationListItem | null;
193
199
  export {};
@@ -80,7 +80,17 @@ function selectAgentConversationsEntryForInstance(state, instanceId, versionFilt
80
80
  const canonicalAgentId = agent?.parentAgentId ?? agent?.id ?? mapKey;
81
81
  return selectAgentConversationsEntry(state, canonicalAgentId, versionFilter);
82
82
  }
83
+ function findConversationRow(state, conversationId) {
84
+ const fromList = state.conversationList.byConversationId[conversationId];
85
+ if (fromList) return fromList;
86
+ for (const scope of Object.values(state.conversationHistory.scopes)) {
87
+ const row = scope.items.find((i) => i.conversationId === conversationId);
88
+ if (row) return row;
89
+ }
90
+ return null;
91
+ }
83
92
  export {
93
+ findConversationRow,
84
94
  makeSelectAgentConversationList,
85
95
  makeSelectAgentConversations,
86
96
  selectAgentConversationIds,
@@ -39,6 +39,12 @@ export interface ConversationListItem {
39
39
  * unknown. Never client-written — display/filter only.
40
40
  */
41
41
  originClass?: string;
42
+ /**
43
+ * Set only on rows in the "Archived" section that were DELETED (soft delete,
44
+ * `deleted_at`) rather than archived (`status = 'archived'`). Restore picks
45
+ * its door from it.
46
+ */
47
+ deletedAt?: string | null;
42
48
  }
43
49
  export type ConversationListLoadStatus = "idle" | "loading" | "succeeded" | "failed";
44
50
  /**
@@ -6,8 +6,11 @@ import { isScopesRpcErr } from "../../../context/sources/scopes.js";
6
6
  import {
7
7
  patchConversation,
8
8
  renameConversation as renameConversationListItem,
9
- revertRename
9
+ revertRename,
10
+ addToTrash,
11
+ removeFromTrash
10
12
  } from "./conversation-list.slice.js";
13
+ import { findConversationRow } from "./conversation-list.selectors.js";
11
14
  import { patchConversationInScopes } from "../conversation-history/slice.js";
12
15
  import { patchConversation as patchConversationRecord } from "../execution-system/conversations/conversations.slice.js";
13
16
  import { setConversationLabel as setMessagesConversationLabel } from "../execution-system/messages/messages.slice.js";
@@ -103,7 +106,8 @@ const setConversationFavorite = createAsyncThunk(
103
106
  );
104
107
  const setConversationArchived = createAsyncThunk(
105
108
  "conversationRow/setArchived",
106
- async ({ conversationId, archived }, { dispatch, rejectWithValue }) => {
109
+ async ({ conversationId, archived }, { dispatch, getState, rejectWithValue }) => {
110
+ const before = findConversationRow(getState(), conversationId);
107
111
  const nextStatus = archived ? "archived" : "active";
108
112
  const action = archived ? "archive this conversation" : "restore this conversation";
109
113
  const { data, error } = await supabase.schema("chat").from("conversation").update({ status: nextStatus, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", conversationId).select("id");
@@ -121,6 +125,11 @@ const setConversationArchived = createAsyncThunk(
121
125
  dispatch(
122
126
  patchConversationRecord({ conversationId, persistedStatus: nextStatus })
123
127
  );
128
+ if (archived) {
129
+ if (before) dispatch(addToTrash({ ...before, status: nextStatus }));
130
+ } else {
131
+ dispatch(removeFromTrash(conversationId));
132
+ }
124
133
  return { conversationId, status: nextStatus };
125
134
  }
126
135
  );