@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
package/CHANGELOG.md CHANGED
@@ -1,6 +1,61 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.6.6
4
+ ### Unreleased
5
+
6
+ - **One "Archived" place for every conversation list** (Arman, 2026-10-08). The separate "Archived (N)" toggle
7
+ and the "Trash" disclosure are gone; `ConversationArchiveSection` (was `ConversationTrashSection`) is the one
8
+ closed-by-default section at the foot of `ConversationHistorySidebar`, listing archived AND deleted
9
+ conversations with Restore on each (restore picks the right door: un-delete RPC, unarchive, or both). Live lists
10
+ never show archived rows. Removed: scope fields `archiveView` / `archivedCount`, actions `setScopeArchiveView` /
11
+ `setScopeArchivedCount`, `rowMatchesArchiveView` (→ `isLiveConversationRow`); `fetchTrashedConversations` →
12
+ `fetchArchivedConversations`. `ConversationListItem.deletedAt` added.
13
+ - Consumer header is ONE row: an always-visible search field + refresh + filter. No heading text: prop
14
+ `historyLabel` removed; `initialSearchOpen` → `autoFocusSearch`.
15
+ - `AgentRunWindow`: title is the open chat's title (else the agent's name) with no "Run" prefix; New chat is a
16
+ quiet theme-token icon beside the sidebar toggle (`actionsLeft`), not a primary disc on the right.
17
+
18
+ Consumer action: required if you pass `historyLabel` (delete it) or `initialSearchOpen` (rename to `autoFocusSearch`).
19
+
20
+ - `ConversationHistorySidebar` takes `keepLoaded`: a host that freezes and wakes the list (a board tile's
21
+ Activity re-runs every effect on wake) no longer re-reads it. The mount read is skipped while the scope
22
+ was last read for the same agents, source filter and lanes; a changed filter, Refresh, search and
23
+ "load more" still read. Default false (the list refreshes on every mount, as before).
24
+
25
+ - `ConversationHistorySidebar` takes `onlyConversationIds`: a list of "the conversations that belong to THIS
26
+ thing" (a board's). The read is `id in (...)` (a `[]` set reads nothing), the archived count follows, a
27
+ conversation minted elsewhere is not live-upserted into it, and a changed set reads again. New scope field
28
+ `onlyConversationIds` + action `setScopeOnlyConversationIds`. Pair with `serverSearch={false}`.
29
+
30
+ - `ConversationHistorySidebar` takes `removeFromList` ({ label, onRemove }): a row-menu entry for a scoped list that
31
+ takes the conversation OUT of the list (never out of existence); the host performs the removal.
32
+
33
+ Consumer action: optional — pass `keepLoaded` where the list can sleep and wake; `onlyConversationIds` (+ `removeFromList`) for a scoped list.
34
+ ## 0.6.5
35
+
36
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
37
+ `git diff npm/chat/v0.6.4..npm/chat/v0.6.5 -- apps/shared/chat`).
38
+ No source changes intended and no consumer action required.
39
+
3
40
  ## 0.6.4
41
+
42
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
43
+ `git diff npm/chat/v0.6.3..npm/chat/v0.6.4 -- apps/shared/chat`).
44
+ No source changes intended and no consumer action required.
45
+
46
+ ## 0.6.3
47
+
48
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
49
+ `git diff npm/chat/v0.6.2..npm/chat/v0.6.3 -- apps/shared/chat`).
50
+ No source changes intended and no consumer action required.
51
+
52
+ ## 0.6.2
53
+
54
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
55
+ `git diff npm/chat/v0.6.1..npm/chat/v0.6.2 -- apps/shared/chat`).
56
+ No source changes intended and no consumer action required.
57
+
58
+ ## 0.6.1
4
59
  ### rendered-output P2 WP4: every artifact type attaches
5
60
 
6
61
  - `RenderedArtifactSource` carries `artifact_type`, the type's own `options` (first = default,
@@ -42,11 +42,11 @@ export interface ChatHistorySidebarProps {
42
42
  * (ALLOW-list). Still honored and AND-ed with the surface filter.
43
43
  */
44
44
  excludeSourceFeatures?: string[];
45
- /** Start with the search field open and focused. */
46
- initialSearchOpen?: boolean;
47
- /** Suppress the built-in inline "Search chats" affordance. */
45
+ /** Put the caret in the search field on mount. */
46
+ autoFocusSearch?: boolean;
47
+ /** Suppress the built-in inline search field. */
48
48
  hideSearchAffordance?: boolean;
49
49
  className?: string;
50
50
  }
51
- export declare function ChatHistorySidebar({ scopeId, activeConversationId, onOpenConversation, openInPlace, headerSlot, topSlot, surfaceId, excludeSourceFeatures, initialSearchOpen, hideSearchAffordance, className, }: ChatHistorySidebarProps): React.JSX.Element;
51
+ export declare function ChatHistorySidebar({ scopeId, activeConversationId, onOpenConversation, openInPlace, headerSlot, topSlot, surfaceId, excludeSourceFeatures, autoFocusSearch, hideSearchAffordance, className, }: ChatHistorySidebarProps): React.JSX.Element;
52
52
  export default ChatHistorySidebar;
@@ -10,7 +10,7 @@ function ChatHistorySidebar({
10
10
  topSlot,
11
11
  surfaceId = "chat",
12
12
  excludeSourceFeatures,
13
- initialSearchOpen = false,
13
+ autoFocusSearch = false,
14
14
  hideSearchAffordance = false,
15
15
  className
16
16
  }) {
@@ -27,7 +27,7 @@ function ChatHistorySidebar({
27
27
  headerSlot,
28
28
  topSlot,
29
29
  excludeSourceFeatures,
30
- initialSearchOpen,
30
+ autoFocusSearch,
31
31
  hideSearchAffordance,
32
32
  serverSearch: true,
33
33
  className
@@ -197,7 +197,7 @@ function ChatSidebarMenu({ expanded }) {
197
197
  }
198
198
  },
199
199
  openInPlace: !!host,
200
- initialSearchOpen: true,
200
+ autoFocusSearch: true,
201
201
  className: "h-full"
202
202
  }
203
203
  ) })
@@ -54,6 +54,14 @@ export interface ConversationMenuContext {
54
54
  onShowOnly?: () => void;
55
55
  /** Remove this conversation's source from the current scope's view. */
56
56
  onHide?: () => void;
57
+ /**
58
+ * A list that holds only what belongs to one thing (a board's chats) offers
59
+ * to take THIS conversation out of it — never out of existence.
60
+ */
61
+ removeFromList?: {
62
+ label: string;
63
+ run: () => void;
64
+ };
57
65
  };
58
66
  /**
59
67
  * Inline rename belongs to ItemRow. Set false when a non-row host consumes
@@ -144,6 +144,14 @@ function buildConversationMenu(ctx) {
144
144
  hidden: !ctx.onAddToProject,
145
145
  onSelect: () => ctx.onAddToProject?.()
146
146
  },
147
+ {
148
+ id: "remove-from-list",
149
+ label: src?.removeFromList?.label ?? "Remove from this list",
150
+ icon: FilterX,
151
+ description: "The conversation itself is kept",
152
+ hidden: !src?.removeFromList,
153
+ onSelect: () => src?.removeFromList?.run()
154
+ },
147
155
  {
148
156
  id: "duplicate",
149
157
  label: "Duplicate",
@@ -249,9 +257,8 @@ function buildConversationMenu(ctx) {
249
257
  const ok = await confirm({
250
258
  title: "Delete conversation",
251
259
  description: /* @__PURE__ */ jsxs(Fragment, { children: [
252
- "This moves ",
253
260
  /* @__PURE__ */ jsx("b", { children: displayTitle(ctx.title) }),
254
- " and every message inside it to the trash. Nothing is destroyed \u2014 it leaves every list, and you can bring it back from Trash at the bottom of the conversation list."
261
+ " moves to Archived at the bottom of the conversation list. You can restore it there."
255
262
  ] }),
256
263
  confirmLabel: "Delete",
257
264
  variant: "destructive"
@@ -264,8 +271,8 @@ function buildConversationMenu(ctx) {
264
271
  toast.error(result.payload?.message ?? "Delete failed");
265
272
  } else {
266
273
  ctx.onMutationSuccess?.();
267
- toast.success("Conversation moved to the trash", {
268
- description: "Restore it from Trash at the bottom of the conversation list."
274
+ toast.success("Moved to Archived", {
275
+ description: "Restore it from Archived at the bottom of the list."
269
276
  });
270
277
  }
271
278
  }
@@ -0,0 +1,7 @@
1
+ interface ConversationArchiveSectionProps {
2
+ /** `"consumer"` matches the comfortable /chat sidebar; `"dense"` the rest. */
3
+ variant?: "dense" | "consumer";
4
+ className?: string;
5
+ }
6
+ export declare function ConversationArchiveSection({ variant, className, }: ConversationArchiveSectionProps): import("react").JSX.Element;
7
+ export default ConversationArchiveSection;
@@ -1,17 +1,17 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useState } from "react";
4
- import { Loader2, Trash2, Undo2 } from "lucide-react";
3
+ import { useCallback, useState } from "react";
4
+ import { Archive, Loader2, Undo2 } from "lucide-react";
5
5
  import { cn } from "@ai-matrx/design-system";
6
6
  import { toast } from "../../../host/notify.js";
7
7
  import { useAppDispatch, useAppSelector } from "../../../store/hooks.js";
8
8
  import {
9
- fetchTrashedConversations,
9
+ fetchArchivedConversations,
10
10
  restoreConversation
11
11
  } from "../../redux/conversation-list/conversation-trash.thunks.js";
12
12
  import { ErrorAlchemyMenu } from "../../../host/ui-slots.js";
13
13
  import { Button, DisclosureHeader } from "@ai-matrx/design-system/controls";
14
- function ConversationTrashSection({
14
+ function ConversationArchiveSection({
15
15
  variant = "dense",
16
16
  className
17
17
  }) {
@@ -26,11 +26,11 @@ function ConversationTrashSection({
26
26
  );
27
27
  const status = useAppSelector((state) => state.conversationList.trashStatus);
28
28
  const error = useAppSelector((state) => state.conversationList.trashError);
29
- useEffect(() => {
30
- if (open && status === "idle") {
31
- void dispatch(fetchTrashedConversations());
32
- }
33
- }, [open, status, dispatch]);
29
+ const toggle = () => {
30
+ const next = !open;
31
+ setOpen(next);
32
+ if (next) void dispatch(fetchArchivedConversations());
33
+ };
34
34
  const handleRestore = useCallback(
35
35
  async (conversationId) => {
36
36
  setRestoringId(conversationId);
@@ -51,22 +51,22 @@ function ConversationTrashSection({
51
51
  {
52
52
  className: "w-full",
53
53
  open,
54
- onClick: () => setOpen((v) => !v),
55
- icon: /* @__PURE__ */ jsx(Trash2, {}),
56
- title: "Trash",
54
+ onClick: toggle,
55
+ icon: /* @__PURE__ */ jsx(Archive, {}),
56
+ title: "Archived",
57
57
  end: open && status === "succeeded" ? ids.length : void 0
58
58
  }
59
59
  ),
60
- open && /* @__PURE__ */ jsxs("div", { className: "pb-1", children: [
61
- status === "loading" && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 text-[11px] text-muted-foreground", children: [
60
+ open && /* @__PURE__ */ jsxs("div", { className: "max-h-64 overflow-y-auto pb-1", children: [
61
+ status === "loading" && ids.length === 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 text-[11px] text-muted-foreground", children: [
62
62
  /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
63
- "Opening the trash\u2026"
63
+ "Opening\u2026"
64
64
  ] }),
65
65
  status === "failed" && /* @__PURE__ */ jsxs("p", { className: "px-3 py-2 text-[11px] text-destructive", children: [
66
- error ?? "The trash could not be read.",
66
+ error ?? "Archived chats could not be read.",
67
67
  /* @__PURE__ */ jsx(ErrorAlchemyMenu, { error })
68
68
  ] }),
69
- status === "succeeded" && ids.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-3 py-2 text-[11px] text-muted-foreground", children: "Nothing deleted. Deleted conversations land here and can be restored." }),
69
+ status === "succeeded" && ids.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-3 py-2 text-[11px] text-muted-foreground", children: "Nothing archived." }),
70
70
  ids.map((id) => {
71
71
  const conv = bodies[id];
72
72
  if (!conv) return null;
@@ -99,8 +99,8 @@ function ConversationTrashSection({
99
99
  ] })
100
100
  ] });
101
101
  }
102
- var ConversationTrashSection_default = ConversationTrashSection;
102
+ var ConversationArchiveSection_default = ConversationArchiveSection;
103
103
  export {
104
- ConversationTrashSection,
105
- ConversationTrashSection_default as default
104
+ ConversationArchiveSection,
105
+ ConversationArchiveSection_default as default
106
106
  };
@@ -97,20 +97,12 @@ export interface ConversationHistorySidebarProps {
97
97
  topSlot?: React.ReactNode;
98
98
  /** Extra header actions (shown next to grouping + search toggles). */
99
99
  headerActions?: React.ReactNode;
100
- /**
101
- * Consumer variant only: the heading over the list. Default "Filtered Chats"
102
- * (the /chat sidebar, which really does filter by source). A surface that
103
- * lists one agent's conversations names what the list IS instead.
104
- */
105
- historyLabel?: string;
106
100
  /** Controls whether the built-in search input is shown. Default: true. */
107
101
  showSearch?: boolean;
108
102
  /** Controls whether the grouping toggle is shown. Default: true (dense). */
109
103
  showGroupingToggle?: boolean;
110
- /**
111
- * Consumer variant only: start with the search field open + focused.
112
- */
113
- initialSearchOpen?: boolean;
104
+ /** Consumer variant only: put the caret in the search field on mount. */
105
+ autoFocusSearch?: boolean;
114
106
  /**
115
107
  * Consumer variant only: suppress the built-in inline search affordance
116
108
  * (when surrounding chrome already provides a search entry point).
@@ -124,6 +116,33 @@ export interface ConversationHistorySidebarProps {
124
116
  * list is not a conversation library.
125
117
  */
126
118
  serverSearch?: boolean;
119
+ /**
120
+ * Show only these conversations — a surface whose list is "what belongs to
121
+ * THIS thing" (a board's own conversations). `null`/omitted = no restriction;
122
+ * `[]` = an empty list with no read. The list reads again when the set
123
+ * changes. Pair with `serverSearch={false}`: the ranked search spans the
124
+ * person's whole library, so a restricted list filters what it holds.
125
+ */
126
+ onlyConversationIds?: string[] | null;
127
+ /**
128
+ * With `onlyConversationIds`: a row menu entry that takes the conversation OUT
129
+ * of the list (e.g. "Remove from this board") without touching the
130
+ * conversation itself. The host does the removal; the list then shrinks when
131
+ * the host passes the smaller set.
132
+ */
133
+ removeFromList?: {
134
+ label: string;
135
+ onRemove: (conv: ConversationListItem) => void;
136
+ };
137
+ /**
138
+ * A host that FREEZES and wakes this component (a board tile's Activity,
139
+ * which re-runs every effect on wake) sets this so a wake never re-reads the
140
+ * list: the mount fetch is skipped while the scope already holds a first page
141
+ * that was read for the same agents, source filter and lanes. A changed
142
+ * filter, Refresh, search and "load more" still read. Default false — the
143
+ * list refreshes each time it mounts.
144
+ */
145
+ keepLoaded?: boolean;
127
146
  className?: string;
128
147
  /** Extra classes for the built-in search input (dense). */
129
148
  searchInputClassName?: string;
@@ -16,8 +16,7 @@ import {
16
16
  Loader2,
17
17
  Search,
18
18
  Star,
19
- StarOff,
20
- X
19
+ StarOff
21
20
  } from "lucide-react";
22
21
  import { cn } from "@ai-matrx/design-system";
23
22
  import { useAppDispatch, useAppSelector } from "../../../store/hooks.js";
@@ -38,8 +37,8 @@ import { selectIsStreaming } from "../../redux/execution-system/selectors/aggreg
38
37
  import {
39
38
  seedScopeSourceFilter,
40
39
  setScopeAgentIds,
41
- setScopeArchiveView,
42
40
  setScopeLanes,
41
+ setScopeOnlyConversationIds,
43
42
  setScopeGrouping,
44
43
  setScopeSearch,
45
44
  setScopeSourceFilter
@@ -51,7 +50,8 @@ import {
51
50
  sourceKey
52
51
  } from "../../redux/conversation-history/source-registry.js";
53
52
  import {
54
- rowMatchesArchiveView
53
+ CONVERSATION_HISTORY_TTL_MS,
54
+ isLiveConversationRow
55
55
  } from "../../redux/conversation-history/types.js";
56
56
  import { useSurfaceSourceFilter } from "../../redux/conversation-history/useSurfaceSourceFilter.js";
57
57
  import {
@@ -65,9 +65,8 @@ import { selectAgentById } from "../../redux/agent-definition/selectors.js";
65
65
  import { EntityDoorControls } from "../../../host/ui-slots.js";
66
66
  import { ConversationSourceFilterTree } from "./ConversationSourceFilterTree.js";
67
67
  import { AllLanesOffNotice } from "./ConversationLaneToggles.js";
68
- import { ConversationTrashSection } from "./ConversationTrashSection.js";
69
- import { ArchivedDisclosure } from "@ai-matrx/design-system";
70
- import { SegmentedControl, Button, DisclosureHeader } from "@ai-matrx/design-system/controls";
68
+ import { ConversationArchiveSection } from "./ConversationArchiveSection.js";
69
+ import { SegmentedControl, Button, DisclosureHeader, SearchField } from "@ai-matrx/design-system/controls";
71
70
  import { ItemRow } from "../../../host/ui-slots.js";
72
71
  import { toast } from "../../../host/notify.js";
73
72
  import {
@@ -89,9 +88,13 @@ function useConversationHistoryController(props) {
89
88
  isFavorite,
90
89
  onToggleFavorite,
91
90
  getConversationHref,
92
- serverSearch = true
91
+ serverSearch = true,
92
+ keepLoaded = false,
93
+ onlyConversationIds = null,
94
+ removeFromList
93
95
  } = props;
94
96
  const dispatch = useAppDispatch();
97
+ const readFor = useRef(null);
95
98
  const selectScope = useMemo(
96
99
  () => makeSelectConversationHistoryScope(scopeId),
97
100
  [scopeId]
@@ -120,6 +123,7 @@ function useConversationHistoryController(props) {
120
123
  [agentIds]
121
124
  );
122
125
  const excludeKey = JSON.stringify(excludeSourceFeatures ?? []);
126
+ const onlyKey = onlyConversationIds ? onlyConversationIds.slice().sort().join(",") : null;
123
127
  const resolvedKey = surfaceId ? JSON.stringify(resolvedSource) : "";
124
128
  const enabledLanes = useAppSelector(selectConversationLanes);
125
129
  const lanesKey = surfaceId ? enabledLanes.join(",") : "none";
@@ -136,6 +140,12 @@ function useConversationHistoryController(props) {
136
140
  });
137
141
  useEffect(() => {
138
142
  dispatch(setScopeAgentIds({ scopeId, agentIds: agentIds.slice() }));
143
+ dispatch(
144
+ setScopeOnlyConversationIds({
145
+ scopeId,
146
+ ids: onlyConversationIds ? onlyConversationIds.slice() : null
147
+ })
148
+ );
139
149
  dispatch(
140
150
  setScopeLanes({ scopeId, lanes: surfaceId ? enabledLanes : null })
141
151
  );
@@ -159,6 +169,19 @@ function useConversationHistoryController(props) {
159
169
  if (excludeSourceFeatures !== void 0) {
160
170
  fetchArgs.excludeSourceFeatures = excludeSourceFeatures;
161
171
  }
172
+ const readKey = [scopeId, agentIdsKey, pageSize, excludeKey, resolvedKey, lanesKey, onlyKey].join("|");
173
+ if (keepLoaded) {
174
+ if (readFor.current === readKey) return;
175
+ const fresh = dispatch((_d, getState) => {
176
+ const held = getState().conversationHistory.scopes[scopeId];
177
+ return !!held && held.status === "succeeded" && held.lastFetchedAt !== null && Date.now() - held.lastFetchedAt < CONVERSATION_HISTORY_TTL_MS;
178
+ });
179
+ if (fresh) {
180
+ readFor.current = readKey;
181
+ return;
182
+ }
183
+ }
184
+ readFor.current = readKey;
162
185
  void dispatch(fetchConversationHistory(fetchArgs));
163
186
  }, [
164
187
  dispatch,
@@ -168,7 +191,9 @@ function useConversationHistoryController(props) {
168
191
  excludeKey,
169
192
  resolvedKey,
170
193
  lanesKey,
171
- surfaceId
194
+ surfaceId,
195
+ keepLoaded,
196
+ onlyKey
172
197
  ]);
173
198
  const onSearchChange = useCallback(
174
199
  (value) => {
@@ -186,12 +211,6 @@ function useConversationHistoryController(props) {
186
211
  if (!hasMore || status === "loading" || status === "loading-more") return;
187
212
  void dispatch(fetchConversationHistory({ scopeId, replace: false }));
188
213
  }, [dispatch, scopeId, hasMore, status]);
189
- const onArchiveViewChange = (open) => {
190
- dispatch(
191
- setScopeArchiveView({ scopeId, view: open ? "archived" : "active" })
192
- );
193
- void dispatch(fetchConversationHistory({ scopeId, replace: true }));
194
- };
195
214
  const onRefresh = () => {
196
215
  void dispatch(fetchConversationHistory({ scopeId, replace: true }));
197
216
  if (surfaceId) {
@@ -228,9 +247,9 @@ function useConversationHistoryController(props) {
228
247
  );
229
248
  const favorites = useMemo(() => {
230
249
  return scope.items.filter(
231
- (i) => isFavoriteResolved(i.conversationId) && rowMatchesArchiveView(i.status, scope.archiveView ?? "active")
250
+ (i) => isFavoriteResolved(i.conversationId) && isLiveConversationRow(i.status)
232
251
  );
233
- }, [scope.items, scope.archiveView, isFavoriteResolved]);
252
+ }, [scope.items, isFavoriteResolved]);
234
253
  const resolveHref = useCallback(
235
254
  (conv) => getConversationHref ? getConversationHref(conv) : `/chat/${conv.conversationId}`,
236
255
  [getConversationHref]
@@ -252,7 +271,7 @@ function useConversationHistoryController(props) {
252
271
  },
253
272
  [dispatch, scopeId]
254
273
  );
255
- const getSourceMenuCtx = useCallback(
274
+ const getSourceMenuBase = useCallback(
256
275
  (conv) => {
257
276
  if (!surfaceId) {
258
277
  return {
@@ -309,6 +328,20 @@ function useConversationHistoryController(props) {
309
328
  },
310
329
  [surfaceId, scope, facets, commitSourceFilter]
311
330
  );
331
+ const getSourceMenuCtx = useCallback(
332
+ (conv) => {
333
+ const base = getSourceMenuBase(conv);
334
+ if (!removeFromList) return base;
335
+ return {
336
+ ...base,
337
+ removeFromList: {
338
+ label: removeFromList.label,
339
+ run: () => removeFromList.onRemove(conv)
340
+ }
341
+ };
342
+ },
343
+ [getSourceMenuBase, removeFromList]
344
+ );
312
345
  return {
313
346
  scope,
314
347
  byDate,
@@ -323,7 +356,6 @@ function useConversationHistoryController(props) {
323
356
  onGroupingChange,
324
357
  onLoadMore,
325
358
  onRefresh,
326
- onArchiveViewChange,
327
359
  isFavoriteResolved,
328
360
  onToggleFavoriteResolved,
329
361
  favorites,
@@ -383,7 +415,7 @@ const DenseView = ({
383
415
  const cachedSearchItems = byDate.flatMap((bucket) => bucket.items);
384
416
  const hasAuthoritativeResults = serverSearchState.isSettled && (serverSearchState.status === "succeeded" || serverSearchState.status === "loading-more" || serverSearchState.status === "failed" && serverSearchState.items.length > 0);
385
417
  const searchItems = hasAuthoritativeResults ? serverSearchState.items.filter(
386
- (item) => rowMatchesArchiveView(item.status, ctl.scope.archiveView ?? "active")
418
+ (item) => isLiveConversationRow(item.status)
387
419
  ) : cachedSearchItems;
388
420
  const empty = !allLanesOff && status !== "loading" && status !== "idle" && !error && count === 0 && !searchTerm.trim() && emptyState;
389
421
  const showControls = showSearch || showGroupingToggle || !!headerActions || !!surfaceId;
@@ -596,8 +628,7 @@ const DenseView = ({
596
628
  count === 1 ? "" : "s"
597
629
  ] })
598
630
  ] }),
599
- /* @__PURE__ */ jsx(ConversationArchivedToggle, { ctl }),
600
- /* @__PURE__ */ jsx(ConversationTrashSection, { variant: "dense" })
631
+ /* @__PURE__ */ jsx(ConversationArchiveSection, { variant: "dense" })
601
632
  ] });
602
633
  };
603
634
  const ConsumerView = ({
@@ -609,13 +640,11 @@ const ConsumerView = ({
609
640
  openInPlace = false,
610
641
  headerSlot,
611
642
  topSlot,
612
- initialSearchOpen = false,
643
+ autoFocusSearch = false,
613
644
  hideSearchAffordance = false,
614
- historyLabel = "Filtered Chats",
615
645
  pageSize = 30,
616
646
  className
617
647
  }) => {
618
- const dispatch = useAppDispatch();
619
648
  const {
620
649
  byDate,
621
650
  status,
@@ -636,27 +665,19 @@ const ConsumerView = ({
636
665
  const cachedSearchItems = byDate.flatMap((bucket) => bucket.items);
637
666
  const hasAuthoritativeResults = serverSearchState.isSettled && (serverSearchState.status === "succeeded" || serverSearchState.status === "loading-more" || serverSearchState.status === "failed" && serverSearchState.items.length > 0);
638
667
  const searchItems = hasAuthoritativeResults ? serverSearchState.items.filter(
639
- (item) => rowMatchesArchiveView(item.status, ctl.scope.archiveView ?? "active")
668
+ (item) => isLiveConversationRow(item.status)
640
669
  ) : cachedSearchItems;
641
670
  const favoriteIds = useMemo(
642
671
  () => new Set(favorites.map((f) => f.conversationId)),
643
672
  [favorites]
644
673
  );
645
- const [searchOpen, setSearchOpen] = useState(initialSearchOpen);
646
674
  const searchInputRef = useRef(null);
647
- const openSearch = useCallback(() => {
648
- setSearchOpen(true);
649
- requestAnimationFrame(() => searchInputRef.current?.focus());
650
- }, []);
651
675
  useEffect(() => {
652
- if (initialSearchOpen) {
676
+ if (autoFocusSearch) {
653
677
  requestAnimationFrame(() => searchInputRef.current?.focus());
654
678
  }
655
- }, [initialSearchOpen]);
656
- const closeSearch = useCallback(() => {
657
- setSearchOpen(false);
658
- dispatch(setScopeSearch({ scopeId, searchTerm: "" }));
659
- }, [dispatch, scopeId]);
679
+ }, [autoFocusSearch]);
680
+ const showSearch = !hideSearchAffordance;
660
681
  return /* @__PURE__ */ jsxs(
661
682
  "div",
662
683
  {
@@ -666,9 +687,22 @@ const ConsumerView = ({
666
687
  ),
667
688
  children: [
668
689
  headerSlot,
669
- surfaceId && /* @__PURE__ */ jsxs("div", { className: "flex h-8 shrink-0 items-center gap-1 px-2", children: [
670
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[11px] font-semibold uppercase tracking-wide text-muted-foreground/70", children: historyLabel }),
671
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center", children: [
690
+ (showSearch || surfaceId) && /* @__PURE__ */ jsxs("div", { className: "flex h-9 shrink-0 items-center gap-1 px-2", children: [
691
+ showSearch ? /* @__PURE__ */ jsx(
692
+ SearchField,
693
+ {
694
+ ref: searchInputRef,
695
+ value: searchTerm,
696
+ onChange: (e) => onSearchChange(e.target.value),
697
+ onKeyDown: (e) => {
698
+ if (e.key === "Escape") onSearchChange("");
699
+ },
700
+ placeholder: "Search",
701
+ "aria-label": "Search conversations",
702
+ className: "min-w-0 flex-1"
703
+ }
704
+ ) : /* @__PURE__ */ jsx("span", { className: "flex-1" }),
705
+ surfaceId && /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center", children: [
672
706
  status === "loading" ? /* @__PURE__ */ jsx(
673
707
  LoadingTapButton,
674
708
  {
@@ -699,25 +733,6 @@ const ConsumerView = ({
699
733
  )
700
734
  ] })
701
735
  ] }),
702
- !hideSearchAffordance && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-1 px-1 pb-1", children: searchOpen ? /* @__PURE__ */ jsxs("div", { className: "relative flex flex-1 items-center", children: [
703
- /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 h-3.5 w-3.5 text-muted-foreground" }),
704
- /* @__PURE__ */ jsx(
705
- "input",
706
- {
707
- ref: searchInputRef,
708
- type: "search",
709
- value: searchTerm,
710
- onChange: (e) => onSearchChange(e.target.value),
711
- onKeyDown: (e) => {
712
- if (e.key === "Escape") closeSearch();
713
- },
714
- placeholder: "Search chats",
715
- className: "h-8 w-full rounded-lg border border-border bg-background pl-8 pr-8 text-sm text-foreground placeholder:text-muted-foreground/70 outline-none focus:border-ring/40",
716
- "aria-label": "Search conversations"
717
- }
718
- ),
719
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: /* @__PURE__ */ jsx(X, {}), onClick: closeSearch, "aria-label": "Close search", className: "absolute right-1.5" })
720
- ] }) : /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: /* @__PURE__ */ jsx(Search, {}), onClick: openSearch, "aria-label": "Search conversations", className: "flex-1", children: "Search chats" }) }),
721
736
  topSlot,
722
737
  /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto pt-1", children: [
723
738
  error && !searchActive && /* @__PURE__ */ jsx(ReadFailure, { error, what: "your conversations", onRetry: onRefresh, className: "m-2" }),
@@ -794,27 +809,11 @@ const ConsumerView = ({
794
809
  }
795
810
  ) })
796
811
  ] }),
797
- /* @__PURE__ */ jsx(ConversationArchivedToggle, { ctl }),
798
- /* @__PURE__ */ jsx(ConversationTrashSection, { variant: "consumer" })
812
+ /* @__PURE__ */ jsx(ConversationArchiveSection, { variant: "consumer" })
799
813
  ]
800
814
  }
801
815
  );
802
816
  };
803
- function ConversationArchivedToggle({ ctl }) {
804
- const open = ctl.scope.archiveView === "archived";
805
- const count = ctl.scope.archivedCount;
806
- return /* @__PURE__ */ jsx(
807
- ArchivedDisclosure,
808
- {
809
- count: count ?? 0,
810
- countLabel: count === null ? null : void 0,
811
- open,
812
- onOpenChange: ctl.onArchiveViewChange,
813
- keepWhileOpen: true,
814
- className: "px-2 py-1"
815
- }
816
- );
817
- }
818
817
  const GroupingToggle = ({ value, onChange }) => /* @__PURE__ */ jsx(
819
818
  SegmentedControl,
820
819
  {
@@ -61,8 +61,7 @@ function ConversationPickerWindowInner({
61
61
  activeConversationId: activeConversationId ?? null,
62
62
  onOpenConversation: handleOpenConversation,
63
63
  openInPlace: true,
64
- historyLabel: "Conversations",
65
- initialSearchOpen: true,
64
+ autoFocusSearch: true,
66
65
  className: "h-full bg-transparent"
67
66
  }
68
67
  )
@@ -22,6 +22,8 @@ export declare function useRunControlsState(conversationId: string, includeAttac
22
22
  resolveTab: (tab: RunControlsTab) => RunControlsTab;
23
23
  tabTrailing: (tabId: RunControlsTab) => ReactNode;
24
24
  addedCount: number;
25
+ addedToolsCount: number;
26
+ addedSkillsCount: number;
25
27
  hasModelOverride: boolean;
26
28
  hasActiveContext: boolean;
27
29
  anyDocActive: boolean;