dsh-composer-plus 0.6.2 → 0.8.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 -6
- package/client.js +122 -10
- package/package.json +3 -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.8.0`.
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
|
@@ -30,13 +30,14 @@ Then restart dsh. (The CLI can't install into the desktop profile; the app manag
|
|
|
30
30
|
| **Changes panel** | Right sidebar → `+` → **Changes**. Pick **Uncommitted**, **Unstaged**, **Staged** or **Branch** to see changed files with `+/-` counts; click a file for its diff. **Last Turn** opens dsh's own review of the latest turn. |
|
|
31
31
|
| **Input history** | In an empty composer, press `↑` / `↓` to walk through messages you sent, like a terminal. |
|
|
32
32
|
| **Plan mode shortcut** | `Shift+Tab` adds `/plan ` to the start of the draft (press again to remove it). If the chat is already in plan mode, `Shift+Tab` leaves it. |
|
|
33
|
-
| **One-click Continue** | When a turn has stopped and the composer is empty, Send turns into **▶**. One click sends `continue
|
|
33
|
+
| **One-click Continue** | When a turn has stopped and the composer is empty, Send turns into **▶**. One click sends “continue” in dsh's language (`continue` / `继续`). |
|
|
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. |
|
|
37
38
|
| **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
39
|
|
|
39
|
-
|
|
40
|
+
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.
|
|
40
41
|
|
|
41
42
|
## Keyboard shortcuts
|
|
42
43
|
|
|
@@ -62,8 +63,8 @@ Tested on dsh `0.2.0-rc.2` (desktop and web) and `0.2.1-alpha.2` (web). CI runs
|
|
|
62
63
|
## Troubleshooting
|
|
63
64
|
|
|
64
65
|
- **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.
|
|
66
|
+
- **▶, 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).
|
|
67
|
+
- **Install fails right after a release:** add the version (`dsh-composer-plus@0.8.0`), see the install note above.
|
|
67
68
|
|
|
68
69
|
## Limits
|
|
69
70
|
|
package/client.js
CHANGED
|
@@ -196,7 +196,6 @@ window.__ModuleLoader__.load({
|
|
|
196
196
|
return React.createElement('span', { ref: anchor, hidden: true });
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
const CONTINUE_TEXT = 'continue';
|
|
200
199
|
/** Dictionary namespace for this plugin's menu copy; dsh ships `en` and `zh`. */
|
|
201
200
|
const LOCALE_NS = 'dsh-composer-plus';
|
|
202
201
|
let createPortal;
|
|
@@ -212,7 +211,7 @@ window.__ModuleLoader__.load({
|
|
|
212
211
|
* Send turns into a play button that submits "continue". Send is not a slot, so the play button is
|
|
213
212
|
* portaled into Send's row with Send's own class and Send is hidden meanwhile.
|
|
214
213
|
* Hidden on a blank chat, while running, in subagent views, or as soon as the user types or attaches. */
|
|
215
|
-
function ContinueButton({ useSession, useInput, inputActions }) {
|
|
214
|
+
function ContinueButton({ useSession, useInput, inputActions, t }) {
|
|
216
215
|
const show = useSession((s) => s !== undefined && !s.blank && !s.running && s.subagent === null && !s.removed);
|
|
217
216
|
const idle = useInput((s) => s !== undefined && s.phase === 'plain' && s.draft.trim() === '' && s.attachmentIds.length === 0);
|
|
218
217
|
const visible = show && idle && inputActions !== undefined;
|
|
@@ -228,15 +227,17 @@ window.__ModuleLoader__.load({
|
|
|
228
227
|
const anchor = React.createElement('span', { ref: anchorRef, hidden: true, 'aria-hidden': true });
|
|
229
228
|
if (!visible || host === null) return anchor;
|
|
230
229
|
const onClick = () => {
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
// The sent message follows dsh's language, so the model reads it in the user's own language.
|
|
231
|
+
const text = t('continue.message');
|
|
232
|
+
if (React.flushSync !== undefined) React.flushSync(() => inputActions.setDraft(text));
|
|
233
|
+
else inputActions.setDraft(text);
|
|
233
234
|
inputActions.submit();
|
|
234
235
|
};
|
|
235
236
|
const button = React.createElement('button', {
|
|
236
237
|
type: 'button',
|
|
237
238
|
className: host.className,
|
|
238
|
-
title: '
|
|
239
|
-
'aria-label': '
|
|
239
|
+
title: t('continue.label'),
|
|
240
|
+
'aria-label': t('continue.label'),
|
|
240
241
|
onMouseDown: (event) => event.preventDefault(),
|
|
241
242
|
onClick,
|
|
242
243
|
}, React.createElement('svg', { viewBox: '0 0 16 16', width: 16, height: 16, 'aria-hidden': true },
|
|
@@ -255,7 +256,7 @@ window.__ModuleLoader__.load({
|
|
|
255
256
|
* so nothing is lost or duplicated.
|
|
256
257
|
* shortcut: grips are portaled into QueueDock rows found in the DOM; if a dsh update changes that markup the
|
|
257
258
|
* grips just stop appearing — upgrade when dsh exposes a queue row slot or a native move action. */
|
|
258
|
-
function QueueReorder({ useSession, useProjection, updateQueue, notify }) {
|
|
259
|
+
function QueueReorder({ useSession, useProjection, updateQueue, notify, t }) {
|
|
259
260
|
const inbox = useProjection('inbox');
|
|
260
261
|
const pending = useSession((s) => (s === undefined ? undefined : s.pendingSubmissions));
|
|
261
262
|
const mutable = useSession((s) => s !== undefined && (s.subagent === null || s.subagent.address.mode === 'continuable'));
|
|
@@ -320,7 +321,7 @@ window.__ModuleLoader__.load({
|
|
|
320
321
|
}
|
|
321
322
|
} catch {
|
|
322
323
|
for (const change of done.reverse()) await edit(change.row, change.prev).catch(() => undefined);
|
|
323
|
-
notify('error', '
|
|
324
|
+
notify('error', t('queue.failed'));
|
|
324
325
|
} finally {
|
|
325
326
|
setBusy(false);
|
|
326
327
|
}
|
|
@@ -375,8 +376,8 @@ window.__ModuleLoader__.load({
|
|
|
375
376
|
const draggable = texts[index] !== null && !busy;
|
|
376
377
|
const grip = React.createElement('button', {
|
|
377
378
|
type: 'button',
|
|
378
|
-
title: draggable ? '
|
|
379
|
-
'aria-label': '
|
|
379
|
+
title: draggable ? t('queue.drag') : t('queue.locked'),
|
|
380
|
+
'aria-label': t('queue.grip'),
|
|
380
381
|
disabled: !draggable,
|
|
381
382
|
onPointerDown: (event) => startDrag(index, event),
|
|
382
383
|
onKeyDown: (event) => {
|
|
@@ -583,6 +584,88 @@ window.__ModuleLoader__.load({
|
|
|
583
584
|
const start = lines.findIndex((l) => l.startsWith('@@'));
|
|
584
585
|
return start === -1 ? [] : lines.slice(start).filter((l, i, all) => !(i === all.length - 1 && l === ''));
|
|
585
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
|
+
const PROJECT_STATUS = 'data-composer-plus-project-status';
|
|
609
|
+
/** Shows the session status (spinner / waiting / done dot) on each project row, so a collapsed project still says
|
|
610
|
+
* that one of its chats is working. dsh has no project-row slot.
|
|
611
|
+
* shortcut: portals into rows found by `data-row-key="workspace:<id>"`; if dsh changes that markup the marker just
|
|
612
|
+
* stops appearing. Upgrade when dsh adds a project-row slot. */
|
|
613
|
+
function ProjectStatusMarkers({ useWorkspaces, useSessions, useSessionStatus, t }) {
|
|
614
|
+
const workspaces = useWorkspaces((state) => state);
|
|
615
|
+
const list = useSessions((state) => state);
|
|
616
|
+
const statuses = useSessionStatus((state) => state);
|
|
617
|
+
const byKey = React.useMemo(() => {
|
|
618
|
+
const archived = new Set(workspaces?.archivedSessionIds ?? []);
|
|
619
|
+
const out = new Map();
|
|
620
|
+
for (const workspace of workspaces?.items ?? []) {
|
|
621
|
+
const status = list === undefined || statuses === undefined ? null : projectStatus(workspace.sessionIds, list, statuses, archived);
|
|
622
|
+
if (status !== null) out.set(workspace.workspaceId, status);
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
}, [workspaces, list, statuses]);
|
|
626
|
+
const [hosts, setHosts] = React.useState(new Map());
|
|
627
|
+
React.useLayoutEffect(() => {
|
|
628
|
+
if (createPortal === undefined) return undefined;
|
|
629
|
+
const scan = () => {
|
|
630
|
+
const next = new Map();
|
|
631
|
+
for (const row of document.querySelectorAll('[data-row-key^="workspace:"]')) {
|
|
632
|
+
const key = row.getAttribute('data-row-key').slice('workspace:'.length);
|
|
633
|
+
let host = row.querySelector(`:scope > [${PROJECT_STATUS}]`);
|
|
634
|
+
if (host === null) {
|
|
635
|
+
host = document.createElement('span');
|
|
636
|
+
host.setAttribute(PROJECT_STATUS, '');
|
|
637
|
+
host.style.cssText = 'display:inline-flex;align-items:center;gap:4px;flex:none;margin-left:4px';
|
|
638
|
+
// Before the hover actions, so the marker sits where a session row shows its status.
|
|
639
|
+
const actions = [...row.children].find((child) => child.querySelector('button') !== null);
|
|
640
|
+
row.insertBefore(host, actions ?? null);
|
|
641
|
+
}
|
|
642
|
+
next.set(key, host);
|
|
643
|
+
}
|
|
644
|
+
setHosts((prev) => (prev.size === next.size && [...next].every(([k, h]) => prev.get(k) === h) ? prev : next));
|
|
645
|
+
};
|
|
646
|
+
scan();
|
|
647
|
+
const observer = new MutationObserver(scan);
|
|
648
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
649
|
+
return () => {
|
|
650
|
+
observer.disconnect();
|
|
651
|
+
for (const host of document.querySelectorAll(`[${PROJECT_STATUS}]`)) host.remove();
|
|
652
|
+
};
|
|
653
|
+
}, []);
|
|
654
|
+
if (createPortal === undefined || primitives.StateDot === undefined) return null;
|
|
655
|
+
const portals = [];
|
|
656
|
+
for (const [key, host] of hosts) {
|
|
657
|
+
const status = byKey.get(key);
|
|
658
|
+
if (status === undefined || !host.isConnected) continue;
|
|
659
|
+
const label = status.state === 'warning' ? t(`project.${status.pending}`)
|
|
660
|
+
: status.state === 'ongoing' ? t('project.running', { count: String(status.running) }) : t('project.done');
|
|
661
|
+
portals.push(createPortal(React.createElement('span', { title: label, style: { display: 'inline-flex', alignItems: 'center', gap: 4 } },
|
|
662
|
+
React.createElement(primitives.StateDot, { state: status.state }),
|
|
663
|
+
React.createElement('span', { style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)', whiteSpace: 'nowrap' } }, label)),
|
|
664
|
+
host, key));
|
|
665
|
+
}
|
|
666
|
+
return React.createElement(React.Fragment, null, ...portals);
|
|
667
|
+
}
|
|
668
|
+
|
|
586
669
|
function ChangesDiff({ sessionId, mode, file, t }) {
|
|
587
670
|
const [state, setState] = React.useState({ status: 'loading' });
|
|
588
671
|
React.useEffect(() => {
|
|
@@ -683,6 +766,7 @@ window.__ModuleLoader__.load({
|
|
|
683
766
|
name: 'conversation.input.dock',
|
|
684
767
|
id: 'queue-reorder',
|
|
685
768
|
order: 21,
|
|
769
|
+
locale: LOCALE_NS,
|
|
686
770
|
inject: (sessionId) => {
|
|
687
771
|
const actx = ctx.sessions.scope(sessionId);
|
|
688
772
|
const conversation = actx === undefined ? undefined : actx.get('conversation');
|
|
@@ -713,9 +797,21 @@ window.__ModuleLoader__.load({
|
|
|
713
797
|
name: 'conversation.input.right',
|
|
714
798
|
id: 'composer-continue',
|
|
715
799
|
order: 1000,
|
|
800
|
+
locale: LOCALE_NS,
|
|
716
801
|
}, ContinueButton));
|
|
717
802
|
if (ctx.locale !== undefined) ctx.effect(() => ctx.locale.register(LOCALE_NS, {
|
|
718
803
|
en: {
|
|
804
|
+
'project.running': '{count} chat(s) running',
|
|
805
|
+
'project.approval': 'A chat is waiting for approval',
|
|
806
|
+
'project.plan-review': 'A plan is waiting for review',
|
|
807
|
+
'project.question': 'A chat is waiting for your answer',
|
|
808
|
+
'project.done': 'A chat finished',
|
|
809
|
+
'continue.label': 'Continue',
|
|
810
|
+
'continue.message': 'continue',
|
|
811
|
+
'queue.drag': 'Drag to reorder (or ↑/↓)',
|
|
812
|
+
'queue.locked': 'Messages with images/files cannot be moved',
|
|
813
|
+
'queue.grip': 'Reorder queued message',
|
|
814
|
+
'queue.failed': 'Reorder failed: a message may have already started sending.',
|
|
719
815
|
copyDeeplink: 'Copy deeplink',
|
|
720
816
|
allowChat: 'Allow for this chat',
|
|
721
817
|
allowChatTitle: 'Allow {toolName} without asking for the rest of this chat, with any arguments. If the tool can write, that includes any write.',
|
|
@@ -750,6 +846,17 @@ window.__ModuleLoader__.load({
|
|
|
750
846
|
'changes.refresh': 'Refresh',
|
|
751
847
|
},
|
|
752
848
|
zh: {
|
|
849
|
+
'project.running': '{count} 个对话进行中',
|
|
850
|
+
'project.approval': '有对话等待审批',
|
|
851
|
+
'project.plan-review': '有计划等待审阅',
|
|
852
|
+
'project.question': '有对话等待回答',
|
|
853
|
+
'project.done': '有对话已完成',
|
|
854
|
+
'continue.label': '继续',
|
|
855
|
+
'continue.message': '继续',
|
|
856
|
+
'queue.drag': '拖动以排序(或按 ↑/↓)',
|
|
857
|
+
'queue.locked': '含图片或文件的消息无法移动',
|
|
858
|
+
'queue.grip': '调整排队消息顺序',
|
|
859
|
+
'queue.failed': '排序失败:某条消息可能已开始发送。',
|
|
753
860
|
copyDeeplink: '复制深层链接',
|
|
754
861
|
allowChat: '本对话内允许',
|
|
755
862
|
allowChatTitle: '本对话剩余时间内,{toolName} 的任何参数调用都无需再询问。若该工具可写入,也包括任何写入操作。',
|
|
@@ -784,6 +891,11 @@ window.__ModuleLoader__.load({
|
|
|
784
891
|
'changes.refresh': '刷新',
|
|
785
892
|
},
|
|
786
893
|
}), 'dsh-composer-plus: dictionaries');
|
|
894
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
895
|
+
name: 'shell.overlay',
|
|
896
|
+
id: 'project-status',
|
|
897
|
+
locale: LOCALE_NS,
|
|
898
|
+
}, ProjectStatusMarkers));
|
|
787
899
|
ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
|
|
788
900
|
name: 'conversation.session.header.utilities',
|
|
789
901
|
id: 'chat-tool-grants',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-composer-plus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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,7 @@
|
|
|
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",
|
|
41
42
|
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
|
42
43
|
"@deepseek-ai/dsh-client-ui-session",
|
|
43
44
|
"@deepseek-ai/dsh-api-remotes",
|