dsh-composer-plus 1.7.0 → 1.7.1
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 +4 -4
- package/client.js +23 -11
- package/index.js +16 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,14 +25,14 @@ dsh plugin --profile web add dsh-composer-plus
|
|
|
25
25
|
|
|
26
26
|
Then restart dsh. (The CLI can't install into the desktop profile; the app manages it.)
|
|
27
27
|
|
|
28
|
-
> dsh's pnpm skips versions younger than 24 hours. Right after a release, add the version: `dsh-composer-plus@1.7.
|
|
28
|
+
> dsh's pnpm skips versions younger than 24 hours. Right after a release, add the version: `dsh-composer-plus@1.7.1`.
|
|
29
29
|
|
|
30
30
|
## Features
|
|
31
31
|
|
|
32
32
|
| Feature | How to use it | Screenshot |
|
|
33
33
|
|---|---|---|
|
|
34
34
|
| **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. | <a href="assets/features/changesPanel.webp"><img src="assets/features/changesPanel.webp" width="160" alt="Changes panel screenshot"></a> |
|
|
35
|
-
| **Feature ideas** | Right sidebar → `+` → **Feature ideas** → **Suggest features**. The chat's own model reads a short summary of the chat's project (top-level files, the README and manifest heads, the last 15 commits) and suggests up to 8 features, shown as a stack of cards. Swipe a card right (or click **✓**, or press
|
|
35
|
+
| **Feature ideas** | Right sidebar → `+` → **Feature ideas** → **Suggest features**. The chat's own model reads a short summary of the chat's project (top-level files, the README and manifest heads, the last 15 commits) and suggests up to 8 features, shown as a stack of cards in dsh's UI language. Swipe a card right (or click **✓**, or press `→`; the card flies off to its side in each case) to start a new chat in the same project: it starts in plan mode with the idea as the request, so nothing changes before you approve the plan. Swipe left (or **✕**, or `←`) to skip; **Undo** (or `Z`) brings a skipped card back. **More ideas** adds cards. The model runs only when you click. | <a href="assets/features/featureIdeas.webp"><img src="assets/features/featureIdeas.webp" width="160" alt="Feature ideas screenshot"></a> |
|
|
36
36
|
| **Input history** | In an empty composer, press `↑` / `↓` to walk through messages you sent, like a terminal. | <a href="assets/features/inputHistory.webp"><img src="assets/features/inputHistory.webp" width="160" alt="Input history screenshot"></a> |
|
|
37
37
|
| **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. | <a href="assets/features/planToggle.webp"><img src="assets/features/planToggle.webp" width="160" alt="Plan mode shortcut screenshot"></a> |
|
|
38
38
|
| **Keep composer focus** | After you send a `/` command or skill, the cursor stays in the composer, and you can type again at once. This also works for keyboard macros that type a command and press `Enter`. dsh makes the composer read-only while it resolves the command, and the browser then removes the focus. This feature puts the focus back, unless a menu, a dialog or another field has it. | <a href="assets/features/keepFocus.webp"><img src="assets/features/keepFocus.webp" width="160" alt="Keep composer focus screenshot"></a> |
|
|
@@ -109,12 +109,12 @@ Tested on dsh `0.2.0-rc.2` (desktop and web) and `0.2.1-rc.2` (web). CI runs the
|
|
|
109
109
|
- **Mobile connect says the gateway is not running:** "no IPv4 address on en0" means this Mac has no Wi-Fi/Ethernet address; "EADDRINUSE" means another dsh (or app) uses port 19390 or 19391. Only one dsh can run Mobile connect at a time.
|
|
110
110
|
- **Mobile connect or Resume after restart says "Restart dsh":** the plugin was updated while dsh was running; the new Host half loads after a restart.
|
|
111
111
|
- **A chat did not continue after a restart:** Resume after restart was off when dsh quit, you stopped that turn yourself, the turn had ended (done or failed), the chat was archived, or more than 20 chats were running (the newest 20 continue).
|
|
112
|
-
- **Install fails right after a release:** add the version (`dsh-composer-plus@1.7.
|
|
112
|
+
- **Install fails right after a release:** add the version (`dsh-composer-plus@1.7.1`), see the install note above.
|
|
113
113
|
|
|
114
114
|
## Limits
|
|
115
115
|
|
|
116
116
|
- Message-free Continue relies on dsh’s `agent/pre-step` → `agent/request` lifecycle. Recheck compatibility when upgrading dsh.
|
|
117
|
-
- **Feature ideas** keeps the cards in memory for each chat until dsh or the page reloads; then **Suggest features** asks the model again. The model sees only the short project summary, not the code, so an idea can repeat something the project already has: skip it. The new chat runs in plan mode only when the profile has dsh's `/plan` command; otherwise the idea goes as an ordinary first message.
|
|
117
|
+
- **Feature ideas** keeps the cards in memory for each chat until dsh or the page reloads; then **Suggest features** asks the model again. The cards come in the UI language that dsh has when you click; after you change the language, cards already on the stack keep theirs and **More ideas** uses the new one. The model sees only the short project summary, not the code, so an idea can repeat something the project already has: skip it. The new chat runs in plan mode only when the profile has dsh's `/plan` command; otherwise the idea goes as an ordinary first message.
|
|
118
118
|
- **Resume after restart** continues the turn; it does not replay it. A tool call that was running at the quit ends with dsh's “outcome unknown” result, and the model is told not to repeat finished work, but a command that is not safe to run twice can run again. Subagents, approvals and questions that were open are not restored: their parent chat continues and asks again. At most 20 chats continue at one start.
|
|
119
119
|
|
|
120
120
|
- 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.
|
package/client.js
CHANGED
|
@@ -1303,11 +1303,13 @@ body:has(${OPEN_PROJECT}) [${COLLAPSE_HOST}] { display: inline-flex; }
|
|
|
1303
1303
|
const deckChanged = (deck, patch) => { Object.assign(deck, patch); for (const fn of [...deck.listeners]) fn(); };
|
|
1304
1304
|
/** One network action per deck: while ideas load or a chat starts, other loads and swipes wait. */
|
|
1305
1305
|
const deckBusy = (deck) => deck.status === 'loading' || deck.status === 'starting';
|
|
1306
|
-
|
|
1306
|
+
/** `locale` is dsh's UI language at the click: the model writes the cards in it (ponytail: cards already on the deck keep
|
|
1307
|
+
* their language when the UI language changes; the next load uses the new one). */
|
|
1308
|
+
async function suggestIdeas(deck, sessionId, locale) {
|
|
1307
1309
|
if (deckBusy(deck)) return;
|
|
1308
1310
|
deckChanged(deck, { status: 'loading', error: undefined });
|
|
1309
1311
|
try {
|
|
1310
|
-
const response = await fetch(IDEAS_ROUTE, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId }) });
|
|
1312
|
+
const response = await fetch(IDEAS_ROUTE, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId, ...(locale ? { locale } : {}) }) });
|
|
1311
1313
|
const data = await response.json().catch(() => ({}));
|
|
1312
1314
|
if (response.status === 404) throw new Error('restart');
|
|
1313
1315
|
if (!response.ok) throw new Error(data?.error ?? `HTTP ${response.status}`);
|
|
@@ -1359,7 +1361,8 @@ body:has(${OPEN_PROJECT}) [${COLLAPSE_HOST}] { display: inline-flex; }
|
|
|
1359
1361
|
.cpi-list{padding:4px 12px 16px}
|
|
1360
1362
|
.cpi-list h3{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary);margin:8px 0}
|
|
1361
1363
|
.cpi-list button{display:block;width:100%;text-align:left;padding:6px 8px;border:0;border-radius:8px;background:none;color:inherit;font:inherit;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1362
|
-
.cpi-list button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
1364
|
+
.cpi-list button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
1365
|
+
@media (prefers-reduced-motion:reduce){.cpi-card{transition:none!important}}`;
|
|
1363
1366
|
function IdeaCard({ card, top, depth, drag, t }) {
|
|
1364
1367
|
const dx = top ? drag.dx : 0;
|
|
1365
1368
|
const style = top
|
|
@@ -1374,15 +1377,24 @@ body:has(${OPEN_PROJECT}) [${COLLAPSE_HOST}] { display: inline-flex; }
|
|
|
1374
1377
|
card.why ? React.createElement('div', { className: 'cpi-why' }, card.why) : null,
|
|
1375
1378
|
card.effort ? React.createElement('span', { className: 'cpi-effort' }, t(`ideas.effort.${card.effort}`)) : null);
|
|
1376
1379
|
}
|
|
1377
|
-
function IdeasTab({ sessionId, t, openSession }) {
|
|
1380
|
+
function IdeasTab({ sessionId, t, openSession, uiLocale }) {
|
|
1378
1381
|
const deck = ideaDeck(sessionId);
|
|
1379
1382
|
const [, setVersion] = React.useState(0);
|
|
1380
1383
|
React.useEffect(() => { const fn = () => setVersion((n) => n + 1); deck.listeners.add(fn); return () => { deck.listeners.delete(fn); }; }, [deck]);
|
|
1381
1384
|
const [drag, setDrag] = React.useState({ dx: 0, dy: 0, active: false });
|
|
1382
1385
|
const stackRef = React.useRef(null);
|
|
1383
1386
|
const decide = (direction) => { setDrag({ dx: 0, dy: 0, active: false }); decideIdea(deck, sessionId, direction, openSession); };
|
|
1387
|
+
// A drag release, ✕/✓ and ←/→ all fly the top card off to its side first, then decide; one card at a time.
|
|
1388
|
+
const flying = React.useRef(false);
|
|
1389
|
+
const fling = (direction) => {
|
|
1390
|
+
if (flying.current || deckBusy(deck) || deck.cards.length === 0) return;
|
|
1391
|
+
flying.current = true;
|
|
1392
|
+
const width = stackRef.current?.getBoundingClientRect().width ?? 300;
|
|
1393
|
+
setDrag({ dx: (direction === 'right' ? 1 : -1) * width * 1.5, dy: 0, active: false });
|
|
1394
|
+
setTimeout(() => { flying.current = false; decide(direction); }, 180);
|
|
1395
|
+
};
|
|
1384
1396
|
const start = (event) => {
|
|
1385
|
-
if (event.button !== 0 || deckBusy(deck)) return;
|
|
1397
|
+
if (event.button !== 0 || deckBusy(deck) || flying.current) return;
|
|
1386
1398
|
const card = event.currentTarget;
|
|
1387
1399
|
card.setPointerCapture?.(event.pointerId);
|
|
1388
1400
|
const x0 = event.clientX; const y0 = event.clientY;
|
|
@@ -1397,19 +1409,19 @@ body:has(${OPEN_PROJECT}) [${COLLAPSE_HOST}] { display: inline-flex; }
|
|
|
1397
1409
|
const width = stackRef.current?.getBoundingClientRect().width ?? 300;
|
|
1398
1410
|
const direction = e.type === 'pointercancel' ? null : swipeDecision(e.clientX - x0, width, speed);
|
|
1399
1411
|
if (direction === null) setDrag({ dx: 0, dy: 0, active: false });
|
|
1400
|
-
else
|
|
1412
|
+
else fling(direction);
|
|
1401
1413
|
};
|
|
1402
1414
|
card.addEventListener('pointermove', move); card.addEventListener('pointerup', end); card.addEventListener('pointercancel', end);
|
|
1403
1415
|
};
|
|
1404
1416
|
const onKeyDown = (event) => {
|
|
1405
1417
|
if (event.altKey || event.ctrlKey || event.metaKey || event.target.closest?.('input, textarea, [contenteditable="true"]')) return;
|
|
1406
|
-
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') { event.preventDefault();
|
|
1418
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') { event.preventDefault(); fling(event.key === 'ArrowRight' ? 'right' : 'left'); }
|
|
1407
1419
|
else if ((event.key === 'z' || event.key === 'Backspace') && deck.history.length > 0) { event.preventDefault(); undoIdea(deck); }
|
|
1408
1420
|
};
|
|
1409
1421
|
const busy = deckBusy(deck);
|
|
1410
1422
|
const errorText = deck.error === undefined ? null : deck.error === 'restart' ? t('ideas.restart') : t('ideas.failed', { error: deck.error });
|
|
1411
1423
|
const suggest = React.createElement('button', { type: 'button', className: `cpi-btn${deck.cards.length === 0 ? ' cpi-primary' : ''}`, disabled: busy,
|
|
1412
|
-
onClick: () => suggestIdeas(deck, sessionId) }, deck.status === 'loading' ? t('ideas.loading') : deck.cards.length === 0 ? t('ideas.suggest') : t('ideas.more'));
|
|
1424
|
+
onClick: () => suggestIdeas(deck, sessionId, uiLocale?.()) }, deck.status === 'loading' ? t('ideas.loading') : deck.cards.length === 0 ? t('ideas.suggest') : t('ideas.more'));
|
|
1413
1425
|
const head = React.createElement('div', { className: 'cpi-head' },
|
|
1414
1426
|
React.createElement('span', { className: 'cpi-grow', style: { color: 'var(--dsw-alias-label-secondary)' } }, deck.cards.length > 0 ? t('ideas.left', { count: String(deck.cards.length) }) : ''),
|
|
1415
1427
|
deck.history.length > 0 ? React.createElement('button', { type: 'button', className: 'cpi-btn', onClick: () => undoIdea(deck), title: t('ideas.undoHint') }, t('ideas.undo')) : null,
|
|
@@ -1424,8 +1436,8 @@ body:has(${OPEN_PROJECT}) [${COLLAPSE_HOST}] { display: inline-flex; }
|
|
|
1424
1436
|
React.createElement('div', { className: 'cpi-stack', ref: stackRef },
|
|
1425
1437
|
...visible.map((card, depth) => React.createElement(IdeaCard, { key: card.id, card, top: depth === 0, depth, drag: { ...drag, start }, t })).reverse()),
|
|
1426
1438
|
React.createElement('div', { className: 'cpi-actions' },
|
|
1427
|
-
React.createElement('button', { type: 'button', className: 'cpi-btn cpi-round', 'data-side': 'left', disabled: busy, 'aria-label': t('ideas.skip'), title: t('ideas.skipHint'), onClick: () =>
|
|
1428
|
-
React.createElement('button', { type: 'button', className: 'cpi-btn cpi-round', 'data-side': 'right', disabled: busy, 'aria-label': t('ideas.build'), title: t('ideas.buildHint'), onClick: () =>
|
|
1439
|
+
React.createElement('button', { type: 'button', className: 'cpi-btn cpi-round', 'data-side': 'left', disabled: busy, 'aria-label': t('ideas.skip'), title: t('ideas.skipHint'), onClick: () => fling('left') }, '✕'),
|
|
1440
|
+
React.createElement('button', { type: 'button', className: 'cpi-btn cpi-round', 'data-side': 'right', disabled: busy, 'aria-label': t('ideas.build'), title: t('ideas.buildHint'), onClick: () => fling('right') }, '✓')),
|
|
1429
1441
|
React.createElement('div', { className: 'cpi-note' }, deck.status === 'starting' ? t('ideas.starting') : t('ideas.hint')));
|
|
1430
1442
|
return React.createElement('section', { className: 'cpi-root', 'data-composer-plus-ideas': '', tabIndex: -1, onKeyDown, 'aria-busy': busy },
|
|
1431
1443
|
React.createElement('style', null, IDEAS_CSS), head,
|
|
@@ -2153,7 +2165,7 @@ body:has(${OPEN_PROJECT}) [${COLLAPSE_HOST}] { display: inline-flex; }
|
|
|
2153
2165
|
name: 'sidebar.right.pane.tab',
|
|
2154
2166
|
key: IDEAS_ID,
|
|
2155
2167
|
locale: LOCALE_NS,
|
|
2156
|
-
inject: () => ({ openSession: (sessionId) => scope.uiWorkspace.openSession(sessionId) }),
|
|
2168
|
+
inject: () => ({ openSession: (sessionId) => scope.uiWorkspace.openSession(sessionId), uiLocale: () => ctx.locale.getLocale?.().active }),
|
|
2157
2169
|
}, IdeasTab)), 'dsh-composer-plus: ideas tab body');
|
|
2158
2170
|
scope.effect(() => ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({
|
|
2159
2171
|
name: 'sidebar.right.pane.tab.title',
|
package/index.js
CHANGED
|
@@ -435,8 +435,16 @@ Answer with one JSON array only, with no other text. Each element is an object w
|
|
|
435
435
|
- "title": the feature name, at most 8 words.
|
|
436
436
|
- "summary": what the feature does, 1 or 2 sentences.
|
|
437
437
|
- "why": the benefit for the users, 1 sentence.
|
|
438
|
-
- "effort": one of "small", "medium", "large".
|
|
439
|
-
|
|
438
|
+
- "effort": one of "small", "medium", "large". Always write this value in English.`;
|
|
439
|
+
/** A locale id such as "tr" or "pt-BR"; anything else is ignored, so the prompt never takes free text from the browser. */
|
|
440
|
+
const LOCALE_ID = /^[a-z]{2,3}(-[a-z0-9]{2,8}){0,3}$/i;
|
|
441
|
+
/** The language line of the idea prompt: the user's UI language when the client sends one, else the brief's language. */
|
|
442
|
+
export function ideasLanguage(locale) {
|
|
443
|
+
if (typeof locale !== 'string' || !LOCALE_ID.test(locale)) return 'Write "title", "summary" and "why" in the language of the project brief.';
|
|
444
|
+
let name;
|
|
445
|
+
try { name = new Intl.DisplayNames(['en'], { type: 'language' }).of(locale); } catch { name = undefined; }
|
|
446
|
+
return `Write "title", "summary" and "why" in ${name && name !== locale ? `${name} (${locale})` : locale}, also when the project brief uses another language.`;
|
|
447
|
+
}
|
|
440
448
|
|
|
441
449
|
const clip = (value, max) => (typeof value === 'string' ? value.replace(/\s+/g, ' ').trim().slice(0, max) : '');
|
|
442
450
|
/** Ideas from the model's answer: the first JSON array in it (a code fence is fine), each idea cleaned and bounded. Throws
|
|
@@ -470,8 +478,9 @@ function ideasRoute(events, defaultModel) {
|
|
|
470
478
|
return undefined;
|
|
471
479
|
}
|
|
472
480
|
|
|
473
|
-
/** One model call for feature ideas about the chat's project
|
|
474
|
-
|
|
481
|
+
/** One model call for feature ideas about the chat's project, written in `locale` (the dsh UI language) when given.
|
|
482
|
+
* Read-only: no chat message, nothing written to the project. */
|
|
483
|
+
export async function suggestIdeas({ llm, sessionQuery, subprocess, defaultModel }, sessionId, signal, locale) {
|
|
475
484
|
const observation = await sessionQuery.observeSession(sessionId, { signal, projectionMode: 'none' });
|
|
476
485
|
let cwd, route;
|
|
477
486
|
try { cwd = observation.header?.cwd; route = ideasRoute(observation.events ?? [], defaultModel); } finally { observation[Symbol.dispose]?.(); }
|
|
@@ -489,7 +498,7 @@ export async function suggestIdeas({ llm, sessionQuery, subprocess, defaultModel
|
|
|
489
498
|
});
|
|
490
499
|
let text = '';
|
|
491
500
|
let finish;
|
|
492
|
-
for await (const chunk of call.stream({ ...call.config, system: IDEAS_SYSTEM
|
|
501
|
+
for await (const chunk of call.stream({ ...call.config, system: `${IDEAS_SYSTEM}\n${ideasLanguage(locale)}`, signal: callSignal,
|
|
493
502
|
messages: [{ role: 'user', content: [{ type: 'text', text: `Project folder name: ${cwd.split(/[\\/]/).filter(Boolean).at(-1) ?? ''}\n\n${brief}` }] }] })) {
|
|
494
503
|
if (chunk.type === 'text-delta') text += chunk.text;
|
|
495
504
|
else if (chunk.type === 'finish') finish = chunk.reason;
|
|
@@ -681,7 +690,7 @@ export function apply(ctx) {
|
|
|
681
690
|
installIdeas(ctx);
|
|
682
691
|
}
|
|
683
692
|
|
|
684
|
-
/** Feature ideas: `POST {sessionId}` asks the chat's model for ideas; `POST {sessionId, idea}` starts a chat for one idea.
|
|
693
|
+
/** Feature ideas: `POST {sessionId, locale?}` asks the chat's model for ideas; `POST {sessionId, idea}` starts a chat for one idea.
|
|
685
694
|
* Both act only on the chat's own project folder; the browser never sends a path. */
|
|
686
695
|
function installIdeas(ctx) {
|
|
687
696
|
const get = (name) => { try { return ctx.get?.(name); } catch { return undefined; } };
|
|
@@ -703,7 +712,7 @@ function installIdeas(ctx) {
|
|
|
703
712
|
const llm = get('llm');
|
|
704
713
|
if (llm === undefined) return json({ error: 'this dsh profile cannot call a model' }, 409);
|
|
705
714
|
try {
|
|
706
|
-
return json(await suggestIdeas({ llm, sessionQuery: ctx.sessionQuery, subprocess: ctx.subprocess, defaultModel: get('agentDefaultModel') }, body.sessionId, request.signal));
|
|
715
|
+
return json(await suggestIdeas({ llm, sessionQuery: ctx.sessionQuery, subprocess: ctx.subprocess, defaultModel: get('agentDefaultModel') }, body.sessionId, request.signal, body.locale));
|
|
707
716
|
} catch (error) { return json({ error: String(error?.message ?? error) }, status(error)); }
|
|
708
717
|
},
|
|
709
718
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-composer-plus",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "DeepSeek Harness helpers: 11 extra UI languages (Türkçe, Español, Français, Deutsch, 日本語…), Changes panel, swipe-card feature ideas, input history, Shift+Tab /plan, one-click Continue, resume running chats after a restart, queue drag-and-drop, deeplinks, Auto Review allow-for-chat, notifications, iPhone access with the Tether app, automatic updates",
|
|
5
5
|
"icon": "./assets/icon.svg",
|
|
6
6
|
"license": "MIT",
|