@morlay/ui-conversation-manager 0.0.2-alpha.5 → 0.0.2-alpha.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/ui-conversation-manager",
3
- "version": "0.0.2-alpha.5",
3
+ "version": "0.0.2-alpha.6",
4
4
  "description": "「对话管理」全局面板页(浏览器半):已归档会话的搜索、取消归档、删除,以及导入为新会话。",
5
5
  "keywords": [
6
6
  "archived-sessions",
@@ -37,18 +37,18 @@
37
37
  "react-dom": "^18.2.0"
38
38
  },
39
39
  "peerDependencies": {
40
- "@deepseek-ai/cordis": "4.0.2",
41
- "@deepseek-ai/dsh-api-session-controller": "0.1.6-alpha.2",
42
- "@deepseek-ai/dsh-client-locale": "0.1.6-alpha.2",
43
- "@deepseek-ai/dsh-client-ui-layout": "0.1.6-alpha.2",
44
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.6-alpha.2",
45
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.2",
46
- "@deepseek-ai/dsh-client-ui-session": "0.1.6-alpha.2",
47
- "@deepseek-ai/dsh-client-ui-sidebar": "0.1.6-alpha.2",
48
- "@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.2",
49
- "@deepseek-ai/dsh-client-ui-workspace": "0.1.6-alpha.2",
50
- "@deepseek-ai/dsh-session": "0.1.6-alpha.2",
51
- "@morlay/dsh-client-ui-primitives": "0.0.2-alpha.5"
40
+ "@deepseek-ai/cordis": "4.0.3",
41
+ "@deepseek-ai/dsh-api-session-controller": "0.1.7-alpha.1",
42
+ "@deepseek-ai/dsh-client-locale": "0.1.7-alpha.1",
43
+ "@deepseek-ai/dsh-client-ui-layout": "0.1.7-alpha.1",
44
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-alpha.1",
45
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-alpha.1",
46
+ "@deepseek-ai/dsh-client-ui-session": "0.1.7-alpha.1",
47
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.1.7-alpha.1",
48
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.7-alpha.1",
49
+ "@deepseek-ai/dsh-client-ui-workspace": "0.1.7-alpha.1",
50
+ "@deepseek-ai/dsh-session": "0.1.7-alpha.1",
51
+ "@morlay/dsh-client-ui-primitives": "0.0.2-alpha.6"
52
52
  },
53
53
  "dsh": {
54
54
  "client": {
@@ -1,9 +1,9 @@
1
- import { IconArchiveOutline20 } from "@deepseek-ai/dsh-client-ui-primitives";
1
+ import { IconArchiveOutlineRegular } from "@deepseek-ai/dsh-client-ui-primitives";
2
2
  import type { PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots";
3
3
 
4
4
  /** nav 图标只画字形:尺寸由 nav 行给,选中态由 nav 行的按钮承担。 */
5
5
  export type ConversationManagerIconProps = PropsRuntime<"sidebar.panellist">;
6
6
 
7
7
  export function ConversationManagerIcon({ size }: ConversationManagerIconProps) {
8
- return <IconArchiveOutline20 size={size} />;
8
+ return <IconArchiveOutlineRegular size={size} />;
9
9
  }
@@ -1,10 +1,11 @@
1
1
  // 「对话管理」页面:已归档会话的搜索、取消归档、导出、删除,以及导入为新会话与孤儿数据 GC。
2
- // 数据面只读框架标准座位(useSessions / useWorkspaces),动作面只读注入面。
3
- import { useMemo, useRef, useState, type ReactNode } from "react";
2
+ // 数据面:会话行读**我们自己的**列表路由(注入面 listRows,含归档;搜索 / 子代理过滤 / 分页都在后端),
3
+ // 动作面只读注入面。
4
+ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
4
5
  import {
5
6
  Button,
6
7
  Checkbox,
7
- IconSearchOutline16,
8
+ IconSearchOutlineRegular,
8
9
  Input,
9
10
  Modal,
10
11
  Tag,
@@ -16,6 +17,7 @@ import type { SessionId } from "@deepseek-ai/dsh-session";
16
17
  import {
17
18
  ConversationManagerRequestError,
18
19
  type ConversationManagerFace,
20
+ type SessionRowsPage,
19
21
  type SessionUsageReport,
20
22
  type UsageBucket,
21
23
  type UsageRangeKey,
@@ -58,15 +60,6 @@ function timeLabel(updatedAt: number, now: number, t: Translate): string {
58
60
  return unit === "now" ? t("time.now") : t(`time.${unit}`, { n });
59
61
  }
60
62
 
61
- /** 标题或工作区名命中归一化后的查询。 */
62
- function matches(row: ConversationRow, normalizedQuery: string): boolean {
63
- return (
64
- normalizedQuery.length === 0 ||
65
- row.title.toLowerCase().includes(normalizedQuery) ||
66
- row.workspace.toLowerCase().includes(normalizedQuery)
67
- );
68
- }
69
-
70
63
  /** host 错误码 → 可读文案;没有码时保留原文。 */
71
64
  function failureText(error: unknown, t: Translate): string {
72
65
  const code = error instanceof ConversationManagerRequestError ? error.code : undefined;
@@ -78,8 +71,7 @@ function failureText(error: unknown, t: Translate): string {
78
71
 
79
72
  export function ConversationManagerPage({
80
73
  t,
81
- useSessions,
82
- useWorkspaces,
74
+ listRows,
83
75
  archive,
84
76
  unarchive,
85
77
  remove,
@@ -88,9 +80,12 @@ export function ConversationManagerPage({
88
80
  collectGarbage,
89
81
  loadUsage,
90
82
  }: ConversationManagerPageProps): ReactNode {
91
- const sessions = useSessions((state) => state);
92
- const workspaces = useWorkspaces((state) => state);
83
+ // 数据面是**我们自己的**列表路由(完整语料,含归档):官方 `session/list` 按部署策略排除归档,
84
+ // 归档集的管理动作要完整集合,两条路不混。搜索、子代理过滤与分页都在后端做(前端分页等于每次拉全量)。
85
+ const [rowsPage, setRowsPage] = useState<SessionRowsPage | null>(null);
86
+ const [rowsFailure, setRowsFailure] = useState<string | null>(null);
93
87
  const [query, setQuery] = useState("");
88
+ const [settledQuery, setSettledQuery] = useState("");
94
89
  const [page, setPage] = useState(1);
95
90
  const [showSubagents, setShowSubagents] = useState(false);
96
91
  const [view, setView] = useState<PageView>("sessions");
@@ -105,41 +100,64 @@ export function ConversationManagerPage({
105
100
  const [notice, setNotice] = useState<string | null>(null);
106
101
  const [failure, setFailure] = useState<string | null>(null);
107
102
  const fileRef = useRef<HTMLInputElement>(null);
103
+ const reloadRows = useCallback(async () => {
104
+ try {
105
+ // 后端分页:每次只拉当前页(PAGE_SIZE 条),翻页与搜索都重新请求。
106
+ setRowsPage(
107
+ await listRows({
108
+ ...(settledQuery === "" ? {} : { query: settledQuery }),
109
+ ...(showSubagents ? { includeSubagents: true } : {}),
110
+ page,
111
+ pageSize: PAGE_SIZE,
112
+ }),
113
+ );
114
+ setRowsFailure(null);
115
+ } catch (error: unknown) {
116
+ setRowsFailure(failureText(error, t));
117
+ }
118
+ }, [listRows, t, settledQuery, showSubagents, page]);
119
+
120
+ useEffect(() => {
121
+ void reloadRows();
122
+ }, [reloadRows]);
123
+
124
+ // 搜索下推到后端:输入停一下再请求(每次按键都打 host 没必要)。
125
+ useEffect(() => {
126
+ if (query === settledQuery) return;
127
+ const timer = window.setTimeout(() => {
128
+ setSettledQuery(query);
129
+ setPage(1);
130
+ }, 250);
131
+ return () => {
132
+ window.clearTimeout(timer);
133
+ };
134
+ }, [query, settledQuery]);
135
+
108
136
  const ungrouped = t("ungrouped");
109
137
 
110
- // 全量会话:会话目录(ids)∪ 归档集,按最近活动在前;没有加载到 summary 的成员不产生行。
111
- const rows = useMemo<ConversationRow[]>(() => {
112
- const owners = new Map<string, string>();
113
- for (const workspace of workspaces.items) {
114
- for (const id of workspace.sessionIds) owners.set(id, workspace.title);
115
- }
116
- const archivedIds = new Set<SessionId>(workspaces.archivedSessionIds);
117
- const ids = new Set<SessionId>([...sessions.ids, ...workspaces.archivedSessionIds]);
118
- return [...ids]
119
- .flatMap((id) => {
120
- const summary = sessions.byId[id];
121
- if (summary === undefined) return [];
122
- return [
123
- {
124
- id,
125
- title: summary.displayTitle,
126
- workspace: owners.get(id) ?? ungrouped,
127
- archived: archivedIds.has(id),
128
- subagent: summary.origin === "subagent",
129
- updatedAt: summary.updatedAt,
130
- },
131
- ];
132
- })
133
- .sort((left, right) => right.updatedAt - left.updatedAt);
134
- }, [workspaces, sessions.ids, sessions.byId, ungrouped]);
138
+ // 当前页的行:标题、工作区归属、归档标记与最后活动时间都随行给出(归属由 host 算,页面不再自己映射)。
139
+ const rows = useMemo<ConversationRow[]>(
140
+ () =>
141
+ (rowsPage?.items ?? []).map((record) => ({
142
+ id: record.sessionId as SessionId,
143
+ title: record.title ?? record.sessionId,
144
+ workspace: record.workspace ?? ungrouped,
145
+ archived: record.archived,
146
+ subagent: record.origin === "subagent",
147
+ updatedAt: record.updatedAt,
148
+ })),
149
+ [rowsPage, ungrouped],
150
+ );
135
151
 
136
152
  // 一个动作的收尾:成败都收掉弹窗,失败把原因落到页面上的提示行。
153
+ // 动作成功即重拉我们自己的列表(归档状态与标题都可能变);失败只报错、不动列表。
137
154
  const run = (action: Promise<unknown>, settle?: () => void): void => {
138
155
  setFailure(null);
139
156
  setNotice(null);
140
157
  void action.then(
141
158
  () => {
142
159
  settle?.();
160
+ void reloadRows();
143
161
  },
144
162
  (error: unknown) => {
145
163
  settle?.();
@@ -156,6 +174,7 @@ export function ConversationManagerPage({
156
174
  (result) => {
157
175
  setGcPhase("idle");
158
176
  setNotice(t("gc.done", { sessions: result.orphanSessions, events: result.orphanEvents }));
177
+ void reloadRows();
159
178
  },
160
179
  (error: unknown) => {
161
180
  setGcPhase("idle");
@@ -188,17 +207,17 @@ export function ConversationManagerPage({
188
207
  if (usage === null && !usageLoading) requestUsage(usageRange);
189
208
  };
190
209
 
191
- if (sessions.phase !== "ready") {
192
- return <p {...styling.props(styles.status)}>{t("loading")}</p>;
210
+ if (rowsPage === null) {
211
+ return <p {...styling.props(styles.status)}>{rowsFailure ?? t("loading")}</p>;
193
212
  }
194
213
 
195
214
  const now = Date.now();
196
- // 子代理派生会话默认不列:它们不可删也不可取消归档,只会淹没真实对话。
197
- const listed = showSubagents ? rows : rows.filter((row) => !row.subagent);
198
- const matched = listed.filter((row) => matches(row, query.trim().toLowerCase()));
199
- const pageCount = Math.max(1, Math.ceil(matched.length / PAGE_SIZE));
215
+ // 这一页就是后端给的那一页:搜索、子代理过滤与排序都在 host 做过,页面只渲染。
216
+ const visible = rows;
217
+ const total = rowsPage.total;
218
+ const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
200
219
  const currentPage = Math.min(page, pageCount);
201
- const visible = matched.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
220
+ const matched = rows;
202
221
  const confirmed = confirming;
203
222
 
204
223
  return (
@@ -305,7 +324,7 @@ export function ConversationManagerPage({
305
324
  className={styling.className(styles.search)}
306
325
  data-filter="search"
307
326
  type="search"
308
- icon={<IconSearchOutline16 />}
327
+ icon={<IconSearchOutlineRegular />}
309
328
  value={query}
310
329
  placeholder={t("search")}
311
330
  aria-label={t("search")}
@@ -336,14 +355,9 @@ export function ConversationManagerPage({
336
355
  {failure}
337
356
  </p>
338
357
  )}
339
- {listed.length === 0 ? (
358
+ {total === 0 ? (
340
359
  <p {...styling.props(styles.status)} data-status="empty">
341
- {t("empty")}
342
- </p>
343
- ) : null}
344
- {listed.length > 0 && matched.length === 0 ? (
345
- <p {...styling.props(styles.status)} data-status="empty-search">
346
- {t("emptySearch")}
360
+ {t(settledQuery === "" ? "empty" : "emptySearch")}
347
361
  </p>
348
362
  ) : null}
349
363
  {visible.length > 0 ? (
@@ -7,6 +7,40 @@ export const SESSION_EXPORT_PATH = "/api/session.export";
7
7
  export const SESSION_GC_PATH = "/api/session.gc";
8
8
  export const SESSION_USAGE_PATH = "/api/session.usage";
9
9
 
10
+ /**
11
+ * 管理面的会话行(**完整语料,含归档**):我们自己的路由,与官方 `session/list` 分开——那条按部署策略
12
+ * 默认排除归档(给上游 UI 用),归档集的管理动作需要完整集合。
13
+ */
14
+ export const SESSION_ROWS_PATH = "/api/session.rows";
15
+
16
+ /** 一行会话:标题、origin、最后活动时间、归档标记与工作区归属都由 host 给出。 */
17
+ export interface SessionRowRecord {
18
+ sessionId: string;
19
+ title: string | null;
20
+ origin: string | null;
21
+ cwd: string | null;
22
+ createdAt: number;
23
+ updatedAt: number;
24
+ archived: boolean;
25
+ workspace: string | null;
26
+ }
27
+
28
+ /** 列表请求:搜索、子代理过滤与分页都在后端做(前端分页等于每次拉全量)。 */
29
+ export interface SessionRowsQuery {
30
+ query?: string;
31
+ includeSubagents?: boolean;
32
+ page?: number;
33
+ pageSize?: number;
34
+ }
35
+
36
+ export interface SessionRowsPage {
37
+ items: SessionRowRecord[];
38
+ /** 过滤后的总数(分页前),页面据此算页数。 */
39
+ total: number;
40
+ page: number;
41
+ pageSize: number;
42
+ }
43
+
10
44
  /** 时间范围的语义键(与 session-rdb `./usage` 的 `UsageRangeKey` 镜像)。 */
11
45
  export type UsageRangeKey = "all" | "day" | "week" | "7d" | "30d" | "90d";
12
46
 
@@ -74,6 +108,8 @@ export interface ConversationManagerGcResult {
74
108
 
75
109
  /** 页面从注入面拿到的动作(属性语法:页面解构后直接调用,不绑 this)。 */
76
110
  export interface ConversationManagerFace {
111
+ /** 管理面自己的列表:完整语料(含归档),标题与最后活动时间随行给出;搜索与分页都在后端。 */
112
+ listRows: (query?: SessionRowsQuery) => Promise<SessionRowsPage>;
77
113
  archive: (sessionId: SessionId) => Promise<void>;
78
114
  unarchive: (sessionId: SessionId) => Promise<void>;
79
115
  remove: (sessionId: SessionId) => Promise<void>;
@@ -134,6 +170,7 @@ export class ConversationManagerController {
134
170
 
135
171
  constructor(private readonly ports: ConversationManagerPorts) {
136
172
  this.face = {
173
+ listRows: (query) => this.loadRows(query),
137
174
  archive: (sessionId) => this.ports.archiveSession(sessionId),
138
175
  unarchive: (sessionId) => this.ports.unarchiveSession(sessionId),
139
176
  remove: (sessionId) => this.remove(sessionId),
@@ -144,6 +181,20 @@ export class ConversationManagerController {
144
181
  };
145
182
  }
146
183
 
184
+ private async loadRows(query: SessionRowsQuery = {}): Promise<SessionRowsPage> {
185
+ const value = await postJson(SESSION_ROWS_PATH, query);
186
+ const items = value["items"];
187
+ if (!Array.isArray(items) || typeof value["total"] !== "number") {
188
+ throw new ConversationManagerRequestError("会话列表响应不可用", undefined);
189
+ }
190
+ return {
191
+ items: items as SessionRowRecord[],
192
+ total: value["total"],
193
+ page: typeof value["page"] === "number" ? value["page"] : (query.page ?? 1),
194
+ pageSize: typeof value["pageSize"] === "number" ? value["pageSize"] : (query.pageSize ?? 20),
195
+ };
196
+ }
197
+
147
198
  private async remove(sessionId: SessionId): Promise<void> {
148
199
  await postJson(SESSION_DELETE_PATH, { sessionId });
149
200
  await this.ports.refresh();