@deepseek-ai/dsh-client-ui-jobs 0.0.1-rc.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 +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +24 -0
- package/README.zh.md +24 -0
- package/lib/client.js +289 -0
- package/lib/index.js +11 -0
- package/lib/invariant.js +25 -0
- package/lib/types/client/JobListAction.d.ts +13 -0
- package/lib/types/client/index.d.ts +23 -0
- package/lib/types/client/locales.d.ts +26 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
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
|
+
README.md: d9720329ee537fdc303b8191b714c315b7535f9c
|
|
6
|
+
README.zh.md: 7e7bc05eefc21e62fc3751d1c069305398759268
|
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-client-ui-jobs
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Web background-job feature owner: contributes one entry to `conversation.session.header.actions` listing the `ctx.jobs` records this session can see. The data arrives entirely through the `jobsBySession` list mirror that [`dsh-client-runtime`](../runtime/README.md) folds from `session/jobs` frames, so this package issues no RPC and holds no state beyond popover visibility.
|
|
6
|
+
|
|
7
|
+
The trigger renders only when the session has at least one job, so an ordinary conversation never grows a control for a capability it is not using. Its badge counts `running` plus `stopping` and is omitted at zero, leaving a session that holds only finished jobs a quiet entry point into its history rather than one advertising a count of nothing. The popover is a flat list: live rows first by `startedAt` ascending, then settled rows by `finishedAt` descending, with a same-millisecond tie broken on start order so the host's map iteration never decides it. A row shows the producer kind, the label, a status marker, the producer's `detail` in place of the generic status word once it has one, and an elapsed duration. That duration advances once per second while the row is live and freezes at `finishedAt`; the clock runs only while an open list holds something that moves. A settled row missing `finishedAt` reads as zero rather than as a negative figure, and a duration past an hour stays in hours rather than growing a day vocabulary no producer currently reaches.
|
|
8
|
+
|
|
9
|
+
Settled rows stay visible and de-emphasized until the registry drops them at owner disposal. They are in the snapshot, a failed job's `detail` is the only place its failure is legible, and filtering them out here is work the output and cancellation phases would undo. A running one-shot background subagent therefore appears both here and in the [subagent catalog](../ui-subagent/README.md): the catalog navigates into the child's transcript, while this list is the only handle a future cancellation can attach to.
|
|
10
|
+
|
|
11
|
+
Escape closes the list and returns focus to the trigger, as does a pointer press outside it. The last job disappearing closes the list before the control unmounts, so focus never vanishes from a removed node. Styling uses tokens only; copy goes through the package's own `job` locale namespace. The behavior is specified by the [Web background-job display Agent Note](../../../.agents/notes/implemented/feature/2026-08-08-web-background-job-display.md).
|
|
12
|
+
|
|
13
|
+
## Model Experience
|
|
14
|
+
|
|
15
|
+
None, as this package renders host-computed registry state for a human and touches no prompt, message, schema, stream, or tool result. The model's own view of the same jobs stays with [`dsh-tool-jobs`](../../jobs/tool-jobs/README.md).
|
|
16
|
+
|
|
17
|
+
#### KV Cache effect
|
|
18
|
+
|
|
19
|
+
None; the package never assembles or sends provider requests.
|
|
20
|
+
|
|
21
|
+
## Known Limitations and Deferred Work
|
|
22
|
+
|
|
23
|
+
- **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 today: `kill()` marks terminal delivery reported, so an interrupt written against the current contract would leave the model believing its job is still running.
|
|
24
|
+
- **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`) is the opposite case: it reaches every session's list, matching what `list(caller)` reports to every caller.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-client-ui-jobs
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
Web 后台任务特性的归属方:向 `conversation.session.header.actions` 贡献一个条目,列出当前会话可见的 `ctx.jobs` 记录。数据完全来自 [`dsh-client-runtime`](../runtime/README.md) 从 `session/jobs` 帧折叠出的 `jobsBySession` 列表镜像,因此本包不发任何 RPC,除弹层开合外不持有任何状态。
|
|
6
|
+
|
|
7
|
+
只有当会话至少有一个任务时才渲染触发器,普通对话不会因为一项未被使用的能力而长出控件。角标计数为 `running` 加 `stopping`,为零时省略,这样只剩已完成任务的会话保留一个安静的历史入口,而不是宣告一个「零」。弹层是一个扁平列表:活跃行在前按 `startedAt` 升序,随后终态行按 `finishedAt` 降序;毫秒相同的并列按启动顺序打破,宿主的 map 迭代顺序永远不参与决定。一行显示生产者 kind、label、状态标记、生产者一旦给出 `detail` 就取代通用状态词的那段文字,以及已耗时。该耗时在活跃时每秒推进,并在 `finishedAt` 冻结;只有当打开的列表里确实有会动的东西时时钟才运行。缺少 `finishedAt` 的终态行读作零而不是负数,超过一小时的耗时停留在小时单位,不会长出任何生产者目前都到不了的「天」词汇。
|
|
8
|
+
|
|
9
|
+
终态行保持可见并弱化,直到注册表在 owner 销毁时把它们丢掉。它们本就在快照里,失败任务的 `detail` 是其失败唯一可读之处,在这里过滤掉它们是输出与中断两期要推翻的工作。因此一个运行中的一次性后台 subagent 会同时出现在这里和 [subagent 目录](../ui-subagent/README.md)里:目录负责进入子会话的 transcript,而这个列表是将来中断能力唯一可能附着的句柄。
|
|
10
|
+
|
|
11
|
+
Escape 关闭列表并把焦点交还触发器,在其外部按下指针同理。最后一个任务消失时先关闭列表再卸载控件,焦点因此不会从一个被移除的节点上凭空消失。样式只用 token;文案走本包自己的 `job` locale 命名空间。行为由 [Web 后台任务展示 Agent Note](../../../.agents/notes/implemented/feature/2026-08-08-web-background-job-display.md) 规定。
|
|
12
|
+
|
|
13
|
+
## 模型体验
|
|
14
|
+
|
|
15
|
+
无,因为本包为人类渲染宿主计算出的注册表状态,不触及 prompt、消息、schema、流或工具结果。模型对同一批任务的视角仍属于 [`dsh-tool-jobs`](../../jobs/tool-jobs/README.md)。
|
|
16
|
+
|
|
17
|
+
#### KV Cache effect
|
|
18
|
+
|
|
19
|
+
无;本包从不组装或发送 provider 请求。
|
|
20
|
+
|
|
21
|
+
## 已知限制与暂缓事项
|
|
22
|
+
|
|
23
|
+
- **行是只读的** —— 任务的流式输出与人类发起的中断是各自独立的阶段。中断还额外欠一个 seam 目前没有回答的、面向模型的决策:`kill()` 会把终态投递标为已上报,所以照当前契约写出来的中断会让模型一直以为它的任务还在跑。
|
|
24
|
+
- **列表不等于注册表自己的集合** —— 它展示的是「一个会话通过线路视图能看到什么」,所以别的会话拥有的任务在这里永远不出现;而进程重启会清空列表,transcript 里启动这些任务的 `run_in_background` 卡片却还在。无主任务(在没有活体 `Agent` 时启动的)是反过来的情形:它会进入每一个会话的列表,与 `list(caller)` 对每个调用方的报告一致。
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@deepseek-ai/dsh-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 _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
+
//#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-jobs/src/client/JobListAction.module.css.mjs
|
|
11
|
+
const css = ".QsffPG_root{position:relative}.QsffPG_trigger{min-height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:6px;align-items:center;gap:3px;padding:3px 2px;font-size:12px;line-height:18px;display:inline-flex}.QsffPG_trigger:hover,.QsffPG_trigger:focus-visible{color:var(--dsw-alias-label-secondary)}.QsffPG_trigger svg{transition:transform .12s}.QsffPG_triggerOpen{transform:rotate(180deg)}.QsffPG_triggerDot{flex:none}.QsffPG_count{margin:0 5px}.QsffPG_menu{z-index:100;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-menu);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);width:336px;max-width:min(400px,100vw - 32px);max-height:min(420px,100vh - 140px);box-shadow:var(--dsw-shadow-lv3);border-radius:12px;flex-direction:column;gap:1px;margin:0;padding:4px;list-style:none;display:flex;position:absolute;top:calc(100% + 5px);left:0;overflow:auto}.QsffPG_row{box-sizing:border-box;width:100%;min-height:32px;color:var(--dsw-alias-label-primary);border-radius:8px;align-items:center;gap:8px;padding:6px 8px;font-size:13px;line-height:18px;display:flex}.QsffPG_rowSettled{color:var(--dsw-alias-label-tertiary)}.QsffPG_rowDot{flex:none}.QsffPG_kind{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-secondary);border-radius:5px;flex:none;padding:0 6px;font-size:11px;line-height:18px}.QsffPG_label{min-width:0;font-family:var(--dsw-font-mono);white-space:nowrap;text-overflow:ellipsis;flex:1;overflow:hidden}.QsffPG_status,.QsffPG_duration{color:var(--dsw-alias-label-tertiary);flex:none;font-size:11px;line-height:18px}.QsffPG_status{white-space:nowrap;text-overflow:ellipsis;max-width:40%;overflow:hidden}.QsffPG_duration{font-variant-numeric:tabular-nums}";
|
|
12
|
+
const tagId = "@deepseek-ai/dsh-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 = "@deepseek-ai/dsh-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
|
+
"kind": "QsffPG_kind",
|
|
22
|
+
"root": "QsffPG_root",
|
|
23
|
+
"triggerDot": "QsffPG_triggerDot",
|
|
24
|
+
"count": "QsffPG_count",
|
|
25
|
+
"row": "QsffPG_row",
|
|
26
|
+
"rowSettled": "QsffPG_rowSettled",
|
|
27
|
+
"rowDot": "QsffPG_rowDot",
|
|
28
|
+
"label": "QsffPG_label",
|
|
29
|
+
"status": "QsffPG_status",
|
|
30
|
+
"menu": "QsffPG_menu",
|
|
31
|
+
"duration": "QsffPG_duration",
|
|
32
|
+
"trigger": "QsffPG_trigger",
|
|
33
|
+
"triggerOpen": "QsffPG_triggerOpen"
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region lib/types/client/JobListAction.js
|
|
37
|
+
/** Stable empty list so a session with no jobs keeps one array identity. */
|
|
38
|
+
const NO_TASKS = [];
|
|
39
|
+
/** A job the registry still holds open, and whose duration therefore ticks. */
|
|
40
|
+
function isLive(job) {
|
|
41
|
+
return job.status === "running" || job.status === "stopping";
|
|
42
|
+
}
|
|
43
|
+
/** Closed-union exhaustiveness fence for the wire status set. */
|
|
44
|
+
/* v8 ignore next 3 -- closed-union backstop; only reached if a status is forged */
|
|
45
|
+
function assertNever(value) {
|
|
46
|
+
throw new Error(`unhandled job status: ${JSON.stringify(value)}`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Status marker semantics. `stopping` and `killed` share the attention color:
|
|
50
|
+
* both mean the work ended (or is ending) on request rather than on its own.
|
|
51
|
+
*/
|
|
52
|
+
function dotState(status) {
|
|
53
|
+
switch (status) {
|
|
54
|
+
case "running": return "ongoing";
|
|
55
|
+
case "stopping": return "warning";
|
|
56
|
+
case "completed": return "done";
|
|
57
|
+
case "killed": return "warning";
|
|
58
|
+
case "failed": return "error";
|
|
59
|
+
/* v8 ignore next -- closed wire status union */
|
|
60
|
+
default: return assertNever(status);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Human status word for the row and its accessible name. */
|
|
64
|
+
function statusLabel(status, t) {
|
|
65
|
+
switch (status) {
|
|
66
|
+
case "running": return t("status.running");
|
|
67
|
+
case "stopping": return t("status.stopping");
|
|
68
|
+
case "completed": return t("status.completed");
|
|
69
|
+
case "killed": return t("status.killed");
|
|
70
|
+
case "failed": return t("status.failed");
|
|
71
|
+
/* v8 ignore next -- closed wire status union */
|
|
72
|
+
default: return assertNever(status);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Elapsed time in at most two adjacent units. A background job that outlives
|
|
77
|
+
* an hour is already exceptional, so hours is the widest unit — beyond that the
|
|
78
|
+
* figure stays in hours rather than growing a day/month vocabulary no producer
|
|
79
|
+
* currently reaches.
|
|
80
|
+
*/
|
|
81
|
+
function formatDuration(elapsedMs, t) {
|
|
82
|
+
const total = Math.max(0, Math.floor(elapsedMs / 1e3));
|
|
83
|
+
const seconds = total % 60;
|
|
84
|
+
const minutes = Math.floor(total / 60) % 60;
|
|
85
|
+
const hours = Math.floor(total / 3600);
|
|
86
|
+
if (hours > 0) return t("duration.hours", {
|
|
87
|
+
hours,
|
|
88
|
+
minutes
|
|
89
|
+
});
|
|
90
|
+
if (minutes > 0) return t("duration.minutes", {
|
|
91
|
+
minutes,
|
|
92
|
+
seconds
|
|
93
|
+
});
|
|
94
|
+
return t("duration.seconds", { seconds });
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Live rows first in start order, then settled rows newest-first. Two jobs
|
|
98
|
+
* that settled in the same millisecond fall back to start order, so the sort
|
|
99
|
+
* never depends on the host's map iteration.
|
|
100
|
+
*/
|
|
101
|
+
function ordered(jobs) {
|
|
102
|
+
return [...jobs].sort((left, right) => {
|
|
103
|
+
const liveLeft = isLive(left);
|
|
104
|
+
if (liveLeft !== isLive(right)) return liveLeft ? -1 : 1;
|
|
105
|
+
if (liveLeft) return left.startedAt - right.startedAt;
|
|
106
|
+
const finished = (right.finishedAt ?? right.startedAt) - (left.finishedAt ?? left.startedAt);
|
|
107
|
+
return finished !== 0 ? finished : left.startedAt - right.startedAt;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Session-header entry point for this session's background jobs. It renders
|
|
112
|
+
* nothing at all until the session has at least one job, so an ordinary
|
|
113
|
+
* conversation never grows a control for a capability it is not using.
|
|
114
|
+
* @param props - runtime slot currency plus the namespace translator.
|
|
115
|
+
* @returns the trigger and its popover list, or null when there is nothing to show.
|
|
116
|
+
*/
|
|
117
|
+
function JobListAction({ sessionId, useSessions, t }) {
|
|
118
|
+
const jobs = useSessions((state) => state.jobsBySession[sessionId]) ?? NO_TASKS;
|
|
119
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
120
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
121
|
+
const rootRef = (0, react.useRef)(null);
|
|
122
|
+
const triggerRef = (0, react.useRef)(null);
|
|
123
|
+
const rows = (0, react.useMemo)(() => ordered(jobs), [jobs]);
|
|
124
|
+
const liveCount = (0, react.useMemo)(() => jobs.filter(isLive).length, [jobs]);
|
|
125
|
+
(0, react.useEffect)(() => {
|
|
126
|
+
if (!open) return;
|
|
127
|
+
const closeOutside = (event) => {
|
|
128
|
+
if (event.target instanceof Node && !rootRef.current?.contains(event.target)) setOpen(false);
|
|
129
|
+
};
|
|
130
|
+
document.addEventListener("pointerdown", closeOutside);
|
|
131
|
+
return () => {
|
|
132
|
+
document.removeEventListener("pointerdown", closeOutside);
|
|
133
|
+
};
|
|
134
|
+
}, [open]);
|
|
135
|
+
(0, react.useEffect)(() => {
|
|
136
|
+
if (!open || liveCount === 0) return;
|
|
137
|
+
setNow(Date.now());
|
|
138
|
+
const timer = setInterval(() => {
|
|
139
|
+
setNow(Date.now());
|
|
140
|
+
}, 1e3);
|
|
141
|
+
return () => {
|
|
142
|
+
clearInterval(timer);
|
|
143
|
+
};
|
|
144
|
+
}, [open, liveCount]);
|
|
145
|
+
(0, react.useEffect)(() => {
|
|
146
|
+
if (jobs.length === 0 && open) setOpen(false);
|
|
147
|
+
}, [jobs.length, open]);
|
|
148
|
+
if (jobs.length === 0) return null;
|
|
149
|
+
const countLabel = t(liveCount > 0 ? liveCount === 1 ? "count.live.one" : "count.live.other" : jobs.length === 1 ? "count.idle.one" : "count.idle.other", { count: liveCount > 0 ? liveCount : jobs.length });
|
|
150
|
+
const onKeyDown = (event) => {
|
|
151
|
+
if (event.key !== "Escape" || !open) return;
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
setOpen(false);
|
|
154
|
+
triggerRef.current?.focus();
|
|
155
|
+
};
|
|
156
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
157
|
+
ref: rootRef,
|
|
158
|
+
className: JobListAction_module_css_default.root,
|
|
159
|
+
onKeyDown,
|
|
160
|
+
children: [(0, react_jsx_runtime.jsxs)("button", {
|
|
161
|
+
ref: triggerRef,
|
|
162
|
+
type: "button",
|
|
163
|
+
className: JobListAction_module_css_default.trigger,
|
|
164
|
+
"aria-expanded": open,
|
|
165
|
+
"aria-label": countLabel,
|
|
166
|
+
onClick: () => {
|
|
167
|
+
setNow(Date.now());
|
|
168
|
+
setOpen((current) => !current);
|
|
169
|
+
},
|
|
170
|
+
children: [
|
|
171
|
+
liveCount > 0 ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
|
|
172
|
+
state: "ongoing",
|
|
173
|
+
className: JobListAction_module_css_default.triggerDot
|
|
174
|
+
}) : null,
|
|
175
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
176
|
+
className: JobListAction_module_css_default.count,
|
|
177
|
+
children: countLabel
|
|
178
|
+
}),
|
|
179
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: open ? JobListAction_module_css_default.triggerOpen : void 0 })
|
|
180
|
+
]
|
|
181
|
+
}), open ? (0, react_jsx_runtime.jsx)("ul", {
|
|
182
|
+
className: JobListAction_module_css_default.menu,
|
|
183
|
+
"aria-label": t("list.aria"),
|
|
184
|
+
children: rows.map((job) => {
|
|
185
|
+
const live = isLive(job);
|
|
186
|
+
const duration = formatDuration(live ? now - job.startedAt : (job.finishedAt ?? job.startedAt) - job.startedAt, t);
|
|
187
|
+
const status = statusLabel(job.status, t);
|
|
188
|
+
return (0, react_jsx_runtime.jsxs)("li", {
|
|
189
|
+
className: live ? JobListAction_module_css_default.row : `${JobListAction_module_css_default.row} ${JobListAction_module_css_default.rowSettled}`,
|
|
190
|
+
children: [
|
|
191
|
+
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
|
|
192
|
+
state: dotState(job.status),
|
|
193
|
+
className: JobListAction_module_css_default.rowDot
|
|
194
|
+
}),
|
|
195
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
196
|
+
className: JobListAction_module_css_default.kind,
|
|
197
|
+
children: job.kind
|
|
198
|
+
}),
|
|
199
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
200
|
+
className: JobListAction_module_css_default.label,
|
|
201
|
+
title: job.label,
|
|
202
|
+
children: job.label
|
|
203
|
+
}),
|
|
204
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
205
|
+
className: JobListAction_module_css_default.status,
|
|
206
|
+
title: job.detail ?? status,
|
|
207
|
+
children: job.detail ?? status
|
|
208
|
+
}),
|
|
209
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
210
|
+
className: JobListAction_module_css_default.duration,
|
|
211
|
+
title: t(live ? "duration.title.live" : "duration.title.done", { duration }),
|
|
212
|
+
children: duration
|
|
213
|
+
})
|
|
214
|
+
]
|
|
215
|
+
}, job.id);
|
|
216
|
+
})
|
|
217
|
+
}) : null]
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region lib/types/client/locales.js
|
|
222
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
223
|
+
const zh = {
|
|
224
|
+
"count.live.one": "{count} 个后台任务运行中",
|
|
225
|
+
"count.live.other": "{count} 个后台任务运行中",
|
|
226
|
+
"count.idle.one": "{count} 个后台任务",
|
|
227
|
+
"count.idle.other": "{count} 个后台任务",
|
|
228
|
+
"list.aria": "后台任务",
|
|
229
|
+
"status.running": "运行中",
|
|
230
|
+
"status.stopping": "正在停止",
|
|
231
|
+
"status.completed": "已完成",
|
|
232
|
+
"status.killed": "已取消",
|
|
233
|
+
"status.failed": "已失败",
|
|
234
|
+
"duration.seconds": "{seconds}秒",
|
|
235
|
+
"duration.minutes": "{minutes}分{seconds}秒",
|
|
236
|
+
"duration.hours": "{hours}小时{minutes}分",
|
|
237
|
+
"duration.title.live": "已运行 {duration}",
|
|
238
|
+
"duration.title.done": "耗时 {duration}"
|
|
239
|
+
};
|
|
240
|
+
/** English dictionary, key-identical to the Chinese source of truth. */
|
|
241
|
+
const en = {
|
|
242
|
+
"count.live.one": "{count} background job running",
|
|
243
|
+
"count.live.other": "{count} background jobs running",
|
|
244
|
+
"count.idle.one": "{count} background job",
|
|
245
|
+
"count.idle.other": "{count} background jobs",
|
|
246
|
+
"list.aria": "Background jobs",
|
|
247
|
+
"status.running": "running",
|
|
248
|
+
"status.stopping": "stopping",
|
|
249
|
+
"status.completed": "completed",
|
|
250
|
+
"status.killed": "cancelled",
|
|
251
|
+
"status.failed": "failed",
|
|
252
|
+
"duration.seconds": "{seconds}s",
|
|
253
|
+
"duration.minutes": "{minutes}m {seconds}s",
|
|
254
|
+
"duration.hours": "{hours}h {minutes}m",
|
|
255
|
+
"duration.title.live": "Running for {duration}",
|
|
256
|
+
"duration.title.done": "Took {duration}"
|
|
257
|
+
};
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region lib/types/client/index.js
|
|
260
|
+
/** Required services for locale registration and header-slot contribution. */
|
|
261
|
+
const inject = [
|
|
262
|
+
"sessions",
|
|
263
|
+
"slots",
|
|
264
|
+
"locale"
|
|
265
|
+
];
|
|
266
|
+
/**
|
|
267
|
+
* Client plugin body: register the dictionaries and the header action.
|
|
268
|
+
* @param ctx - client root context.
|
|
269
|
+
*/
|
|
270
|
+
function apply(ctx) {
|
|
271
|
+
ctx.effect(() => ctx.locale.register("job", {
|
|
272
|
+
zh,
|
|
273
|
+
en
|
|
274
|
+
}), "ui-job: dictionaries");
|
|
275
|
+
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
|
|
276
|
+
name: "conversation.session.header.actions",
|
|
277
|
+
id: "job-list",
|
|
278
|
+
order: 20,
|
|
279
|
+
locale: "job"
|
|
280
|
+
}, JobListAction));
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
exports.apply = apply;
|
|
284
|
+
exports.inject = inject;
|
|
285
|
+
return module.exports;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
//# sourceMappingURL=client.js.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region lib/types/index.js
|
|
2
|
+
/**
|
|
3
|
+
* Background-job list plugin, node half. Pure UI plugin: the empty apply
|
|
4
|
+
* exists so the plugin appears in the host cordis.yml / Loader; the browser
|
|
5
|
+
* half ships via exports["./client"], discovered through the package.json
|
|
6
|
+
* dshClient declaration.
|
|
7
|
+
*/
|
|
8
|
+
/** Host plugin body — no host-side behavior for this source plugin. */
|
|
9
|
+
function apply() {}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { apply };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-jobs`.
|
|
4
|
+
* @module @deepseek-ai/dsh-client-ui-jobs/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-client-ui-jobs";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "client-ui-jobs-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this package is a read-only projection of the
|
|
13
|
+
* `jobsBySession` mirror onto one header slot entry. It emits no cordis
|
|
14
|
+
* events, owns no cross-plugin mutable state, and its single slot registration
|
|
15
|
+
* proves disposal through the HMR-safety spec.
|
|
16
|
+
*/
|
|
17
|
+
const install = () => {};
|
|
18
|
+
/**
|
|
19
|
+
* Register this package's invariant companion.
|
|
20
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
21
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
22
|
+
*/
|
|
23
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
24
|
+
//#endregion
|
|
25
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
import { NS } from './locales.ts';
|
|
3
|
+
/** Full props for the session-header background-job action. */
|
|
4
|
+
export type JobListActionProps = PropsRuntime<'conversation.session.header.actions'> & PropsLocale<typeof NS>;
|
|
5
|
+
/**
|
|
6
|
+
* Session-header entry point for this session's background jobs. It renders
|
|
7
|
+
* nothing at all until the session has at least one job, so an ordinary
|
|
8
|
+
* conversation never grows a control for a capability it is not using.
|
|
9
|
+
* @param props - runtime slot currency plus the namespace translator.
|
|
10
|
+
* @returns the trigger and its popover list, or null when there is nothing to show.
|
|
11
|
+
*/
|
|
12
|
+
export declare function JobListAction({ sessionId, useSessions, t }: JobListActionProps): import("react").JSX.Element | null;
|
|
13
|
+
//# 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 `ctx.jobs` records. The data arrives entirely
|
|
4
|
+
* through the `jobsBySession` list mirror, so the plugin issues no RPC and
|
|
5
|
+
* holds no state of its own beyond popover visibility.
|
|
6
|
+
*/
|
|
7
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
8
|
+
import { type JobKey } from './locales.ts';
|
|
9
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
10
|
+
interface LocaleNamespaceMap {
|
|
11
|
+
/** Background-job list copy. */
|
|
12
|
+
'job': JobKey;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export type { JobListActionProps } from './JobListAction.tsx';
|
|
16
|
+
/** Required services for locale registration and header-slot contribution. */
|
|
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,26 @@
|
|
|
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 'status.running': "运行中";
|
|
12
|
+
readonly 'status.stopping': "正在停止";
|
|
13
|
+
readonly 'status.completed': "已完成";
|
|
14
|
+
readonly 'status.killed': "已取消";
|
|
15
|
+
readonly 'status.failed': "已失败";
|
|
16
|
+
readonly 'duration.seconds': "{seconds}秒";
|
|
17
|
+
readonly 'duration.minutes': "{minutes}分{seconds}秒";
|
|
18
|
+
readonly 'duration.hours': "{hours}小时{minutes}分";
|
|
19
|
+
readonly 'duration.title.live': "已运行 {duration}";
|
|
20
|
+
readonly 'duration.title.done': "耗时 {duration}";
|
|
21
|
+
};
|
|
22
|
+
/** English dictionary, key-identical to the Chinese source of truth. */
|
|
23
|
+
export declare const en: Record<JobKey, string>;
|
|
24
|
+
/** Key domain of the `job` namespace (zh is the source of truth). */
|
|
25
|
+
export type JobKey = keyof typeof zh;
|
|
26
|
+
//# sourceMappingURL=locales.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-job list plugin, node half. Pure UI plugin: the empty apply
|
|
3
|
+
* exists so the plugin appears in the host cordis.yml / Loader; the browser
|
|
4
|
+
* half ships via exports["./client"], discovered through the package.json
|
|
5
|
+
* dshClient declaration.
|
|
6
|
+
*/
|
|
7
|
+
/** Host plugin body — no host-side behavior for this source plugin. */
|
|
8
|
+
export declare function apply(): void;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-jobs`.
|
|
3
|
+
* @module @deepseek-ai/dsh-client-ui-jobs/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "client-ui-jobs-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-client-ui-jobs",
|
|
3
|
+
"description": "Session-header background-job list: live registry state mirrored from session/jobs frames",
|
|
4
|
+
"version": "0.0.1-rc.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
|
+
"./invariant": {
|
|
14
|
+
"types": "./lib/types/invariant.d.ts",
|
|
15
|
+
"default": "./lib/invariant.js"
|
|
16
|
+
},
|
|
17
|
+
"./client": {
|
|
18
|
+
"types": "./lib/types/client/index.d.ts",
|
|
19
|
+
"default": "./lib/client.js"
|
|
20
|
+
},
|
|
21
|
+
"./src/*": "./src/*",
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"dsh": {
|
|
25
|
+
"client": {
|
|
26
|
+
"inject": [
|
|
27
|
+
"@deepseek-ai/dsh-client-locale",
|
|
28
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
29
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
30
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
31
|
+
],
|
|
32
|
+
"platform": "web"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"license": "BSD-3-Clause",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
39
|
+
"directory": "packages/client/ui-jobs"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "restricted"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"react": "^18.2.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@deepseek-ai/dsh-client-locale": "^0.0.1-rc.3",
|
|
49
|
+
"@deepseek-ai/dsh-client-runtime": "^0.0.1-rc.3",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1-rc.3",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1-rc.3",
|
|
52
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.0.1-rc.3",
|
|
54
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/react": "~18.3.1",
|
|
58
|
+
"@deepseek-ai/dsh-client-runtime": "^0.0.1-rc.3",
|
|
59
|
+
"@deepseek-ai/dsh-client-locale": "^0.0.1-rc.3",
|
|
60
|
+
"@deepseek-ai/dsh-client-test-runtime": "^0.0.1-rc.3",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.0.1-rc.3",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1-rc.3",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1-rc.3",
|
|
64
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
|
|
65
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
66
|
+
},
|
|
67
|
+
"files": [
|
|
68
|
+
"lib/index.js",
|
|
69
|
+
"lib/invariant.js",
|
|
70
|
+
"lib/client.js",
|
|
71
|
+
"lib/types/**/*.d.ts"
|
|
72
|
+
],
|
|
73
|
+
"scripts": {
|
|
74
|
+
"bundle": "tsdown",
|
|
75
|
+
"watch": "tsdown --watch"
|
|
76
|
+
}
|
|
77
|
+
}
|