@morlay/ui-conversation-manager 0.0.2-alpha.4 → 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/README.md CHANGED
@@ -5,7 +5,9 @@ ui-plugin-manager 同一种注册方式)。页面分两层 Tabs(官方 `Pill
5
5
  会话视图给出全量会话的搜索、分页与归档 / 取消归档 / 导出 / 删除(确认弹窗),并带导入为新会话与
6
6
  孤儿数据 GC;统计视图给出全部对话的 token 用量。
7
7
 
8
- 会话列表取会话目录 ∪ 归档集,按最近活动在前;**已归档**的行带标记,也只有这些行的「删除」可用
8
+ 会话列表取 `POST /api/session.rows` 的完整语料(含归档),按最近活动在前;搜索(标题或工作区名)、
9
+ 子代理开关与翻页都是**后端**请求(前端只渲染当前页,输入停 250ms 再发);**已归档**的行带标记,
10
+ 也只有这些行的「删除」可用
9
11
  (未归档行提供「归档」,两者互斥)。子代理派生会话(`origin: 'subagent'`)默认不列(它们既不可删
10
12
  也多数无意义,实测在真实库里占七成),勾选「显示子代理会话」即真全量。
11
13
 
@@ -21,11 +23,14 @@ ui-plugin-manager 同一种注册方式)。页面分两层 Tabs(官方 `Pill
21
23
  ## 用法
22
24
 
23
25
  页面是 profile 的一行(由 `@morlay/better-session` 的 patch 插入),装好即在侧栏出现「对话管理」。
24
- 它不新增 host 面,只读既有服务与路由:
26
+ 它不新增 host 面,只读既有服务与路由。**会话行走我们自己的列表路由**(`POST /api/session.rows`,
27
+ `@morlay/session-rdb`):完整语料(含归档)+ 标题 + 最后活动时间随行给出——官方 `session/list` 按部署策略
28
+ **默认排除已归档**(给上游 UI 用),归档集的管理动作要完整集合,两条路不混(决策见
29
+ [session-rdb 的 ADR](../session-rdb/.agents/adrs/20260922-会话列表两条路.md)):
25
30
 
26
31
  | 动作 | 接缝 |
27
32
  | ------------ | ----------------------------------------------------------------------------------------------------------- |
28
- | 列表与标题 | 框架标准座位 `useSessions`(会话目录、标题、时间)+ `useWorkspaces`(工作区归属、归档集) |
33
+ | 列表与标题 | `POST /api/session.rows`(`@morlay/session-rdb`:完整语料 + 标题 + 最后活动时间 + 工作区归属;搜索 / 子代理过滤 / 分页都在后端) |
29
34
  | 归档 | `ctx.uiWorkspace.archiveSession`(上游 ui-workspace,未归档行提供) |
30
35
  | 取消归档 | `ctx.uiWorkspace.unarchiveSession`(上游 ui-workspace,已归档行提供) |
31
36
  | 导出 | `POST /api/session.export`(`@morlay/session-rdb`,直接下载 zip) |
@@ -34,7 +39,7 @@ ui-plugin-manager 同一种注册方式)。页面分两层 Tabs(官方 `Pill
34
39
  | 清理孤儿数据 | `POST /api/session.gc`(停 agent → 回收孤儿 subagent 会话 → 回收孤儿事件行 → VACUUM;执行期间阻塞界面) |
35
40
  | 用量统计 | `POST /api/session.usage`(`@morlay/session-rdb` 读专用用量表聚合;进入统计视图时拉一次,维度切换本地折叠) |
36
41
 
37
- 删除、导入与 GC 成功后刷新会话列表(`ctx.sessions.refresh`);host 拒绝(未归档 / 正在使用 / 不存在)
42
+ 归档 / 取消归档 / 删除 / 导入 / GC 成功后重拉会话行;host 拒绝(未归档 / 正在使用 / 不存在)
38
43
  时按错误码给出可读文案。列表每页 20 条,搜索框复用官方 `Input`(连同官方图标与焦点样式)。
39
44
 
40
45
  GC 是唯一会**停止所有运行中 agent** 的动作:确认后进入不可关闭的等待弹窗,避免用户在 VACUUM 期间
@@ -61,15 +66,14 @@ GC 是唯一会**停止所有运行中 agent** 的动作:确认后进入不可
61
66
  | `data-tab` | 一层 tab 按钮 | 该按钮切到的视图 |
62
67
  | `data-action` | 按钮 | `import` / `gc` / `archive` / `unarchive` / `export` / `remove` |
63
68
  | `data-filter` | 过滤器 | `search` / `subagents` |
64
- | `data-session-list` / `data-session-row` | 列表与行 | 会话列表、一条会话行 |
69
+ | `data-scroll` | 滚动容器 | `page`(会话列表与统计各一处) |
65
70
  | `data-session-id` / `data-archived` / `data-subagent` | 会话行 | 该行的会话 id 与状态(`true` / `false`) |
66
- | `data-session-title` / `data-session-meta` | 行内字段 | 标题、`工作区 · 时间` |
67
71
  | `data-pagination` / `data-page-current` / `data-page-total` | 分页条 | 当前页与总页数 |
68
72
  | `data-notice` / `data-failure` / `data-status` | 顶部提示与空态 | 结果、错误、`empty` / `empty-search` |
69
- | `data-usage-view` / `data-usage-tab` | 统计视图与二层 tab | 当前维度(`overview` / `daily` / `models` / `sessions`) |
73
+ | `data-usage-view` / `data-usage-tab` / `data-usage-range` | 统计视图与二级 tab | 当前维度(`overview` / `models` / `sessions`)与时间范围 |
74
+ | `data-range` | 时间范围选项 | 该选项对应的 range 值 |
70
75
  | `data-usage-cell` / `data-usage-value` | 总览格子 | 维度键与原始数值(未格式化) |
71
- | `data-usage-row` / `data-usage-key` | 统计划表行 | 维度键(日期 / `provider / model` / 会话 id / `subagent`) |
72
- | `data-usage-label` / `data-usage-total` / `data-usage-meta` | 统计行内字段 | 名称、合计、明细 |
76
+ | `data-usage-key` | 统计划表行 | 维度键(日期 / `provider / model` / 会话 id / `subagent`) |
73
77
  | `data-usage-status` | 统计状态行 | `loading` / `error` / `empty` |
74
78
 
75
79
  ## 已知限制
package/dist/client.cjs CHANGED
@@ -15,6 +15,11 @@ window.__ModuleLoader__.load({
15
15
  const SESSION_EXPORT_PATH = "/api/session.export";
16
16
  const SESSION_GC_PATH = "/api/session.gc";
17
17
  const SESSION_USAGE_PATH = "/api/session.usage";
18
+ /**
19
+ * 管理面的会话行(**完整语料,含归档**):我们自己的路由,与官方 `session/list` 分开——那条按部署策略
20
+ * 默认排除归档(给上游 UI 用),归档集的管理动作需要完整集合。
21
+ */
22
+ const SESSION_ROWS_PATH = "/api/session.rows";
18
23
  /** 带 host 错误码的请求失败:页面据此选本地化文案。 */
19
24
  var ConversationManagerRequestError = class extends Error {
20
25
  code;
@@ -58,6 +63,7 @@ window.__ModuleLoader__.load({
58
63
  constructor(ports) {
59
64
  this.ports = ports;
60
65
  this.face = {
66
+ listRows: (query) => this.loadRows(query),
61
67
  archive: (sessionId) => this.ports.archiveSession(sessionId),
62
68
  unarchive: (sessionId) => this.ports.unarchiveSession(sessionId),
63
69
  remove: (sessionId) => this.remove(sessionId),
@@ -67,6 +73,17 @@ window.__ModuleLoader__.load({
67
73
  loadUsage: (range) => this.loadUsage(range)
68
74
  };
69
75
  }
76
+ async loadRows(query = {}) {
77
+ const value = await postJson(SESSION_ROWS_PATH, query);
78
+ const items = value["items"];
79
+ if (!Array.isArray(items) || typeof value["total"] !== "number") throw new ConversationManagerRequestError("会话列表响应不可用", void 0);
80
+ return {
81
+ items,
82
+ total: value["total"],
83
+ page: typeof value["page"] === "number" ? value["page"] : query.page ?? 1,
84
+ pageSize: typeof value["pageSize"] === "number" ? value["pageSize"] : query.pageSize ?? 20
85
+ };
86
+ }
70
87
  async remove(sessionId) {
71
88
  await postJson(SESSION_DELETE_PATH, { sessionId });
72
89
  await this.ports.refresh();
@@ -410,10 +427,6 @@ window.__ModuleLoader__.load({
410
427
  const { unit, n } = (0, _deepseek_ai_dsh_client_ui_primitives.relativeTime)(updatedAt, now);
411
428
  return unit === "now" ? t("time.now") : t(`time.${unit}`, { n });
412
429
  }
413
- /** 标题或工作区名命中归一化后的查询。 */
414
- function matches(row, normalizedQuery) {
415
- return normalizedQuery.length === 0 || row.title.toLowerCase().includes(normalizedQuery) || row.workspace.toLowerCase().includes(normalizedQuery);
416
- }
417
430
  /** host 错误码 → 可读文案;没有码时保留原文。 */
418
431
  function failureText(error, t) {
419
432
  const code = error instanceof ConversationManagerRequestError ? error.code : void 0;
@@ -422,10 +435,11 @@ window.__ModuleLoader__.load({
422
435
  if (code === "SESSION_NOT_FOUND") return t("failure.missing");
423
436
  return t("failure.other", { reason: error instanceof Error ? error.message : String(error) });
424
437
  }
425
- function ConversationManagerPage({ t, useSessions, useWorkspaces, archive, unarchive, remove, exportZip, importZip, collectGarbage, loadUsage }) {
426
- const sessions = useSessions((state) => state);
427
- const workspaces = useWorkspaces((state) => state);
438
+ function ConversationManagerPage({ t, listRows, archive, unarchive, remove, exportZip, importZip, collectGarbage, loadUsage }) {
439
+ const [rowsPage, setRowsPage] = (0, react.useState)(null);
440
+ const [rowsFailure, setRowsFailure] = (0, react.useState)(null);
428
441
  const [query, setQuery] = (0, react.useState)("");
442
+ const [settledQuery, setSettledQuery] = (0, react.useState)("");
429
443
  const [page, setPage] = (0, react.useState)(1);
430
444
  const [showSubagents, setShowSubagents] = (0, react.useState)(false);
431
445
  const [view, setView] = (0, react.useState)("sessions");
@@ -440,34 +454,53 @@ window.__ModuleLoader__.load({
440
454
  const [notice, setNotice] = (0, react.useState)(null);
441
455
  const [failure, setFailure] = (0, react.useState)(null);
442
456
  const fileRef = (0, react.useRef)(null);
443
- const ungrouped = t("ungrouped");
444
- const rows = (0, react.useMemo)(() => {
445
- const owners = /* @__PURE__ */ new Map();
446
- for (const workspace of workspaces.items) for (const id of workspace.sessionIds) owners.set(id, workspace.title);
447
- const archivedIds = new Set(workspaces.archivedSessionIds);
448
- return [.../* @__PURE__ */ new Set([...sessions.ids, ...workspaces.archivedSessionIds])].flatMap((id) => {
449
- const summary = sessions.byId[id];
450
- if (summary === void 0) return [];
451
- return [{
452
- id,
453
- title: summary.displayTitle,
454
- workspace: owners.get(id) ?? ungrouped,
455
- archived: archivedIds.has(id),
456
- subagent: summary.origin === "subagent",
457
- updatedAt: summary.updatedAt
458
- }];
459
- }).sort((left, right) => right.updatedAt - left.updatedAt);
457
+ const reloadRows = (0, react.useCallback)(async () => {
458
+ try {
459
+ setRowsPage(await listRows({
460
+ ...settledQuery === "" ? {} : { query: settledQuery },
461
+ ...showSubagents ? { includeSubagents: true } : {},
462
+ page,
463
+ pageSize: PAGE_SIZE
464
+ }));
465
+ setRowsFailure(null);
466
+ } catch (error) {
467
+ setRowsFailure(failureText(error, t));
468
+ }
460
469
  }, [
461
- workspaces,
462
- sessions.ids,
463
- sessions.byId,
464
- ungrouped
470
+ listRows,
471
+ t,
472
+ settledQuery,
473
+ showSubagents,
474
+ page
465
475
  ]);
476
+ (0, react.useEffect)(() => {
477
+ reloadRows();
478
+ }, [reloadRows]);
479
+ (0, react.useEffect)(() => {
480
+ if (query === settledQuery) return;
481
+ const timer = window.setTimeout(() => {
482
+ setSettledQuery(query);
483
+ setPage(1);
484
+ }, 250);
485
+ return () => {
486
+ window.clearTimeout(timer);
487
+ };
488
+ }, [query, settledQuery]);
489
+ const ungrouped = t("ungrouped");
490
+ const rows = (0, react.useMemo)(() => (rowsPage?.items ?? []).map((record) => ({
491
+ id: record.sessionId,
492
+ title: record.title ?? record.sessionId,
493
+ workspace: record.workspace ?? ungrouped,
494
+ archived: record.archived,
495
+ subagent: record.origin === "subagent",
496
+ updatedAt: record.updatedAt
497
+ })), [rowsPage, ungrouped]);
466
498
  const run = (action, settle) => {
467
499
  setFailure(null);
468
500
  setNotice(null);
469
501
  action.then(() => {
470
502
  settle?.();
503
+ reloadRows();
471
504
  }, (error) => {
472
505
  settle?.();
473
506
  setFailure(failureText(error, t));
@@ -483,6 +516,7 @@ window.__ModuleLoader__.load({
483
516
  sessions: result.orphanSessions,
484
517
  events: result.orphanEvents
485
518
  }));
519
+ reloadRows();
486
520
  }, (error) => {
487
521
  setGcPhase("idle");
488
522
  setFailure(failureText(error, t));
@@ -504,16 +538,16 @@ window.__ModuleLoader__.load({
504
538
  setView("usage");
505
539
  if (usage === null && !usageLoading) requestUsage(usageRange);
506
540
  };
507
- if (sessions.phase !== "ready") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
541
+ if (rowsPage === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
508
542
  ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
509
- children: t("loading")
543
+ children: rowsFailure ?? t("loading")
510
544
  });
511
545
  const now = Date.now();
512
- const listed = showSubagents ? rows : rows.filter((row) => !row.subagent);
513
- const matched = listed.filter((row) => matches(row, query.trim().toLowerCase()));
514
- const pageCount = Math.max(1, Math.ceil(matched.length / PAGE_SIZE));
546
+ const visible = rows;
547
+ const total = rowsPage.total;
548
+ const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
515
549
  const currentPage = Math.min(page, pageCount);
516
- const visible = matched.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
550
+ const matched = rows;
517
551
  const confirmed = confirming;
518
552
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
519
553
  ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.page),
@@ -618,7 +652,7 @@ window.__ModuleLoader__.load({
618
652
  className: _morlay_dsh_client_ui_primitives_client.styling.className(styles.search),
619
653
  "data-filter": "search",
620
654
  type: "search",
621
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}),
655
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, {}),
622
656
  value: query,
623
657
  placeholder: t("search"),
624
658
  "aria-label": t("search"),
@@ -652,15 +686,10 @@ window.__ModuleLoader__.load({
652
686
  role: "alert",
653
687
  children: failure
654
688
  }),
655
- listed.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
689
+ total === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
656
690
  ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
657
691
  "data-status": "empty",
658
- children: t("empty")
659
- }) : null,
660
- listed.length > 0 && matched.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
661
- ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
662
- "data-status": "empty-search",
663
- children: t("emptySearch")
692
+ children: t(settledQuery === "" ? "empty" : "emptySearch")
664
693
  }) : null,
665
694
  visible.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
666
695
  ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.list),
@@ -1103,7 +1132,7 @@ window.__ModuleLoader__.load({
1103
1132
  //#endregion
1104
1133
  //#region src/client/ConversationManagerIcon.tsx
1105
1134
  function ConversationManagerIcon({ size }) {
1106
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutline20, { size });
1135
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutlineRegular, { size });
1107
1136
  }
1108
1137
  //#endregion
1109
1138
  //#region src/client/locales.ts