dsh-composer-plus 0.7.0 → 0.9.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/README.md +7 -4
- package/client.js +141 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://github.com/furkancak1r/dsh-composer-plus/actions/workflows/test.yml)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
Codex-style quality-of-life features for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) desktop and web UI: a Changes panel, input history, a plan-mode shortcut, one-click Continue, queue drag-and-drop, session deeplinks, per-chat Auto Review grants and background notifications.
|
|
7
|
+
Codex-style quality-of-life features for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) desktop and web UI: a Changes panel, project status in the sidebar, input history, a plan-mode shortcut, one-click Continue, queue drag-and-drop, session deeplinks, per-chat Auto Review grants and background notifications.
|
|
8
8
|
|
|
9
9
|
Unofficial community plugin. No external network calls.
|
|
10
10
|
|
|
@@ -21,7 +21,7 @@ dsh plugin --profile web add dsh-composer-plus
|
|
|
21
21
|
|
|
22
22
|
Then restart dsh. (The CLI can't install into the desktop profile; the app manages it.)
|
|
23
23
|
|
|
24
|
-
> dsh's pnpm skips versions younger than 24 hours. Right after a release, add the version: `dsh-composer-plus@0.
|
|
24
|
+
> dsh's pnpm skips versions younger than 24 hours. Right after a release, add the version: `dsh-composer-plus@0.9.0`.
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
|
@@ -34,6 +34,8 @@ Then restart dsh. (The CLI can't install into the desktop profile; the app manag
|
|
|
34
34
|
| **Queue drag-and-drop** | Queued messages get a grip (⠿). Drag to reorder, or focus the grip and press `↑` / `↓`. `Esc` cancels. |
|
|
35
35
|
| **Copy deeplink** | Right-click a chat in the sidebar → **Copy deeplink**. Paste the `dsh-session:…` link into another chat to attach that chat as read-only context. |
|
|
36
36
|
| **Allow for this chat** | When Auto Review asks you, the approval card gets **Allow for this chat**: the same tool in the same chat is allowed from then on, with any arguments. A header chip lists the grants and revokes them. Grants survive a restart. |
|
|
37
|
+
| **Project status** | A project row in the sidebar shows the same spinner / waiting / done marker as its chats, so a collapsed project still tells you a chat inside is working or needs you. Hover it for details. |
|
|
38
|
+
| **Active projects first** | When a chat starts running or needs you, its project moves to the top of the sidebar with dsh's own row animation (the same move as dragging it there). Finished projects stay where they are; drag them anywhere as usual. |
|
|
37
39
|
| **Background notifications** | While dsh is in the background you get a notification when a turn finishes (first line of the reply), or when a chat needs an approval (tool name), an answer (the question) or a plan review. The Dock/taskbar badge counts waiting chats. Clicking opens that chat. |
|
|
38
40
|
|
|
39
41
|
All text — buttons, tooltips, notices, notifications and the Continue message — follows the language selected in dsh (English or Chinese, the two languages dsh ships). Switching the language in dsh settings updates it live.
|
|
@@ -62,14 +64,15 @@ Tested on dsh `0.2.0-rc.2` (desktop and web) and `0.2.1-alpha.2` (web). CI runs
|
|
|
62
64
|
## Troubleshooting
|
|
63
65
|
|
|
64
66
|
- **No notifications on macOS:** System Settings → Notifications → DeepSeek Harness → allow. Choose **Alerts** to keep them on screen.
|
|
65
|
-
- **▶, queue grips, Allow for this chat or right-click missing after a dsh update:** these attach to dsh's current HTML because dsh has no slot for them yet. If dsh changes that markup they hide themselves; nothing else breaks. Please open an [issue](https://github.com/furkancak1r/dsh-composer-plus/issues).
|
|
66
|
-
- **Install fails right after a release:** add the version (`dsh-composer-plus@0.
|
|
67
|
+
- **▶, queue grips, project status, Allow for this chat or right-click missing after a dsh update:** these attach to dsh's current HTML because dsh has no slot for them yet. If dsh changes that markup they hide themselves; nothing else breaks. Please open an [issue](https://github.com/furkancak1r/dsh-composer-plus/issues).
|
|
68
|
+
- **Install fails right after a release:** add the version (`dsh-composer-plus@0.9.0`), see the install note above.
|
|
67
69
|
|
|
68
70
|
## Limits
|
|
69
71
|
|
|
70
72
|
- dsh's queue has no `move` action, so a reorder rewrites the affected rows' text. If the agent picks up a row mid-move, the move is undone and nothing is lost. Messages with images or files can't be moved.
|
|
71
73
|
- A deeplink works inside chats; clicking it does not open the chat.
|
|
72
74
|
- A notification sent before a restart only brings dsh to the front when clicked.
|
|
75
|
+
- Active projects are moved with dsh's saved project order, so the new order is kept after a restart. Chats outside any project ("Ungrouped") can't be moved; dsh always lists them last.
|
|
73
76
|
- Built-in support is requested upstream: queue `move` in [#773](https://github.com/deepseek-ai/deepseek-harness/discussions/773), a Continue state in [#9269](https://github.com/deepseek-ai/deepseek-harness/discussions/9269).
|
|
74
77
|
|
|
75
78
|
## Development
|
package/client.js
CHANGED
|
@@ -584,6 +584,128 @@ window.__ModuleLoader__.load({
|
|
|
584
584
|
const start = lines.findIndex((l) => l.startsWith('@@'));
|
|
585
585
|
return start === -1 ? [] : lines.slice(start).filter((l, i, all) => !(i === all.length - 1 && l === ''));
|
|
586
586
|
}
|
|
587
|
+
/** One project's status from its visible chats, with dsh's session-row precedence: a pending question/approval/
|
|
588
|
+
* plan review wins, then running (a chat or its subagents), then unread completion. Archived and blank chats
|
|
589
|
+
* never show status in dsh, so they don't count here either. */
|
|
590
|
+
function projectStatus(sessionIds, list, statuses, archived) {
|
|
591
|
+
let pending; let running = 0; let done = 0;
|
|
592
|
+
for (const id of sessionIds) {
|
|
593
|
+
const summary = list.byId[id];
|
|
594
|
+
if (summary === undefined || summary.blank || summary.origin === 'subagent' || archived.has(id)) continue;
|
|
595
|
+
const status = statuses.get(id);
|
|
596
|
+
const kind = status?.pendingInteraction?.kind;
|
|
597
|
+
if (pending === undefined && (kind === 'approval' || kind === 'plan-review' || kind === 'question')) pending = kind;
|
|
598
|
+
const children = list.projectionsBySession?.[id]?.values?.subagentCatalog ?? [];
|
|
599
|
+
if ((status?.running ?? summary.running) === true
|
|
600
|
+
|| children.some((child) => (statuses.get(child.id)?.running ?? list.byId[child.id]?.running) === true)) running += 1;
|
|
601
|
+
if (status?.completionUnread === true) done += 1;
|
|
602
|
+
}
|
|
603
|
+
if (pending !== undefined) return { state: 'warning', pending, running };
|
|
604
|
+
if (running > 0) return { state: 'ongoing', running };
|
|
605
|
+
if (done > 0) return { state: 'done' };
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
/** Moves that bring active projects (running or waiting for the user) to the top, keeping their relative order:
|
|
609
|
+
* each active project that sits below an idle one goes before the first idle project. */
|
|
610
|
+
function activeFirstMoves(items, active) {
|
|
611
|
+
const firstIdle = items.findIndex((item) => !active.has(item.workspaceId));
|
|
612
|
+
if (firstIdle === -1) return [];
|
|
613
|
+
const anchor = items[firstIdle].workspaceId;
|
|
614
|
+
return items.slice(firstIdle + 1).filter((item) => active.has(item.workspaceId)).map((item) => [item.workspaceId, anchor]);
|
|
615
|
+
}
|
|
616
|
+
/** Keeps active projects at the top of the sidebar with dsh's own reorder (the same move as dragging a project,
|
|
617
|
+
* so dsh animates the rows). Projects are only ever moved up; finished ones stay where they are. */
|
|
618
|
+
function watchActiveProjects(workspaces, sessionsList, status) {
|
|
619
|
+
let moving = false; let again = false;
|
|
620
|
+
const run = async () => {
|
|
621
|
+
if (moving) { again = true; return; }
|
|
622
|
+
moving = true;
|
|
623
|
+
try {
|
|
624
|
+
do {
|
|
625
|
+
again = false;
|
|
626
|
+
const snapshot = workspaces.list.getSnapshot();
|
|
627
|
+
const list = sessionsList.getSnapshot();
|
|
628
|
+
const statuses = status.getSnapshot();
|
|
629
|
+
if (snapshot?.items === undefined || list === undefined || statuses === undefined) break;
|
|
630
|
+
const archived = new Set(snapshot.archivedSessionIds ?? []);
|
|
631
|
+
const active = new Set(snapshot.items.filter((item) => {
|
|
632
|
+
const state = projectStatus(item.sessionIds, list, statuses, archived);
|
|
633
|
+
return state !== null && state.state !== 'done';
|
|
634
|
+
}).map((item) => item.workspaceId));
|
|
635
|
+
for (const [id, before] of activeFirstMoves(snapshot.items, active)) await workspaces.insertBefore(id, before);
|
|
636
|
+
} while (again);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
console.warn('dsh-composer-plus: project reorder rejected', error);
|
|
639
|
+
} finally {
|
|
640
|
+
moving = false;
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
const schedule = () => { queueMicrotask(run); };
|
|
644
|
+
const stops = [workspaces.list.subscribe(schedule), sessionsList.subscribe(schedule), status.subscribe(schedule)];
|
|
645
|
+
schedule();
|
|
646
|
+
return () => { for (const stop of stops) stop(); };
|
|
647
|
+
}
|
|
648
|
+
const PROJECT_STATUS = 'data-composer-plus-project-status';
|
|
649
|
+
/** Shows the session status (spinner / waiting / done dot) on each project row, so a collapsed project still says
|
|
650
|
+
* that one of its chats is working. dsh has no project-row slot.
|
|
651
|
+
* shortcut: portals into rows found by `data-row-key="workspace:<id>"`; if dsh changes that markup the marker just
|
|
652
|
+
* stops appearing. Upgrade when dsh adds a project-row slot. */
|
|
653
|
+
function ProjectStatusMarkers({ useWorkspaces, useSessions, useSessionStatus, t }) {
|
|
654
|
+
const workspaces = useWorkspaces((state) => state);
|
|
655
|
+
const list = useSessions((state) => state);
|
|
656
|
+
const statuses = useSessionStatus((state) => state);
|
|
657
|
+
const byKey = React.useMemo(() => {
|
|
658
|
+
const archived = new Set(workspaces?.archivedSessionIds ?? []);
|
|
659
|
+
const out = new Map();
|
|
660
|
+
for (const workspace of workspaces?.items ?? []) {
|
|
661
|
+
const status = list === undefined || statuses === undefined ? null : projectStatus(workspace.sessionIds, list, statuses, archived);
|
|
662
|
+
if (status !== null) out.set(workspace.workspaceId, status);
|
|
663
|
+
}
|
|
664
|
+
return out;
|
|
665
|
+
}, [workspaces, list, statuses]);
|
|
666
|
+
const [hosts, setHosts] = React.useState(new Map());
|
|
667
|
+
React.useLayoutEffect(() => {
|
|
668
|
+
if (createPortal === undefined) return undefined;
|
|
669
|
+
const scan = () => {
|
|
670
|
+
const next = new Map();
|
|
671
|
+
for (const row of document.querySelectorAll('[data-row-key^="workspace:"]')) {
|
|
672
|
+
const key = row.getAttribute('data-row-key').slice('workspace:'.length);
|
|
673
|
+
let host = row.querySelector(`:scope > [${PROJECT_STATUS}]`);
|
|
674
|
+
if (host === null) {
|
|
675
|
+
host = document.createElement('span');
|
|
676
|
+
host.setAttribute(PROJECT_STATUS, '');
|
|
677
|
+
host.style.cssText = 'display:inline-flex;align-items:center;gap:4px;flex:none;margin-left:4px';
|
|
678
|
+
// Before the hover actions, so the marker sits where a session row shows its status.
|
|
679
|
+
const actions = [...row.children].find((child) => child.querySelector('button') !== null);
|
|
680
|
+
row.insertBefore(host, actions ?? null);
|
|
681
|
+
}
|
|
682
|
+
next.set(key, host);
|
|
683
|
+
}
|
|
684
|
+
setHosts((prev) => (prev.size === next.size && [...next].every(([k, h]) => prev.get(k) === h) ? prev : next));
|
|
685
|
+
};
|
|
686
|
+
scan();
|
|
687
|
+
const observer = new MutationObserver(scan);
|
|
688
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
689
|
+
return () => {
|
|
690
|
+
observer.disconnect();
|
|
691
|
+
for (const host of document.querySelectorAll(`[${PROJECT_STATUS}]`)) host.remove();
|
|
692
|
+
};
|
|
693
|
+
}, []);
|
|
694
|
+
if (createPortal === undefined || primitives.StateDot === undefined) return null;
|
|
695
|
+
const portals = [];
|
|
696
|
+
for (const [key, host] of hosts) {
|
|
697
|
+
const status = byKey.get(key);
|
|
698
|
+
if (status === undefined || !host.isConnected) continue;
|
|
699
|
+
const label = status.state === 'warning' ? t(`project.${status.pending}`)
|
|
700
|
+
: status.state === 'ongoing' ? t('project.running', { count: String(status.running) }) : t('project.done');
|
|
701
|
+
portals.push(createPortal(React.createElement('span', { title: label, style: { display: 'inline-flex', alignItems: 'center', gap: 4 } },
|
|
702
|
+
React.createElement(primitives.StateDot, { state: status.state }),
|
|
703
|
+
React.createElement('span', { style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)', whiteSpace: 'nowrap' } }, label)),
|
|
704
|
+
host, key));
|
|
705
|
+
}
|
|
706
|
+
return React.createElement(React.Fragment, null, ...portals);
|
|
707
|
+
}
|
|
708
|
+
|
|
587
709
|
function ChangesDiff({ sessionId, mode, file, t }) {
|
|
588
710
|
const [state, setState] = React.useState({ status: 'loading' });
|
|
589
711
|
React.useEffect(() => {
|
|
@@ -719,6 +841,11 @@ window.__ModuleLoader__.load({
|
|
|
719
841
|
}, ContinueButton));
|
|
720
842
|
if (ctx.locale !== undefined) ctx.effect(() => ctx.locale.register(LOCALE_NS, {
|
|
721
843
|
en: {
|
|
844
|
+
'project.running': '{count} chat(s) running',
|
|
845
|
+
'project.approval': 'A chat is waiting for approval',
|
|
846
|
+
'project.plan-review': 'A plan is waiting for review',
|
|
847
|
+
'project.question': 'A chat is waiting for your answer',
|
|
848
|
+
'project.done': 'A chat finished',
|
|
722
849
|
'continue.label': 'Continue',
|
|
723
850
|
'continue.message': 'continue',
|
|
724
851
|
'queue.drag': 'Drag to reorder (or ↑/↓)',
|
|
@@ -759,6 +886,11 @@ window.__ModuleLoader__.load({
|
|
|
759
886
|
'changes.refresh': 'Refresh',
|
|
760
887
|
},
|
|
761
888
|
zh: {
|
|
889
|
+
'project.running': '{count} 个对话进行中',
|
|
890
|
+
'project.approval': '有对话等待审批',
|
|
891
|
+
'project.plan-review': '有计划等待审阅',
|
|
892
|
+
'project.question': '有对话等待回答',
|
|
893
|
+
'project.done': '有对话已完成',
|
|
762
894
|
'continue.label': '继续',
|
|
763
895
|
'continue.message': '继续',
|
|
764
896
|
'queue.drag': '拖动以排序(或按 ↑/↓)',
|
|
@@ -799,6 +931,11 @@ window.__ModuleLoader__.load({
|
|
|
799
931
|
'changes.refresh': '刷新',
|
|
800
932
|
},
|
|
801
933
|
}), 'dsh-composer-plus: dictionaries');
|
|
934
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
935
|
+
name: 'shell.overlay',
|
|
936
|
+
id: 'project-status',
|
|
937
|
+
locale: LOCALE_NS,
|
|
938
|
+
}, ProjectStatusMarkers));
|
|
802
939
|
ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
|
|
803
940
|
name: 'conversation.session.header.utilities',
|
|
804
941
|
id: 'chat-tool-grants',
|
|
@@ -812,6 +949,10 @@ window.__ModuleLoader__.load({
|
|
|
812
949
|
// First use asks the OS once; a denied permission stays denied (re-enable it in System Settings).
|
|
813
950
|
if (typeof Notification !== 'undefined' && Notification.permission === 'default') Notification.requestPermission().catch(() => {});
|
|
814
951
|
});
|
|
952
|
+
if (ctx.inject !== undefined) ctx.inject(['workspaces', 'uiSession', 'sessions'], (scope) => {
|
|
953
|
+
scope.effect(() => watchActiveProjects(scope.workspaces, scope.sessions.list, scope.uiSession.sessionStatus),
|
|
954
|
+
'dsh-composer-plus: active projects first');
|
|
955
|
+
});
|
|
815
956
|
if (ctx.inject !== undefined) ctx.inject(['sidebarRight', 'sidebarRightTabs'], (scope) => {
|
|
816
957
|
const t = ctx.locale.bind(LOCALE_NS);
|
|
817
958
|
scope.effect(() => scope.sidebarRightTabs.register({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-composer-plus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "DeepSeek Harness helpers: Codex-style Changes panel, input history, Shift+Tab /plan, one-click Continue, queue drag-and-drop, Copy deeplink, Auto Review allow-for-chat, background notifications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"LICENSE"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"test": "node test/continue.test.mjs && node test/queue-drag.test.mjs && node test/plan-toggle.test.mjs && node test/deeplink.test.mjs && node test/chat-grant.test.mjs && node test/notify.test.mjs && node test/changes.test.mjs"
|
|
28
|
+
"test": "node test/continue.test.mjs && node test/queue-drag.test.mjs && node test/plan-toggle.test.mjs && node test/deeplink.test.mjs && node test/chat-grant.test.mjs && node test/notify.test.mjs && node test/changes.test.mjs && node test/project-status.test.mjs"
|
|
29
29
|
},
|
|
30
30
|
"dsh": {
|
|
31
31
|
"bundle": {
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"@deepseek-ai/dsh-client-locale",
|
|
39
39
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
40
40
|
"@deepseek-ai/dsh-client-ui-workspace",
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
42
|
+
"@deepseek-ai/dsh-api-workspace-controller",
|
|
41
43
|
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
|
42
44
|
"@deepseek-ai/dsh-client-ui-session",
|
|
43
45
|
"@deepseek-ai/dsh-api-remotes",
|