@qilin-agent/client-ui-jobs 0.0.0-stage → 3.1.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kkutysllb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,40 @@
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
3
+ # After editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/client/ui-jobs/README.md
5
+ /:
6
+ en: 5c00e054d1e02700
7
+ zh: 678f43f0276422f8
8
+ /qilin-client-ui-jobs:
9
+ en: a0116b43189cb96c
10
+ zh: cae51d35bf4c13db
11
+ /qilin-client-ui-jobs/summary:
12
+ en: fac5c2092b57d9df
13
+ zh: ac05650a6f9dc77a
14
+ /qilin-client-ui-jobs/table-of-contents:
15
+ en: d152484eb41ac6b4
16
+ zh: 09388d293f9be9cb
17
+ /qilin-client-ui-jobs/use-this-package:
18
+ en: 32646e32b47172e7
19
+ zh: 83e8fff6de2e8b05
20
+ /qilin-client-ui-jobs/use-this-package/dismissal-and-limits:
21
+ en: 63efd5a2b0986bfb
22
+ zh: ecfc938bdd5c83c6
23
+ /qilin-client-ui-jobs/understand-the-implementation:
24
+ en: cee083b101337f44
25
+ zh: 38e5c4a8278d339b
26
+ /qilin-client-ui-jobs/further-exploration:
27
+ en: c58e82a3ce217c32
28
+ zh: d6bd297d31bc3c4e
29
+ /qilin-client-ui-jobs/model-experience:
30
+ en: f9ec590439742792
31
+ zh: 5914634c4b1c0912
32
+ /qilin-client-ui-jobs/model-experience/kv-cache-effect:
33
+ en: 47f982e326fe77fd
34
+ zh: c2241315dc0c89c9
35
+ /qilin-client-ui-jobs/known-limitations-and-deferred-work:
36
+ en: 82b0b7f1c1dcde46
37
+ zh: 03344ce9b638799f
38
+ /qilin-client-ui-jobs/known-limitations-and-deferred-work/dev-note:
39
+ en: e67f633dc6fe7773
40
+ zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -1,3 +1,87 @@
1
- # Temporary Holding Version
1
+ ---
2
+ description: "Web background-job surface: the session-header action listing the jobs this session can see; for users and maintainers of the background-job experience."
3
+ kind: "package-reference"
4
+ ---
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # @qilin-agent/client-ui-jobs
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ This package renders the background-job surface of the Web GUI: a session-header action that opens a popover listing the jobs this session can see. It reads host-computed registry state through the runtime's `jobsBySession` mirror and issues no RPC of its own. The trigger appears only when the session has at least one job, with a badge counting running and stopping jobs; settled rows stay visible and de-emphasized until the registry drops them. The model's own view of the same jobs belongs to `qilin-tool-jobs`; this package is a read-only projection for the human.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Mount this plugin alongside the runtime; the job action then appears in the session header whenever the session has at least one job. A click opens the popover: live rows first by start time, then settled rows by finish time, each showing the producer kind, label, status, and an elapsed duration that ticks once per second while live and freezes at completion.
29
+
30
+ ### Dismissal and limits
31
+
32
+ Escape closes the list and returns focus to the trigger, as does a pointer press outside it. The list shows what one session can see through the wire view, so a job owned by another session never appears here; a process restart empties the list while the transcript keeps the `run_in_background` cards that started those jobs.
33
+
34
+ -----
35
+
36
+ <a id="understand-the-implementation"></a>
37
+ ## Understand the implementation
38
+
39
+ <details>
40
+ <summary>Implementation internals — click to expand</summary>
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).
43
+
44
+ </details>
45
+
46
+ -----
47
+
48
+ <a id="further-exploration"></a>
49
+ ## Further Exploration
50
+
51
+ Read these pages when the job surface is not enough. They move from the browser list to the registry and the model-facing tool.
52
+
53
+ - [qilin-tool-jobs](../../jobs/tool-jobs/README.md) — the model-facing jobs tool over the same registry.
54
+ - [Session Controller](../../api/session-controller/README.md) — folds the `jobsBySession` mirror this package reads.
55
+ - [ui-subagent](../ui-subagent/README.md) — the subagent catalog, where a running one-shot background subagent also appears.
56
+ - [Web client architecture](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) — how browser plugin rows load and register slots.
57
+
58
+ -----
59
+
60
+ <a id="model-experience"></a>
61
+ ## Model Experience
62
+
63
+ None, as this package renders host-computed registry state for a human and touches no prompt, message, schema, stream, or tool result.
64
+
65
+ #### KV Cache effect
66
+
67
+ None; the package never assembles or sends provider requests.
68
+
69
+ ## Known Limitations and Deferred Work
70
+
71
+ <a id="known-limitations-and-deferred-work"></a>
72
+
73
+
74
+ These limits define the current job list. They are current package constraints, not a general job-management comparison or a task backlog.
75
+
76
+ - **Rows are read-only** — a job's streamed output and a human-initiated cancellation are separate phases. Cancellation additionally owes a model-facing decision the seam does not answer: `kill()` marks terminal delivery reported, so an interrupt written against the current contract would leave the model believing its job is still running.
77
+ - **The list is not the registry's own set** — it shows what one session can see through the wire view, so a job owned by another session never appears here, and a process restart empties the list while the transcript keeps the `run_in_background` cards that started those jobs. An unowned job (one started without a live `Agent`) reaches every session's list, matching what `list(caller)` reports to every caller.
78
+
79
+ <a id="dev-note"></a>
80
+ ### Dev Note
81
+
82
+ <details>
83
+ <summary>Working context for maintainers — click to expand</summary>
84
+
85
+ None.
86
+
87
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,87 @@
1
+ ---
2
+ description: "Web 后台任务界面:列出本会话可见任务的会话头部动作;供后台任务体验的用户与维护者阅读。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @qilin-agent/client-ui-jobs
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 本包渲染 Web GUI 的后台任务界面:一个会话头部动作,打开后以弹层列出本会话可见的任务。它经运行时提供的 `jobsBySession` 镜像读取宿主计算的注册表状态,自身不发任何 RPC。触发器只在会话至少有一个任务时出现,角标计数运行中与停止中的任务;终态行保持可见并弱化,直到注册表把它们丢弃。模型对同一批任务的视角属于 `qilin-tool-jobs`;本包是给人类看的只读投影。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 与运行时一起挂载本插件;只要会话至少有一个任务,任务动作就会出现在会话头部。点击打开弹层:活跃行在前按开始时间升序,随后终态行按结束时间降序,每行显示生产者 kind、标签、状态,以及一个活跃时每秒跳动、完成后冻结的已耗时。
29
+
30
+ ### 关闭与边界
31
+
32
+ Escape 关闭列表并把焦点交还触发器,在其外部按下指针同理。列表展示的是「一个会话通过协议视图能看到什么」,因此别的会话拥有的任务在这里永不出现;进程重启会清空列表,而 transcript(文本记录)里启动这些任务的 `run_in_background` 卡片仍在。
33
+
34
+ -----
35
+
36
+ <a id="understand-the-implementation"></a>
37
+ ## 理解实现
38
+
39
+ <details>
40
+ <summary>实现细节——点击展开</summary>
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) 规定。
43
+
44
+ </details>
45
+
46
+ -----
47
+
48
+ <a id="further-exploration"></a>
49
+ ## 进一步探索
50
+
51
+ 当任务界面本身无法满足需要时,请阅读以下页面。它们从浏览器列表进入注册表与面向模型的工具。
52
+
53
+ - [qilin-tool-jobs](../../jobs/tool-jobs/README.zh.md)——同一注册表之上的面向模型任务工具。
54
+ - [会话控制器](../../api/session-controller/README.zh.md)——折叠出本包读取的 `jobsBySession` 镜像。
55
+ - [ui-subagent](../ui-subagent/README.zh.md)——subagent 目录,运行中的一次性后台 subagent 也会出现在那里。
56
+ - [Web 客户端架构](../../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md)——浏览器插件行如何加载并注册 slot。
57
+
58
+ -----
59
+
60
+ <a id="model-experience"></a>
61
+ ## 模型体验
62
+
63
+ 无,因为本包为人类渲染宿主计算出的注册表状态,不触及提示词、消息、schema、流或工具结果。
64
+
65
+ #### KV Cache 影响
66
+
67
+ 无;本包从不组装或发送提供方请求。
68
+
69
+ ## 已知限制与延期工作
70
+
71
+ <a id="known-limitations-and-deferred-work"></a>
72
+
73
+
74
+ 这些限制界定了当前任务列表。它们是当前包约束,不是通用任务管理对比或任务积压。
75
+
76
+ - **行是只读的**——任务的流式输出与人类发起的取消是各自独立的阶段。取消还额外欠一个 seam 没有回答的、面向模型的决策:`kill()` 会把终态投递标为已上报,所以照当前契约写出来的中断会让模型一直以为它的任务还在跑。
77
+ - **列表不等于注册表自己的集合**——它展示的是一个会话通过协议视图能看到什么,因此别的会话拥有的任务在这里永不出现;进程重启会清空列表,而 transcript 里启动这些任务的 `run_in_background` 卡片仍在。无主任务(没有活体 `Agent` 时启动的)反过来会进入每个会话的列表,与 `list(caller)` 对每个调用方的报告一致。
78
+
79
+ <a id="dev-note"></a>
80
+ ### 开发备注
81
+
82
+ <details>
83
+ <summary>维护者的工作上下文——点击展开</summary>
84
+
85
+ 无。
86
+
87
+ </details>
package/lib/client.js ADDED
@@ -0,0 +1,628 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@qilin-agent/client-ui-jobs",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react_jsx_runtime = require("react/jsx-runtime");
8
+ let react = require("react");
9
+ let _qilin_agent_client_ui_primitives = require("@qilin-agent/client-ui-primitives");
10
+ //#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 tagId = "@qilin-agent/client-ui-jobs/JobListAction.module.css";
13
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
14
+ const tag = document.createElement("style");
15
+ tag.dataset.plugin = "@qilin-agent/client-ui-jobs";
16
+ tag.dataset.pluginCss = tagId;
17
+ tag.textContent = css;
18
+ document.head.appendChild(tag);
19
+ }
20
+ var JobListAction_module_css_default = {
21
+ "chevron": "IxCCqa_chevron",
22
+ "chevronBox": "IxCCqa_chevronBox",
23
+ "chevronOpen": "IxCCqa_chevronOpen",
24
+ "count": "IxCCqa_count",
25
+ "duration": "IxCCqa_duration",
26
+ "item": "IxCCqa_item",
27
+ "kind": "IxCCqa_kind",
28
+ "label": "IxCCqa_label",
29
+ "main": "IxCCqa_main",
30
+ "menu": "IxCCqa_menu",
31
+ "notice": "IxCCqa_notice",
32
+ "noticeError": "IxCCqa_noticeError",
33
+ "panel": "IxCCqa_panel",
34
+ "primary": "IxCCqa_primary",
35
+ "root": "IxCCqa_root",
36
+ "row": "IxCCqa_row",
37
+ "rowDot": "IxCCqa_rowDot",
38
+ "rowLine": "IxCCqa_rowLine",
39
+ "rowLineLive": "IxCCqa_rowLineLive",
40
+ "rowSettled": "IxCCqa_rowSettled",
41
+ "rowStatic": "IxCCqa_rowStatic",
42
+ "secondary": "IxCCqa_secondary",
43
+ "sectionChevron": "IxCCqa_sectionChevron",
44
+ "sectionChevronOpen": "IxCCqa_sectionChevronOpen",
45
+ "sectionClear": "IxCCqa_sectionClear",
46
+ "sectionHeader": "IxCCqa_sectionHeader",
47
+ "sectionToggle": "IxCCqa_sectionToggle",
48
+ "status": "IxCCqa_status",
49
+ "stop": "IxCCqa_stop",
50
+ "stopArmed": "IxCCqa_stopArmed",
51
+ "stopFailed": "IxCCqa_stopFailed",
52
+ "stopLabel": "IxCCqa_stopLabel",
53
+ "trigger": "IxCCqa_trigger",
54
+ "triggerDot": "IxCCqa_triggerDot",
55
+ "triggerOpen": "IxCCqa_triggerOpen"
56
+ };
57
+ //#endregion
58
+ //#region lib/types/client/JobListAction.js
59
+ /** Stable empty list so a session with no jobs keeps one array identity. */
60
+ const NO_JOBS = [];
61
+ /** Minimum gap kept between the popover and the viewport edges (the Menu primitive's portal margin). */
62
+ const VIEWPORT_MARGIN = 12;
63
+ /** How long an armed kill waits for its confirming press before disarming. */
64
+ const KILL_ARM_MS = 3e3;
65
+ /** How long a failed kill keeps its hint before the button resets. */
66
+ const KILL_FAILED_MS = 4e3;
67
+ function isLive(job) {
68
+ return job.status === "running" || job.status === "stopping";
69
+ }
70
+ /**
71
+ * Whether the row offers an output panel: every live job (its output may
72
+ * still arrive) and a settled one that left retained output behind.
73
+ */
74
+ function isObservable(job) {
75
+ return isLive(job) || job.output.total > 0;
76
+ }
77
+ /** The one-line qualifier beside the status: live progress while running, the terminal reason once settled. */
78
+ function jobDetail(job) {
79
+ return job.progress ?? job.detail;
80
+ }
81
+ /** Closed-union exhaustiveness fence for the wire status set. */
82
+ /* v8 ignore next 3 -- closed-union backstop; only reached if a status is forged */
83
+ function assertNever(value) {
84
+ throw new Error(`unhandled job status: ${JSON.stringify(value)}`);
85
+ }
86
+ /**
87
+ * Status marker semantics. `stopping` and `killed` share the attention color:
88
+ * both mean the work ended (or is ending) on request rather than on its own.
89
+ */
90
+ function dotState(status) {
91
+ switch (status) {
92
+ case "running": return "ongoing";
93
+ case "stopping": return "warning";
94
+ case "completed": return "done";
95
+ case "killed": return "warning";
96
+ case "failed": return "error";
97
+ /* v8 ignore next -- closed wire status union */
98
+ default: return assertNever(status);
99
+ }
100
+ }
101
+ function statusLabel(status, t) {
102
+ switch (status) {
103
+ case "running": return t("status.running");
104
+ case "stopping": return t("status.stopping");
105
+ case "completed": return t("status.completed");
106
+ case "killed": return t("status.killed");
107
+ case "failed": return t("status.failed");
108
+ /* v8 ignore next -- closed wire status union */
109
+ default: return assertNever(status);
110
+ }
111
+ }
112
+ /**
113
+ * Elapsed time in at most two adjacent units. A job that outlives an hour is
114
+ * already exceptional, so hours is the widest unit — beyond that the figure
115
+ * stays in hours rather than growing a day/month vocabulary no producer
116
+ * currently reaches.
117
+ */
118
+ function formatDuration(elapsedMs, t) {
119
+ const total = Math.max(0, Math.floor(elapsedMs / 1e3));
120
+ const seconds = total % 60;
121
+ const minutes = Math.floor(total / 60) % 60;
122
+ const hours = Math.floor(total / 3600);
123
+ if (hours > 0) return t("duration.hours", {
124
+ hours,
125
+ minutes
126
+ });
127
+ if (minutes > 0) return t("duration.minutes", {
128
+ minutes,
129
+ seconds
130
+ });
131
+ return t("duration.seconds", { seconds });
132
+ }
133
+ /** Localized display copy for the embedded terminal panel. */
134
+ function terminalLabels(t) {
135
+ return {
136
+ /* v8 ignore next */
137
+ signal: (signal) => t("terminal.signal", { signal }),
138
+ /* v8 ignore next */
139
+ exitCode: (code) => t("terminal.exitCode", { code }),
140
+ noExitCode: t("terminal.noExitCode"),
141
+ running: t("terminal.running"),
142
+ failed: t("terminal.failed"),
143
+ done: t("terminal.done"),
144
+ copy: t("terminal.copy"),
145
+ copied: t("terminal.copied"),
146
+ noOutput: t("terminal.noOutput"),
147
+ collapseAria: t("terminal.collapseAria"),
148
+ collapse: t("terminal.collapse"),
149
+ /* v8 ignore next */
150
+ expandAria: (hidden) => t("terminal.expandAria", { n: hidden }),
151
+ /* v8 ignore next */
152
+ expand: (hidden) => t("terminal.expand", { n: hidden })
153
+ };
154
+ }
155
+ /**
156
+ * Live rows first in start order, then settled rows newest-first. Two rows
157
+ * that settled in the same millisecond fall back to start order, so the sort
158
+ * never depends on the host's map iteration.
159
+ */
160
+ function ordered(jobs) {
161
+ return [...jobs].sort((left, right) => {
162
+ const liveLeft = isLive(left);
163
+ if (liveLeft !== isLive(right)) return liveLeft ? -1 : 1;
164
+ if (liveLeft) return left.startedAt - right.startedAt;
165
+ const finished = (right.finishedAt ?? right.startedAt) - (left.finishedAt ?? left.startedAt);
166
+ return finished !== 0 ? finished : left.startedAt - right.startedAt;
167
+ });
168
+ }
169
+ /** One job row plus, when observable and expanded, its live output panel. */
170
+ function JobItem({ job, view, expanded, now, onToggle, kill, t }) {
171
+ const live = isLive(job);
172
+ const status = statusLabel(job.status, t);
173
+ const detail = jobDetail(job);
174
+ const observable = isObservable(job);
175
+ const labels = (0, react.useMemo)(() => terminalLabels(t), [t]);
176
+ const duration = formatDuration(live ? now - job.startedAt : (job.finishedAt ?? job.startedAt) - job.startedAt, t);
177
+ const durationCell = (0, react_jsx_runtime.jsx)("span", {
178
+ className: JobListAction_module_css_default.duration,
179
+ title: t(live ? "duration.title.live" : "duration.title.done", { duration }),
180
+ children: duration
181
+ });
182
+ const body = live ? (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
183
+ (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.StateDot, {
184
+ state: dotState(job.status),
185
+ className: JobListAction_module_css_default.rowDot
186
+ }),
187
+ (0, react_jsx_runtime.jsxs)("span", {
188
+ className: JobListAction_module_css_default.main,
189
+ children: [(0, react_jsx_runtime.jsx)("span", {
190
+ className: JobListAction_module_css_default.primary,
191
+ children: (0, react_jsx_runtime.jsx)("span", {
192
+ className: JobListAction_module_css_default.label,
193
+ title: job.label,
194
+ children: job.label
195
+ })
196
+ }), (0, react_jsx_runtime.jsxs)("span", {
197
+ className: JobListAction_module_css_default.secondary,
198
+ title: detail ?? status,
199
+ children: [
200
+ (0, react_jsx_runtime.jsx)("span", {
201
+ className: JobListAction_module_css_default.kind,
202
+ children: job.kind
203
+ }),
204
+ detail !== void 0 ? (0, react_jsx_runtime.jsx)("span", {
205
+ className: JobListAction_module_css_default.status,
206
+ children: detail
207
+ }) : null,
208
+ durationCell
209
+ ]
210
+ })]
211
+ }),
212
+ (0, react_jsx_runtime.jsx)("span", {
213
+ className: JobListAction_module_css_default.chevronBox,
214
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, {
215
+ size: 12,
216
+ className: expanded ? `${JobListAction_module_css_default.chevron} ${JobListAction_module_css_default.chevronOpen}` : JobListAction_module_css_default.chevron
217
+ })
218
+ })
219
+ ] }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
220
+ (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.StateDot, {
221
+ state: dotState(job.status),
222
+ className: JobListAction_module_css_default.rowDot
223
+ }),
224
+ (0, react_jsx_runtime.jsx)("span", {
225
+ className: JobListAction_module_css_default.kind,
226
+ children: job.kind
227
+ }),
228
+ (0, react_jsx_runtime.jsx)("span", {
229
+ className: JobListAction_module_css_default.label,
230
+ title: job.label,
231
+ children: job.label
232
+ }),
233
+ (0, react_jsx_runtime.jsx)("span", {
234
+ className: JobListAction_module_css_default.status,
235
+ title: detail ?? status,
236
+ children: detail ?? status
237
+ }),
238
+ durationCell,
239
+ observable ? (0, react_jsx_runtime.jsx)("span", {
240
+ className: JobListAction_module_css_default.chevronBox,
241
+ children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, {
242
+ size: 12,
243
+ className: expanded ? `${JobListAction_module_css_default.chevron} ${JobListAction_module_css_default.chevronOpen}` : JobListAction_module_css_default.chevron
244
+ })
245
+ }) : null
246
+ ] });
247
+ const killTitle = kill === void 0 ? void 0 : kill.state === "armed" ? t("kill.confirm") : kill.state === "failed" ? t("kill.failed") : t("kill.stop", { label: job.label });
248
+ return (0, react_jsx_runtime.jsxs)("li", {
249
+ className: JobListAction_module_css_default.item,
250
+ children: [(0, react_jsx_runtime.jsxs)("div", {
251
+ className: live ? `${JobListAction_module_css_default.rowLine} ${JobListAction_module_css_default.rowLineLive}` : JobListAction_module_css_default.rowLine,
252
+ children: [observable ? (0, react_jsx_runtime.jsx)("button", {
253
+ type: "button",
254
+ className: live ? JobListAction_module_css_default.row : `${JobListAction_module_css_default.row} ${JobListAction_module_css_default.rowSettled}`,
255
+ "aria-expanded": expanded,
256
+ "aria-label": t(expanded ? "row.collapseAria" : "row.expandAria", { label: job.label }),
257
+ onClick: onToggle,
258
+ children: body
259
+ }) : (0, react_jsx_runtime.jsx)("span", {
260
+ className: `${JobListAction_module_css_default.row} ${JobListAction_module_css_default.rowSettled} ${JobListAction_module_css_default.rowStatic}`,
261
+ children: body
262
+ }), kill !== void 0 ? (0, react_jsx_runtime.jsxs)("button", {
263
+ type: "button",
264
+ className: kill.state === "armed" ? `${JobListAction_module_css_default.stop} ${JobListAction_module_css_default.stopArmed}` : kill.state === "failed" ? `${JobListAction_module_css_default.stop} ${JobListAction_module_css_default.stopFailed}` : JobListAction_module_css_default.stop,
265
+ "data-kill-state": kill.state,
266
+ disabled: kill.state === "pending",
267
+ "aria-label": killTitle,
268
+ title: killTitle,
269
+ onClick: kill.onPress,
270
+ children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconStopFill16, { size: 10 }), kill.state === "armed" ? (0, react_jsx_runtime.jsx)("span", {
271
+ className: JobListAction_module_css_default.stopLabel,
272
+ children: t("kill.confirmAction")
273
+ }) : null]
274
+ }) : null]
275
+ }), expanded && view !== void 0 ? (0, react_jsx_runtime.jsxs)("div", {
276
+ className: JobListAction_module_css_default.panel,
277
+ children: [
278
+ view.gapBefore ? (0, react_jsx_runtime.jsx)("div", {
279
+ className: JobListAction_module_css_default.notice,
280
+ children: t("output.gap")
281
+ }) : null,
282
+ view.error !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
283
+ className: `${JobListAction_module_css_default.notice} ${JobListAction_module_css_default.noticeError}`,
284
+ children: t("output.error", { error: view.error })
285
+ }) : null,
286
+ (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.TerminalBlock, {
287
+ command: job.label,
288
+ output: view.text,
289
+ running: live,
290
+ maxLines: Number.POSITIVE_INFINITY,
291
+ labels
292
+ })
293
+ ]
294
+ }) : null]
295
+ });
296
+ }
297
+ /**
298
+ * Session-header entry point for this session's background jobs. Mounting it
299
+ * keeps the session's roster stream open; it renders nothing at all until the
300
+ * session can see at least one job. Expanding an observable row (a live job,
301
+ * or a settled one with retained output) starts its observation stream, and
302
+ * collapsing (or closing the popover) stops it — output only flows while
303
+ * someone is watching. A running row carries a two-press stop button that
304
+ * requests a human kill through the job controller.
305
+ * @param props - runtime slot currency, the jobs snapshot hook, the roster,
306
+ * observation, and kill controls, and the namespace translator.
307
+ * @returns the trigger and its popover list, or null when there is nothing to show.
308
+ */
309
+ function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, t }) {
310
+ const jobs = useJobs((state) => state.rows[sessionId]) ?? NO_JOBS;
311
+ const observedViews = useJobs((state) => state.observed);
312
+ const [open, setOpen] = (0, react.useState)(false);
313
+ const [expandedKey, setExpandedKey] = (0, react.useState)(void 0);
314
+ const [now, setNow] = (0, react.useState)(() => Date.now());
315
+ const [settledOpen, setSettledOpen] = (0, react.useState)(void 0);
316
+ const [clearedKeys, setClearedKeys] = (0, react.useState)(() => /* @__PURE__ */ new Set());
317
+ const [killPhase, setKillPhase] = (0, react.useState)(void 0);
318
+ const rootRef = (0, react.useRef)(null);
319
+ const triggerRef = (0, react.useRef)(null);
320
+ const menuRef = (0, react.useRef)(null);
321
+ const [menuShift, setMenuShift] = (0, react.useState)(0);
322
+ const rows = (0, react.useMemo)(() => ordered(jobs), [jobs]);
323
+ const liveRows = (0, react.useMemo)(() => rows.filter(isLive), [rows]);
324
+ const settledRows = (0, react.useMemo)(() => rows.filter((job) => !isLive(job) && !clearedKeys.has(String(job.id))), [rows, clearedKeys]);
325
+ const settledExpanded = settledOpen ?? liveRows.length === 0;
326
+ const visibleCount = liveRows.length + settledRows.length;
327
+ (0, _qilin_agent_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, setOpen);
328
+ (0, react.useEffect)(() => watchRows(sessionId), [sessionId, watchRows]);
329
+ (0, react.useEffect)(() => {
330
+ if (!open || liveRows.length === 0) return;
331
+ setNow(Date.now());
332
+ const timer = setInterval(() => {
333
+ setNow(Date.now());
334
+ }, 1e3);
335
+ return () => {
336
+ clearInterval(timer);
337
+ };
338
+ }, [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
+ const expandedRow = open && expandedKey !== void 0 ? rows.find((job) => String(job.id) === expandedKey) : void 0;
361
+ const activeJob = expandedRow !== void 0 && isObservable(expandedRow) ? expandedRow.id : void 0;
362
+ (0, react.useEffect)(() => {
363
+ if (activeJob === void 0) return;
364
+ return observe(sessionId, activeJob);
365
+ }, [
366
+ sessionId,
367
+ activeJob,
368
+ observe
369
+ ]);
370
+ (0, react.useEffect)(() => {
371
+ if (visibleCount === 0 && open) setOpen(false);
372
+ }, [visibleCount, open]);
373
+ (0, react.useEffect)(() => {
374
+ if (expandedKey !== void 0 && !rows.some((job) => String(job.id) === expandedKey)) setExpandedKey(void 0);
375
+ }, [rows, expandedKey]);
376
+ (0, react.useEffect)(() => {
377
+ if (killPhase === void 0 || killPhase.state === "pending") return;
378
+ const timer = setTimeout(() => {
379
+ setKillPhase(void 0);
380
+ }, killPhase.state === "armed" ? KILL_ARM_MS : KILL_FAILED_MS);
381
+ return () => {
382
+ clearTimeout(timer);
383
+ };
384
+ }, [killPhase]);
385
+ (0, react.useEffect)(() => {
386
+ if (killPhase !== void 0 && !rows.some((job) => String(job.id) === killPhase.key && job.status === "running")) setKillPhase(void 0);
387
+ }, [rows, killPhase]);
388
+ const pressKill = (job) => {
389
+ const key = String(job.id);
390
+ if (killPhase?.key !== key || killPhase.state !== "armed") {
391
+ setKillPhase({
392
+ key,
393
+ state: "armed"
394
+ });
395
+ return;
396
+ }
397
+ setKillPhase({
398
+ key,
399
+ state: "pending"
400
+ });
401
+ killJob(sessionId, key).then((ok) => {
402
+ setKillPhase((current) => current?.key === key && !ok ? {
403
+ key,
404
+ state: "failed"
405
+ } : current);
406
+ });
407
+ };
408
+ if (visibleCount === 0) return null;
409
+ const countLabel = t(liveRows.length > 0 ? liveRows.length === 1 ? "count.live.one" : "count.live.other" : visibleCount === 1 ? "count.idle.one" : "count.idle.other", { count: liveRows.length > 0 ? liveRows.length : visibleCount });
410
+ const clearSettled = () => {
411
+ setClearedKeys((current) => {
412
+ const next = new Set(current);
413
+ for (const job of settledRows) next.add(String(job.id));
414
+ return next;
415
+ });
416
+ if (expandedKey !== void 0 && settledRows.some((job) => String(job.id) === expandedKey)) setExpandedKey(void 0);
417
+ };
418
+ const onKeyDown = (event) => {
419
+ if (event.key !== "Escape" || !open) return;
420
+ event.preventDefault();
421
+ setOpen(false);
422
+ triggerRef.current?.focus();
423
+ };
424
+ const item = (job) => (0, react_jsx_runtime.jsx)(JobItem, {
425
+ job,
426
+ view: isObservable(job) ? observedViews[String(job.id)] : void 0,
427
+ expanded: expandedKey === String(job.id),
428
+ now,
429
+ onToggle: () => {
430
+ setExpandedKey((current) => current === String(job.id) ? void 0 : String(job.id));
431
+ },
432
+ ...job.status === "running" ? { kill: {
433
+ state: killPhase?.key === String(job.id) ? killPhase.state : "idle",
434
+ onPress: () => {
435
+ pressKill(job);
436
+ }
437
+ } } : {},
438
+ t
439
+ }, String(job.id));
440
+ return (0, react_jsx_runtime.jsxs)("div", {
441
+ ref: rootRef,
442
+ className: JobListAction_module_css_default.root,
443
+ onKeyDown,
444
+ children: [(0, react_jsx_runtime.jsxs)("button", {
445
+ ref: triggerRef,
446
+ type: "button",
447
+ className: JobListAction_module_css_default.trigger,
448
+ "aria-expanded": open,
449
+ "aria-label": countLabel,
450
+ onClick: () => {
451
+ setNow(Date.now());
452
+ setOpen((current) => !current);
453
+ },
454
+ children: [
455
+ liveRows.length > 0 ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.StateDot, {
456
+ state: "ongoing",
457
+ className: JobListAction_module_css_default.triggerDot
458
+ }) : null,
459
+ (0, react_jsx_runtime.jsx)("span", {
460
+ className: JobListAction_module_css_default.count,
461
+ children: countLabel
462
+ }),
463
+ (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, {
464
+ size: 12,
465
+ className: open ? JobListAction_module_css_default.triggerOpen : void 0
466
+ })
467
+ ]
468
+ }), open ? (0, react_jsx_runtime.jsxs)("ul", {
469
+ ref: menuRef,
470
+ className: JobListAction_module_css_default.menu,
471
+ style: { left: menuShift },
472
+ "aria-label": t("list.aria"),
473
+ children: [
474
+ liveRows.length > 0 ? (0, react_jsx_runtime.jsx)("li", {
475
+ className: JobListAction_module_css_default.sectionHeader,
476
+ "aria-hidden": "true",
477
+ children: t("section.live")
478
+ }) : null,
479
+ liveRows.map(item),
480
+ settledRows.length > 0 ? (0, react_jsx_runtime.jsxs)("li", {
481
+ className: JobListAction_module_css_default.sectionHeader,
482
+ children: [(0, react_jsx_runtime.jsxs)("button", {
483
+ type: "button",
484
+ className: JobListAction_module_css_default.sectionToggle,
485
+ "aria-expanded": settledExpanded,
486
+ onClick: () => {
487
+ setSettledOpen(!settledExpanded);
488
+ },
489
+ children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, {
490
+ size: 12,
491
+ className: settledExpanded ? `${JobListAction_module_css_default.sectionChevron} ${JobListAction_module_css_default.sectionChevronOpen}` : JobListAction_module_css_default.sectionChevron
492
+ }), t("section.settledCount", { count: settledRows.length })]
493
+ }), (0, react_jsx_runtime.jsx)("button", {
494
+ type: "button",
495
+ className: JobListAction_module_css_default.sectionClear,
496
+ onClick: clearSettled,
497
+ children: t("section.clear")
498
+ })]
499
+ }) : null,
500
+ settledExpanded ? settledRows.map(item) : null
501
+ ]
502
+ }) : null]
503
+ });
504
+ }
505
+ //#endregion
506
+ //#region lib/types/client/locales.js
507
+ /** Simplified Chinese dictionary (the key-set source of truth). */
508
+ const zh = {
509
+ "count.live.one": "{count} 个后台任务运行中",
510
+ "count.live.other": "{count} 个后台任务运行中",
511
+ "count.idle.one": "{count} 个后台任务",
512
+ "count.idle.other": "{count} 个后台任务",
513
+ "list.aria": "后台任务",
514
+ "section.live": "进行中",
515
+ "section.settledCount": "已结束 {count}",
516
+ "section.clear": "清空",
517
+ "row.expandAria": "展开 {label} 的实时输出",
518
+ "row.collapseAria": "收起 {label} 的实时输出",
519
+ "kill.stop": "停止任务 {label}",
520
+ "kill.confirm": "再次点击确认停止",
521
+ "kill.confirmAction": "确认停止",
522
+ "kill.failed": "停止失败",
523
+ "status.running": "运行中",
524
+ "status.stopping": "正在停止",
525
+ "status.completed": "已完成",
526
+ "status.killed": "已取消",
527
+ "status.failed": "已失败",
528
+ "duration.seconds": "{seconds}秒",
529
+ "duration.minutes": "{minutes}分{seconds}秒",
530
+ "duration.hours": "{hours}小时{minutes}分",
531
+ "duration.title.live": "已运行 {duration}",
532
+ "duration.title.done": "耗时 {duration}",
533
+ "output.gap": "……较早的输出已丢弃……",
534
+ "output.error": "实时输出流中断:{error}",
535
+ "terminal.signal": "信号 {signal}",
536
+ "terminal.exitCode": "退出码 {code}",
537
+ "terminal.noExitCode": "未正常退出",
538
+ "terminal.running": "运行中",
539
+ "terminal.failed": "已失败",
540
+ "terminal.done": "已完成",
541
+ "terminal.copy": "复制",
542
+ "terminal.copied": "已复制",
543
+ "terminal.noOutput": "(无输出)",
544
+ "terminal.collapse": "收起",
545
+ "terminal.collapseAria": "收起输出",
546
+ "terminal.expand": "展开其余 {n} 行",
547
+ "terminal.expandAria": "展开被折叠的 {n} 行输出"
548
+ };
549
+ /** English dictionary, key-identical to the Chinese source of truth. */
550
+ const en = {
551
+ "count.live.one": "{count} background job running",
552
+ "count.live.other": "{count} background jobs running",
553
+ "count.idle.one": "{count} background job",
554
+ "count.idle.other": "{count} background jobs",
555
+ "list.aria": "Background jobs",
556
+ "section.live": "Running",
557
+ "section.settledCount": "Finished {count}",
558
+ "section.clear": "Clear",
559
+ "row.expandAria": "Show live output of {label}",
560
+ "row.collapseAria": "Hide live output of {label}",
561
+ "kill.stop": "Stop task {label}",
562
+ "kill.confirm": "Click again to confirm",
563
+ "kill.confirmAction": "Confirm stop",
564
+ "kill.failed": "Stop failed",
565
+ "status.running": "running",
566
+ "status.stopping": "stopping",
567
+ "status.completed": "completed",
568
+ "status.killed": "cancelled",
569
+ "status.failed": "failed",
570
+ "duration.seconds": "{seconds}s",
571
+ "duration.minutes": "{minutes}m {seconds}s",
572
+ "duration.hours": "{hours}h {minutes}m",
573
+ "duration.title.live": "Running for {duration}",
574
+ "duration.title.done": "Took {duration}",
575
+ "output.gap": "… earlier output dropped …",
576
+ "output.error": "live output stream interrupted: {error}",
577
+ "terminal.signal": "signal {signal}",
578
+ "terminal.exitCode": "exit {code}",
579
+ "terminal.noExitCode": "no exit code",
580
+ "terminal.running": "running",
581
+ "terminal.failed": "failed",
582
+ "terminal.done": "done",
583
+ "terminal.copy": "Copy",
584
+ "terminal.copied": "Copied",
585
+ "terminal.noOutput": "(no output)",
586
+ "terminal.collapse": "Collapse",
587
+ "terminal.collapseAria": "Collapse output",
588
+ "terminal.expand": "Show {n} more lines",
589
+ "terminal.expandAria": "Expand {n} collapsed output lines"
590
+ };
591
+ //#endregion
592
+ //#region lib/types/client/index.js
593
+ /** Required services: the jobs rosters, observations, and kill, the slot registry, and dictionaries. */
594
+ const inject = [
595
+ "jobs",
596
+ "slots",
597
+ "locale"
598
+ ];
599
+ /**
600
+ * Client plugin body: register the dictionaries and the header action.
601
+ * @param ctx - client root context.
602
+ */
603
+ function apply(ctx) {
604
+ ctx.effect(() => ctx.locale.register("job", {
605
+ zh,
606
+ en
607
+ }), "ui-jobs: dictionaries");
608
+ ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
609
+ name: "conversation.session.header.actions",
610
+ id: "job-list",
611
+ order: 20,
612
+ locale: "job",
613
+ inject: () => ({
614
+ hooks: { jobs: ctx.jobs.state },
615
+ watchRows: (sessionId) => ctx.jobs.watchRows(sessionId),
616
+ observe: (sessionId, id) => ctx.jobs.observe(sessionId, id),
617
+ killJob: async (sessionId, jobId) => (await ctx.jobs.kill(sessionId, jobId)).ok
618
+ })
619
+ }, JobListAction));
620
+ }
621
+ //#endregion
622
+ exports.apply = apply;
623
+ exports.inject = inject;
624
+ return module.exports;
625
+ }
626
+ });
627
+
628
+ //# sourceMappingURL=client.js.map
package/lib/index.js ADDED
@@ -0,0 +1,11 @@
1
+ //#region lib/types/index.js
2
+ /**
3
+ * Node half of the background-job list plugin. The browser half in
4
+ * `src/client/` owns every contribution; this entry exists so the package
5
+ * appears as an ordinary Loader row.
6
+ * @module @qilin-agent/client-ui-jobs
7
+ */
8
+ /** Loader-visible no-op body; the browser half carries the feature. */
9
+ function apply() {}
10
+ //#endregion
11
+ export { apply };
@@ -0,0 +1,46 @@
1
+ import type { JobsSnapshot, JobView } from '@qilin-agent/api-job-controller/client';
2
+ import type { SessionId } from '@qilin-agent/session/types';
3
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@qilin-agent/client-ui-slots';
4
+ import { NS } from './locales.ts';
5
+ /** Registration-side business face for the job list. */
6
+ export interface JobListInjected {
7
+ hooks: {
8
+ /** Client jobs snapshot (rosters and observations) bound by the renderer as useJobs. */
9
+ jobs: {
10
+ getSnapshot(): JobsSnapshot;
11
+ subscribe(listener: () => void): () => void;
12
+ };
13
+ };
14
+ /**
15
+ * Keep one session's roster current while the list is mounted; returns the
16
+ * stop function. Reference-counted by the client service.
17
+ */
18
+ watchRows: (sessionId: SessionId) => () => void;
19
+ /**
20
+ * Start observing one job's live output; returns the stop function.
21
+ * Reference-counted by the client service, so panels can overlap safely.
22
+ */
23
+ observe: (sessionId: SessionId | undefined, id: JobView['id']) => () => void;
24
+ /**
25
+ * Kill one background job on the human's behalf. Resolves `true` when the
26
+ * registry admitted the request (`requested` or `already-finished`); row
27
+ * state itself converges through the jobs control frames.
28
+ */
29
+ killJob: (sessionId: SessionId, jobId: string) => Promise<boolean>;
30
+ }
31
+ /** Full props for the session-header job-list action. */
32
+ export type JobListActionProps = PropsRuntime<'conversation.session.header.actions'> & PropsLocale<typeof NS> & InjectFace<JobListInjected>;
33
+ /**
34
+ * Session-header entry point for this session's background jobs. Mounting it
35
+ * keeps the session's roster stream open; it renders nothing at all until the
36
+ * session can see at least one job. Expanding an observable row (a live job,
37
+ * or a settled one with retained output) starts its observation stream, and
38
+ * collapsing (or closing the popover) stops it — output only flows while
39
+ * someone is watching. A running row carries a two-press stop button that
40
+ * requests a human kill through the job controller.
41
+ * @param props - runtime slot currency, the jobs snapshot hook, the roster,
42
+ * observation, and kill controls, and the namespace translator.
43
+ * @returns the trigger and its popover list, or null when there is nothing to show.
44
+ */
45
+ export declare function JobListAction({ sessionId, useJobs, watchRows, observe, killJob, t }: JobListActionProps): import("react").JSX.Element | null;
46
+ //# sourceMappingURL=JobListAction.d.ts.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Background-job plugin, browser half: contributes one session-header action
3
+ * that renders this session's jobs. Job rows, per-row observation streams,
4
+ * and the human kill all go through the `jobs` client service; this plugin
5
+ * holds no transport state of its own.
6
+ */
7
+ import type { Context as ClientContext } from '@qilin-agent/kylin';
8
+ import { type JobKey } from './locales.ts';
9
+ declare module '@qilin-agent/client-ui-slots' {
10
+ interface LocaleNamespaceMap {
11
+ /** Background-job list copy. */
12
+ 'job': JobKey;
13
+ }
14
+ }
15
+ export type { JobListActionProps, JobListInjected } from './JobListAction.tsx';
16
+ /** Required services: the jobs rosters, observations, and kill, the slot registry, and dictionaries. */
17
+ export declare const inject: string[];
18
+ /**
19
+ * Client plugin body: register the dictionaries and the header action.
20
+ * @param ctx - client root context.
21
+ */
22
+ export declare function apply(ctx: ClientContext): void;
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,50 @@
1
+ /** `job` namespace dictionaries. */
2
+ /** Dictionary namespace owned by this plugin. */
3
+ export declare const NS = "job";
4
+ /** Simplified Chinese dictionary (the key-set source of truth). */
5
+ export declare const zh: {
6
+ readonly 'count.live.one': "{count} 个后台任务运行中";
7
+ readonly 'count.live.other': "{count} 个后台任务运行中";
8
+ readonly 'count.idle.one': "{count} 个后台任务";
9
+ readonly 'count.idle.other': "{count} 个后台任务";
10
+ readonly 'list.aria': "后台任务";
11
+ readonly 'section.live': "进行中";
12
+ readonly 'section.settledCount': "已结束 {count}";
13
+ readonly 'section.clear': "清空";
14
+ readonly 'row.expandAria': "展开 {label} 的实时输出";
15
+ readonly 'row.collapseAria': "收起 {label} 的实时输出";
16
+ readonly 'kill.stop': "停止任务 {label}";
17
+ readonly 'kill.confirm': "再次点击确认停止";
18
+ readonly 'kill.confirmAction': "确认停止";
19
+ readonly 'kill.failed': "停止失败";
20
+ readonly 'status.running': "运行中";
21
+ readonly 'status.stopping': "正在停止";
22
+ readonly 'status.completed': "已完成";
23
+ readonly 'status.killed': "已取消";
24
+ readonly 'status.failed': "已失败";
25
+ readonly 'duration.seconds': "{seconds}秒";
26
+ readonly 'duration.minutes': "{minutes}分{seconds}秒";
27
+ readonly 'duration.hours': "{hours}小时{minutes}分";
28
+ readonly 'duration.title.live': "已运行 {duration}";
29
+ readonly 'duration.title.done': "耗时 {duration}";
30
+ readonly 'output.gap': "……较早的输出已丢弃……";
31
+ readonly 'output.error': "实时输出流中断:{error}";
32
+ readonly 'terminal.signal': "信号 {signal}";
33
+ readonly 'terminal.exitCode': "退出码 {code}";
34
+ readonly 'terminal.noExitCode': "未正常退出";
35
+ readonly 'terminal.running': "运行中";
36
+ readonly 'terminal.failed': "已失败";
37
+ readonly 'terminal.done': "已完成";
38
+ readonly 'terminal.copy': "复制";
39
+ readonly 'terminal.copied': "已复制";
40
+ readonly 'terminal.noOutput': "(无输出)";
41
+ readonly 'terminal.collapse': "收起";
42
+ readonly 'terminal.collapseAria': "收起输出";
43
+ readonly 'terminal.expand': "展开其余 {n} 行";
44
+ readonly 'terminal.expandAria': "展开被折叠的 {n} 行输出";
45
+ };
46
+ /** English dictionary, key-identical to the Chinese source of truth. */
47
+ export declare const en: Record<JobKey, string>;
48
+ /** Key domain of the `job` namespace (zh is the source of truth). */
49
+ export type JobKey = keyof typeof zh;
50
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Node half of the background-job list plugin. The browser half in
3
+ * `src/client/` owns every contribution; this entry exists so the package
4
+ * appears as an ordinary Loader row.
5
+ * @module @qilin-agent/client-ui-jobs
6
+ */
7
+ /** Loader-visible no-op body; the browser half carries the feature. */
8
+ export declare function apply(): void;
9
+ //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,70 @@
1
1
  {
2
2
  "name": "@qilin-agent/client-ui-jobs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Session-header background-job list with on-demand streaming record panels",
4
+ "version": "3.1.3",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./lib/types/client/index.d.ts",
15
+ "default": "./lib/client.js"
16
+ },
17
+ "./src/*": "./src/*",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "qilin": {
21
+ "client": {
22
+ "inject": [
23
+ "@qilin-agent/api-job-controller",
24
+ "@qilin-agent/client-locale",
25
+ "@qilin-agent/client-ui-conversation",
26
+ "@qilin-agent/client-ui-primitives"
27
+ ],
28
+ "platform": "web"
29
+ }
30
+ },
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/kkutysllb/QiLin.git",
35
+ "directory": "packages/client/ui-jobs"
36
+ },
37
+ "engines": {
38
+ "node": "^22.19.0 || >=24.0.0"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "peerDependencies": {
44
+ "@qilin-agent/kylin": "~4.0.3"
45
+ },
46
+ "devDependencies": {
47
+ "@types/react": "~18.3.1",
48
+ "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"
60
+ },
61
+ "files": [
62
+ "lib/index.js",
63
+ "lib/client.js",
64
+ "lib/types/**/*.d.ts"
65
+ ],
66
+ "scripts": {
67
+ "bundle": "tsdown",
68
+ "watch": "tsdown --watch"
69
+ }
6
70
  }