@qilin-agent/client-ui-workspace 3.1.5 → 3.1.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/lib/client.js CHANGED
@@ -4,11 +4,11 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _qilin_agent_client_ui_primitives = require("@qilin-agent/client-ui-primitives");
7
8
  let _qilin_agent_client_store = require("@qilin-agent/client-store");
8
9
  let _qilin_agent_kylin = require("@qilin-agent/kylin");
9
10
  let react_jsx_runtime = require("react/jsx-runtime");
10
11
  let react = require("react");
11
- let _qilin_agent_client_ui_primitives = require("@qilin-agent/client-ui-primitives");
12
12
  //#region lib/types/client/shortcuts.js
13
13
  /**
14
14
  * Create the private browser request source shared by commands and controls.
@@ -688,6 +688,7 @@ window.__ModuleLoader__.load({
688
688
  workspaces;
689
689
  sessions;
690
690
  view;
691
+ notify;
691
692
  connecting = /* @__PURE__ */ new Map();
692
693
  lifetime = new AbortController();
693
694
  /**
@@ -703,13 +704,15 @@ window.__ModuleLoader__.load({
703
704
  * @param workspaces - pure Workspace Controller.
704
705
  * @param sessions - pure Session Controller.
705
706
  * @param view - the browser's viewing-store write set, which owns saved Session order.
707
+ * @param notify - show one notice through the Workspace notice channel.
706
708
  */
707
- constructor(ctx, directoryPicker, workspaces, sessions, view) {
709
+ constructor(ctx, directoryPicker, workspaces, sessions, view, notify) {
708
710
  super(ctx, "uiWorkspace");
709
711
  this.directoryPicker = directoryPicker;
710
712
  this.workspaces = workspaces;
711
713
  this.sessions = sessions;
712
714
  this.view = view;
715
+ this.notify = notify;
713
716
  ctx.effect(() => {
714
717
  const stop = this.watchNavigation();
715
718
  return () => {
@@ -755,7 +758,9 @@ window.__ModuleLoader__.load({
755
758
  });
756
759
  if (!navigation.aborted) this.replaceMain(childId, navigation);
757
760
  }
758
- startSession(workspaceId) {
761
+ startSession(workspaceId, options) {
762
+ const draftOptions = options === void 0 ? void 0 : structuredClone(options);
763
+ const initializeDraft = draftOptions !== void 0 && (draftOptions.prompt !== void 0 || draftOptions.clearPreviousDraft === true);
759
764
  const workspace = this.workspaces.list.getSnapshot();
760
765
  const sessions = this.sessions.list.getSnapshot();
761
766
  const current = this.mainReference?.sessionId;
@@ -763,13 +768,37 @@ window.__ModuleLoader__.load({
763
768
  const recent = workspace.phase === "ready" && sessions.phase === "ready" ? recentWorkspace(workspace.items, sessions.byId) : void 0;
764
769
  const target = workspaceId ?? currentWorkspaceId ?? recent;
765
770
  if (target === void 0) {
771
+ if (initializeDraft) {
772
+ this.notify({
773
+ kind: "createFailed",
774
+ message: this.ctx.locale.bind("workspace")("draft.workspaceRequired")
775
+ });
776
+ return;
777
+ }
766
778
  this.clearMain();
767
779
  return;
768
780
  }
769
- this.openWorkspace(target).catch((reason) => {
781
+ this.openWorkspace(target, initializeDraft ? (id) => {
782
+ const binding = this.sessions.binding(id);
783
+ if (binding === void 0) this.draftPreparationFailed();
784
+ this.prepareDraft(binding, draftOptions);
785
+ } : void 0).catch((reason) => {
770
786
  console.warn("new session failed:", reason);
771
787
  });
772
788
  }
789
+ prepareDraft(binding, options) {
790
+ const conversation = this.ctx.get("conversation");
791
+ if (conversation === void 0) this.draftPreparationFailed();
792
+ if (conversation.input.requestDraftInitialization(binding, options) === "blocked") this.draftPreparationFailed();
793
+ }
794
+ draftPreparationFailed() {
795
+ const message = this.ctx.locale.bind("workspace")("draft.initializationFailed");
796
+ this.notify({
797
+ kind: "createFailed",
798
+ message
799
+ });
800
+ throw new Error(message);
801
+ }
773
802
  async archiveSession(sessionId) {
774
803
  await this.workspaces.archiveSession(sessionId);
775
804
  if (this.mainReference?.sessionId === sessionId) this.clearMain();
@@ -927,7 +956,7 @@ window.__ModuleLoader__.load({
927
956
  }
928
957
  //#endregion
929
958
  //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-workspace/src/client/rows/Rows.module.css.mjs
930
- const css$2 = ".RrwzeW_projectRow,.RrwzeW_sessionRow{padding:0 8px;cursor:pointer;user-select:none;color:var(--qilin-alias-label-primary);border-radius:8px;align-items:center;gap:6px;padding-inline-start:calc(8px + var(--dsh-workspace-indent,0px));display:flex}.RrwzeW_projectRow:hover,.RrwzeW_sessionRow:hover,.RrwzeW_sessionRow.RrwzeW_selected{background:var(--qilin-alias-interactive-bg-hover)}.RrwzeW_searchResultRow{box-sizing:border-box;cursor:pointer;text-align:left;width:100%;min-height:48px;color:var(--qilin-alias-label-primary);background:0 0;border:none;border-radius:8px;flex-direction:column;align-items:stretch;padding:4px 8px;display:flex}.RrwzeW_searchResultRow:hover,.RrwzeW_searchResultRow.RrwzeW_selected{background:var(--qilin-alias-interactive-bg-hover)}.RrwzeW_searchResultHeading{align-items:center;min-width:0;display:flex}.RrwzeW_searchResultTitle{text-overflow:ellipsis;white-space:nowrap;flex:0 auto;min-width:0;margin-left:4px;font-size:14px;line-height:20px;overflow:hidden}.RrwzeW_searchResultMeta{align-items:center;gap:6px;min-width:0;margin-left:20px;display:flex}.RrwzeW_searchResultWorkspace,.RrwzeW_searchResultSnippet{text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:17px;overflow:hidden}.RrwzeW_searchResultWorkspace{max-width:40%;color:var(--qilin-alias-label-tertiary);flex:none}.RrwzeW_searchResultSnippet{min-width:0;color:var(--qilin-alias-label-secondary);flex:1}.RrwzeW_projectRow{box-sizing:border-box;align-items:center;height:34px}.RrwzeW_projectRow .RrwzeW_rowActions{height:20px}.RrwzeW_sessionRow{height:32px;animation:RrwzeW_row-in .15s var(--ds-ease-in-out);gap:0}.RrwzeW_sessionRow .RrwzeW_title{margin:0 6px 0 4px}@keyframes RrwzeW_row-in{0%{opacity:0}}.RrwzeW_slot{width:16px;height:20px;color:var(--qilin-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.RrwzeW_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.RrwzeW_folderActive{color:var(--qilin-alias-state-business-primary)}.RrwzeW_projectRow .RrwzeW_chevron{display:none}.RrwzeW_projectRow:hover .RrwzeW_chevron{display:inline-flex}.RrwzeW_projectRow:hover .RrwzeW_folder{display:none}.RrwzeW_arrow{transition:transform .15s var(--ds-ease-in-out)}.RrwzeW_arrowOpen{transform:rotate(90deg)}.RrwzeW_projectText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.RrwzeW_title{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:14px;line-height:20px;overflow:hidden}.RrwzeW_renameInput{border:.5px solid var(--qilin-alias-border-l4);background:var(--qilin-alias-button-elevated-fill);min-width:0;color:inherit;border-radius:4px;outline:none;padding:0 2px;font-size:14px;line-height:20px}.RrwzeW_sessionRow .RrwzeW_title{scroll-behavior:smooth;flex:1}@media (hover:hover){.RrwzeW_sessionRow:hover .RrwzeW_title{text-overflow:clip}}.RrwzeW_meta{text-overflow:ellipsis;white-space:nowrap;color:var(--qilin-alias-label-tertiary);font-size:12px;line-height:20px;overflow:hidden}.RrwzeW_time{color:var(--qilin-alias-label-tertiary);flex:none;font-size:12px;line-height:20px}.RrwzeW_dot{flex:none}.RrwzeW_pinIndicator{width:16px;height:20px;color:var(--qilin-alias-label-caption);flex:none;justify-content:center;align-items:center;margin-left:6px;display:inline-flex}.RrwzeW_sessionRow:hover .RrwzeW_pinIndicator,.RrwzeW_sessionRow.RrwzeW_menuOpen .RrwzeW_pinIndicator{display:none}.RrwzeW_rowActions{flex:none;align-items:center;gap:12px;display:none}.RrwzeW_projectRow:hover .RrwzeW_rowActions,.RrwzeW_sessionRow:hover .RrwzeW_rowActions,.RrwzeW_projectRow.RrwzeW_menuOpen .RrwzeW_rowActions,.RrwzeW_sessionRow.RrwzeW_menuOpen .RrwzeW_rowActions{display:inline-flex}.RrwzeW_sessionRow:hover .RrwzeW_time,.RrwzeW_sessionRow.RrwzeW_menuOpen .RrwzeW_time{display:none}.RrwzeW_projectRow.RrwzeW_menuOpen,.RrwzeW_sessionRow.RrwzeW_menuOpen{background:var(--qilin-alias-interactive-bg-hover)}.RrwzeW_sessionRow.RrwzeW_dropBefore,.RrwzeW_sessionRow.RrwzeW_dropAfter{position:relative}.RrwzeW_sessionRow.RrwzeW_dropBefore:before,.RrwzeW_sessionRow.RrwzeW_dropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--qilin-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--qilin-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--qilin-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:4px}.RrwzeW_sessionRow.RrwzeW_dropBefore:before{top:-7px}.RrwzeW_sessionRow.RrwzeW_dropAfter:after{bottom:-7px}.RrwzeW_hoverContent{flex-direction:column;gap:8px;display:flex}.RrwzeW_hoverTitle{color:#fff;overflow-wrap:break-word;font-size:14px;line-height:20px}.RrwzeW_hoverPath{color:#cfd3d6;word-break:break-all;font-size:12px;line-height:16px}.RrwzeW_hoverTime{color:#cfd3d6;font-size:12px;line-height:16px}.RrwzeW_hoverStatus{color:#adb2b8;align-items:center;gap:8px;font-size:12px;line-height:20px;display:flex}.RrwzeW_iconButton{cursor:pointer;width:16px;height:16px;color:var(--qilin-alias-label-tertiary);background:0 0;border:none;border-radius:4px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.RrwzeW_iconButton:hover{color:var(--qilin-alias-label-primary)}.RrwzeW_chevron{color:var(--qilin-alias-label-caption)}@media (prefers-reduced-motion:reduce){.RrwzeW_sessionRow,.RrwzeW_arrow{transition:none;animation:none}.RrwzeW_sessionRow .RrwzeW_title{scroll-behavior:auto}}";
959
+ const css$2 = ".RrwzeW_projectRow,.RrwzeW_sessionRow{padding:0 8px;cursor:pointer;user-select:none;color:var(--qilin-alias-label-primary);border-radius:8px;align-items:center;gap:6px;padding-inline-start:calc(8px + var(--dsh-workspace-indent,0px));display:flex}.RrwzeW_projectRow:hover,.RrwzeW_sessionRow:hover,.RrwzeW_sessionRow.RrwzeW_selected{background:var(--qilin-alias-interactive-bg-hover)}.RrwzeW_searchResultRow{box-sizing:border-box;cursor:pointer;text-align:left;width:100%;min-height:48px;color:var(--qilin-alias-label-primary);background:0 0;border:none;border-radius:8px;flex-direction:column;align-items:stretch;padding:4px 8px;display:flex}.RrwzeW_searchResultRow:hover,.RrwzeW_searchResultRow.RrwzeW_selected{background:var(--qilin-alias-interactive-bg-hover)}.RrwzeW_searchResultHeading{align-items:center;min-width:0;display:flex}.RrwzeW_searchResultTitle{text-overflow:ellipsis;white-space:nowrap;flex:0 auto;min-width:0;margin-left:4px;font-size:14px;line-height:20px;overflow:hidden}.RrwzeW_searchResultMeta{align-items:center;gap:6px;min-width:0;margin-left:20px;display:flex}.RrwzeW_searchResultWorkspace,.RrwzeW_searchResultSnippet{text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:17px;overflow:hidden}.RrwzeW_searchResultWorkspace{max-width:40%;color:var(--qilin-alias-label-tertiary);flex:none}.RrwzeW_searchResultSnippet{min-width:0;color:var(--qilin-alias-label-secondary);flex:1}.RrwzeW_projectRow{box-sizing:border-box;align-items:center;height:34px}.RrwzeW_projectRow .RrwzeW_rowActions{height:20px}.RrwzeW_sessionRow{height:32px;animation:RrwzeW_row-in .15s var(--ds-ease-in-out);gap:0}.RrwzeW_sessionRow .RrwzeW_title{margin:0 6px 0 4px}@keyframes RrwzeW_row-in{0%{opacity:0}}.RrwzeW_slot{width:16px;height:20px;color:var(--qilin-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.RrwzeW_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.RrwzeW_folderActive{color:var(--qilin-alias-state-business-primary)}.RrwzeW_projectRow .RrwzeW_chevron{display:none}.RrwzeW_projectRow:hover .RrwzeW_chevron{display:inline-flex}.RrwzeW_projectRow:hover .RrwzeW_folder{display:none}.RrwzeW_arrow{transition:transform .15s var(--ds-ease-in-out)}.RrwzeW_arrowOpen{transform:rotate(90deg)}.RrwzeW_projectText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.RrwzeW_title{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:14px;line-height:20px;overflow:hidden}.RrwzeW_renameInput{border:.5px solid var(--qilin-alias-border-l4);background:var(--qilin-alias-button-elevated-fill);min-width:0;color:inherit;border-radius:4px;outline:none;padding:0 2px;font-size:14px;line-height:20px}.RrwzeW_sessionRow .RrwzeW_title{flex:1}.RrwzeW_sessionRow .RrwzeW_title[data-scrolled]{mask-image:linear-gradient(90deg,#0000,#000 12px)}.RrwzeW_sessionRow .RrwzeW_title[data-clipped]{mask-image:linear-gradient(270deg,#0000,#000 12px)}.RrwzeW_sessionRow .RrwzeW_title[data-scrolled][data-clipped]{mask-image:linear-gradient(90deg,#0000,#000 12px calc(100% - 12px),#0000)}@media (hover:hover){.RrwzeW_sessionRow:hover .RrwzeW_title,.RrwzeW_sessionRow.RrwzeW_menuOpen .RrwzeW_title{text-overflow:clip}}.RrwzeW_meta{text-overflow:ellipsis;white-space:nowrap;color:var(--qilin-alias-label-tertiary);font-size:12px;line-height:20px;overflow:hidden}.RrwzeW_time{color:var(--qilin-alias-label-tertiary);flex:none;font-size:12px;line-height:20px}.RrwzeW_dot{flex:none}.RrwzeW_pinIndicator{width:16px;height:20px;color:var(--qilin-alias-label-caption);flex:none;justify-content:center;align-items:center;margin-left:6px;display:inline-flex}.RrwzeW_sessionRow:hover .RrwzeW_pinIndicator,.RrwzeW_sessionRow.RrwzeW_menuOpen .RrwzeW_pinIndicator{display:none}.RrwzeW_rowActions{flex:none;align-items:center;gap:15px;display:none}.RrwzeW_projectRow:hover .RrwzeW_rowActions,.RrwzeW_sessionRow:hover .RrwzeW_rowActions,.RrwzeW_projectRow.RrwzeW_menuOpen .RrwzeW_rowActions,.RrwzeW_sessionRow.RrwzeW_menuOpen .RrwzeW_rowActions{display:inline-flex}.RrwzeW_sessionRow:hover .RrwzeW_time,.RrwzeW_sessionRow.RrwzeW_menuOpen .RrwzeW_time{display:none}.RrwzeW_projectRow.RrwzeW_menuOpen,.RrwzeW_sessionRow.RrwzeW_menuOpen{background:var(--qilin-alias-interactive-bg-hover)}.RrwzeW_sessionRow.RrwzeW_dropBefore,.RrwzeW_sessionRow.RrwzeW_dropAfter{position:relative}.RrwzeW_sessionRow.RrwzeW_dropBefore:before,.RrwzeW_sessionRow.RrwzeW_dropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--qilin-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--qilin-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--qilin-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:4px}.RrwzeW_sessionRow.RrwzeW_dropBefore:before{top:-7px}.RrwzeW_sessionRow.RrwzeW_dropAfter:after{bottom:-7px}.RrwzeW_hoverContent{flex-direction:column;gap:8px;display:flex}.RrwzeW_hoverTitle{color:#fff;overflow-wrap:break-word;font-size:14px;line-height:20px}.RrwzeW_hoverPath{color:#cfd3d6;word-break:break-all;font-size:12px;line-height:16px}.RrwzeW_hoverTime{color:#cfd3d6;font-size:12px;line-height:16px}.RrwzeW_hoverStatus{color:#adb2b8;align-items:center;gap:8px;font-size:12px;line-height:20px;display:flex}.RrwzeW_iconButton{cursor:pointer;width:16px;height:16px;color:var(--qilin-alias-label-tertiary);background:0 0;border:none;border-radius:4px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.RrwzeW_iconButton:hover{color:var(--qilin-alias-label-primary)}.RrwzeW_chevron{color:var(--qilin-alias-label-caption)}@media (prefers-reduced-motion:reduce){.RrwzeW_sessionRow,.RrwzeW_arrow{transition:none;animation:none}}";
931
960
  const tagId$2 = "@qilin-agent/client-ui-workspace/Rows.module.css";
932
961
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
933
962
  const tag = document.createElement("style");
@@ -978,7 +1007,8 @@ window.__ModuleLoader__.load({
978
1007
  * Workspace browser tree row components (figma Cell set 14:3080): pure presentational —
979
1008
  * all data and callbacks arrive via props. Hover swaps (folder->chevron,
980
1009
  * time->ellipsis, action buttons) are CSS-only, and a session row's clipped
981
- * title is scrolled programmatically while the row is hovered. Row ... menus are
1010
+ * title marquees programmatically while the row is hovered, fading both
1011
+ * clipping edges from the depth of its travel. Row ... menus are
982
1012
  * visual-only except workspace Rename/Delete and session Rename/Fork/Archive; the
983
1013
  * session and workspace hover cards are suppressed while a menu is open.
984
1014
  */
@@ -986,29 +1016,91 @@ window.__ModuleLoader__.load({
986
1016
  function displayTitle(node, t) {
987
1017
  return node.blank ? t("session.new") : node.title || t("session.untitled");
988
1018
  }
1019
+ const MIN_TITLE_REVEAL_PX = 8;
1020
+ const TITLE_MARQUEE_PX_PER_MS = .03;
1021
+ /** Whether the platform asked for reduced motion; jsdom implements no matchMedia. */
1022
+ function prefersReducedMotion() {
1023
+ return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
1024
+ }
989
1025
  /**
990
- * Reveal a title wider than its one-line cell while its row is hovered: the
991
- * title clips its own text, so the far edge (a fork's incremented title, for
992
- * example) is reachable by scrolling the element to its end. Leaving returns it
993
- * to the start in one step, because the resting ellipsis and the narrowed cell
994
- * would otherwise meet the text while it travelled back. A title that fits has
995
- * no scroll range to move, and the stylesheet decides whether either move
996
- * glides or jumps.
1026
+ * Place the title's scroll position and publish the stylesheet's fade-mask
1027
+ * hooks: `data-scrolled` while the title has left its start (left fade) and
1028
+ * `data-clipped` while text remains beyond the right edge (right fade).
997
1029
  * @param title - the row's clipping title element.
998
- * @param revealed - whether the pointer is on the row.
1030
+ * @param left - scroll offset in CSS pixels.
1031
+ * @param range - the title's maximum scroll offset in CSS pixels.
999
1032
  */
1000
- function revealClippedTitle(title, revealed) {
1001
- /* v8 ignore next -- defensive: the title span renders unconditionally. */
1002
- if (title === null) return;
1003
- if (revealed) {
1004
- title.scrollLeft = title.scrollWidth - title.clientWidth;
1005
- return;
1006
- }
1033
+ function placeTitle(title, left, range) {
1034
+ if (typeof title.scrollTo === "function") title.scrollTo({
1035
+ left,
1036
+ behavior: "instant"
1037
+ });
1038
+ else title.scrollLeft = left;
1039
+ if (left > 0) title.dataset.scrolled = "";
1040
+ else delete title.dataset.scrolled;
1041
+ if (left < range) title.dataset.clipped = "";
1042
+ else delete title.dataset.clipped;
1043
+ }
1044
+ /**
1045
+ * Return the title to its resting state: scrolled to the start with both fade
1046
+ * masks off, so the resting ellipsis renders at full strength.
1047
+ * @param title - the row's clipping title element.
1048
+ */
1049
+ function restTitle(title) {
1007
1050
  if (typeof title.scrollTo === "function") title.scrollTo({
1008
1051
  left: 0,
1009
1052
  behavior: "instant"
1010
1053
  });
1011
1054
  else title.scrollLeft = 0;
1055
+ delete title.dataset.scrolled;
1056
+ delete title.dataset.clipped;
1057
+ }
1058
+ /**
1059
+ * Marquee a title wider than its one-line cell while its row is hovered: the
1060
+ * title clips its own text, so entering crawls it at a constant speed until the
1061
+ * far edge (a fork's incremented title, for example) is in view, then rests
1062
+ * there under the pointer. Overflow of at most {@link MIN_TITLE_REVEAL_PX}
1063
+ * stays put — a barely-clipped title moving a few pixels reads as jitter, not a
1064
+ * reveal. Leaving returns the title to the start in one step, because the
1065
+ * resting ellipsis and the narrowed cell would otherwise meet the text while it
1066
+ * travelled back. Reduced motion jumps to the far edge instead of crawling.
1067
+ * @param title - ref to the row's clipping title element.
1068
+ * @returns stable pointer enter/leave handlers for the row.
1069
+ */
1070
+ function useTitleMarquee(title) {
1071
+ const frame = (0, react.useRef)(0);
1072
+ (0, react.useEffect)(() => () => {
1073
+ cancelAnimationFrame(frame.current);
1074
+ }, []);
1075
+ return (0, react.useMemo)(() => ({
1076
+ enter: () => {
1077
+ /* v8 ignore next -- defensive: the title span renders unconditionally. */
1078
+ if (title.current === null) return;
1079
+ const element = title.current;
1080
+ const range = element.scrollWidth - element.clientWidth;
1081
+ if (range <= MIN_TITLE_REVEAL_PX) return;
1082
+ if (prefersReducedMotion()) {
1083
+ placeTitle(element, range, range);
1084
+ return;
1085
+ }
1086
+ cancelAnimationFrame(frame.current);
1087
+ let previous;
1088
+ let position = 0;
1089
+ const step = (now) => {
1090
+ position += previous === void 0 ? 0 : (now - previous) * TITLE_MARQUEE_PX_PER_MS;
1091
+ previous = now;
1092
+ placeTitle(element, Math.min(position, range), range);
1093
+ if (position < range) frame.current = requestAnimationFrame(step);
1094
+ };
1095
+ frame.current = requestAnimationFrame(step);
1096
+ },
1097
+ leave: () => {
1098
+ cancelAnimationFrame(frame.current);
1099
+ /* v8 ignore next -- defensive: the title span renders unconditionally. */
1100
+ if (title.current === null) return;
1101
+ restTitle(title.current);
1102
+ }
1103
+ }), [title]);
1012
1104
  }
1013
1105
  /** Localized compact relative time ("刚刚"/"5分钟" in zh, "now"/"5min" in en). */
1014
1106
  function timeLabel(updatedAt, now, t) {
@@ -1314,8 +1406,9 @@ window.__ModuleLoader__.load({
1314
1406
  });
1315
1407
  }
1316
1408
  /**
1317
- * One top-level 34px session row: status dot (pending user interaction outranks
1318
- * own or descendant activity), title, relative time, and the row actions menu.
1409
+ * One top-level 32px session row: status dot (pending user interaction outranks
1410
+ * own or descendant activity), a title that marquees while the row is hovered,
1411
+ * relative time, and the hover action strip (menu, archive, pin).
1319
1412
  * @param props.node - derived session node.
1320
1413
  * @param props.currentId - selected session id (row highlight).
1321
1414
  * @param props.now - epoch ms for relative-time formatting.
@@ -1323,6 +1416,7 @@ window.__ModuleLoader__.load({
1323
1416
  * @param props.onRename - open the session rename dialog (id + current title).
1324
1417
  * @param props.onFork - fork a session at its last completed turn.
1325
1418
  * @param props.onArchive - archive a session by id.
1419
+ * @param props.onCopyId - copy a session's complete id without opening it.
1326
1420
  * @param props.onPin - pin a session by id (leading its group and the flat list).
1327
1421
  * @param props.onUnpin - drop a session's pin.
1328
1422
  * @param props.onReveal - scroll this row into view after search navigation, then acknowledge it.
@@ -1330,7 +1424,7 @@ window.__ModuleLoader__.load({
1330
1424
  * @param props.t - the browser root's locale seat.
1331
1425
  * @returns the session row.
1332
1426
  */
1333
- function SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork, onArchive, onPin, onUnpin, onReveal, drag, renderSlot, t }) {
1427
+ function SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork, onArchive, onCopyId, onPin, onUnpin, onReveal, drag, renderSlot, t }) {
1334
1428
  const row = node;
1335
1429
  const title = displayTitle(node, t);
1336
1430
  const selected = node.id === currentId;
@@ -1340,6 +1434,7 @@ window.__ModuleLoader__.load({
1340
1434
  const [menuOpen, setMenuOpen] = (0, react.useState)(false);
1341
1435
  const rowRef = (0, react.useRef)(null);
1342
1436
  const titleRef = (0, react.useRef)(null);
1437
+ const marquee = useTitleMarquee(titleRef);
1343
1438
  (0, react.useEffect)(() => {
1344
1439
  if (onReveal === void 0) return;
1345
1440
  rowRef.current?.scrollIntoView({ block: "nearest" });
@@ -1365,6 +1460,11 @@ window.__ModuleLoader__.load({
1365
1460
  id: "archive",
1366
1461
  label: t("menu.archiveSession"),
1367
1462
  icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconArchiveOutline20, { size: 16 })
1463
+ },
1464
+ {
1465
+ id: "copyId",
1466
+ label: t("menu.sessionId"),
1467
+ icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCopyOutlineRegular, {})
1368
1468
  }
1369
1469
  ];
1370
1470
  return (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.HoverCard, {
@@ -1376,12 +1476,8 @@ window.__ModuleLoader__.load({
1376
1476
  onClick: () => {
1377
1477
  onOpen(node.id);
1378
1478
  },
1379
- onPointerEnter: () => {
1380
- revealClippedTitle(titleRef.current, true);
1381
- },
1382
- onPointerLeave: () => {
1383
- revealClippedTitle(titleRef.current, false);
1384
- },
1479
+ onPointerEnter: marquee.enter,
1480
+ onPointerLeave: marquee.leave,
1385
1481
  draggable,
1386
1482
  onDragStart: drag === void 0 || row.blank ? void 0 : (e) => {
1387
1483
  e.dataTransfer.effectAllowed = "move";
@@ -1417,41 +1513,67 @@ window.__ModuleLoader__.load({
1417
1513
  row.pinned && !row.blank && (0, react_jsx_runtime.jsx)(PinnedIndicator, { t }),
1418
1514
  !row.blank && (0, react_jsx_runtime.jsxs)("span", {
1419
1515
  className: Rows_module_css_default.rowActions,
1420
- children: [(0, react_jsx_runtime.jsx)("button", {
1421
- type: "button",
1422
- className: Rows_module_css_default.iconButton,
1423
- "aria-label": t(row.pinned ? "actions.unpin" : "actions.pin"),
1424
- onClick: (e) => {
1425
- e.stopPropagation();
1426
- (row.pinned ? onUnpin : onPin)(node.id);
1427
- },
1428
- children: row.pinned ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPinFillRegular, { size: 14 }) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPinOutlineRegular, { size: 14 })
1429
- }), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Menu, {
1430
- open: menuOpen,
1431
- onClose: () => {
1432
- setMenuOpen(false);
1433
- },
1434
- items: sessionMenuItems,
1435
- onSelect: (id) => {
1436
- setMenuOpen(false);
1437
- if (id === "pin") (row.pinned ? onUnpin : onPin)(node.id);
1438
- if (id === "rename") onRename(node.id, row.title);
1439
- if (id === "fork") onFork(node.id);
1440
- if (id === "archive") onArchive(node.id);
1441
- },
1442
- portal: true,
1443
- closeOnPointerLeave: true,
1444
- anchor: (0, react_jsx_runtime.jsx)("button", {
1445
- type: "button",
1446
- className: Rows_module_css_default.iconButton,
1447
- "aria-label": t("actions.session.aria", { name: title }),
1448
- onClick: (e) => {
1449
- e.stopPropagation();
1450
- setMenuOpen((v) => !v);
1516
+ children: [
1517
+ (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Menu, {
1518
+ open: menuOpen,
1519
+ onClose: () => {
1520
+ setMenuOpen(false);
1521
+ },
1522
+ items: sessionMenuItems,
1523
+ onSelect: (id) => {
1524
+ setMenuOpen(false);
1525
+ if (id === "pin") (row.pinned ? onUnpin : onPin)(node.id);
1526
+ if (id === "rename") onRename(node.id, row.title);
1527
+ if (id === "fork") onFork(node.id);
1528
+ if (id === "archive") onArchive(node.id);
1529
+ if (id === "copyId") onCopyId(node.id);
1451
1530
  },
1452
- children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconEllipsisOutline16, {})
1531
+ portal: true,
1532
+ closeOnPointerLeave: true,
1533
+ anchor: (0, react_jsx_runtime.jsx)("button", {
1534
+ type: "button",
1535
+ className: Rows_module_css_default.iconButton,
1536
+ "aria-label": t("actions.session.aria", { name: title }),
1537
+ onClick: (e) => {
1538
+ e.stopPropagation();
1539
+ setMenuOpen((v) => !v);
1540
+ },
1541
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconEllipsisOutline16, {})
1542
+ })
1543
+ }),
1544
+ (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Tooltip, {
1545
+ label: t("actions.archive"),
1546
+ side: "bottom",
1547
+ delayMs: 500,
1548
+ portal: true,
1549
+ children: (0, react_jsx_runtime.jsx)("button", {
1550
+ type: "button",
1551
+ className: Rows_module_css_default.iconButton,
1552
+ "aria-label": t("menu.archiveSession"),
1553
+ onClick: (e) => {
1554
+ e.stopPropagation();
1555
+ onArchive(node.id);
1556
+ },
1557
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconArchiveOutline20, { size: 16 })
1558
+ })
1559
+ }),
1560
+ (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Tooltip, {
1561
+ label: t(row.pinned ? "actions.unpin" : "actions.pin"),
1562
+ side: "bottom",
1563
+ delayMs: 500,
1564
+ portal: true,
1565
+ children: (0, react_jsx_runtime.jsx)("button", {
1566
+ type: "button",
1567
+ className: Rows_module_css_default.iconButton,
1568
+ "aria-label": t(row.pinned ? "actions.unpin" : "actions.pin"),
1569
+ onClick: (e) => {
1570
+ e.stopPropagation();
1571
+ (row.pinned ? onUnpin : onPin)(node.id);
1572
+ },
1573
+ children: row.pinned ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPinFillRegular, { size: 14 }) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPinOutlineRegular, { size: 14 })
1574
+ })
1453
1575
  })
1454
- })]
1576
+ ]
1455
1577
  })
1456
1578
  ]
1457
1579
  }),
@@ -1710,22 +1832,46 @@ window.__ModuleLoader__.load({
1710
1832
  const SEARCH_DEBOUNCE_MS = 250;
1711
1833
  /** `session.search` wire bound, measured in JavaScript UTF-16 code units. */
1712
1834
  const SEARCH_QUERY_MAX_CODE_UNITS = 500;
1713
- /** Session rows visible per Workspace before the local overflow control. */
1714
- const COLLAPSED_SESSION_LIMIT = 5;
1715
- /** Fold one Workspace without charging its provisional New Session against the ordinary-row limit. */
1716
- function collapsedSessionRows(sessions) {
1717
- let ordinaryCount = 0;
1835
+ /** Non-blank Session rows visible per Workspace while no Session raises its watermark. */
1836
+ const SESSION_WATERMARK_BASE = 5;
1837
+ /** Highest watermark that attention-bearing Sessions raise automatically; Show more may exceed it. */
1838
+ const SESSION_WATERMARK_AUTO_MAX = 50;
1839
+ /** Show the blank row plus the first `watermark` non-blank rows, in list order. */
1840
+ function watermarkRows(sessions, watermark) {
1841
+ let count = 0;
1718
1842
  const rows = sessions.filter((session) => {
1719
1843
  if (session.blank) return true;
1720
- if (ordinaryCount >= COLLAPSED_SESSION_LIMIT) return false;
1721
- ordinaryCount += 1;
1722
- return true;
1844
+ count += 1;
1845
+ return count <= watermark;
1723
1846
  });
1724
1847
  return {
1725
1848
  rows,
1726
1849
  hiddenCount: sessions.length - rows.length
1727
1850
  };
1728
1851
  }
1852
+ /**
1853
+ * Smallest watermark that covers every non-blank row within the automatic maximum that is running, has a running
1854
+ * child, awaits the user, carries an unread completion, or is current, and the reveal target at any position.
1855
+ */
1856
+ function requiredWatermark(sessions, current, revealSessionId) {
1857
+ let index = 0;
1858
+ let required = 0;
1859
+ for (const session of sessions) {
1860
+ if (session.blank) continue;
1861
+ index += 1;
1862
+ if (index <= SESSION_WATERMARK_AUTO_MAX && (session.running || session.runningSubagentCount > 0 || session.pendingInteraction !== void 0 || session.completed || session.id === current) || session.id === revealSessionId) required = index;
1863
+ }
1864
+ return required;
1865
+ }
1866
+ /** Raise each listed group's watermark to its requirement; returns `marks` itself when nothing rises. */
1867
+ function raiseWatermarks(marks, required) {
1868
+ let raised = marks;
1869
+ for (const [key, watermark] of required) if (watermark > (raised[key] ?? SESSION_WATERMARK_BASE)) raised = {
1870
+ ...raised,
1871
+ [key]: watermark
1872
+ };
1873
+ return raised;
1874
+ }
1729
1875
  /** Keep controlled input and RPC payload inside the session.search wire contract. */
1730
1876
  function sanitizeSearchQuery(value) {
1731
1877
  const withoutNul = value.replaceAll("\0", "");
@@ -1736,10 +1882,6 @@ window.__ModuleLoader__.load({
1736
1882
  if (last >= 55296 && last <= 56319 && next >= 56320 && next <= 57343) end--;
1737
1883
  return withoutNul.slice(0, end);
1738
1884
  }
1739
- /** Immutable membership toggle for the local expand-all array. */
1740
- function toggled(list, key) {
1741
- return list.includes(key) ? list.filter((k) => k !== key) : [...list, key];
1742
- }
1743
1885
  /**
1744
1886
  * Accept the native drag at document level while a row drag is active: row
1745
1887
  * hover still owns the insertion marker, and releasing outside the list must
@@ -1838,12 +1980,12 @@ window.__ModuleLoader__.load({
1838
1980
  return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
1839
1981
  }
1840
1982
  /** The scrolling session tree; unmounting drops the sessions subscription and expand-all state. */
1841
- function SessionTree({ list, useSessionStatus, startSession, open, forkSession, workspaces, ungroupedSessionIds, archivedSessionIds, pinnedSessionIds, onSessionPin, onSessionUnpin, workspaceReady, usePanelInfo, onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive, insertWorkspaceBefore, nestWorkspaces, groupExpansion, setGroupExpanded, setSessionOrder, home, t, renderSlot, revealSessionId, onSessionRevealed }) {
1983
+ function SessionTree({ list, useSessionStatus, startSession, open, forkSession, copySessionId, workspaces, ungroupedSessionIds, archivedSessionIds, pinnedSessionIds, onSessionPin, onSessionUnpin, workspaceReady, usePanelInfo, onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive, insertWorkspaceBefore, nestWorkspaces, groupExpansion, setGroupExpanded, setSessionOrder, home, t, renderSlot, revealSessionId, onSessionRevealed }) {
1842
1984
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
1843
1985
  const statuses = useSessionStatus((s) => s);
1844
1986
  const current = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
1845
1987
  const revealGroup = revealSessionId === void 0 || !workspaceReady ? void 0 : owningGroupKey(workspaces, revealSessionId);
1846
- const [expandedSessionGroups, setExpandedSessionGroups] = (0, react.useState)([]);
1988
+ const [storedWatermarks, setStoredWatermarks] = (0, react.useState)({});
1847
1989
  const [drag, setDrag] = (0, react.useState)(null);
1848
1990
  const sessionDropCommitted = (0, react.useRef)(false);
1849
1991
  const [workspaceDrag, setWorkspaceDrag] = (0, react.useState)(null);
@@ -1904,17 +2046,23 @@ window.__ModuleLoader__.load({
1904
2046
  revealGroup,
1905
2047
  setGroupExpanded
1906
2048
  ]);
1907
- (0, react.useEffect)(() => {
1908
- if (revealSessionId === void 0 || revealGroup === void 0) return;
1909
- const group = groups.find((candidate) => candidate.key === revealGroup);
1910
- if (group === void 0 || !group.expanded || !group.sessions.some((row) => row.id === revealSessionId)) return;
1911
- if (collapsedSessionRows(group.sessions).rows.some((row) => row.id === revealSessionId)) return;
1912
- setExpandedSessionGroups((keys) => keys.includes(revealGroup) ? keys : [...keys, revealGroup]);
1913
- }, [
1914
- groups,
1915
- revealGroup,
1916
- revealSessionId
1917
- ]);
2049
+ const baselines = new Map(groups.map((group) => [group.key, Math.max(SESSION_WATERMARK_BASE, requiredWatermark(group.sessions, current, group.key === revealGroup ? revealSessionId : void 0))]));
2050
+ const foldedKeys = new Set(groups.filter((group) => !group.expanded).map((group) => group.key));
2051
+ const underFoldedAncestor = (key) => {
2052
+ for (let owner = parents.get(key); owner !== void 0; owner = parents.get(owner)) if (foldedKeys.has(owner)) return true;
2053
+ return false;
2054
+ };
2055
+ const requiredWatermarks = new Map([...baselines].filter(([key]) => !underFoldedAncestor(key)));
2056
+ const sessionWatermarks = raiseWatermarks(storedWatermarks, requiredWatermarks);
2057
+ if (sessionWatermarks !== storedWatermarks) setStoredWatermarks((marks) => raiseWatermarks(marks, requiredWatermarks));
2058
+ const dropWatermarks = (drop) => {
2059
+ if (!Object.keys(storedWatermarks).some(drop)) return;
2060
+ setStoredWatermarks((marks) => Object.fromEntries(Object.entries(marks).filter(([key]) => !drop(key))));
2061
+ };
2062
+ const withinGroup = (key, root) => {
2063
+ for (let owner = key; owner !== void 0; owner = parents.get(owner)) if (owner === root) return true;
2064
+ return false;
2065
+ };
1918
2066
  const now = Date.now();
1919
2067
  const commitSessionDrag = (activeDrag, over) => {
1920
2068
  if (sessionDropCommitted.current) return;
@@ -1922,8 +2070,10 @@ window.__ModuleLoader__.load({
1922
2070
  setDrag(null);
1923
2071
  const group = groups.find((candidate) => candidate.key === activeDrag.accountKey);
1924
2072
  if (group === void 0) return;
1925
- const sessionsExpanded = expandedSessionGroups.includes(group.key);
1926
- const renderedSessions = sessionsExpanded ? group.sessions : collapsedSessionRows(group.sessions).rows;
2073
+ const watermark = sessionWatermarks[group.key] ?? SESSION_WATERMARK_BASE;
2074
+ const visible = watermarkRows(group.sessions, watermark);
2075
+ const sessionsExpanded = visible.hiddenCount === 0;
2076
+ const renderedSessions = visible.rows;
1927
2077
  const targetIndex = renderedSessions.findIndex((session) => session.id === over.id);
1928
2078
  if (targetIndex === -1) return;
1929
2079
  const sourceIndex = renderedSessions.findIndex((session) => session.id === activeDrag.sessionId);
@@ -1951,10 +2101,10 @@ window.__ModuleLoader__.load({
1951
2101
  nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId);
1952
2102
  if (!sessionsExpanded && sourceIndex !== -1) {
1953
2103
  const nodes = new Map(group.sessions.map((node) => [node.id, node]));
1954
- if (!collapsedSessionRows(nextOrder.flatMap((id) => {
2104
+ if (!watermarkRows(nextOrder.flatMap((id) => {
1955
2105
  const node = nodes.get(id);
1956
2106
  return node === void 0 ? [] : [node];
1957
- })).rows.some((node) => node.id === activeDrag.sessionId)) return;
2107
+ }), watermark).rows.some((node) => node.id === activeDrag.sessionId)) return;
1958
2108
  }
1959
2109
  const currentBlank = group.sessions.find((node) => node.blank)?.id;
1960
2110
  setSessionOrder(activeDrag.accountKey, pinCurrentBlank(nextOrder, currentBlank));
@@ -1992,8 +2142,10 @@ window.__ModuleLoader__.load({
1992
2142
  const workspaceId = group.workspaceId;
1993
2143
  const children = childrenByParent.get(group.key) ?? [];
1994
2144
  const compatibleDrag = workspaceDrag !== null && parents.get(workspaceDrag.workspaceId) === parents.get(group.key);
1995
- const collapsed = collapsedSessionRows(group.sessions);
1996
- const sessionsExpanded = expandedSessionGroups.includes(group.key);
2145
+ const watermark = sessionWatermarks[group.key] ?? SESSION_WATERMARK_BASE;
2146
+ const collapsed = watermarkRows(group.sessions, baselines.get(group.key) ?? SESSION_WATERMARK_BASE);
2147
+ const visible = watermarkRows(group.sessions, watermark);
2148
+ const sessionsExpanded = visible.hiddenCount === 0;
1997
2149
  const workspaceMarker = workspaceId !== void 0 && workspaceDrag?.over?.id === workspaceId ? workspaceDrag.over.half : null;
1998
2150
  const workspaceDragProps = workspaceId === void 0 ? void 0 : {
1999
2151
  start: () => {
@@ -2058,7 +2210,7 @@ window.__ModuleLoader__.load({
2058
2210
  home,
2059
2211
  t,
2060
2212
  onToggle: () => {
2061
- if (group.expanded) setExpandedSessionGroups((keys) => keys.filter((key) => key !== group.key));
2213
+ if (group.expanded) dropWatermarks((key) => withinGroup(key, group.key));
2062
2214
  setGroupExpanded(group.key, !group.expanded);
2063
2215
  },
2064
2216
  onCreate: () => {
@@ -2083,7 +2235,7 @@ window.__ModuleLoader__.load({
2083
2235
  role: "group",
2084
2236
  children: children.map((child) => renderGroup(child, depth + 1))
2085
2237
  }),
2086
- (sessionsExpanded ? group.sessions : collapsed.rows).map((node) => {
2238
+ visible.rows.map((node) => {
2087
2239
  const sameGroupDrag = drag !== null && drag.accountKey === group.key;
2088
2240
  const normalizeHalf = (half) => node.blank ? "after" : half;
2089
2241
  const dragProps = {
@@ -2129,6 +2281,9 @@ window.__ModuleLoader__.load({
2129
2281
  onRename: onSessionRename,
2130
2282
  onFork: forkSession,
2131
2283
  onArchive: onSessionArchive,
2284
+ onCopyId: (sessionId) => {
2285
+ copySessionId(sessionId);
2286
+ },
2132
2287
  onPin: onSessionPin,
2133
2288
  onUnpin: onSessionUnpin,
2134
2289
  onReveal: node.id === revealSessionId && group.key === revealGroup ? () => {
@@ -2144,9 +2299,16 @@ window.__ModuleLoader__.load({
2144
2299
  className: WorkspaceBrowser_module_css_default.sessionOverflowButton,
2145
2300
  "aria-expanded": sessionsExpanded,
2146
2301
  onClick: () => {
2147
- setExpandedSessionGroups((keys) => toggled(keys, group.key));
2302
+ if (sessionsExpanded) {
2303
+ dropWatermarks((key) => key === group.key);
2304
+ return;
2305
+ }
2306
+ setStoredWatermarks((marks) => ({
2307
+ ...marks,
2308
+ [group.key]: visible.hiddenCount <= SESSION_WATERMARK_BASE ? Infinity : watermark + SESSION_WATERMARK_BASE
2309
+ }));
2148
2310
  },
2149
- children: sessionsExpanded ? t("sessions.collapse") : t("sessions.expand", { n: collapsed.hiddenCount })
2311
+ children: sessionsExpanded ? t("sessions.collapse") : t("sessions.expand", { n: visible.hiddenCount })
2150
2312
  })
2151
2313
  ]
2152
2314
  }, group.key);
@@ -2172,7 +2334,7 @@ window.__ModuleLoader__.load({
2172
2334
  });
2173
2335
  }
2174
2336
  /** The flat "In one list" body: every session is one draggable top-level row. */
2175
- function FlatList({ list, sessionIds, pinnedSessionIds, useSessionStatus, open, forkSession, onSessionRename, onSessionArchive, onSessionPin, onSessionUnpin, usePanelInfo, setSessionOrder, revealSessionId, onSessionRevealed, t, renderSlot }) {
2337
+ function FlatList({ list, sessionIds, pinnedSessionIds, useSessionStatus, open, forkSession, copySessionId, onSessionRename, onSessionArchive, onSessionPin, onSessionUnpin, usePanelInfo, setSessionOrder, revealSessionId, onSessionRevealed, t, renderSlot }) {
2176
2338
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
2177
2339
  const statuses = useSessionStatus((s) => s);
2178
2340
  const rows = (0, react.useMemo)(() => deriveFlat(list, sessionIds, pinnedSessionIds, statuses), [
@@ -2223,6 +2385,9 @@ window.__ModuleLoader__.load({
2223
2385
  onRename: onSessionRename,
2224
2386
  onFork: forkSession,
2225
2387
  onArchive: onSessionArchive,
2388
+ onCopyId: (sessionId) => {
2389
+ copySessionId(sessionId);
2390
+ },
2226
2391
  onPin: onSessionPin,
2227
2392
  onUnpin: onSessionUnpin,
2228
2393
  onReveal: node.id === revealSessionId ? () => {
@@ -2333,7 +2498,7 @@ window.__ModuleLoader__.load({
2333
2498
  * @param props - composed slot props (shell owner share + store + injected actions).
2334
2499
  * @returns the region element tree.
2335
2500
  */
2336
- function WorkspaceBrowser({ wide, usePanelInfo, expandSidebar, useSessions, useSessionStatus, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, pinSession, unpinSession, createWorkspace, searchSessions, searchResultLimit, requestSearch, requestAddWorkspace, closeAddWorkspace, closeRenameRequest, setDirectoryBusy, useDirectoryFlow, useHostInfo, useWorkspaceShortcuts, useShortcuts, renderSlot, t }) {
2501
+ function WorkspaceBrowser({ wide, usePanelInfo, expandSidebar, useSessions, useSessionStatus, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, copySessionId, pinSession, unpinSession, createWorkspace, searchSessions, searchResultLimit, requestSearch, requestAddWorkspace, closeAddWorkspace, closeRenameRequest, setDirectoryBusy, useDirectoryFlow, useHostInfo, useWorkspaceShortcuts, useShortcuts, renderSlot, t }) {
2337
2502
  const home = useHostInfo((info) => info.home);
2338
2503
  const list = useSessions((state) => state);
2339
2504
  const workspaces = useWorkspaces((state) => state.items);
@@ -2839,6 +3004,7 @@ window.__ModuleLoader__.load({
2839
3004
  useSessionStatus,
2840
3005
  open,
2841
3006
  forkSession,
3007
+ copySessionId,
2842
3008
  onSessionRename,
2843
3009
  onSessionArchive,
2844
3010
  onSessionPin,
@@ -2853,6 +3019,7 @@ window.__ModuleLoader__.load({
2853
3019
  usePanelInfo,
2854
3020
  list,
2855
3021
  useSessionStatus,
3022
+ copySessionId,
2856
3023
  renderSlot,
2857
3024
  onSessionRename,
2858
3025
  onSessionArchive,
@@ -3026,6 +3193,91 @@ window.__ModuleLoader__.load({
3026
3193
  });
3027
3194
  }
3028
3195
  //#endregion
3196
+ //#region lib/types/client/toast/RowActionToast.js
3197
+ /**
3198
+ * Dispatch the current notice without interpreting its business payload.
3199
+ * @param props - Current notice, dismissal, and the declared keyed renderer.
3200
+ * @returns The selected notice presentation, or nothing.
3201
+ */
3202
+ function RowActionToast({ useToast, dismissToast, renderSlot }) {
3203
+ const notice = useToast((current) => current);
3204
+ if (notice === null) return null;
3205
+ return renderSlot("workspace.toast", {
3206
+ ...notice,
3207
+ dismissToast
3208
+ }, { entryKey: notice.kind });
3209
+ }
3210
+ //#endregion
3211
+ //#region lib/types/client/toast/Toast.js
3212
+ /** One presentation Factory and the adapters for keyed Workspace notices. */
3213
+ /**
3214
+ * Render one notice using the shared Factory.
3215
+ * @param props - Keyed payload, injected display mapping, locale, and Factory renderer.
3216
+ * @returns The shared Toast occurrence.
3217
+ */
3218
+ function ToastEntry(props) {
3219
+ const { dismissToast, display, t, renderFactorySlot, seq } = props;
3220
+ return renderFactorySlot("workspace.toast.content", {
3221
+ ...display(props, t, dismissToast),
3222
+ seq,
3223
+ onDone: dismissToast
3224
+ });
3225
+ }
3226
+ /**
3227
+ * Present plain display data with the shared primitive's timing and portal.
3228
+ * @param props - Localized display fields, display identity, and dismissal.
3229
+ * @returns One Toast, remounted only for a new display identity.
3230
+ */
3231
+ function WorkspaceToast({ seq, text, tone, onDone }) {
3232
+ return (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Toast, {
3233
+ text,
3234
+ onDone,
3235
+ ...tone === "success" ? { tone: "success" } : {},
3236
+ ...tone === "warning" ? { icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconWarningOutlineRegular, {}) } : {}
3237
+ }, seq);
3238
+ }
3239
+ //#endregion
3240
+ //#region lib/types/client/toast/register.js
3241
+ /**
3242
+ * Register notice presentations with their business-owned display fields.
3243
+ * @param ctx - Workspace plugin context.
3244
+ */
3245
+ function registerToasts(ctx) {
3246
+ ctx.slots.registerFactory({
3247
+ name: "workspace.toast.content",
3248
+ scope: "root"
3249
+ }, WorkspaceToast);
3250
+ ctx.slots.inject("workspace.toast", function* () {
3251
+ yield ctx.slots.register({
3252
+ name: "workspace.toast",
3253
+ key: "sessionIdCopied",
3254
+ locale: "workspace",
3255
+ inject: () => ({ display: (_notice, t) => ({
3256
+ text: t("toast.sessionIdCopied"),
3257
+ tone: "success"
3258
+ }) })
3259
+ }, ToastEntry);
3260
+ yield ctx.slots.register({
3261
+ name: "workspace.toast",
3262
+ key: "createFailed",
3263
+ locale: "workspace",
3264
+ inject: () => ({ display: (notice) => ({
3265
+ text: notice.message,
3266
+ tone: "warning"
3267
+ }) })
3268
+ }, ToastEntry);
3269
+ yield ctx.slots.register({
3270
+ name: "workspace.toast",
3271
+ key: "sessionIdCopyFailed",
3272
+ locale: "workspace",
3273
+ inject: () => ({ display: (_notice, t) => ({
3274
+ text: t("toast.sessionIdCopyFailed"),
3275
+ tone: "warning"
3276
+ }) })
3277
+ }, ToastEntry);
3278
+ });
3279
+ }
3280
+ //#endregion
3029
3281
  //#region lib/types/client/locales.js
3030
3282
  /**
3031
3283
  * `workspace` namespace dictionaries: the browsing region (section header,
@@ -3034,6 +3286,8 @@ window.__ModuleLoader__.load({
3034
3286
  */
3035
3287
  /** Simplified Chinese dictionary (the key-set source of truth). */
3036
3288
  const zh = {
3289
+ "draft.workspaceRequired": "请先选择工作区",
3290
+ "draft.initializationFailed": "暂时无法填入草稿,请稍后重试",
3037
3291
  "group.ungrouped": "未分组",
3038
3292
  "session.new": "新会话",
3039
3293
  "session.untitled": "未命名",
@@ -3081,12 +3335,16 @@ window.__ModuleLoader__.load({
3081
3335
  "menu.unpinSession": "取消置顶",
3082
3336
  "menu.fork": "分叉会话",
3083
3337
  "menu.archiveSession": "归档会话",
3338
+ "menu.sessionId": "复制 Session ID",
3339
+ "toast.sessionIdCopied": "Session ID 已复制",
3340
+ "toast.sessionIdCopyFailed": "复制失败,请重试",
3084
3341
  "row.pinned": "已置顶",
3085
3342
  "sessions.count.one": "{n} 个会话",
3086
3343
  "sessions.count.other": "{n} 个会话",
3087
3344
  "actions.workspace.aria": "工作区“{name}”的操作",
3088
3345
  "actions.session.aria": "会话“{name}”的操作",
3089
3346
  "actions.newSession.aria": "在“{name}”中新建会话",
3347
+ "actions.archive": "归档会话",
3090
3348
  "actions.pin": "置顶会话",
3091
3349
  "actions.unpin": "取消置顶",
3092
3350
  "status.running": "进行中",
@@ -3110,6 +3368,8 @@ window.__ModuleLoader__.load({
3110
3368
  };
3111
3369
  /** English dictionary, checked complete against the zh key set. */
3112
3370
  const en = {
3371
+ "draft.workspaceRequired": "Choose a workspace first.",
3372
+ "draft.initializationFailed": "Could not fill the draft. Please try again shortly.",
3113
3373
  "group.ungrouped": "Ungrouped",
3114
3374
  "session.new": "New Session",
3115
3375
  "session.untitled": "Untitled",
@@ -3157,12 +3417,16 @@ window.__ModuleLoader__.load({
3157
3417
  "menu.unpinSession": "Unpin session",
3158
3418
  "menu.fork": "Fork session",
3159
3419
  "menu.archiveSession": "Archive session",
3420
+ "menu.sessionId": "Copy Session ID",
3421
+ "toast.sessionIdCopied": "Session ID copied",
3422
+ "toast.sessionIdCopyFailed": "Copy failed. Try again.",
3160
3423
  "row.pinned": "Pinned",
3161
3424
  "sessions.count.one": "{n} session",
3162
3425
  "sessions.count.other": "{n} sessions",
3163
3426
  "actions.workspace.aria": "Workspace actions for {name}",
3164
3427
  "actions.session.aria": "Session actions for {name}",
3165
3428
  "actions.newSession.aria": "New session in {name}",
3429
+ "actions.archive": "Archive",
3166
3430
  "actions.pin": "Pin",
3167
3431
  "actions.unpin": "Unpin",
3168
3432
  "status.running": "Running",
@@ -3221,7 +3485,15 @@ window.__ModuleLoader__.load({
3221
3485
  ...viewHandle,
3222
3486
  create: () => viewInstance
3223
3487
  };
3224
- const uiWorkspace = new UiWorkspaceService(ctx, ctx.remote.directoryPicker, workspaces, sessions, viewInstance.actions);
3488
+ const rowToast = (0, _qilin_agent_client_store.createSnapshotStore)(null);
3489
+ let toastSeq = 0;
3490
+ const notify = (toast) => {
3491
+ rowToast.set({
3492
+ ...toast,
3493
+ seq: ++toastSeq
3494
+ });
3495
+ };
3496
+ const uiWorkspace = new UiWorkspaceService(ctx, ctx.remote.directoryPicker, workspaces, sessions, viewInstance.actions, notify);
3225
3497
  ctx.slots.provideRoot({ hooks: { workspaces: workspaces.list } });
3226
3498
  ctx.effect(() => ctx.locale.register(NS, {
3227
3499
  zh,
@@ -3280,6 +3552,9 @@ window.__ModuleLoader__.load({
3280
3552
  closeAddWorkspace: shortcutControls.closeAdd,
3281
3553
  closeRenameRequest: shortcutControls.closeRename,
3282
3554
  setDirectoryBusy: shortcutControls.directoryBusy,
3555
+ copySessionId: async (sessionId) => {
3556
+ notify({ kind: await (0, _qilin_agent_client_ui_primitives.writeClipboard)(sessionId) ? "sessionIdCopied" : "sessionIdCopyFailed" });
3557
+ },
3283
3558
  hooks: {
3284
3559
  directoryFlow: browserFlowSource,
3285
3560
  hostInfo,
@@ -3290,6 +3565,13 @@ window.__ModuleLoader__.load({
3290
3565
  installWorkspaceShortcuts(ctx, uiWorkspace, shortcutControls, (sessionId) => {
3291
3566
  uiWorkspace.archiveSession(sessionId);
3292
3567
  });
3568
+ registerToasts(ctx);
3569
+ const rowToastInjected = () => ({
3570
+ hooks: { toast: rowToast },
3571
+ dismissToast: () => {
3572
+ rowToast.set(null);
3573
+ }
3574
+ });
3293
3575
  const pickerInjected = () => ({
3294
3576
  createWorkspace: (input) => workspaces.create(input),
3295
3577
  hooks: { directoryFlow: pickerFlowSource }
@@ -3314,6 +3596,15 @@ window.__ModuleLoader__.load({
3314
3596
  inject: browserInjected,
3315
3597
  locale: NS
3316
3598
  }, WorkspaceBrowser));
3599
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
3600
+ name: "shell.overlay",
3601
+ id: "workspace.row-toast",
3602
+ inject: rowToastInjected,
3603
+ children: { "workspace.toast": {
3604
+ kind: "keyed",
3605
+ scope: "root"
3606
+ } }
3607
+ }, RowActionToast));
3317
3608
  ctx.slots.inject("conversation.hero.workspace", () => ctx.slots.register({
3318
3609
  name: "conversation.hero.workspace",
3319
3610
  children: { "conversation.hero.workspace.directoryFlow": {