dsh-plugin-taskflow 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/client/client.js +80 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,10 +14,16 @@
|
|
|
14
14
|
# 从 npm 安装
|
|
15
15
|
dsh plugin --profile web add dsh-plugin-taskflow
|
|
16
16
|
|
|
17
|
+
# 若包管理器因「新版本发布冷却期」装到了 0.1.0(无 dsh.bundle 的旧版),显式指定:
|
|
18
|
+
dsh plugin --profile web add dsh-plugin-taskflow@0.1.1
|
|
19
|
+
|
|
17
20
|
# 或直接从 GitHub 源码安装
|
|
18
21
|
dsh plugin --profile web add github:wk-xjd/dsh-plugin-taskflow
|
|
19
22
|
```
|
|
20
23
|
|
|
24
|
+
`dsh plugin add` 会把本包装成一个 **profile bundle**(自动写入 `dsh.profile.bundles` 并加载
|
|
25
|
+
`cordis.patch.yml`),无需手工编辑 patch 文件。
|
|
26
|
+
|
|
21
27
|
安装后**刷新页面**(桌面客户端:开发者工具里执行 `location.reload()`,或重启 App);
|
|
22
28
|
宿主半部更新需重启一次 App。
|
|
23
29
|
|
|
@@ -66,8 +72,14 @@ node install.mjs --profile web # 装到别的 profile
|
|
|
66
72
|
|
|
67
73
|
## 安装细节
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
本包同时是一个 **DSH bundle**:`package.json` 里声明 `dsh.bundle.patch = ./cordis.patch.yml`,
|
|
76
|
+
`dsh plugin --profile <p> add dsh-plugin-taskflow` 会把它写进该 profile 的 `dsh.profile.bundles`:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{"profile": {"bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-plugin-taskflow"]}}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
bundle 里的 patch 只有一行 insert:
|
|
71
83
|
|
|
72
84
|
```yaml
|
|
73
85
|
- insert:
|
|
@@ -75,6 +87,8 @@ node install.mjs --profile web # 装到别的 profile
|
|
|
75
87
|
name: 'dsh-plugin-taskflow'
|
|
76
88
|
```
|
|
77
89
|
|
|
90
|
+
从源码手动安装(不想走 npm 时)也可以用 `install.mjs` 直接改 profile 的 `cordis.patch.yml`。
|
|
91
|
+
|
|
78
92
|
### 生效方式
|
|
79
93
|
|
|
80
94
|
| 改了什么 | 怎么生效 |
|
package/client/client.js
CHANGED
|
@@ -13,7 +13,7 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
const ID = 'dsh-plugin-taskflow'
|
|
14
14
|
// Bumped whenever the browser half changes: the host reports the live value
|
|
15
15
|
// in /taskflow/health, which is how a stale page is detected.
|
|
16
|
-
const CLIENT_BUILD = '
|
|
16
|
+
const CLIENT_BUILD = 'c98'
|
|
17
17
|
/**
|
|
18
18
|
* true = shadow DSH's own todo panel (id 'todo', priority -1)
|
|
19
19
|
* false = render beside it (unique id), so both lists are visible
|
|
@@ -206,13 +206,13 @@ window.__ModuleLoader__.load({
|
|
|
206
206
|
.tf-chevron { transition: transform .18s ease; }
|
|
207
207
|
.tf-inline-toggle[data-collapsed="true"] .tf-chevron { transform: rotate(-90deg); }
|
|
208
208
|
.tf-inline-toggle:hover { opacity: .8; }
|
|
209
|
-
.tf-inline-list { margin: 4px 0 0; padding: 0 0 0 1px; list-style: none; display: flex; flex-direction: column; gap:
|
|
209
|
+
.tf-inline-list { margin: 4px 0 0; padding: 0 0 0 1px; list-style: none; display: flex; flex-direction: column; gap: 4px; max-height: 172px; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; scrollbar-color: color-mix(in oklab, currentColor 16%, transparent) transparent; }
|
|
210
210
|
.tf-inline-list::-webkit-scrollbar { width: 6px; height: 0; }
|
|
211
211
|
.tf-inline-list::-webkit-scrollbar-track { background: transparent; }
|
|
212
212
|
.tf-inline-list::-webkit-scrollbar-thumb { background: color-mix(in oklab, currentColor 14%, transparent); border-radius: 3px; }
|
|
213
213
|
.tf-inline-list::-webkit-scrollbar-thumb:hover { background: color-mix(in oklab, currentColor 26%, transparent); }
|
|
214
214
|
.tf-item { list-style: none; }
|
|
215
|
-
.tf-item-btn { position: relative; display: flex; align-items: flex-start; gap: 9px; width: 100%; box-sizing: border-box; padding:
|
|
215
|
+
.tf-item-btn { position: relative; display: flex; align-items: flex-start; gap: 9px; width: 100%; box-sizing: border-box; padding: 6px 7px; border: 0; border-radius: 11px; background: transparent; color: inherit; font: inherit; font-size: 12.5px; line-height: 1.6; text-align: left; cursor: pointer; }
|
|
216
216
|
.tf-item-btn:hover, .tf-item-btn[data-active="true"] { background: color-mix(in oklab, currentColor 7%, transparent); }
|
|
217
217
|
.tf-item-btn[data-delegated="true"] { background: color-mix(in oklab, #8b7cff 10%, transparent); box-shadow: inset 0 0 0 1px color-mix(in oklab, #8b7cff 24%, transparent); }
|
|
218
218
|
.tf-inline-head[data-state="delegated"] .tf-inline-current { color: #a99bff; opacity: .95; }
|
|
@@ -404,12 +404,7 @@ window.__ModuleLoader__.load({
|
|
|
404
404
|
setActive(sessionId) {
|
|
405
405
|
if (this.activeSession === sessionId) return
|
|
406
406
|
this.activeSession = sessionId
|
|
407
|
-
if (sessionId)
|
|
408
|
-
void refresh(sessionId)
|
|
409
|
-
startStream(sessionId)
|
|
410
|
-
} else {
|
|
411
|
-
stopStream()
|
|
412
|
-
}
|
|
407
|
+
if (sessionId) void refresh(sessionId)
|
|
413
408
|
this.emit()
|
|
414
409
|
},
|
|
415
410
|
put(sessionId, value) {
|
|
@@ -439,74 +434,99 @@ window.__ModuleLoader__.load({
|
|
|
439
434
|
|
|
440
435
|
let hostAvailable = true
|
|
441
436
|
/** Live SSE subscription for the active session (null when unavailable). */
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
437
|
+
/** sessionId → { refs, source, healthy, failures, retry } — one push per open conversation. */
|
|
438
|
+
const streams = new Map()
|
|
439
|
+
|
|
440
|
+
function streamHealthy(sessionId) {
|
|
441
|
+
const entry = streams.get(sessionId)
|
|
442
|
+
return entry ? entry.healthy === true : false
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function closeStream(sessionId) {
|
|
446
|
+
const entry = streams.get(sessionId)
|
|
447
|
+
if (!entry) return
|
|
448
|
+
if (entry.retry !== null) window.clearTimeout(entry.retry)
|
|
449
|
+
if (entry.source) {
|
|
450
450
|
try {
|
|
451
|
-
|
|
451
|
+
entry.source.close()
|
|
452
452
|
} catch {
|
|
453
453
|
// already closed
|
|
454
454
|
}
|
|
455
455
|
}
|
|
456
|
-
|
|
457
|
-
streamSession = null
|
|
458
|
-
streamHealthy = false
|
|
456
|
+
streams.delete(sessionId)
|
|
459
457
|
}
|
|
460
458
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (typeof
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
459
|
+
function scheduleRetry(sessionId) {
|
|
460
|
+
const entry = streams.get(sessionId)
|
|
461
|
+
if (!entry || entry.failures > 6 || typeof window === 'undefined') return
|
|
462
|
+
if (entry.retry !== null) window.clearTimeout(entry.retry)
|
|
463
|
+
const delay = Math.min(30000, 2000 * 2 ** (entry.failures - 1))
|
|
464
|
+
entry.retry = window.setTimeout(() => {
|
|
465
|
+
if (!streams.has(sessionId)) return
|
|
466
|
+
entry.retry = null
|
|
467
|
+
openStream(sessionId)
|
|
468
|
+
}, delay)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Open the push connection for one session (idempotent). */
|
|
472
|
+
function openStream(sessionId) {
|
|
473
|
+
const entry = streams.get(sessionId)
|
|
474
|
+
if (!entry || entry.source || typeof EventSource === 'undefined') return
|
|
467
475
|
let source
|
|
468
476
|
try {
|
|
469
477
|
source = new EventSource(`/taskflow/events?session=${encodeURIComponent(sessionId)}`)
|
|
470
478
|
} catch {
|
|
471
|
-
|
|
479
|
+
entry.failures += 1
|
|
480
|
+
scheduleRetry(sessionId)
|
|
472
481
|
return
|
|
473
482
|
}
|
|
474
|
-
|
|
483
|
+
entry.source = source
|
|
475
484
|
source.addEventListener('state', (event) => {
|
|
476
485
|
try {
|
|
477
486
|
const data = JSON.parse(event.data)
|
|
478
487
|
if (!data || data.ok !== true) return
|
|
479
|
-
|
|
480
|
-
|
|
488
|
+
entry.healthy = true
|
|
489
|
+
entry.failures = 0
|
|
481
490
|
store.put(sessionId, { ...data, loaded: true, fetchedAt: Date.now() })
|
|
482
491
|
} catch {
|
|
483
492
|
// ignore malformed frame
|
|
484
493
|
}
|
|
485
494
|
})
|
|
486
495
|
source.addEventListener('error', () => {
|
|
487
|
-
|
|
496
|
+
entry.healthy = false
|
|
488
497
|
try {
|
|
489
498
|
source.close()
|
|
490
499
|
} catch {
|
|
491
|
-
//
|
|
492
|
-
}
|
|
493
|
-
if (stream === source) {
|
|
494
|
-
stream = null
|
|
495
|
-
streamSession = null
|
|
500
|
+
// already closed
|
|
496
501
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
// session switch retries from scratch.
|
|
501
|
-
if (streamFailures > 6) return
|
|
502
|
-
const delay = Math.min(30000, 2000 * 2 ** (streamFailures - 1))
|
|
503
|
-
streamRetry = window.setTimeout(() => {
|
|
504
|
-
streamRetry = null
|
|
505
|
-
const active = store.activeSession
|
|
506
|
-
if (active) startStream(active)
|
|
507
|
-
}, delay)
|
|
502
|
+
if (entry.source === source) entry.source = null
|
|
503
|
+
entry.failures += 1
|
|
504
|
+
scheduleRetry(sessionId)
|
|
508
505
|
})
|
|
509
506
|
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Watch one session while a surface is mounted. Every open conversation gets
|
|
510
|
+
* its own push + refresh, so switching sessions never starves the others.
|
|
511
|
+
*/
|
|
512
|
+
function watchSession(sessionId) {
|
|
513
|
+
if (typeof window === 'undefined' || !sessionId) return () => {}
|
|
514
|
+
let entry = streams.get(sessionId)
|
|
515
|
+
if (!entry) {
|
|
516
|
+
entry = { refs: 0, source: null, healthy: false, failures: 0, retry: null }
|
|
517
|
+
streams.set(sessionId, entry)
|
|
518
|
+
}
|
|
519
|
+
entry.refs += 1
|
|
520
|
+
void refresh(sessionId)
|
|
521
|
+
openStream(sessionId)
|
|
522
|
+
return () => {
|
|
523
|
+
const current = streams.get(sessionId)
|
|
524
|
+
if (!current) return
|
|
525
|
+
current.refs -= 1
|
|
526
|
+
if (current.refs <= 0) closeStream(sessionId)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
510
530
|
/** Opens the sidebar tab that lists every segment summary. */
|
|
511
531
|
let openSidebar = null
|
|
512
532
|
/** Which task's detail popover is open; module scope so remounts keep it. */
|
|
@@ -568,12 +588,17 @@ window.__ModuleLoader__.load({
|
|
|
568
588
|
const tick = () => {
|
|
569
589
|
if (poller === null) return
|
|
570
590
|
const hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden'
|
|
571
|
-
const
|
|
572
|
-
if (!hidden
|
|
573
|
-
|
|
574
|
-
|
|
591
|
+
const watched = [...streams.keys()]
|
|
592
|
+
if (!hidden) {
|
|
593
|
+
for (const sessionId of watched) void refresh(sessionId)
|
|
594
|
+
}
|
|
595
|
+
const busy = watched.some((sessionId) => {
|
|
596
|
+
const snapshot = store.peek(sessionId)
|
|
597
|
+
return snapshot ? (typeof snapshot.openTurn === 'number' || snapshot.pending === true) : false
|
|
598
|
+
})
|
|
599
|
+
const allPushed = watched.length > 0 && watched.every((sessionId) => streamHealthy(sessionId))
|
|
575
600
|
// A healthy push stream makes polling a pure safety net.
|
|
576
|
-
const next =
|
|
601
|
+
const next = allPushed ? 8000 : (busy ? POLL_MS_ACTIVE : POLL_MS_IDLE)
|
|
577
602
|
window.setTimeout(tick, next)
|
|
578
603
|
}
|
|
579
604
|
window.setTimeout(tick, POLL_MS_ACTIVE)
|
|
@@ -801,6 +826,7 @@ window.__ModuleLoader__.load({
|
|
|
801
826
|
|
|
802
827
|
React.useEffect(() => {
|
|
803
828
|
store.setActive(sessionId)
|
|
829
|
+
return watchSession(sessionId)
|
|
804
830
|
}, [sessionId])
|
|
805
831
|
|
|
806
832
|
// The projection changes exactly when the agent writes the list, so pull
|
|
@@ -888,7 +914,7 @@ window.__ModuleLoader__.load({
|
|
|
888
914
|
}
|
|
889
915
|
}, [total, collapsed])
|
|
890
916
|
|
|
891
|
-
useReportGeometry('dock', rootRef, `${collapsed ? 'collapsed' : 'open'} / ${total} tasks / ${segmentCount} segments / sse:${streamHealthy ? 'live' : 'poll'}${layoutNote ? ` / ${layoutNote}` : ''}`)
|
|
917
|
+
useReportGeometry('dock', rootRef, `${collapsed ? 'collapsed' : 'open'} / ${total} tasks / ${segmentCount} segments / sse:${streamHealthy(sessionId) ? 'live' : 'poll'}${layoutNote ? ` / ${layoutNote}` : ''}`)
|
|
892
918
|
|
|
893
919
|
|
|
894
920
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-taskflow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "TaskFlow for DeepSeek Harness — a liquid-glass task list above the composer that folds finished rounds into the transcript, with a one-line summary per task and per-turn segment digests.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|