@morlay/ui-conversation-manager 0.0.2-alpha.5 → 0.0.2-alpha.7
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/README.md +14 -10
- package/dist/client.cjs +72 -43
- package/dist/client.d.cts +331 -170
- package/dist/client.d.mts +331 -170
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +16 -14
- package/src/client/ConversationManagerIcon.tsx +2 -2
- package/src/client/ConversationManagerPage.tsx +70 -56
- package/src/client/controller.ts +51 -0
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/ui-conversation-manager",
|
|
3
|
-
"version": "0.0.2-alpha.
|
|
3
|
+
"version": "0.0.2-alpha.7",
|
|
4
4
|
"description": "「对话管理」全局面板页(浏览器半):已归档会话的搜索、取消归档、删除,以及导入为新会话。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"archived-sessions",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"url": "https://github.com/morlay/dsh-plugin.git"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
+
"locale/*.json",
|
|
19
20
|
"dist",
|
|
20
21
|
"src",
|
|
21
22
|
"!**/__tests__"
|
|
@@ -27,7 +28,8 @@
|
|
|
27
28
|
"./client": {
|
|
28
29
|
"types": "./dist/client.d.cts",
|
|
29
30
|
"default": "./dist/client.cjs"
|
|
30
|
-
}
|
|
31
|
+
},
|
|
32
|
+
"./locale/*.json": "./locale/*.json"
|
|
31
33
|
},
|
|
32
34
|
"devDependencies": {
|
|
33
35
|
"@testing-library/react": "^16.1.0",
|
|
@@ -37,18 +39,18 @@
|
|
|
37
39
|
"react-dom": "^18.2.0"
|
|
38
40
|
},
|
|
39
41
|
"peerDependencies": {
|
|
40
|
-
"@deepseek-ai/cordis": "4.0.
|
|
41
|
-
"@deepseek-ai/dsh-api-session-controller": "0.1.
|
|
42
|
-
"@deepseek-ai/dsh-client-locale": "0.1.
|
|
43
|
-
"@deepseek-ai/dsh-client-ui-layout": "0.1.
|
|
44
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.1.
|
|
45
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.1.
|
|
46
|
-
"@deepseek-ai/dsh-client-ui-session": "0.1.
|
|
47
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "0.1.
|
|
48
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.1.
|
|
49
|
-
"@deepseek-ai/dsh-client-ui-workspace": "0.1.
|
|
50
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
51
|
-
"@morlay/dsh-client-ui-primitives": "0.0.2-alpha.
|
|
42
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
43
|
+
"@deepseek-ai/dsh-api-session-controller": "0.1.7-alpha.2",
|
|
44
|
+
"@deepseek-ai/dsh-client-locale": "0.1.7-alpha.2",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.1.7-alpha.2",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.7-alpha.2",
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.7-alpha.2",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-session": "0.1.7-alpha.2",
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "0.1.7-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.7-alpha.2",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-workspace": "0.1.7-alpha.2",
|
|
52
|
+
"@deepseek-ai/dsh-session": "0.1.7-alpha.2",
|
|
53
|
+
"@morlay/dsh-client-ui-primitives": "0.0.2-alpha.7"
|
|
52
54
|
},
|
|
53
55
|
"dsh": {
|
|
54
56
|
"client": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
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 <
|
|
8
|
+
return <IconArchiveOutlineRegular size={size} />;
|
|
9
9
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// 「对话管理」页面:已归档会话的搜索、取消归档、导出、删除,以及导入为新会话与孤儿数据 GC。
|
|
2
|
-
//
|
|
3
|
-
|
|
2
|
+
// 数据面:会话行读**我们自己的**列表路由(注入面 listRows,含归档;搜索 / 子代理过滤 / 分页都在后端),
|
|
3
|
+
// 动作面只读注入面。
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
4
5
|
import {
|
|
5
6
|
Button,
|
|
6
7
|
Checkbox,
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
//
|
|
111
|
-
const rows = useMemo<ConversationRow[]>(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 (
|
|
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
|
|
198
|
-
const
|
|
199
|
-
const pageCount = Math.max(1, Math.ceil(
|
|
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
|
|
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={<
|
|
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
|
-
{
|
|
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 ? (
|
package/src/client/controller.ts
CHANGED
|
@@ -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();
|