dsh-workspace-status 0.0.0-stage → 0.1.0
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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +156 -2
- package/client.js +510 -0
- package/cordis.patch.yml +6 -0
- package/icon.svg +8 -0
- package/index.js +18 -0
- package/locale/en.json +13 -0
- package/locale/zh.json +13 -0
- package/package.json +75 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-10-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- First public release: coloured status marks on workspace rows (waiting, running,
|
|
8
|
+
recently finished) with locale-aware labels and a settings toggle.
|
|
9
|
+
- Declares `dsh.catalog` so plugin markets place it under UI with a real summary.
|
|
10
|
+
|
|
11
|
+
All notable changes to this package are documented here. The format follows
|
|
12
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
|
|
13
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
14
|
+
|
|
15
|
+
## [Unreleased]
|
|
16
|
+
|
|
17
|
+
## [0.1.0] - 2026-10-25
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Workspace rows in the sidebar show a status mark aggregated from their
|
|
22
|
+
sessions: amber dot while a session waits for the user (approval, plan
|
|
23
|
+
review, answer), blue spinning ring while a session or subagent runs, green
|
|
24
|
+
dot for a recently finished unread session (one-hour notice window).
|
|
25
|
+
- One mark per workspace with the shipped session-row precedence: waiting
|
|
26
|
+
outranks running, which outranks the finished reminder.
|
|
27
|
+
- Hidden screen-reader description with per-state counts and workspace names.
|
|
28
|
+
- Archived sessions ignored; ungrouped sessions mark the Ungrouped bucket.
|
|
29
|
+
- Zero-fork implementation: an additive `sidebar.footer.action` entry that
|
|
30
|
+
publishes row CSS keyed on `data-row-key="workspace:<id>"`, with no DOM
|
|
31
|
+
writes, observers or host services.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-workspace-status contributors
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,157 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-workspace-status
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Colored status marks on the **workspace rows** of the DeepSeek Harness sidebar:
|
|
4
|
+
waiting for you, running, and just finished.
|
|
5
|
+
|
|
6
|
+
Harness draws a status dot on every conversation row, but nothing on the
|
|
7
|
+
workspace (folder) row above it. Collapse a group, or scroll it out of view, and
|
|
8
|
+
the folder row cannot tell you that an agent inside is stuck waiting for an
|
|
9
|
+
approval or still working. This plugin gives the folder row the same kind of
|
|
10
|
+
mark, aggregated from its sessions:
|
|
11
|
+
|
|
12
|
+
| Mark | Meaning | Color |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| solid dot | at least one session is waiting for you — approval, plan review, or an answer | amber (`--dsw-alias-state-warn-primary`) |
|
|
15
|
+
| stroked ring, spinning | at least one session is running, including a running subagent | blue (`--dsw-static-deepseek-450`) |
|
|
16
|
+
| solid dot | at least one session finished recently and has not been opened since | green (`--dsw-alias-state-success-primary`) |
|
|
17
|
+
|
|
18
|
+
The mark sits in the empty right-hand end of the workspace row — the same place
|
|
19
|
+
a session row puts its timestamp — and does not displace the title. It appears
|
|
20
|
+
for expanded and collapsed groups alike, and its state is described to
|
|
21
|
+
screen-reader users in a hidden sentence ("Workspace activity: 2 running, 1
|
|
22
|
+
waiting for you — api, web").
|
|
23
|
+
|
|
24
|
+
## Preview
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
This is the plugin's own stylesheet rendered over a local reproduction of the
|
|
29
|
+
sidebar row DOM and theme tokens — a design preview, not a live screenshot. The
|
|
30
|
+
shipped app draws the same marks on the real rows.
|
|
31
|
+
|
|
32
|
+
## Rules
|
|
33
|
+
|
|
34
|
+
- **Precedence matches the shipped session row:** waiting for the user beats
|
|
35
|
+
running, which beats the finished reminder. A workspace therefore shows one
|
|
36
|
+
mark at a time; the hidden description still reports every count.
|
|
37
|
+
- **Subagents count as their parent's activity.** A running subagent keeps the
|
|
38
|
+
parent session's workspace marked even when the parent turn has settled.
|
|
39
|
+
Subagent rows never produce a mark of their own — they are not workspace
|
|
40
|
+
members.
|
|
41
|
+
- **Archived sessions are ignored**, the same way the default browser view
|
|
42
|
+
hides them.
|
|
43
|
+
- **The finished mark is a notice, not a flag.** It lapses one hour after the
|
|
44
|
+
session's last update, or one hour after the plugin watched it stop if that
|
|
45
|
+
is later. The shipped per-row green dot persists until opened; a folder row
|
|
46
|
+
has no timestamp to contradict it, so a permanent green dot would outlive the
|
|
47
|
+
news. A finished session that was never seen running is judged by its own
|
|
48
|
+
updated time only.
|
|
49
|
+
- **Ungrouped sessions** mark the "Ungrouped" bucket row, which behaves like a
|
|
50
|
+
workspace row in the sidebar.
|
|
51
|
+
- **No mark when everything is idle.** The plugin renders nothing at all in
|
|
52
|
+
that case — no stylesheet, no hidden text.
|
|
53
|
+
|
|
54
|
+
The active workspace (the one containing the open conversation) is already
|
|
55
|
+
marked natively: Harness paints the folder icon of the active chain blue. This
|
|
56
|
+
plugin does not duplicate that.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
From npm:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
dsh plugin --profile <profile> add dsh-workspace-status
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
From a local checkout (development):
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
dsh plugin --profile desktop add /absolute/path/to/dsh-workspace-status
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`dsh plugin add` links the checkout and appends this package to
|
|
73
|
+
`dsh.profile.bundles` because it declares `dsh.bundle`. Use the profile that
|
|
74
|
+
runs your client — `desktop` for the macOS/Windows app, `web` for `dsh web`.
|
|
75
|
+
Or use sidebar → **Plugins** → **Add plugin** and pass the absolute path to this
|
|
76
|
+
directory. The host half contributes nothing, but the profile composition still
|
|
77
|
+
changes, so **restart the profile's Host** (quit and reopen the desktop app, or
|
|
78
|
+
restart `dsh web`) after installing. Then reload the page if the client half
|
|
79
|
+
does not appear.
|
|
80
|
+
|
|
81
|
+
Manual mount without the CLI: copy the package into the profile's
|
|
82
|
+
`node_modules`, add `"dsh-workspace-status"` to `dsh.profile.bundles` in the
|
|
83
|
+
profile's `package.json`, and insert the row from `cordis.patch.yml` into the
|
|
84
|
+
profile's patch layer.
|
|
85
|
+
|
|
86
|
+
## Verify
|
|
87
|
+
|
|
88
|
+
1. Start a session in a workspace and collapse that group while it runs — a
|
|
89
|
+
blue ring appears at the end of the folder row.
|
|
90
|
+
2. Trigger an approval (or a plan review / question) in a session of another
|
|
91
|
+
workspace — that folder row shows an amber dot even while expanded.
|
|
92
|
+
3. Let a session finish without opening it — a green dot appears, and lapses an
|
|
93
|
+
hour later.
|
|
94
|
+
4. Expand the group: the mark stays on the folder row; the session rows keep
|
|
95
|
+
their own dots unchanged.
|
|
96
|
+
5. With every session idle, no extra element appears in the sidebar.
|
|
97
|
+
|
|
98
|
+
The client half writes nothing into the sidebar DOM — it reacts to the public
|
|
99
|
+
Workspace/Session feeds and emits CSS keyed on the row the browser already
|
|
100
|
+
renders. If the mark is missing, check the browser console for a failed client
|
|
101
|
+
bundle load first; nothing else in the sidebar is affected.
|
|
102
|
+
|
|
103
|
+
## How it works
|
|
104
|
+
|
|
105
|
+
- The mount is an entry in the additive `sidebar.footer.action` seat. It renders
|
|
106
|
+
only a `<style>` element and a visually hidden `<span>`; it is not a control
|
|
107
|
+
and adds nothing visible at the foot.
|
|
108
|
+
- Aggregation reads the same public data the browser uses: `useWorkspaces`
|
|
109
|
+
(`items`, `archivedSessionIds`), `useSessions` (`byId`) and
|
|
110
|
+
`useSessionStatus` (`running`, `pendingInteraction`, `completionUnread`).
|
|
111
|
+
- The visible mark is the workspace row's trailing pseudo-element, emitted as a
|
|
112
|
+
rule targeting `[data-row-key="workspace:<id>"]::after`.
|
|
113
|
+
- Removing the plugin removes the mount, which removes the stylesheet; no state
|
|
114
|
+
is written anywhere.
|
|
115
|
+
|
|
116
|
+
### The one internal dependency
|
|
117
|
+
|
|
118
|
+
`data-row-key="workspace:<id>"` is an internal detail of
|
|
119
|
+
`@deepseek-ai/dsh-client-ui-workspace`, not a public slot contract. A future
|
|
120
|
+
Harness release could rename it, in which case the marks would silently stop
|
|
121
|
+
appearing until this package is updated. The failure mode is deliberately
|
|
122
|
+
**"no indicator"**, not "broken sidebar": the plugin never touches the DOM, so a
|
|
123
|
+
stale selector can only produce a missing dot.
|
|
124
|
+
|
|
125
|
+
## Compatibility
|
|
126
|
+
|
|
127
|
+
- DeepSeek Harness `>=0.2.1-rc.2 <0.3.0-0` (developed and verified against
|
|
128
|
+
`0.2.1-rc.3`); Web and Desktop clients.
|
|
129
|
+
- Node `>=22.18.0` for the build-free package and its tests.
|
|
130
|
+
- No runtime dependencies, no host services, no settings, no model-visible
|
|
131
|
+
tools or prompt changes.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
npm test
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The suite loads the classic-script client envelope in a sandbox, drives the slot
|
|
140
|
+
component with fake standard hooks, and asserts the emitted stylesheet and
|
|
141
|
+
hidden description across waiting/running/finished, subagent, archived,
|
|
142
|
+
ungrouped, expiry and selector-escaping cases. A manifest test checks the bundle
|
|
143
|
+
patch, export map, locale parity and the loader envelope.
|
|
144
|
+
|
|
145
|
+
There is no build step: `index.js` and `client.js` are the published artifacts.
|
|
146
|
+
|
|
147
|
+
## Remove
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
dsh plugin --profile <profile> remove dsh-workspace-status
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Restart the Host. The sidebar returns to stock; nothing else is left behind.
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
package/client.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of workspace-status.
|
|
3
|
+
*
|
|
4
|
+
* The sidebar draws a status dot on every Session row but nothing on the
|
|
5
|
+
* Workspace (folder) row above it, so a collapsed group — or a group scrolled
|
|
6
|
+
* out of view — hides whether any agent inside it is waiting, running, or
|
|
7
|
+
* just finished. Workspace rows declare no slot, so this package does not
|
|
8
|
+
* render markup into them: it publishes one stylesheet aimed at the row the
|
|
9
|
+
* Workspace browser already renders (`data-row-key="workspace:<id>"`) and
|
|
10
|
+
* draws the mark as that row's trailing pseudo-element. React keeps owning
|
|
11
|
+
* the row; nothing is written to the document and nothing is observed.
|
|
12
|
+
*
|
|
13
|
+
* The entry mounts in the additive `sidebar.footer.action` seat, which is
|
|
14
|
+
* mounted for the whole browsing region even when no Session exists. It shows
|
|
15
|
+
* no control of its own: it publishes the row rules plus a visually hidden
|
|
16
|
+
* description of the same state for readers who cannot see the mark.
|
|
17
|
+
*
|
|
18
|
+
* Data comes from the same public feeds the browser itself reads:
|
|
19
|
+
* `useWorkspaces` (items + archive set), `useSessions` (summaries, including
|
|
20
|
+
* subagent rows) and `useSessionStatus` (running / pendingInteraction /
|
|
21
|
+
* completionUnread). The precedence mirrors the shipped Session row:
|
|
22
|
+
* waiting for the user outranks running, which outranks the finished
|
|
23
|
+
* reminder; a subagent keeps its parent's workspace busy.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
window.__ModuleLoader__.load({
|
|
27
|
+
id: 'dsh-workspace-status',
|
|
28
|
+
factory(require) {
|
|
29
|
+
const React = require('react')
|
|
30
|
+
|
|
31
|
+
/** Additive seat beside Settings at the sidebar foot. */
|
|
32
|
+
const SLOT = 'sidebar.footer.action'
|
|
33
|
+
/** Stable list id; reusing it would shadow this entry rather than add one. */
|
|
34
|
+
const ENTRY_ID = 'workspace-status'
|
|
35
|
+
/** The stylesheet this mount owns; unmounting the entry removes it. */
|
|
36
|
+
const STYLE_ID = 'dsh-workspace-status/rows.css'
|
|
37
|
+
/** Last among the shipped footer actions. */
|
|
38
|
+
const ENTRY_ORDER = 950
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How long a finished-but-unread Session keeps its Workspace marked.
|
|
42
|
+
*
|
|
43
|
+
* The browser's finished reminder persists until the Session is opened.
|
|
44
|
+
* A folder row owns no timestamp that could age it out, so it would keep a
|
|
45
|
+
* green dot on a workspace whose work stopped days ago. This mark is a
|
|
46
|
+
* notice: it lapses once the Session's last update stops reading in
|
|
47
|
+
* minutes. A Session watched stopping keeps the mark for the same window
|
|
48
|
+
* even when its last prompt is older than that.
|
|
49
|
+
*/
|
|
50
|
+
const FINISHED_WINDOW_MS = 60 * 60 * 1000
|
|
51
|
+
|
|
52
|
+
/** How many workspace names the hidden description lists. */
|
|
53
|
+
const MAX_NAMES = 4
|
|
54
|
+
|
|
55
|
+
/** The visually hidden description's class. */
|
|
56
|
+
const SR_CLASS = 'dsws-sr'
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The hidden description's own rule. The visible mark carries no markup of
|
|
60
|
+
* its own (the generated row rules draw it), so this sheet only has to
|
|
61
|
+
* keep the description out of the layout.
|
|
62
|
+
*/
|
|
63
|
+
const BASE_CSS = [
|
|
64
|
+
`.${SR_CLASS}{position:absolute;width:1px;height:1px;margin:-1px;padding:0;`,
|
|
65
|
+
'overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}',
|
|
66
|
+
].join('')
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Every user-facing string lives here rather than in a locale service: the
|
|
70
|
+
* only copy this package produces is a hidden description, so the built-in
|
|
71
|
+
* dictionaries are enough and the plugin keeps working without the locale
|
|
72
|
+
* face. The `locale/*.json` files mirror them for tooling.
|
|
73
|
+
*/
|
|
74
|
+
const DICTIONARIES = {
|
|
75
|
+
en: {
|
|
76
|
+
'activity.summary': 'Workspace activity: {clauses} — {names}',
|
|
77
|
+
'activity.waiting': '{n} waiting for you',
|
|
78
|
+
'activity.running': '{n} running',
|
|
79
|
+
'activity.done': '{n} recently finished',
|
|
80
|
+
'activity.unnamed': 'Ungrouped',
|
|
81
|
+
'activity.more': 'and {n} more',
|
|
82
|
+
'activity.listSeparator': ', ',
|
|
83
|
+
},
|
|
84
|
+
zh: {
|
|
85
|
+
'activity.summary': '工作区状态:{clauses} — {names}',
|
|
86
|
+
'activity.waiting': '{n} 个等待你处理',
|
|
87
|
+
'activity.running': '{n} 个运行中',
|
|
88
|
+
'activity.done': '{n} 个刚刚完成',
|
|
89
|
+
'activity.unnamed': '未分组',
|
|
90
|
+
'activity.more': '等 {n} 个',
|
|
91
|
+
'activity.listSeparator': '、',
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the host's language without assuming the locale service exists.
|
|
97
|
+
* @returns 'zh' for Chinese hosts, otherwise 'en'.
|
|
98
|
+
*/
|
|
99
|
+
function currentLanguage() {
|
|
100
|
+
const candidates = []
|
|
101
|
+
if (typeof document !== 'undefined') {
|
|
102
|
+
const lang = document?.documentElement?.lang
|
|
103
|
+
if (typeof lang === 'string' && lang !== '') candidates.push(lang)
|
|
104
|
+
}
|
|
105
|
+
if (typeof navigator !== 'undefined' && typeof navigator.language === 'string') {
|
|
106
|
+
candidates.push(navigator.language)
|
|
107
|
+
}
|
|
108
|
+
for (const value of candidates) {
|
|
109
|
+
if (/^zh/i.test(value)) return 'zh'
|
|
110
|
+
}
|
|
111
|
+
return 'en'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Interpolate `{name}` placeholders in one template. */
|
|
115
|
+
function format(template, params) {
|
|
116
|
+
return template.replace(/\{(\w+)\}/g, (match, name) => (
|
|
117
|
+
params !== undefined && Object.prototype.hasOwnProperty.call(params, name)
|
|
118
|
+
? String(params[name])
|
|
119
|
+
: match
|
|
120
|
+
))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One localized message, falling back to English and then the key. */
|
|
124
|
+
function message(key, params) {
|
|
125
|
+
const language = currentLanguage()
|
|
126
|
+
const dictionary = DICTIONARIES[language] ?? DICTIONARIES.en
|
|
127
|
+
const template = dictionary[key] ?? DICTIONARIES.en[key] ?? key
|
|
128
|
+
return format(template, params)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Escape one group key for a double-quoted CSS attribute selector.
|
|
133
|
+
*
|
|
134
|
+
* A control character has no literal form inside a CSS string: an unescaped
|
|
135
|
+
* newline ends the string token and the whole rule is dropped as invalid,
|
|
136
|
+
* so the row would lose its mark silently. Each one is written as a hex
|
|
137
|
+
* escape with a trailing space, which stops a following hex digit being
|
|
138
|
+
* absorbed. Host Workspace ids are UUIDs, so this is a correctness guard,
|
|
139
|
+
* not a path expected in practice.
|
|
140
|
+
* @param value - raw group key.
|
|
141
|
+
* @returns the key escaped for use inside quotes.
|
|
142
|
+
*/
|
|
143
|
+
function quote(value) {
|
|
144
|
+
let escaped = ''
|
|
145
|
+
for (const char of String(value)) {
|
|
146
|
+
const code = char.codePointAt(0) ?? 0
|
|
147
|
+
if (char === '\\' || char === '"') escaped += `\\${char}`
|
|
148
|
+
else if (code < 0x20 || code === 0x7f) escaped += `\\${code.toString(16)} `
|
|
149
|
+
else escaped += char
|
|
150
|
+
}
|
|
151
|
+
return escaped
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The running mark, carried as an alpha mask rather than a border.
|
|
156
|
+
*
|
|
157
|
+
* A CSS border ring cannot read as a circle at this size: its corners are
|
|
158
|
+
* mitred, so the gap between the two ends looks like a clipped square. The
|
|
159
|
+
* mask keeps a round 2px-equivalent stroke with round caps and a faint
|
|
160
|
+
* track, matching the geometry of the product's own running indicator.
|
|
161
|
+
*/
|
|
162
|
+
const RING = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Ccircle cx='5' cy='5' r='4' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-opacity='.25'/%3E%3Ccircle cx='5' cy='5' r='4' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-dasharray='5 20'/%3E%3C/svg%3E\")"
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* One Workspace's tally before the states are split.
|
|
166
|
+
* @typedef {{ key: string, title: string, waiting: number, running: number, done: number }} GroupTally
|
|
167
|
+
*/
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Whether a Session's own timestamp is recent enough to still deserve a
|
|
171
|
+
* finished mark. A missing or zero timestamp cannot be corroborated, so it
|
|
172
|
+
* never earns one (subagent rows carry a zero timestamp).
|
|
173
|
+
* @param updatedAt - the Session summary's epoch-millisecond timestamp.
|
|
174
|
+
* @param now - render-time clock.
|
|
175
|
+
* @returns true while the timestamp reads within the notice window.
|
|
176
|
+
*/
|
|
177
|
+
function isFresh(updatedAt, now) {
|
|
178
|
+
if (typeof updatedAt !== 'number' || !Number.isFinite(updatedAt) || updatedAt <= 0) return false
|
|
179
|
+
return now - updatedAt <= FINISHED_WINDOW_MS
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* What this mount has watched happen, keyed by Session id.
|
|
184
|
+
*
|
|
185
|
+
* `running` holds every Session seen running since the mount; `stoppedAt`
|
|
186
|
+
* holds the time a Session was watched stop while still carrying the unread
|
|
187
|
+
* mark, and only for Sessions first seen running. Together they let the
|
|
188
|
+
* finished mark expire on its own rather than wait for the browser to clear
|
|
189
|
+
* it — and they cover a Session that ran for hours, whose `updatedAt` is
|
|
190
|
+
* the time of its last prompt, not the time it stopped.
|
|
191
|
+
* @typedef {{ running: Set<string>, stoppedAt: Map<string, number> }} Watch
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
/** Drop one Session from the watch (archived or gone). */
|
|
195
|
+
function forget(watch, sessionId) {
|
|
196
|
+
watch.running.delete(sessionId)
|
|
197
|
+
watch.stoppedAt.delete(sessionId)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Whether any direct subagent child of one Session is currently running. */
|
|
201
|
+
function runsChild(sessionId, children, sessions, statuses) {
|
|
202
|
+
const kids = children.get(sessionId)
|
|
203
|
+
if (kids === undefined) return false
|
|
204
|
+
for (const childId of kids) {
|
|
205
|
+
const status = statuses?.get?.(childId)
|
|
206
|
+
// The status feed is authoritative where it knows the child; the
|
|
207
|
+
// summary's own running flag is the fallback, matching the browser's
|
|
208
|
+
// precedence.
|
|
209
|
+
const running = status?.running === undefined
|
|
210
|
+
? sessions[childId]?.running === true
|
|
211
|
+
: status.running === true
|
|
212
|
+
if (running) return true
|
|
213
|
+
}
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Which Workspace groups own a waiting, running, or just-finished Session.
|
|
219
|
+
*
|
|
220
|
+
* Precedence matches the shipped Session row: waiting for the user outranks
|
|
221
|
+
* running, which outranks the finished reminder. Each Session therefore
|
|
222
|
+
* lands in exactly one tally, so the counts in the hidden description add
|
|
223
|
+
* up to the number of marked Sessions. Archived Sessions are skipped, the
|
|
224
|
+
* same way the default browser view hides them. A running subagent keeps
|
|
225
|
+
* its parent's Workspace busy even when the parent turn already settled;
|
|
226
|
+
* subagent rows are not workspace members, so they never create a mark of
|
|
227
|
+
* their own.
|
|
228
|
+
*
|
|
229
|
+
* @param items - Workspace rows from the Workspace snapshot.
|
|
230
|
+
* @param byId - Session summaries keyed by id.
|
|
231
|
+
* @param statuses - Session UI status map (running / pendingInteraction / completionUnread).
|
|
232
|
+
* @param archivedSessionIds - registry-global archive set.
|
|
233
|
+
* @param now - render-time clock, epoch milliseconds.
|
|
234
|
+
* @param watch - this mount's running/stopped observations.
|
|
235
|
+
* @returns marked group keys by state, counts, and display titles.
|
|
236
|
+
*/
|
|
237
|
+
function deriveActivity(items, byId, statuses, archivedSessionIds, now, watch) {
|
|
238
|
+
const sessions = byId !== null && typeof byId === 'object' ? byId : {}
|
|
239
|
+
const archived = new Set(Array.isArray(archivedSessionIds) ? archivedSessionIds : [])
|
|
240
|
+
const statusMap = statuses !== null && typeof statuses?.get === 'function' ? statuses : undefined
|
|
241
|
+
|
|
242
|
+
/** Session id → owning Workspace key (ungrouped bucket is `''`). */
|
|
243
|
+
const workspaceOf = new Map()
|
|
244
|
+
/** Workspace key → display title. */
|
|
245
|
+
const titles = new Map()
|
|
246
|
+
for (const workspace of Array.isArray(items) ? items : []) {
|
|
247
|
+
if (workspace === null || typeof workspace !== 'object') continue
|
|
248
|
+
const key = typeof workspace.workspaceId === 'string' ? workspace.workspaceId : ''
|
|
249
|
+
titles.set(key, typeof workspace.title === 'string' ? workspace.title : '')
|
|
250
|
+
for (const sessionId of Array.isArray(workspace.sessionIds) ? workspace.sessionIds : []) {
|
|
251
|
+
workspaceOf.set(sessionId, key)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Parent Session id → direct subagent child ids. */
|
|
256
|
+
const children = new Map()
|
|
257
|
+
for (const sessionId of Object.keys(sessions)) {
|
|
258
|
+
const row = sessions[sessionId]
|
|
259
|
+
if (row?.origin !== 'subagent' || typeof row.parentId !== 'string') continue
|
|
260
|
+
const list = children.get(row.parentId)
|
|
261
|
+
if (list === undefined) children.set(row.parentId, [sessionId])
|
|
262
|
+
else list.push(sessionId)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** @type {Map<string, GroupTally>} */
|
|
266
|
+
const groups = new Map()
|
|
267
|
+
const tally = (key) => {
|
|
268
|
+
let group = groups.get(key)
|
|
269
|
+
if (group === undefined) {
|
|
270
|
+
group = { key, title: titles.get(key) ?? '', waiting: 0, running: 0, done: 0 }
|
|
271
|
+
groups.set(key, group)
|
|
272
|
+
}
|
|
273
|
+
return group
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const sessionId of Object.keys(sessions).sort()) {
|
|
277
|
+
const row = sessions[sessionId]
|
|
278
|
+
if (row === null || row === undefined) continue
|
|
279
|
+
if (row.origin === 'subagent') continue
|
|
280
|
+
if (archived.has(sessionId)) {
|
|
281
|
+
forget(watch, sessionId)
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const status = statusMap?.get?.(sessionId)
|
|
286
|
+
const running = status?.running === true
|
|
287
|
+
|| (status?.running === undefined && row.running === true)
|
|
288
|
+
|| runsChild(sessionId, children, sessions, statusMap)
|
|
289
|
+
const waiting = status?.pendingInteraction !== undefined && status?.pendingInteraction !== null
|
|
290
|
+
|
|
291
|
+
if (running) {
|
|
292
|
+
watch.running.add(sessionId)
|
|
293
|
+
watch.stoppedAt.delete(sessionId)
|
|
294
|
+
} else if (watch.running.delete(sessionId) && status?.completionUnread === true) {
|
|
295
|
+
watch.stoppedAt.set(sessionId, now)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const recent = isFresh(row.updatedAt, now)
|
|
299
|
+
|| (watch.stoppedAt.get(sessionId) !== undefined
|
|
300
|
+
&& now - watch.stoppedAt.get(sessionId) <= FINISHED_WINDOW_MS)
|
|
301
|
+
const key = workspaceOf.has(sessionId) ? workspaceOf.get(sessionId) : ''
|
|
302
|
+
|
|
303
|
+
if (waiting) {
|
|
304
|
+
tally(key).waiting += 1
|
|
305
|
+
} else if (running) {
|
|
306
|
+
tally(key).running += 1
|
|
307
|
+
} else if (status?.completionUnread === true && recent) {
|
|
308
|
+
tally(key).done += 1
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Prune observations that can no longer produce a mark.
|
|
313
|
+
for (const sessionId of watch.running) {
|
|
314
|
+
if (sessions[sessionId] === undefined) watch.running.delete(sessionId)
|
|
315
|
+
}
|
|
316
|
+
for (const [sessionId, stoppedAt] of watch.stoppedAt) {
|
|
317
|
+
if (sessions[sessionId] === undefined || now - stoppedAt > FINISHED_WINDOW_MS) {
|
|
318
|
+
watch.stoppedAt.delete(sessionId)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const waitingKeys = []
|
|
323
|
+
const runningKeys = []
|
|
324
|
+
const doneKeys = []
|
|
325
|
+
const names = new Map()
|
|
326
|
+
const totals = { waiting: 0, running: 0, done: 0 }
|
|
327
|
+
let marked = 0
|
|
328
|
+
for (const group of groups.values()) {
|
|
329
|
+
const total = group.waiting + group.running + group.done
|
|
330
|
+
if (total === 0) continue
|
|
331
|
+
if (group.waiting > 0) waitingKeys.push(group.key)
|
|
332
|
+
else if (group.running > 0) runningKeys.push(group.key)
|
|
333
|
+
else doneKeys.push(group.key)
|
|
334
|
+
totals.waiting += group.waiting
|
|
335
|
+
totals.running += group.running
|
|
336
|
+
totals.done += group.done
|
|
337
|
+
marked += total
|
|
338
|
+
names.set(group.key, group.title)
|
|
339
|
+
}
|
|
340
|
+
waitingKeys.sort()
|
|
341
|
+
runningKeys.sort()
|
|
342
|
+
doneKeys.sort()
|
|
343
|
+
|
|
344
|
+
return { waiting: waitingKeys, running: runningKeys, done: doneKeys, totals, marked, names }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The selectors for one state's rows, each carrying its own pseudo-element.
|
|
349
|
+
*
|
|
350
|
+
* Every rule repeats the whole selector list. A comma-joined list applies
|
|
351
|
+
* the trailing `::after` to its last member only, so sharing one prefix
|
|
352
|
+
* would leave every row but one undecorated once two Workspaces share a
|
|
353
|
+
* state.
|
|
354
|
+
*/
|
|
355
|
+
function selectors(keys, suffix) {
|
|
356
|
+
return keys.map((key) => `[data-row-key="workspace:${quote(key)}"]${suffix}`).join(',')
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The stylesheet that draws each marked row's trailing mark.
|
|
361
|
+
*
|
|
362
|
+
* The mark is the row's `::after`, a flex child at the empty right-hand end
|
|
363
|
+
* beside the row actions, so it never displaces the title and needs no
|
|
364
|
+
* positioning. Waiting is a solid amber dot, running is the product's blue
|
|
365
|
+
* as a stroked ring, finished is the product's green dot. Colors come from
|
|
366
|
+
* the same theme tokens the Session rows use, so both levels agree and both
|
|
367
|
+
* themes are covered.
|
|
368
|
+
* @param activity - derived activity for the current render.
|
|
369
|
+
* @returns CSS text (no base rules).
|
|
370
|
+
*/
|
|
371
|
+
function rowCss(activity) {
|
|
372
|
+
const parts = []
|
|
373
|
+
|
|
374
|
+
if (activity.waiting.length > 0) {
|
|
375
|
+
const waiting = selectors(activity.waiting, '::after')
|
|
376
|
+
parts.push(
|
|
377
|
+
`${waiting}{content:"";flex:none;box-sizing:border-box;width:6px;height:6px;`,
|
|
378
|
+
'margin-inline-start:2px;border-radius:50%;',
|
|
379
|
+
'background:var(--dsw-alias-state-warn-primary);',
|
|
380
|
+
'box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-warn-primary) 16%, transparent)}',
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (activity.running.length > 0) {
|
|
385
|
+
const running = selectors(activity.running, '::after')
|
|
386
|
+
parts.push(
|
|
387
|
+
'@keyframes dsws-spin{to{transform:rotate(360deg)}}',
|
|
388
|
+
`${running}{content:"";flex:none;box-sizing:border-box;width:10px;height:10px;`,
|
|
389
|
+
'margin-inline-start:2px;background-color:var(--dsw-static-deepseek-450);',
|
|
390
|
+
`-webkit-mask-image:${RING};mask-image:${RING};`,
|
|
391
|
+
'-webkit-mask-size:100% 100%;mask-size:100% 100%;',
|
|
392
|
+
'-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;',
|
|
393
|
+
'animation:dsws-spin 1.5s linear infinite}',
|
|
394
|
+
`@media (prefers-reduced-motion:reduce){${running}{animation:none;opacity:.7}}`,
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (activity.done.length > 0) {
|
|
399
|
+
const done = selectors(activity.done, '::after')
|
|
400
|
+
parts.push(
|
|
401
|
+
`${done}{content:"";flex:none;box-sizing:border-box;width:6px;height:6px;`,
|
|
402
|
+
'margin-inline-start:2px;border-radius:50%;',
|
|
403
|
+
'background:var(--dsw-alias-state-success-primary);',
|
|
404
|
+
'box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent)}',
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return parts.join('')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The hidden description of the same state.
|
|
413
|
+
*
|
|
414
|
+
* Pseudo-elements are not reliably announced, so the aggregate travels as
|
|
415
|
+
* text: counts by state followed by the Workspace names, bounded so a large
|
|
416
|
+
* fleet does not turn into a paragraph.
|
|
417
|
+
* @param activity - derived activity for the current render.
|
|
418
|
+
* @returns one localized sentence.
|
|
419
|
+
*/
|
|
420
|
+
function describe(activity) {
|
|
421
|
+
const separator = message('activity.listSeparator')
|
|
422
|
+
const clauses = []
|
|
423
|
+
if (activity.totals.waiting > 0) {
|
|
424
|
+
clauses.push(message('activity.waiting', { n: activity.totals.waiting }))
|
|
425
|
+
}
|
|
426
|
+
if (activity.totals.running > 0) {
|
|
427
|
+
clauses.push(message('activity.running', { n: activity.totals.running }))
|
|
428
|
+
}
|
|
429
|
+
if (activity.totals.done > 0) {
|
|
430
|
+
clauses.push(message('activity.done', { n: activity.totals.done }))
|
|
431
|
+
}
|
|
432
|
+
const orderedKeys = [...activity.waiting, ...activity.running, ...activity.done]
|
|
433
|
+
const names = orderedKeys.map((key) => {
|
|
434
|
+
const title = activity.names.get(key)
|
|
435
|
+
return title !== undefined && title !== '' ? title : message('activity.unnamed')
|
|
436
|
+
})
|
|
437
|
+
const listed = names.slice(0, MAX_NAMES)
|
|
438
|
+
const rest = names.length - listed.length
|
|
439
|
+
if (rest > 0) listed.push(message('activity.more', { n: rest }))
|
|
440
|
+
return message('activity.summary', {
|
|
441
|
+
clauses: clauses.join(separator),
|
|
442
|
+
names: listed.join(separator),
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Read one standard selector hook, tolerating an assembly without it.
|
|
448
|
+
* @param hook - the standard hook, possibly absent.
|
|
449
|
+
* @param selector - projection over the hook's snapshot.
|
|
450
|
+
* @param fallback - snapshot used when the hook is absent.
|
|
451
|
+
* @returns the selected value.
|
|
452
|
+
*/
|
|
453
|
+
function useStandard(hook, selector, fallback) {
|
|
454
|
+
return typeof hook === 'function' ? hook(selector) : selector(fallback)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const EMPTY_WORKSPACES = { items: [], archivedSessionIds: [] }
|
|
458
|
+
const EMPTY_SESSIONS = { byId: {} }
|
|
459
|
+
const EMPTY_STATUSES = new Map()
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* The slot entry. It shows no control of its own: it publishes the row
|
|
463
|
+
* rules, plus a description of the same state for readers who cannot see
|
|
464
|
+
* the mark.
|
|
465
|
+
* @param props - standard selector hooks.
|
|
466
|
+
* @returns the style + hidden description, or null when nothing is marked.
|
|
467
|
+
*/
|
|
468
|
+
function WorkspaceStatus({ useWorkspaces, useSessions, useSessionStatus }) {
|
|
469
|
+
const items = useStandard(useWorkspaces, (state) => state?.items, EMPTY_WORKSPACES)
|
|
470
|
+
const archived = useStandard(useWorkspaces, (state) => state?.archivedSessionIds, EMPTY_WORKSPACES)
|
|
471
|
+
const byId = useStandard(useSessions, (state) => state?.byId, EMPTY_SESSIONS)
|
|
472
|
+
const statuses = useStandard(useSessionStatus, (state) => state, EMPTY_STATUSES)
|
|
473
|
+
|
|
474
|
+
const watchRef = React.useRef(undefined)
|
|
475
|
+
if (watchRef.current === undefined) {
|
|
476
|
+
watchRef.current = { running: new Set(), stoppedAt: new Map() }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const activity = React.useMemo(
|
|
480
|
+
// The window is measured at render time, the same way the browser reads
|
|
481
|
+
// its own clock: no timer, because any feed change re-renders anyway.
|
|
482
|
+
// Advancing the watch is idempotent, so a repeated render costs nothing.
|
|
483
|
+
() => deriveActivity(items, byId, statuses, archived, Date.now(), watchRef.current),
|
|
484
|
+
[items, byId, statuses, archived, watchRef],
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
if (activity.marked === 0) return null
|
|
488
|
+
|
|
489
|
+
return React.createElement(
|
|
490
|
+
React.Fragment,
|
|
491
|
+
null,
|
|
492
|
+
React.createElement('style', { 'data-plugin-css': STYLE_ID }, BASE_CSS + rowCss(activity)),
|
|
493
|
+
React.createElement('span', { className: SR_CLASS }, describe(activity)),
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
name: 'workspace-status',
|
|
499
|
+
inject: ['slots'],
|
|
500
|
+
apply(ctx) {
|
|
501
|
+
ctx.slots.inject(SLOT, () => ctx.slots.register(
|
|
502
|
+
{ name: SLOT, id: ENTRY_ID, order: ENTRY_ORDER },
|
|
503
|
+
WorkspaceStatus,
|
|
504
|
+
))
|
|
505
|
+
},
|
|
506
|
+
/** Exposed for the package's own tests; the Loader ignores it. */
|
|
507
|
+
__test: { deriveActivity, rowCss, describe, quote, WorkspaceStatus, FINISHED_WINDOW_MS },
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
})
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# The bundle layer this package contributes to a dsh profile.
|
|
2
|
+
# Rows inserted here become Loader entries once the profile lists this package
|
|
3
|
+
# in package.json -> dsh.profile.bundles (the Plugins page does both steps).
|
|
4
|
+
- insert:
|
|
5
|
+
- id: workspace-status
|
|
6
|
+
name: dsh-workspace-status
|
package/icon.svg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" role="img" aria-label="Workspace Status">
|
|
2
|
+
<rect x="2.25" y="2.25" width="19.5" height="19.5" rx="4.25" fill="#151A22"/>
|
|
3
|
+
<rect x="5" y="5.5" width="5" height="5" rx="1.3" fill="#3B4A5A"/>
|
|
4
|
+
<rect x="12.5" y="5.5" width="5" height="5" rx="1.3" fill="#3B4A5A"/>
|
|
5
|
+
<circle cx="7.5" cy="15.75" r="2.4" fill="none" stroke="#4D8DFF" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="4 9"/>
|
|
6
|
+
<circle cx="15" cy="15.75" r="1.8" fill="#F59E0B"/>
|
|
7
|
+
<circle cx="19.4" cy="15.75" r="1.8" fill="#22C55E"/>
|
|
8
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of workspace-status.
|
|
3
|
+
*
|
|
4
|
+
* The Client module owns the indicator: it reads Workspace and Session state
|
|
5
|
+
* through the slot's standard hooks and publishes the row rules from its own
|
|
6
|
+
* component, so the Host has no service to provide and no resource to register.
|
|
7
|
+
* This empty `apply` exists only to give the Loader a host-side row, exactly
|
|
8
|
+
* like any other bundle that is purely a browser feature.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Loader entry name; matches the patch row in `cordis.patch.yml`. */
|
|
12
|
+
export const name = 'workspace-status'
|
|
13
|
+
|
|
14
|
+
/** No Host services are required. */
|
|
15
|
+
export const inject = []
|
|
16
|
+
|
|
17
|
+
/** No Host-side behavior. */
|
|
18
|
+
export function apply() {}
|
package/locale/en.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "Workspace Status",
|
|
4
|
+
"description": "Marks each workspace in the sidebar while one of its sessions is waiting for you, running, or recently finished."
|
|
5
|
+
},
|
|
6
|
+
"activity.summary": "Workspace activity: {clauses} — {names}",
|
|
7
|
+
"activity.waiting": "{n} waiting for you",
|
|
8
|
+
"activity.running": "{n} running",
|
|
9
|
+
"activity.done": "{n} recently finished",
|
|
10
|
+
"activity.unnamed": "Ungrouped",
|
|
11
|
+
"activity.more": "and {n} more",
|
|
12
|
+
"activity.listSeparator": ", "
|
|
13
|
+
}
|
package/locale/zh.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "工作区状态",
|
|
4
|
+
"description": "侧边栏里,某个工作区有会话等待你处理、正在运行或刚刚完成时,在该工作区行上显示状态标记。"
|
|
5
|
+
},
|
|
6
|
+
"activity.summary": "工作区状态:{clauses} — {names}",
|
|
7
|
+
"activity.waiting": "{n} 个等待你处理",
|
|
8
|
+
"activity.running": "{n} 个运行中",
|
|
9
|
+
"activity.done": "{n} 个刚刚完成",
|
|
10
|
+
"activity.unnamed": "未分组",
|
|
11
|
+
"activity.more": "等 {n} 个",
|
|
12
|
+
"activity.listSeparator": "、"
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-workspace-status",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Colored status marks on DeepSeek Harness sidebar workspaces: waiting for you, running, and recently finished.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "node --test"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"dsh",
|
|
13
|
+
"dsh-plugin",
|
|
14
|
+
"deepseek-harness",
|
|
15
|
+
"sidebar",
|
|
16
|
+
"workspace",
|
|
17
|
+
"status",
|
|
18
|
+
"indicator"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./index.js",
|
|
25
|
+
"./client": "./client.js",
|
|
26
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
27
|
+
"./locale/*.json": "./locale/*.json",
|
|
28
|
+
"./icon": "./icon.svg",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"index.js",
|
|
33
|
+
"client.js",
|
|
34
|
+
"cordis.patch.yml",
|
|
35
|
+
"locale/*.json",
|
|
36
|
+
"icon.svg",
|
|
37
|
+
"README.md",
|
|
38
|
+
"CHANGELOG.md"
|
|
39
|
+
],
|
|
40
|
+
"dsh": {
|
|
41
|
+
"bundle": {
|
|
42
|
+
"patch": "./cordis.patch.yml"
|
|
43
|
+
},
|
|
44
|
+
"client": {
|
|
45
|
+
"platform": "web",
|
|
46
|
+
"immediately": true,
|
|
47
|
+
"inject": [
|
|
48
|
+
"@deepseek-ai/dsh-client-locale",
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-workspace",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-session"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"compatibility": {
|
|
55
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0-0"
|
|
56
|
+
},
|
|
57
|
+
"catalog": {
|
|
58
|
+
"summary": {
|
|
59
|
+
"en": "Colored status marks on sidebar workspace rows: waiting for you, running, and recently finished, so the next workspace to open is obvious.",
|
|
60
|
+
"zh": "在侧边栏工作区行上显示彩色状态:等待你处理、运行中、刚刚完成,一眼看出下一个该打开的工作区。"
|
|
61
|
+
},
|
|
62
|
+
"category": "ui"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0-0",
|
|
67
|
+
"node": ">=22.18.0"
|
|
68
|
+
},
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "git+https://github.com/Stijnus/dsh-workspace-status.git"
|
|
72
|
+
},
|
|
73
|
+
"homepage": "https://github.com/Stijnus/dsh-workspace-status#readme",
|
|
74
|
+
"bugs": {
|
|
75
|
+
"url": "https://github.com/Stijnus/dsh-workspace-status/issues"
|
|
76
|
+
}
|
|
77
|
+
}
|