@qilin-agent/client-ui-jobs 3.1.3 → 3.1.5

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.i18n.yaml CHANGED
@@ -5,36 +5,36 @@
5
5
  /:
6
6
  en: 5c00e054d1e02700
7
7
  zh: 678f43f0276422f8
8
- /qilin-client-ui-jobs:
9
- en: a0116b43189cb96c
10
- zh: cae51d35bf4c13db
11
- /qilin-client-ui-jobs/summary:
8
+ /qilin-agent-client-ui-jobs:
9
+ en: dd3fc08db8d1c34b
10
+ zh: 89c7e9a5f88ebfc5
11
+ /qilin-agent-client-ui-jobs/summary:
12
12
  en: fac5c2092b57d9df
13
13
  zh: ac05650a6f9dc77a
14
- /qilin-client-ui-jobs/table-of-contents:
14
+ /qilin-agent-client-ui-jobs/table-of-contents:
15
15
  en: d152484eb41ac6b4
16
16
  zh: 09388d293f9be9cb
17
- /qilin-client-ui-jobs/use-this-package:
17
+ /qilin-agent-client-ui-jobs/use-this-package:
18
18
  en: 32646e32b47172e7
19
19
  zh: 83e8fff6de2e8b05
20
- /qilin-client-ui-jobs/use-this-package/dismissal-and-limits:
20
+ /qilin-agent-client-ui-jobs/use-this-package/dismissal-and-limits:
21
21
  en: 63efd5a2b0986bfb
22
22
  zh: ecfc938bdd5c83c6
23
- /qilin-client-ui-jobs/understand-the-implementation:
24
- en: cee083b101337f44
25
- zh: 38e5c4a8278d339b
26
- /qilin-client-ui-jobs/further-exploration:
23
+ /qilin-agent-client-ui-jobs/understand-the-implementation:
24
+ en: 39c4e09c14ef354e
25
+ zh: 8f55f8bbcbad7620
26
+ /qilin-agent-client-ui-jobs/further-exploration:
27
27
  en: c58e82a3ce217c32
28
28
  zh: d6bd297d31bc3c4e
29
- /qilin-client-ui-jobs/model-experience:
29
+ /qilin-agent-client-ui-jobs/model-experience:
30
30
  en: f9ec590439742792
31
31
  zh: 5914634c4b1c0912
32
- /qilin-client-ui-jobs/model-experience/kv-cache-effect:
32
+ /qilin-agent-client-ui-jobs/model-experience/kv-cache-effect:
33
33
  en: 47f982e326fe77fd
34
34
  zh: c2241315dc0c89c9
35
- /qilin-client-ui-jobs/known-limitations-and-deferred-work:
35
+ /qilin-agent-client-ui-jobs/known-limitations-and-deferred-work:
36
36
  en: 82b0b7f1c1dcde46
37
37
  zh: 03344ce9b638799f
38
- /qilin-client-ui-jobs/known-limitations-and-deferred-work/dev-note:
38
+ /qilin-agent-client-ui-jobs/known-limitations-and-deferred-work/dev-note:
39
39
  en: e67f633dc6fe7773
40
40
  zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -39,7 +39,7 @@ Escape closes the list and returns focus to the trigger, as does a pointer press
39
39
  <details>
40
40
  <summary>Implementation internals — click to expand</summary>
41
41
 
42
- The package contributes one entry to `conversation.session.header.actions` (`JobListAction`), and the data arrives entirely through the `jobsBySession` list mirror that the Session Controller binding folds from `session/jobs` frames — no RPC, and no state beyond popover visibility. The badge counts `running` plus `stopping` and is omitted at zero. Rows are ordered with live rows first by `startedAt` ascending, then settled rows by `finishedAt` descending, with a same-millisecond tie broken on start order; a settled row missing `finishedAt` reads as zero rather than as a negative figure, and a duration past an hour stays in hours. Settled rows stay visible because a failed job's `detail` is the only place its failure is legible. The behavior is specified by the [Web background-job display Agent Note](../../../.agents/notes/implemented/feature/2026-08-08-web-background-job-display.md).
42
+ The package contributes one entry to `conversation.session.header.actions` (`JobListAction`), and the data arrives entirely through the `jobsBySession` list mirror that the Session Controller binding folds from `session/jobs` frames — no RPC, and no state beyond popover visibility. The popover portals to the document body and is placed from its anchor and clamped inside the viewport, so the clipping header column and the right sidebar cannot cut it off, and Tab is bridged between the trigger and the list controls. The badge counts `running` plus `stopping` and is omitted at zero. Rows are ordered with live rows first by `startedAt` ascending, then settled rows by `finishedAt` descending, with a same-millisecond tie broken on start order; a settled row missing `finishedAt` reads as zero rather than as a negative figure, and a duration past an hour stays in hours. Settled rows stay visible because a failed job's `detail` is the only place its failure is legible. The behavior is specified by the [Web background-job display Agent Note](../../../.agents/notes/implemented/feature/2026-08-08-web-background-job-display.md).
43
43
 
44
44
  </details>
45
45
 
package/README.zh.md CHANGED
@@ -39,7 +39,7 @@ Escape 关闭列表并把焦点交还触发器,在其外部按下指针同理
39
39
  <details>
40
40
  <summary>实现细节——点击展开</summary>
41
41
 
42
- 本包向 `conversation.session.header.actions` 贡献一个条目(`JobListAction`),数据完全来自会话控制器绑定从 `session/jobs` 帧折叠出的 `jobsBySession` 列表镜像——不发 RPC,除弹层开合外不持有任何状态。角标计数 `running` 加 `stopping`,为零时省略。行序为活跃行在前按 `startedAt` 升序、终态行按 `finishedAt` 降序,毫秒并列按启动顺序打破;缺少 `finishedAt` 的终态行读作零而不是负数,超过一小时的耗时停留在小时单位。终态行保持可见,因为失败任务的 `detail` 是其失败唯一可读之处。行为由 [Web 后台任务展示 Agent Note](../../../.agents/notes/implemented/feature/2026-08-08-web-background-job-display.zh.md) 规定。
42
+ 本包向 `conversation.session.header.actions` 贡献一个条目(`JobListAction`),数据完全来自会话控制器绑定从 `session/jobs` 帧折叠出的 `jobsBySession` 列表镜像——不发 RPC,除弹层开合外不持有任何状态。弹层挂载到 document body,按锚点定位并限制在视口内,因此不会被裁剪的头部列或右侧边栏截断,Tab 也在触发器与列表控件之间做了桥接。角标计数 `running` 加 `stopping`,为零时省略。行序为活跃行在前按 `startedAt` 升序、终态行按 `finishedAt` 降序,毫秒并列按启动顺序打破;缺少 `finishedAt` 的终态行读作零而不是负数,超过一小时的耗时停留在小时单位。终态行保持可见,因为失败任务的 `detail` 是其失败唯一可读之处。行为由 [Web 后台任务展示 Agent Note](../../../.agents/notes/implemented/feature/2026-08-08-web-background-job-display.zh.md) 规定。
43
43
 
44
44
  </details>
45
45
 
package/lib/client.js CHANGED
@@ -6,9 +6,10 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
8
  let react = require("react");
9
+ let react_dom = require("react-dom");
9
10
  let _qilin_agent_client_ui_primitives = require("@qilin-agent/client-ui-primitives");
10
11
  //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-jobs/src/client/JobListAction.module.css.mjs
11
- const css = ".IxCCqa_root{position:relative}.IxCCqa_trigger{border-radius:var(--qilin-radius-sm);min-height:28px;color:var(--qilin-alias-label-tertiary);cursor:pointer;background:0 0;border:0;align-items:center;gap:3px;padding:3px 2px;font-size:12px;line-height:18px;display:inline-flex}.IxCCqa_trigger:hover,.IxCCqa_trigger:focus-visible{color:var(--qilin-alias-label-secondary)}.IxCCqa_trigger svg{transition:transform .12s}.IxCCqa_triggerOpen{transform:rotate(180deg)}.IxCCqa_triggerDot{flex:none}.IxCCqa_count{margin:0 5px}.IxCCqa_menu{z-index:100;box-sizing:border-box;border-radius:var(--qilin-radius-lg);background:var(--qilin-specific-menu);width:500px;max-width:min(560px,100vw - 32px);max-height:min(480px,100vh - 140px);backdrop-filter:var(--qilin-menu-backdrop-filter);--qilin-scrollbar-thumb:var(--qilin-alias-scrollbar-bg-l2);--qilin-scrollbar-thumb-hover:var(--qilin-alias-scrollbar-hover-l2);--qilin-elevation-stroke-color:var(--qilin-alias-border-l1);box-shadow:var(--qilin-elevation-prominent);border:0;flex-direction:column;gap:1px;margin:0;padding:3px;list-style:none;display:flex;position:absolute;top:calc(100% + 5px);left:0;overflow:auto}.IxCCqa_sectionHeader{border-top:.5px solid var(--qilin-alias-border-l1);color:var(--qilin-alias-label-tertiary);justify-content:space-between;align-items:center;margin:4px 4px 1px;padding:4px 0 3px;font-size:11px;line-height:16px;display:flex}.IxCCqa_sectionHeader:first-child{border-top:0;margin-top:0}.IxCCqa_sectionToggle,.IxCCqa_sectionClear{border-radius:var(--qilin-radius-sm);color:var(--qilin-alias-label-tertiary);cursor:pointer;background:0 0;border:0;align-items:center;gap:3px;padding:2px 4px;font-size:11px;line-height:16px;display:inline-flex}.IxCCqa_sectionToggle:hover,.IxCCqa_sectionToggle:focus-visible,.IxCCqa_sectionClear:hover,.IxCCqa_sectionClear:focus-visible{background:var(--qilin-alias-fill-l1);color:var(--qilin-alias-label-secondary)}.IxCCqa_sectionChevron{transition:transform .12s;transform:rotate(-90deg)}.IxCCqa_sectionChevronOpen{transform:none}.IxCCqa_item{flex-direction:column;display:flex}.IxCCqa_rowLine{align-items:stretch;gap:6px;display:flex}.IxCCqa_rowLineLive{border-radius:var(--qilin-radius-md);background:var(--qilin-alias-fill-l2);padding:4px 8px 4px 2px}.IxCCqa_rowLineLive:hover{background:color-mix(in srgb, var(--qilin-alias-label-primary) 9%, transparent)}.IxCCqa_rowLineLive .IxCCqa_kind{background:var(--qilin-specific-menu)}.IxCCqa_rowLine .IxCCqa_row{flex:1;min-width:0}.IxCCqa_rowLineLive .IxCCqa_row:hover{background:0 0}.IxCCqa_rowLineLive .IxCCqa_row:focus-visible{background:color-mix(in srgb, var(--qilin-alias-label-primary) 6%, transparent)}.IxCCqa_chevronBox{border:.5px solid var(--qilin-alias-border-l2);border-radius:var(--qilin-radius-sm);background:var(--qilin-specific-menu);flex:none;justify-content:center;align-self:center;align-items:center;width:20px;height:20px;display:inline-flex}.IxCCqa_stop{border:.5px solid var(--qilin-alias-border-l2);border-radius:var(--qilin-radius-sm);background:var(--qilin-specific-menu);width:20px;height:20px;color:var(--qilin-alias-label-secondary);cursor:pointer;flex:none;justify-content:center;align-self:center;align-items:center;padding:0;display:inline-flex}.IxCCqa_stop:hover,.IxCCqa_stop:focus-visible{border-color:color-mix(in srgb, var(--qilin-alias-state-error-primary) 45%, transparent);color:var(--qilin-alias-state-error-primary)}.IxCCqa_stopArmed,.IxCCqa_stopArmed:hover,.IxCCqa_stopArmed:focus-visible{border-color:color-mix(in srgb, var(--qilin-alias-state-error-primary) 45%, transparent);background:color-mix(in srgb, var(--qilin-alias-state-error-primary) 12%, transparent);width:auto;color:var(--qilin-alias-state-error-primary);gap:4px;padding:0 7px}.IxCCqa_stopLabel{white-space:nowrap;font-size:11px;line-height:18px}.IxCCqa_stopFailed{color:var(--qilin-alias-state-error-primary)}.IxCCqa_row{box-sizing:border-box;border-radius:var(--qilin-radius-sm);width:100%;min-height:28px;color:var(--qilin-alias-label-primary);text-align:left;cursor:pointer;background:0 0;border:0;align-items:center;gap:6px;padding:4px 7px;font-size:12px;line-height:17px;display:flex}.IxCCqa_row:hover,.IxCCqa_row:focus-visible{background:var(--qilin-alias-fill-l1)}.IxCCqa_rowStatic{cursor:default}.IxCCqa_rowStatic:hover{background:0 0}.IxCCqa_rowSettled{color:var(--qilin-alias-label-tertiary)}.IxCCqa_row .IxCCqa_rowDot{flex:none}.IxCCqa_main{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.IxCCqa_primary{align-items:baseline;gap:8px;min-width:0;display:flex}.IxCCqa_secondary{align-items:center;gap:6px;min-width:0;display:flex}.IxCCqa_kind{border-radius:var(--qilin-radius-xs);background:var(--qilin-alias-fill-l2);color:var(--qilin-alias-label-secondary);flex:none;padding:0 5px;font-size:10px;line-height:16px}.IxCCqa_label{min-width:0;font-family:var(--qilin-font-mono);white-space:nowrap;text-overflow:ellipsis;flex:1;overflow:hidden}.IxCCqa_status{max-width:60%;color:var(--qilin-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;flex:none;font-size:10px;line-height:16px;overflow:hidden}.IxCCqa_duration{color:var(--qilin-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;font-size:11px;line-height:18px}.IxCCqa_chevron{color:var(--qilin-alias-label-tertiary);flex:none}.IxCCqa_chevronOpen{transform:rotate(180deg)}.IxCCqa_panel{--dsl-terminal-command-whitespace:pre-wrap;--dsl-terminal-line-whitespace:pre-wrap;--dsl-terminal-output-max-height:288px;margin:0 8px 8px}.IxCCqa_panel [data-terminal]{--dsl-terminal-gutter:12px}.IxCCqa_notice{color:var(--qilin-alias-label-tertiary);margin:0 8px 6px;font-size:11px;line-height:16px}.IxCCqa_noticeError{color:var(--qilin-alias-state-error-primary)}";
12
+ const css = ".IxCCqa_trigger{border-radius:var(--qilin-radius-sm);min-height:28px;color:var(--qilin-alias-label-tertiary);cursor:pointer;background:0 0;border:0;align-items:center;gap:3px;padding:3px 2px;font-size:12px;line-height:18px;display:inline-flex}.IxCCqa_trigger:hover,.IxCCqa_trigger:focus-visible{color:var(--qilin-alias-label-secondary)}.IxCCqa_trigger svg{transition:transform .12s}.IxCCqa_triggerOpen{transform:rotate(180deg)}.IxCCqa_triggerDot{flex:none}.IxCCqa_count{margin:0 5px}.IxCCqa_menu{z-index:100;box-sizing:border-box;border-radius:var(--qilin-radius-lg);background:var(--qilin-specific-menu);width:500px;max-width:min(560px,100vw - 32px);max-height:min(480px,100vh - 140px);backdrop-filter:var(--qilin-menu-backdrop-filter);--qilin-scrollbar-thumb:var(--qilin-alias-scrollbar-bg-l2);--qilin-scrollbar-thumb-hover:var(--qilin-alias-scrollbar-hover-l2);--qilin-elevation-stroke-color:var(--qilin-alias-border-l1);box-shadow:var(--qilin-elevation-prominent);border:0;flex-direction:column;gap:1px;margin:0;padding:3px;list-style:none;display:flex;position:fixed;overflow:auto}.IxCCqa_sectionHeader{border-top:.5px solid var(--qilin-alias-border-l1);color:var(--qilin-alias-label-tertiary);justify-content:space-between;align-items:center;margin:4px 4px 1px;padding:4px 0 3px;font-size:11px;line-height:16px;display:flex}.IxCCqa_sectionHeader:first-child{border-top:0;margin-top:0}.IxCCqa_sectionToggle,.IxCCqa_sectionClear{border-radius:var(--qilin-radius-sm);color:var(--qilin-alias-label-tertiary);cursor:pointer;background:0 0;border:0;align-items:center;gap:3px;padding:2px 4px;font-size:11px;line-height:16px;display:inline-flex}.IxCCqa_sectionToggle:hover,.IxCCqa_sectionToggle:focus-visible,.IxCCqa_sectionClear:hover,.IxCCqa_sectionClear:focus-visible{background:var(--qilin-alias-fill-l1);color:var(--qilin-alias-label-secondary)}.IxCCqa_sectionChevron{transition:transform .12s;transform:rotate(-90deg)}.IxCCqa_sectionChevronOpen{transform:none}.IxCCqa_item{flex-direction:column;display:flex}.IxCCqa_rowLine{align-items:stretch;gap:6px;display:flex}.IxCCqa_rowLineLive{border-radius:var(--qilin-radius-md);background:var(--qilin-alias-fill-l2);padding:4px 8px 4px 2px}.IxCCqa_rowLineLive:hover{background:color-mix(in srgb, var(--qilin-alias-label-primary) 9%, transparent)}.IxCCqa_rowLineLive .IxCCqa_kind{background:var(--qilin-specific-menu)}.IxCCqa_rowLine .IxCCqa_row{flex:1;min-width:0}.IxCCqa_rowLineLive .IxCCqa_row:hover{background:0 0}.IxCCqa_rowLineLive .IxCCqa_row:focus-visible{background:color-mix(in srgb, var(--qilin-alias-label-primary) 6%, transparent)}.IxCCqa_chevronBox{border:.5px solid var(--qilin-alias-border-l2);border-radius:var(--qilin-radius-sm);background:var(--qilin-specific-menu);flex:none;justify-content:center;align-self:center;align-items:center;width:20px;height:20px;display:inline-flex}.IxCCqa_stop{border:.5px solid var(--qilin-alias-border-l2);border-radius:var(--qilin-radius-sm);background:var(--qilin-specific-menu);width:20px;height:20px;color:var(--qilin-alias-label-secondary);cursor:pointer;flex:none;justify-content:center;align-self:center;align-items:center;padding:0;display:inline-flex}.IxCCqa_stop:hover,.IxCCqa_stop:focus-visible{border-color:color-mix(in srgb, var(--qilin-alias-state-error-primary) 45%, transparent);color:var(--qilin-alias-state-error-primary)}.IxCCqa_stopArmed,.IxCCqa_stopArmed:hover,.IxCCqa_stopArmed:focus-visible{border-color:color-mix(in srgb, var(--qilin-alias-state-error-primary) 45%, transparent);background:color-mix(in srgb, var(--qilin-alias-state-error-primary) 12%, transparent);width:auto;color:var(--qilin-alias-state-error-primary);gap:4px;padding:0 7px}.IxCCqa_stopLabel{white-space:nowrap;font-size:11px;line-height:18px}.IxCCqa_stopFailed{color:var(--qilin-alias-state-error-primary)}.IxCCqa_row{box-sizing:border-box;border-radius:var(--qilin-radius-sm);width:100%;min-height:28px;color:var(--qilin-alias-label-primary);text-align:left;cursor:pointer;background:0 0;border:0;align-items:center;gap:6px;padding:4px 7px;font-size:12px;line-height:17px;display:flex}.IxCCqa_row:hover,.IxCCqa_row:focus-visible{background:var(--qilin-alias-fill-l1)}.IxCCqa_rowStatic{cursor:default}.IxCCqa_rowStatic:hover{background:0 0}.IxCCqa_rowSettled{color:var(--qilin-alias-label-tertiary)}.IxCCqa_row .IxCCqa_rowDot{flex:none}.IxCCqa_main{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.IxCCqa_primary{align-items:baseline;gap:8px;min-width:0;display:flex}.IxCCqa_secondary{align-items:center;gap:6px;min-width:0;display:flex}.IxCCqa_kind{border-radius:var(--qilin-radius-xs);background:var(--qilin-alias-fill-l2);color:var(--qilin-alias-label-secondary);flex:none;padding:0 5px;font-size:10px;line-height:16px}.IxCCqa_label{min-width:0;font-family:var(--qilin-font-mono);white-space:nowrap;text-overflow:ellipsis;flex:1;overflow:hidden}.IxCCqa_status{max-width:60%;color:var(--qilin-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;flex:none;font-size:10px;line-height:16px;overflow:hidden}.IxCCqa_duration{color:var(--qilin-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;font-size:11px;line-height:18px}.IxCCqa_chevron{color:var(--qilin-alias-label-tertiary);flex:none}.IxCCqa_chevronOpen{transform:rotate(180deg)}.IxCCqa_panel{--dsl-terminal-command-whitespace:pre-wrap;--dsl-terminal-line-whitespace:pre-wrap;--dsl-terminal-output-max-height:288px;margin:0 8px 8px}.IxCCqa_panel [data-terminal]{--dsl-terminal-gutter:12px}.IxCCqa_notice{color:var(--qilin-alias-label-tertiary);margin:0 8px 6px;font-size:11px;line-height:16px}.IxCCqa_noticeError{color:var(--qilin-alias-state-error-primary)}";
12
13
  const tagId = "@qilin-agent/client-ui-jobs/JobListAction.module.css";
13
14
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
14
15
  const tag = document.createElement("style");
@@ -32,7 +33,6 @@ window.__ModuleLoader__.load({
32
33
  "noticeError": "IxCCqa_noticeError",
33
34
  "panel": "IxCCqa_panel",
34
35
  "primary": "IxCCqa_primary",
35
- "root": "IxCCqa_root",
36
36
  "row": "IxCCqa_row",
37
37
  "rowDot": "IxCCqa_rowDot",
38
38
  "rowLine": "IxCCqa_rowLine",
@@ -60,6 +60,16 @@ window.__ModuleLoader__.load({
60
60
  const NO_JOBS = [];
61
61
  /** Minimum gap kept between the popover and the viewport edges (the Menu primitive's portal margin). */
62
62
  const VIEWPORT_MARGIN = 12;
63
+ /** Distance between the trigger's bottom edge and the popover. */
64
+ const MENU_GAP = 5;
65
+ /** Unplaced list: hidden but laid out so the first placement measures real dimensions. */
66
+ const MEASURE_STYLE = {
67
+ visibility: "hidden",
68
+ left: 0,
69
+ top: 0
70
+ };
71
+ /** Keyboard-reachable controls inside the list, in document order. */
72
+ const FOCUSABLE = "button:not(:disabled), a[href], [tabindex]:not([tabindex=\"-1\"])";
63
73
  /** How long an armed kill waits for its confirming press before disarming. */
64
74
  const KILL_ARM_MS = 3e3;
65
75
  /** How long a failed kill keeps its hint before the button resets. */
@@ -144,6 +154,7 @@ window.__ModuleLoader__.load({
144
154
  copy: t("terminal.copy"),
145
155
  copied: t("terminal.copied"),
146
156
  noOutput: t("terminal.noOutput"),
157
+ commandLine: (line) => t("terminal.commandLine", { n: line }),
147
158
  collapseAria: t("terminal.collapseAria"),
148
159
  collapse: t("terminal.collapse"),
149
160
  /* v8 ignore next */
@@ -318,13 +329,19 @@ window.__ModuleLoader__.load({
318
329
  const rootRef = (0, react.useRef)(null);
319
330
  const triggerRef = (0, react.useRef)(null);
320
331
  const menuRef = (0, react.useRef)(null);
321
- const [menuShift, setMenuShift] = (0, react.useState)(0);
322
332
  const rows = (0, react.useMemo)(() => ordered(jobs), [jobs]);
323
333
  const liveRows = (0, react.useMemo)(() => rows.filter(isLive), [rows]);
324
334
  const settledRows = (0, react.useMemo)(() => rows.filter((job) => !isLive(job) && !clearedKeys.has(String(job.id))), [rows, clearedKeys]);
325
335
  const settledExpanded = settledOpen ?? liveRows.length === 0;
326
336
  const visibleCount = liveRows.length + settledRows.length;
327
- (0, _qilin_agent_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, setOpen);
337
+ const menuPosition = (0, _qilin_agent_client_ui_primitives.useAnchoredPosition)({
338
+ open,
339
+ anchorRef: rootRef,
340
+ panelRef: menuRef,
341
+ gap: MENU_GAP,
342
+ margin: VIEWPORT_MARGIN
343
+ });
344
+ (0, _qilin_agent_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, setOpen, menuRef);
328
345
  (0, react.useEffect)(() => watchRows(sessionId), [sessionId, watchRows]);
329
346
  (0, react.useEffect)(() => {
330
347
  if (!open || liveRows.length === 0) return;
@@ -336,27 +353,6 @@ window.__ModuleLoader__.load({
336
353
  clearInterval(timer);
337
354
  };
338
355
  }, [open, liveRows.length]);
339
- (0, react.useLayoutEffect)(() => {
340
- if (!open) {
341
- setMenuShift(0);
342
- return;
343
- }
344
- const fit = () => {
345
- const root = rootRef.current;
346
- const menu = menuRef.current;
347
- /* v8 ignore next -- both refs are attached while the open popover renders. */
348
- if (root === null || menu === null) return;
349
- const width = menu.offsetWidth;
350
- if (width === 0) return;
351
- const anchorLeft = root.getBoundingClientRect().left;
352
- setMenuShift(Math.max(VIEWPORT_MARGIN - anchorLeft, Math.min(0, window.innerWidth - VIEWPORT_MARGIN - width - anchorLeft)));
353
- };
354
- fit();
355
- window.addEventListener("resize", fit);
356
- return () => {
357
- window.removeEventListener("resize", fit);
358
- };
359
- }, [open]);
360
356
  const expandedRow = open && expandedKey !== void 0 ? rows.find((job) => String(job.id) === expandedKey) : void 0;
361
357
  const activeJob = expandedRow !== void 0 && isObservable(expandedRow) ? expandedRow.id : void 0;
362
358
  (0, react.useEffect)(() => {
@@ -416,10 +412,26 @@ window.__ModuleLoader__.load({
416
412
  if (expandedKey !== void 0 && settledRows.some((job) => String(job.id) === expandedKey)) setExpandedKey(void 0);
417
413
  };
418
414
  const onKeyDown = (event) => {
419
- if (event.key !== "Escape" || !open) return;
420
- event.preventDefault();
421
- setOpen(false);
422
- triggerRef.current?.focus();
415
+ if (!open) return;
416
+ const trigger = triggerRef.current;
417
+ if (event.key === "Escape") {
418
+ event.preventDefault();
419
+ setOpen(false);
420
+ trigger?.focus();
421
+ return;
422
+ }
423
+ const menu = menuRef.current;
424
+ /* v8 ignore next -- both refs are attached while the open list renders. */
425
+ if (event.key !== "Tab" || trigger === null || menu === null) return;
426
+ const focusable = [...menu.querySelectorAll(FOCUSABLE)];
427
+ if (event.target === trigger) {
428
+ if (event.shiftKey) return;
429
+ event.preventDefault();
430
+ focusable[0]?.focus();
431
+ } else if (event.shiftKey ? event.target === focusable[0] : event.target === focusable.at(-1)) {
432
+ if (event.shiftKey) event.preventDefault();
433
+ trigger.focus();
434
+ }
423
435
  };
424
436
  const item = (job) => (0, react_jsx_runtime.jsx)(JobItem, {
425
437
  job,
@@ -439,7 +451,6 @@ window.__ModuleLoader__.load({
439
451
  }, String(job.id));
440
452
  return (0, react_jsx_runtime.jsxs)("div", {
441
453
  ref: rootRef,
442
- className: JobListAction_module_css_default.root,
443
454
  onKeyDown,
444
455
  children: [(0, react_jsx_runtime.jsxs)("button", {
445
456
  ref: triggerRef,
@@ -465,10 +476,10 @@ window.__ModuleLoader__.load({
465
476
  className: open ? JobListAction_module_css_default.triggerOpen : void 0
466
477
  })
467
478
  ]
468
- }), open ? (0, react_jsx_runtime.jsxs)("ul", {
479
+ }), open ? (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)("ul", {
469
480
  ref: menuRef,
470
481
  className: JobListAction_module_css_default.menu,
471
- style: { left: menuShift },
482
+ style: menuPosition ?? MEASURE_STYLE,
472
483
  "aria-label": t("list.aria"),
473
484
  children: [
474
485
  liveRows.length > 0 ? (0, react_jsx_runtime.jsx)("li", {
@@ -499,7 +510,7 @@ window.__ModuleLoader__.load({
499
510
  }) : null,
500
511
  settledExpanded ? settledRows.map(item) : null
501
512
  ]
502
- }) : null]
513
+ }), document.body) : null]
503
514
  });
504
515
  }
505
516
  //#endregion
@@ -543,6 +554,7 @@ window.__ModuleLoader__.load({
543
554
  "terminal.noOutput": "(无输出)",
544
555
  "terminal.collapse": "收起",
545
556
  "terminal.collapseAria": "收起输出",
557
+ "terminal.commandLine": "命令第 {n} 行",
546
558
  "terminal.expand": "展开其余 {n} 行",
547
559
  "terminal.expandAria": "展开被折叠的 {n} 行输出"
548
560
  };
@@ -585,6 +597,7 @@ window.__ModuleLoader__.load({
585
597
  "terminal.noOutput": "(no output)",
586
598
  "terminal.collapse": "Collapse",
587
599
  "terminal.collapseAria": "Collapse output",
600
+ "terminal.commandLine": "Command line {n}",
588
601
  "terminal.expand": "Show {n} more lines",
589
602
  "terminal.expandAria": "Expand {n} collapsed output lines"
590
603
  };
@@ -40,6 +40,7 @@ export declare const zh: {
40
40
  readonly 'terminal.noOutput': "(无输出)";
41
41
  readonly 'terminal.collapse': "收起";
42
42
  readonly 'terminal.collapseAria': "收起输出";
43
+ readonly 'terminal.commandLine': "命令第 {n} 行";
43
44
  readonly 'terminal.expand': "展开其余 {n} 行";
44
45
  readonly 'terminal.expandAria': "展开被折叠的 {n} 行输出";
45
46
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qilin-agent/client-ui-jobs",
3
3
  "description": "Session-header background-job list with on-demand streaming record panels",
4
- "version": "3.1.3",
4
+ "version": "3.1.5",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -45,18 +45,20 @@
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/react": "~18.3.1",
48
+ "@types/react-dom": "~18.3.0",
48
49
  "react": "^18.2.0",
49
- "@qilin-agent/client-test-runtime": "3.1.3",
50
- "@qilin-agent/client-locale": "3.1.3",
51
- "@qilin-agent/client-ui-primitives": "3.1.3",
52
- "@qilin-agent/client-ui-slots": "3.1.3",
53
- "@qilin-agent/api-job-controller": "3.1.3",
54
- "@qilin-agent/kylin": "~4.0.3",
55
- "@qilin-agent/client-ui-conversation": "3.1.3",
56
- "@qilin-agent/client-ui-renderer": "3.1.3",
57
- "@qilin-agent/client-ui-session": "3.1.3",
58
- "@qilin-agent/jobs": "3.1.3",
59
- "@qilin-agent/session": "3.1.3"
50
+ "react-dom": "^18.2.0",
51
+ "@qilin-agent/api-job-controller": "3.1.5",
52
+ "@qilin-agent/client-locale": "3.1.5",
53
+ "@qilin-agent/client-test-runtime": "3.1.5",
54
+ "@qilin-agent/client-ui-conversation": "3.1.5",
55
+ "@qilin-agent/client-ui-primitives": "3.1.5",
56
+ "@qilin-agent/client-ui-renderer": "3.1.5",
57
+ "@qilin-agent/client-ui-session": "3.1.5",
58
+ "@qilin-agent/client-ui-slots": "3.1.5",
59
+ "@qilin-agent/jobs": "3.1.5",
60
+ "@qilin-agent/session": "3.1.5",
61
+ "@qilin-agent/kylin": "~4.0.3"
60
62
  },
61
63
  "files": [
62
64
  "lib/index.js",