dsh-composer-plus 1.6.1 → 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 +16 -7
- package/assets/features/copyDeeplink.webp +0 -0
- package/assets/features/featureIdeas.webp +0 -0
- package/client.js +256 -8
- package/index.js +193 -4
- package/locale/de.json +1 -1
- package/locale/en.json +1 -1
- package/locale/es.json +1 -1
- package/locale/fr.json +1 -1
- package/locale/hi.json +1 -1
- package/locale/id.json +1 -1
- package/locale/ja.json +1 -1
- package/locale/ko.json +1 -1
- package/locale/pt-BR.json +1 -1
- package/locale/ru.json +1 -1
- package/locale/tr.json +1 -1
- package/locale/vi.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +4 -3
package/index.js
CHANGED
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
* on; the switch is saved under the dsh home and is off by default.
|
|
10
10
|
* - Resume after restart: remembers the chats that were running when dsh quit and continues them at the next start; off by
|
|
11
11
|
* default (it starts model turns without a click).
|
|
12
|
+
* - Feature ideas: on a click, one model call (the chat's own model) reads a short brief of the chat's project and returns
|
|
13
|
+
* feature ideas as cards. Read-only: no chat message, nothing written to the project.
|
|
12
14
|
*/
|
|
13
|
-
import { readFileSync } from 'node:fs';
|
|
14
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { constants, readFileSync } from 'node:fs';
|
|
16
|
+
import { mkdir, open, readdir, readFile, realpath, rename, stat, writeFile } from 'node:fs/promises';
|
|
15
17
|
import { randomUUID } from 'node:crypto';
|
|
16
18
|
import { homedir } from 'node:os';
|
|
17
|
-
import { dirname, join } from 'node:path';
|
|
19
|
+
import { dirname, join, sep } from 'node:path';
|
|
18
20
|
import { createGateway } from './tether/gateway/server.mjs';
|
|
19
21
|
|
|
20
22
|
export const name = 'dsh-composer-plus';
|
|
@@ -28,8 +30,9 @@ export const UPDATE_PATH = '/api/composer-plus.update';
|
|
|
28
30
|
export const SHOT_PATH = '/api/composer-plus.shot';
|
|
29
31
|
export const MOBILE_PATH = '/api/composer-plus.mobile';
|
|
30
32
|
export const RESUME_PATH = '/api/composer-plus.resume';
|
|
33
|
+
export const IDEAS_PATH = '/api/composer-plus.ideas';
|
|
31
34
|
/** Feature screenshots shipped in assets/features/<id>.webp; only these ids are served (no path input). */
|
|
32
|
-
export const SHOTS = ['changesPanel', 'projectStatus', 'recentProjects', 'collapseProjects', 'statusOrder', 'notifications', 'allowForChat', 'continueButton', 'resumeAfterRestart', 'inputHistory', 'planToggle', 'keepFocus',
|
|
35
|
+
export const SHOTS = ['changesPanel', 'featureIdeas', 'projectStatus', 'recentProjects', 'collapseProjects', 'statusOrder', 'notifications', 'allowForChat', 'continueButton', 'resumeAfterRestart', 'inputHistory', 'planToggle', 'keepFocus',
|
|
33
36
|
'queueReorder', 'mobileConnect', 'copyDeeplink', 'copyProjectPath', 'language', 'autoUpdate'];
|
|
34
37
|
const PACKAGE = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
35
38
|
const NPM_NAME = 'dsh-composer-plus';
|
|
@@ -375,6 +378,163 @@ export async function wakeSession({ sessionController, sessionQuery, goals, arch
|
|
|
375
378
|
return 'continued';
|
|
376
379
|
}
|
|
377
380
|
|
|
381
|
+
/** Feature ideas: at most this many cards per call; the brief and the answer are bounded too. */
|
|
382
|
+
export const IDEAS_MAX = 8;
|
|
383
|
+
const BRIEF_FILE_BYTES = 6000;
|
|
384
|
+
const BRIEF_MAX = 16000;
|
|
385
|
+
const IDEAS_TIMEOUT_MS = 90000;
|
|
386
|
+
const IDEAS_MAX_TOKENS = 4000;
|
|
387
|
+
/** Top-level names that never go into the brief: secrets and bulky generated folders. */
|
|
388
|
+
const BRIEF_SKIP = /^(\.env.*|.*\.(pem|key|p12|pfx)|id_[a-z0-9]+|\.git|node_modules|dist|build|coverage|\.next|\.venv|venv|__pycache__|\.DS_Store)$/i;
|
|
389
|
+
const BRIEF_FILES = ['README.md', 'readme.md', 'README', 'AGENTS.md', 'package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', 'composer.json', 'pom.xml'];
|
|
390
|
+
|
|
391
|
+
/** The first `max` bytes of one regular file whose real path starts with `prefix` (the chat folder's real path and a
|
|
392
|
+
* separator), or undefined. A symlink counts only when it ends inside the folder. A FIFO or device is never opened;
|
|
393
|
+
* O_NOFOLLOW and O_NONBLOCK keep a file swapped in after the check from escaping or blocking. */
|
|
394
|
+
async function head(path, max, prefix) {
|
|
395
|
+
let file;
|
|
396
|
+
try {
|
|
397
|
+
const real = await realpath(path);
|
|
398
|
+
if (!real.startsWith(prefix) || !(await stat(real)).isFile()) return undefined;
|
|
399
|
+
file = await open(real, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
400
|
+
if (!(await file.stat()).isFile()) return undefined;
|
|
401
|
+
const buffer = Buffer.alloc(max);
|
|
402
|
+
const { bytesRead } = await file.read(buffer, 0, max, 0);
|
|
403
|
+
return buffer.subarray(0, bytesRead).toString('utf8');
|
|
404
|
+
} catch { return undefined; } finally { await file?.close().catch(() => {}); }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** A short, read-only picture of a project for the idea prompt: top-level entries, the main docs and manifests (heads
|
|
408
|
+
* only), and the latest commits. Secrets files and generated folders are skipped; the whole brief is bounded. */
|
|
409
|
+
export async function projectBrief(cwd, { subprocess, signal } = {}) {
|
|
410
|
+
const parts = [];
|
|
411
|
+
try {
|
|
412
|
+
const entries = (await readdir(cwd, { withFileTypes: true })).filter((e) => !BRIEF_SKIP.test(e.name)).slice(0, 80);
|
|
413
|
+
parts.push(`Top-level entries:\n${entries.map((e) => `${e.name}${e.isDirectory() ? '/' : ''}`).join('\n')}`);
|
|
414
|
+
} catch { /* unreadable folder: the brief says less */ }
|
|
415
|
+
const seen = new Set();
|
|
416
|
+
const prefix = await realpath(cwd).then((real) => (real.endsWith(sep) ? real : real + sep), () => undefined);
|
|
417
|
+
for (const name of prefix === undefined ? [] : BRIEF_FILES) {
|
|
418
|
+
const text = await head(join(cwd, name), BRIEF_FILE_BYTES, prefix);
|
|
419
|
+
// Case-insensitive file systems answer README.md and readme.md with the same file.
|
|
420
|
+
if (text === undefined || text.trim() === '' || seen.has(text)) continue;
|
|
421
|
+
seen.add(text);
|
|
422
|
+
parts.push(`--- ${name} (first ${BRIEF_FILE_BYTES} bytes) ---\n${text}`);
|
|
423
|
+
}
|
|
424
|
+
if (subprocess !== undefined) {
|
|
425
|
+
try {
|
|
426
|
+
const log = await runGit(subprocess, cwd, ['log', '--oneline', '--no-color', '-15'], signal);
|
|
427
|
+
if (log.code === 0 && log.stdout.trim() !== '') parts.push(`Latest commits:\n${log.stdout.trim()}`);
|
|
428
|
+
} catch { /* not a repository, or git is missing */ }
|
|
429
|
+
}
|
|
430
|
+
return parts.join('\n\n').slice(0, BRIEF_MAX);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const IDEAS_SYSTEM = `You suggest new features for a software project. Read the project brief and suggest ${IDEAS_MAX} specific features that fit this project and its users. Prefer small, useful, concrete features over rewrites. Do not suggest a feature that the brief shows the project already has.
|
|
434
|
+
Answer with one JSON array only, with no other text. Each element is an object with these keys:
|
|
435
|
+
- "title": the feature name, at most 8 words.
|
|
436
|
+
- "summary": what the feature does, 1 or 2 sentences.
|
|
437
|
+
- "why": the benefit for the users, 1 sentence.
|
|
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
|
+
}
|
|
448
|
+
|
|
449
|
+
const clip = (value, max) => (typeof value === 'string' ? value.replace(/\s+/g, ' ').trim().slice(0, max) : '');
|
|
450
|
+
/** Ideas from the model's answer: the first JSON array in it (a code fence is fine), each idea cleaned and bounded. Throws
|
|
451
|
+
* when the answer holds no usable idea. */
|
|
452
|
+
export function parseIdeas(text) {
|
|
453
|
+
const raw = String(text ?? '');
|
|
454
|
+
const start = raw.indexOf('[');
|
|
455
|
+
const end = raw.lastIndexOf(']');
|
|
456
|
+
let list;
|
|
457
|
+
try { list = start === -1 || end <= start ? undefined : JSON.parse(raw.slice(start, end + 1)); } catch { list = undefined; }
|
|
458
|
+
if (!Array.isArray(list)) throw new Error('the model did not answer with a list of ideas');
|
|
459
|
+
const ideas = [];
|
|
460
|
+
for (const item of list) {
|
|
461
|
+
const title = clip(item?.title, 120);
|
|
462
|
+
const summary = clip(item?.summary, 600);
|
|
463
|
+
if (title === '' || summary === '') continue;
|
|
464
|
+
const effort = ['small', 'medium', 'large'].includes(item?.effort) ? item.effort : undefined;
|
|
465
|
+
ideas.push({ id: randomUUID(), title, summary, ...(clip(item?.why, 300) === '' ? {} : { why: clip(item.why, 300) }), ...(effort === undefined ? {} : { effort }) });
|
|
466
|
+
if (ideas.length === IDEAS_MAX) break;
|
|
467
|
+
}
|
|
468
|
+
if (ideas.length === 0) throw new Error('the model answered without a usable idea');
|
|
469
|
+
return ideas;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** The chat's own model: the newest logged request header, else dsh's default model. */
|
|
473
|
+
function ideasRoute(events, defaultModel) {
|
|
474
|
+
const header = events.findLast((e) => e.type === 'request/header')?.data?.header?.config;
|
|
475
|
+
if (isText(header?.provider) && isText(header?.model)) return { provider: header.provider, model: header.model };
|
|
476
|
+
const selection = defaultModel?.currentSelection?.();
|
|
477
|
+
if (isText(selection?.provider) && isText(selection?.model)) return { provider: selection.provider, model: selection.model };
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
|
|
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) {
|
|
484
|
+
const observation = await sessionQuery.observeSession(sessionId, { signal, projectionMode: 'none' });
|
|
485
|
+
let cwd, route;
|
|
486
|
+
try { cwd = observation.header?.cwd; route = ideasRoute(observation.events ?? [], defaultModel); } finally { observation[Symbol.dispose]?.(); }
|
|
487
|
+
if (!isText(cwd, 4096)) throw Object.assign(new Error('this chat has no project folder'), { status: 409 });
|
|
488
|
+
if (route === undefined) throw Object.assign(new Error('no model is selected'), { status: 409 });
|
|
489
|
+
const brief = await projectBrief(cwd, { subprocess, signal });
|
|
490
|
+
const timeout = AbortSignal.timeout(IDEAS_TIMEOUT_MS);
|
|
491
|
+
const callSignal = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
|
|
492
|
+
const call = await llm.prepareCall(route, callSignal, (controls, model) => {
|
|
493
|
+
// The lowest reasoning effort keeps the answer fast; the model only reads a short brief. The output cap never goes
|
|
494
|
+
// above the model's own default.
|
|
495
|
+
const first = model?.reasoning?.efforts?.[0];
|
|
496
|
+
const cap = Number.isSafeInteger(model?.defaultMaxTokens) && model.defaultMaxTokens > 0 ? Math.min(IDEAS_MAX_TOKENS, model.defaultMaxTokens) : IDEAS_MAX_TOKENS;
|
|
497
|
+
return { ...controls, maxTokens: cap, ...(first === undefined ? {} : { reasoningEffort: first.id }) };
|
|
498
|
+
});
|
|
499
|
+
let text = '';
|
|
500
|
+
let finish;
|
|
501
|
+
for await (const chunk of call.stream({ ...call.config, system: `${IDEAS_SYSTEM}\n${ideasLanguage(locale)}`, signal: callSignal,
|
|
502
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: `Project folder name: ${cwd.split(/[\\/]/).filter(Boolean).at(-1) ?? ''}\n\n${brief}` }] }] })) {
|
|
503
|
+
if (chunk.type === 'text-delta') text += chunk.text;
|
|
504
|
+
else if (chunk.type === 'finish') finish = chunk.reason;
|
|
505
|
+
}
|
|
506
|
+
if (finish?.kind === 'error' || finish?.kind === 'aborted') throw new Error(finish.failure?.message || `the model call ended: ${finish.kind}`);
|
|
507
|
+
return { ideas: parseIdeas(text), model: route.model };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** The first message of the chat that works on an accepted idea. It starts in plan mode, so nothing changes before the user
|
|
511
|
+
* approves the plan. */
|
|
512
|
+
export function ideaPrompt(idea) {
|
|
513
|
+
return [`Implement this feature in this project: ${idea.title}`, '', idea.summary, ...(idea.why ? ['', `Why: ${idea.why}`] : []), '',
|
|
514
|
+
'First read the related code. Then plan the smallest change that delivers this feature, with the checks that prove it works.'].join('\n');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Start a new chat in the same project (the same sidebar group as the source chat) for one accepted idea, and enter plan
|
|
518
|
+
* mode with the idea as the request. When the profile has no /plan command, or /plan fails, the idea goes as an ordinary
|
|
519
|
+
* message. */
|
|
520
|
+
export async function startIdeaChat({ sessionController, sessionQuery, commands, workspaces }, sessionId, idea, signal = new AbortController().signal) {
|
|
521
|
+
const observation = await sessionQuery.observeSession(sessionId, { signal, projectionMode: 'none' });
|
|
522
|
+
let cwd;
|
|
523
|
+
try { cwd = observation.header?.cwd; } finally { observation[Symbol.dispose]?.(); }
|
|
524
|
+
if (!isText(cwd, 4096)) throw Object.assign(new Error('this chat has no project folder'), { status: 409 });
|
|
525
|
+
let workspace;
|
|
526
|
+
try { workspace = workspaces?.list?.().find((w) => w.sessionIds.includes(sessionId)); } catch { workspace = undefined; }
|
|
527
|
+
const { sessionId: created } = await sessionController.create(workspace === undefined ? { cwd } : { workspaceId: workspace.id });
|
|
528
|
+
const found = await sessionController.resolveAgent(created);
|
|
529
|
+
if ('error' in found) throw new Error('the new chat could not start');
|
|
530
|
+
const text = ideaPrompt(idea);
|
|
531
|
+
let planned;
|
|
532
|
+
try { planned = commands?.find?.(found.agent, 'plan') === undefined ? undefined : await commands.execute(found.agent, `/plan ${text}`, [], signal); } catch { planned = undefined; }
|
|
533
|
+
const plan = planned?.result?.kind === 'success';
|
|
534
|
+
if (!plan) await sessionController.prompt({ requestId: randomUUID(), sessionId: created, mode: 'queue', content: [{ type: 'text', text }] }, signal);
|
|
535
|
+
return { sessionId: created, plan };
|
|
536
|
+
}
|
|
537
|
+
|
|
378
538
|
const json = (value, status = 200) => Response.json(value, { status, headers: { 'cache-control': 'no-store' } });
|
|
379
539
|
const isText = (v, max = 512) => typeof v === 'string' && v.length > 0 && v.length <= max;
|
|
380
540
|
|
|
@@ -527,6 +687,35 @@ export function apply(ctx) {
|
|
|
527
687
|
},
|
|
528
688
|
});
|
|
529
689
|
installResume(ctx, home);
|
|
690
|
+
installIdeas(ctx);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Feature ideas: `POST {sessionId, locale?}` asks the chat's model for ideas; `POST {sessionId, idea}` starts a chat for one idea.
|
|
694
|
+
* Both act only on the chat's own project folder; the browser never sends a path. */
|
|
695
|
+
function installIdeas(ctx) {
|
|
696
|
+
const get = (name) => { try { return ctx.get?.(name); } catch { return undefined; } };
|
|
697
|
+
const status = (error) => (Number.isInteger(error?.status) ? error.status : 502);
|
|
698
|
+
ctx.connection.fetch.register({
|
|
699
|
+
path: IDEAS_PATH, methods: ['POST'], requestBody: 'buffered',
|
|
700
|
+
fetch: async (request) => {
|
|
701
|
+
let body;
|
|
702
|
+
try { body = await request.json(); } catch { return json({ error: 'bad request' }, 400); }
|
|
703
|
+
if (!isText(body?.sessionId)) return json({ error: 'bad request' }, 400);
|
|
704
|
+
if (body.idea !== undefined) {
|
|
705
|
+
const idea = { title: clip(body.idea?.title, 120), summary: clip(body.idea?.summary, 600), why: clip(body.idea?.why, 300) || undefined };
|
|
706
|
+
if (idea.title === '' || idea.summary === '') return json({ error: 'bad request' }, 400);
|
|
707
|
+
try {
|
|
708
|
+
return json(await startIdeaChat({ sessionController: ctx.sessionController, sessionQuery: ctx.sessionQuery, commands: get('commands'), workspaces: get('workspaceRegistry') },
|
|
709
|
+
body.sessionId, idea, request.signal));
|
|
710
|
+
} catch (error) { return json({ error: String(error?.message ?? error) }, status(error)); }
|
|
711
|
+
}
|
|
712
|
+
const llm = get('llm');
|
|
713
|
+
if (llm === undefined) return json({ error: 'this dsh profile cannot call a model' }, 409);
|
|
714
|
+
try {
|
|
715
|
+
return json(await suggestIdeas({ llm, sessionQuery: ctx.sessionQuery, subprocess: ctx.subprocess, defaultModel: get('agentDefaultModel') }, body.sessionId, request.signal, body.locale));
|
|
716
|
+
} catch (error) { return json({ error: String(error?.message ?? error) }, status(error)); }
|
|
717
|
+
},
|
|
718
|
+
});
|
|
530
719
|
}
|
|
531
720
|
|
|
532
721
|
/** Resume after restart: follow the top-level chats, and once dsh is ready, continue the ones its last shutdown stopped.
|
package/locale/de.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "Änderungsansicht, Eingabeverlauf, Shift+Tab /plan, Fortsetzen mit einem Klick, Fortsetzen nach Neustart, Warteschlange per Drag-and-drop, Links, Benachrichtigungen, iPhone-Zugriff (Tether), 11 Sprachen und automatische Updates."
|
|
4
|
+
"description": "Änderungsansicht, Karten mit Funktionsideen, Eingabeverlauf, Shift+Tab /plan, Fortsetzen mit einem Klick, Fortsetzen nach Neustart, Warteschlange per Drag-and-drop, Links, Benachrichtigungen, iPhone-Zugriff (Tether), 11 Sprachen und automatische Updates."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "Changes panel, input history, Shift+Tab /plan, one-click Continue, resume after restart, queue drag-and-drop, deeplinks, notifications, iPhone access (Tether), 11 UI languages and automatic updates."
|
|
4
|
+
"description": "Changes panel, feature idea cards, input history, Shift+Tab /plan, one-click Continue, resume after restart, queue drag-and-drop, deeplinks, notifications, iPhone access (Tether), 11 UI languages and automatic updates."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/es.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "Panel de cambios, historial de entrada, Shift+Tab /plan, Continuar con un clic, reanudar tras reiniciar, reordenar la cola arrastrando, enlaces, notificaciones, acceso desde iPhone (Tether), 11 idiomas y actualizaciones automáticas."
|
|
4
|
+
"description": "Panel de cambios, tarjetas de ideas de funciones, historial de entrada, Shift+Tab /plan, Continuar con un clic, reanudar tras reiniciar, reordenar la cola arrastrando, enlaces, notificaciones, acceso desde iPhone (Tether), 11 idiomas y actualizaciones automáticas."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/fr.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "Panneau des modifications, historique de saisie, Shift+Tab /plan, Continuer en un clic, reprise après redémarrage, file réorganisable par glisser-déposer, liens, notifications, accès depuis l’iPhone (Tether), 11 langues et mises à jour automatiques."
|
|
4
|
+
"description": "Panneau des modifications, cartes d’idées de fonctionnalités, historique de saisie, Shift+Tab /plan, Continuer en un clic, reprise après redémarrage, file réorganisable par glisser-déposer, liens, notifications, accès depuis l’iPhone (Tether), 11 langues et mises à jour automatiques."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/hi.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "बदलाव पैनल, इनपुट इतिहास, Shift+Tab /plan, एक क्लिक में जारी रखें, रीस्टार्ट के बाद जारी रखें, कतार ड्रैग-एंड-ड्रॉप, लिंक, सूचनाएं, iPhone से एक्सेस (Tether), 11 भाषाएं और अपने-आप अपडेट।"
|
|
4
|
+
"description": "बदलाव पैनल, फ़ीचर आइडिया कार्ड, इनपुट इतिहास, Shift+Tab /plan, एक क्लिक में जारी रखें, रीस्टार्ट के बाद जारी रखें, कतार ड्रैग-एंड-ड्रॉप, लिंक, सूचनाएं, iPhone से एक्सेस (Tether), 11 भाषाएं और अपने-आप अपडेट।"
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/id.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "Panel perubahan, riwayat input, Shift+Tab /plan, Lanjutkan sekali klik, lanjutkan setelah mulai ulang, urutkan antrean dengan seret, tautan, notifikasi, akses dari iPhone (Tether), 11 bahasa, dan pembaruan otomatis."
|
|
4
|
+
"description": "Panel perubahan, kartu ide fitur, riwayat input, Shift+Tab /plan, Lanjutkan sekali klik, lanjutkan setelah mulai ulang, urutkan antrean dengan seret, tautan, notifikasi, akses dari iPhone (Tether), 11 bahasa, dan pembaruan otomatis."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/ja.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "変更パネル、機能アイデアのカード、入力履歴、Shift+Tab で /plan、ワンクリック続行、再起動後の再開、キューのドラッグ並べ替え、リンク、通知、iPhone からの操作(Tether)、11 言語、自動アップデート。"
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/ko.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "변경 사항 패널, 입력 기록, Shift+Tab /plan, 원클릭 계속, 재시작 후 이어서 실행, 대기열 끌어서 정렬, 링크, 알림, iPhone 연결(Tether), 11개 언어와 자동 업데이트."
|
|
4
|
+
"description": "변경 사항 패널, 기능 아이디어 카드, 입력 기록, Shift+Tab /plan, 원클릭 계속, 재시작 후 이어서 실행, 대기열 끌어서 정렬, 링크, 알림, iPhone 연결(Tether), 11개 언어와 자동 업데이트."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/pt-BR.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "Painel de alterações, histórico de entrada, Shift+Tab /plan, Continuar com um clique, retomar após reiniciar, reordenar a fila arrastando, links, notificações, acesso pelo iPhone (Tether), 11 idiomas e atualizações automáticas."
|
|
4
|
+
"description": "Painel de alterações, cartões de ideias de recursos, histórico de entrada, Shift+Tab /plan, Continuar com um clique, retomar após reiniciar, reordenar a fila arrastando, links, notificações, acesso pelo iPhone (Tether), 11 idiomas e atualizações automáticas."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/ru.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "Панель изменений, история ввода, Shift+Tab /plan, продолжение в один клик, продолжение после перезапуска, перетаскивание очереди, ссылки, уведомления, доступ с iPhone (Tether), 11 языков и автообновления."
|
|
4
|
+
"description": "Панель изменений, карточки идей функций, история ввода, Shift+Tab /plan, продолжение в один клик, продолжение после перезапуска, перетаскивание очереди, ссылки, уведомления, доступ с iPhone (Tether), 11 языков и автообновления."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/tr.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "Değişiklikler paneli, giriş geçmişi, Shift+Tab /plan, tek tıkla devam, yeniden açılışta sürdürme, sıradaki mesajları sürükle-bırak, bağlantılar, bildirimler, iPhone’dan erişim (Tether), 11 arayüz dili ve otomatik güncellemeler."
|
|
4
|
+
"description": "Değişiklikler paneli, özellik fikri kartları, giriş geçmişi, Shift+Tab /plan, tek tıkla devam, yeniden açılışta sürdürme, sıradaki mesajları sürükle-bırak, bağlantılar, bildirimler, iPhone’dan erişim (Tether), 11 arayüz dili ve otomatik güncellemeler."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/vi.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "Bảng thay đổi, lịch sử nhập, Shift+Tab /plan, Tiếp tục một chạm, tiếp tục sau khi khởi động lại, kéo thả hàng đợi, liên kết, thông báo, truy cập từ iPhone (Tether), 11 ngôn ngữ và tự động cập nhật."
|
|
4
|
+
"description": "Bảng thay đổi, thẻ ý tưởng tính năng, lịch sử nhập, Shift+Tab /plan, Tiếp tục một chạm, tiếp tục sau khi khởi động lại, kéo thả hàng đợi, liên kết, thông báo, truy cập từ iPhone (Tether), 11 ngôn ngữ và tự động cập nhật."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Composer Plus",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "改动面板、功能创意卡片、输入历史、Shift+Tab /plan、一键继续、重启后继续、队列拖拽排序、会话链接、通知、iPhone 远程使用(Tether)、11 种界面语言和自动更新。"
|
|
5
5
|
}
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-composer-plus",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "DeepSeek Harness helpers: 11 extra UI languages (Türkçe, Español, Français, Deutsch, 日本語…), Changes panel, 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",
|
|
3
|
+
"version": "1.7.1",
|
|
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",
|
|
7
7
|
"type": "module",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"dsh-composer-plus",
|
|
25
25
|
"dsh-plugin",
|
|
26
26
|
"dsh-plugins",
|
|
27
|
+
"feature-ideas",
|
|
27
28
|
"git",
|
|
28
29
|
"git-diff",
|
|
29
30
|
"i18n",
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
"assets/features/*.webp"
|
|
67
68
|
],
|
|
68
69
|
"scripts": {
|
|
69
|
-
"test": "node scripts/build-locales.mjs --check && node test/locale.test.mjs && node test/continue.test.mjs && node test/continue-host.test.mjs && node test/resume-restart.test.mjs && node test/queue-drag.test.mjs && node test/plan-toggle.test.mjs && node test/keep-focus.test.mjs && node test/mobile-connect.test.mjs && node test/tether-gateway.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 && node test/recent-projects.test.mjs && node test/project-tools.test.mjs && node test/status-order.test.mjs && node test/toggles.test.mjs && node test/update.test.mjs && node test/meta.test.mjs && node test/shots.test.mjs",
|
|
70
|
+
"test": "node scripts/build-locales.mjs --check && node test/locale.test.mjs && node test/continue.test.mjs && node test/continue-host.test.mjs && node test/resume-restart.test.mjs && node test/queue-drag.test.mjs && node test/plan-toggle.test.mjs && node test/keep-focus.test.mjs && node test/mobile-connect.test.mjs && node test/tether-gateway.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/feature-ideas.test.mjs && node test/feature-ideas-client.test.mjs && node test/project-status.test.mjs && node test/recent-projects.test.mjs && node test/project-tools.test.mjs && node test/status-order.test.mjs && node test/toggles.test.mjs && node test/update.test.mjs && node test/meta.test.mjs && node test/shots.test.mjs",
|
|
70
71
|
"build:locales": "node scripts/build-locales.mjs",
|
|
71
72
|
"prepublishOnly": "npm test"
|
|
72
73
|
},
|