dsh-composer-plus 1.9.0 → 1.10.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.de.md +236 -0
- package/README.es.md +236 -0
- package/README.fr.md +236 -0
- package/README.hi.md +236 -0
- package/README.id.md +236 -0
- package/README.ja.md +236 -0
- package/README.ko.md +236 -0
- package/README.md +20 -14
- package/README.pt-BR.md +236 -0
- package/README.ru.md +236 -0
- package/README.tr.md +236 -0
- package/README.vi.md +236 -0
- package/README.zh.md +236 -0
- package/assets/features/featureIdeas.webp +0 -0
- package/assets/features/projectName.webp +0 -0
- package/assets/features/turnRollback.webp +0 -0
- package/client.js +259 -47
- package/index.js +122 -33
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -33,12 +33,13 @@ export const MOBILE_PATH = '/api/composer-plus.mobile';
|
|
|
33
33
|
export const RESUME_PATH = '/api/composer-plus.resume';
|
|
34
34
|
export const IDEAS_PATH = '/api/composer-plus.ideas';
|
|
35
35
|
export const LATER_PATH = '/api/composer-plus.later';
|
|
36
|
+
export const HISTORY_PATH = '/api/composer-plus.idea-history';
|
|
36
37
|
export const CONTENT_SEARCH_PATH = '/api/composer-plus.content-search';
|
|
37
38
|
export const ROLLBACK_PATH = '/api/composer-plus.rollback';
|
|
38
39
|
export const DELETE_CHAT_PATH = '/api/composer-plus.delete-chat';
|
|
39
40
|
/** Feature screenshots shipped in assets/features/<id>.webp; only these ids are served (no path input). */
|
|
40
41
|
export const SHOTS = ['changesPanel', 'featureIdeas', 'projectStatus', 'recentProjects', 'collapseProjects', 'statusOrder', 'notifications', 'allowForChat', 'continueButton', 'resumeAfterRestart', 'inputHistory', 'planToggle', 'keepFocus',
|
|
41
|
-
'queueReorder', 'mobileConnect', 'copyDeeplink', 'copyProjectPath', 'language', 'autoUpdate', 'contentSearch', 'turnRollback', 'deleteChat'];
|
|
42
|
+
'queueReorder', 'mobileConnect', 'copyDeeplink', 'copyProjectPath', 'language', 'autoUpdate', 'contentSearch', 'turnRollback', 'deleteChat', 'projectName'];
|
|
42
43
|
const PACKAGE = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
43
44
|
const NPM_NAME = 'dsh-composer-plus';
|
|
44
45
|
const RELEASES_URL = 'https://api.github.com/repos/furkancak1r/dsh-composer-plus/releases/latest';
|
|
@@ -389,8 +390,10 @@ export async function wakeSession({ sessionController, sessionQuery, goals, arch
|
|
|
389
390
|
export const IDEAS_MAX = 8;
|
|
390
391
|
const BRIEF_FILE_BYTES = 6000;
|
|
391
392
|
const BRIEF_MAX = 16000;
|
|
392
|
-
|
|
393
|
-
|
|
393
|
+
// A reasoning model counts its thinking in the output cap. A route that forces a high effort (AIProxy's aiproxy-max used
|
|
394
|
+
// about 7.7k tokens and 72 s for 8 ideas) was cut at 4000 tokens with `output_limit`; this cap leaves room for the thinking.
|
|
395
|
+
const IDEAS_TIMEOUT_MS = 180000;
|
|
396
|
+
const IDEAS_MAX_TOKENS = 32000;
|
|
394
397
|
/** Top-level names that never go into the brief: secrets and bulky generated folders. */
|
|
395
398
|
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;
|
|
396
399
|
const BRIEF_FILES = ['README.md', 'readme.md', 'README', 'AGENTS.md', 'package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', 'composer.json', 'pom.xml'];
|
|
@@ -492,15 +495,49 @@ function ideasRoute(events, defaultModel) {
|
|
|
492
495
|
return undefined;
|
|
493
496
|
}
|
|
494
497
|
|
|
498
|
+
/** Same idea title: case and spaces do not count. */
|
|
499
|
+
const titleKey = (title) => String(title ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
500
|
+
/** Earlier ideas per project, newest first: everything the model suggested there, `started` once a chat started for it.
|
|
501
|
+
* At most this many; the oldest leave first. The model is told not to suggest them again. */
|
|
502
|
+
export const HISTORY_MAX = 200;
|
|
503
|
+
const AVOID_SHOWN = 120;
|
|
504
|
+
export function ideaHistory(store) {
|
|
505
|
+
const entry = (i, extra = {}) => ({ id: randomUUID(), title: i.title, summary: i.summary, ...(i.why ? { why: i.why } : {}), ...(i.effort ? { effort: i.effort } : {}), at: Date.now(), ...extra });
|
|
506
|
+
return {
|
|
507
|
+
list: store.list,
|
|
508
|
+
remove: store.remove,
|
|
509
|
+
clear: (cwd) => store.change(cwd, () => []),
|
|
510
|
+
/** New ideas go on top; a title that is already there stays where it is. */
|
|
511
|
+
record: (cwd, ideas) => store.change(cwd, (list) => {
|
|
512
|
+
const known = new Set(list.map((i) => titleKey(i.title)));
|
|
513
|
+
const fresh = ideas.filter((i) => !known.has(titleKey(i.title)) && known.add(titleKey(i.title))).map((i) => entry(i));
|
|
514
|
+
return [...fresh, ...list].slice(0, HISTORY_MAX);
|
|
515
|
+
}),
|
|
516
|
+
/** A chat started for these ideas (also ideas saved for later before the history existed). */
|
|
517
|
+
started: (cwd, ideas) => store.change(cwd, (list) => {
|
|
518
|
+
const keys = new Set(ideas.map((i) => titleKey(i.title)));
|
|
519
|
+
const known = new Set(list.map((i) => titleKey(i.title)));
|
|
520
|
+
const missing = ideas.filter((i) => !known.has(titleKey(i.title))).map((i) => entry(i, { started: true }));
|
|
521
|
+
return [...missing, ...list.map((i) => (keys.has(titleKey(i.title)) ? { ...i, started: true } : i))].slice(0, HISTORY_MAX);
|
|
522
|
+
}),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
495
526
|
/** One model call for feature ideas about the chat's project, written in `locale` (the dsh UI language) when given.
|
|
496
|
-
*
|
|
497
|
-
|
|
527
|
+
* `avoid(cwd)` names the earlier ideas (history and the Later list): the model is told to skip them, and a repeated title
|
|
528
|
+
* is dropped. `record(cwd, ideas)` keeps the new ones. Read-only for the project: no chat message, nothing written there. */
|
|
529
|
+
export async function suggestIdeas({ llm, sessionQuery, subprocess, defaultModel, avoid, record }, sessionId, signal, locale) {
|
|
498
530
|
const observation = await sessionQuery.observeSession(sessionId, { signal, projectionMode: 'none' });
|
|
499
531
|
let cwd, route;
|
|
500
532
|
try { cwd = observation.header?.cwd; route = ideasRoute(observation.events ?? [], defaultModel); } finally { observation[Symbol.dispose]?.(); }
|
|
501
533
|
if (!isText(cwd, 4096)) throw Object.assign(new Error('this chat has no project folder'), { status: 409 });
|
|
502
534
|
if (route === undefined) throw Object.assign(new Error('no model is selected'), { status: 409 });
|
|
503
535
|
const brief = await projectBrief(cwd, { subprocess, signal });
|
|
536
|
+
const earlier = [...new Set((await avoid?.(cwd) ?? []).map((title) => clip(title, 120)).filter(Boolean))];
|
|
537
|
+
const seen = new Set(earlier.map(titleKey));
|
|
538
|
+
// The model sees the newest titles only (about 15 kB at most, like the brief); the filter below checks all of them.
|
|
539
|
+
const shown = earlier.slice(0, AVOID_SHOWN);
|
|
540
|
+
const asked = shown.length === 0 ? '' : `\n\nEarlier ideas for this project. Do not suggest any of them again, also not under another name:\n${shown.map((title) => `- ${title}`).join('\n')}`;
|
|
504
541
|
const timeout = AbortSignal.timeout(IDEAS_TIMEOUT_MS);
|
|
505
542
|
const callSignal = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
|
|
506
543
|
const call = await llm.prepareCall(route, callSignal, (controls, model) => {
|
|
@@ -513,19 +550,31 @@ export async function suggestIdeas({ llm, sessionQuery, subprocess, defaultModel
|
|
|
513
550
|
let text = '';
|
|
514
551
|
let finish;
|
|
515
552
|
for await (const chunk of call.stream({ ...call.config, system: `${IDEAS_SYSTEM}\n${ideasLanguage(locale)}`, signal: callSignal,
|
|
516
|
-
messages: [{ role: 'user', content: [{ type: 'text', text: `Project folder name: ${cwd.split(/[\\/]/).filter(Boolean).at(-1) ?? ''}\n\n${brief}` }] }] })) {
|
|
553
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: `Project folder name: ${cwd.split(/[\\/]/).filter(Boolean).at(-1) ?? ''}\n\n${brief}${asked}` }] }] })) {
|
|
517
554
|
if (chunk.type === 'text-delta') text += chunk.text;
|
|
518
555
|
else if (chunk.type === 'finish') finish = chunk.reason;
|
|
519
556
|
}
|
|
520
557
|
if (finish?.kind === 'error' || finish?.kind === 'aborted') throw new Error(finish.failure?.message || `the model call ended: ${finish.kind}`);
|
|
521
|
-
|
|
558
|
+
const ideas = parseIdeas(text).filter((i) => !seen.has(titleKey(i.title)) && seen.add(titleKey(i.title)));
|
|
559
|
+
if (ideas.length === 0) throw new Error('the model suggested only earlier ideas; try again');
|
|
560
|
+
// ponytail: a failed history write does not hide the new cards; they can come again next time.
|
|
561
|
+
try { await record?.(cwd, ideas); } catch { /* best-effort */ }
|
|
562
|
+
return { ideas, model: route.model };
|
|
522
563
|
}
|
|
523
564
|
|
|
524
|
-
/** The first message of the chat that works on an accepted idea.
|
|
525
|
-
* approves the plan. */
|
|
526
|
-
export function ideaPrompt(idea) {
|
|
565
|
+
/** The first message of the chat that works on an accepted idea. With `plan` (the default) it starts in plan mode, so
|
|
566
|
+
* nothing changes before the user approves the plan; without it the model builds the feature at once. */
|
|
567
|
+
export function ideaPrompt(idea, plan = true) {
|
|
568
|
+
// Plan all (Later list): several ideas in one plan-mode chat, so shared code changes once and in a working order.
|
|
569
|
+
if (Array.isArray(idea)) {
|
|
570
|
+
if (idea.length === 1) return ideaPrompt(idea[0], plan);
|
|
571
|
+
return [`Implement these ${idea.length} features in this project, as one plan:`, '',
|
|
572
|
+
...idea.flatMap((i, n) => [`${n + 1}. ${i.title}`, i.summary, ...(i.why ? [`Why: ${i.why}`] : []), '']),
|
|
573
|
+
'First read the related code. Then plan one set of changes that delivers all of them: make shared parts once, use an order that keeps the project working after each step, and name the checks that prove each feature works.'].join('\n');
|
|
574
|
+
}
|
|
527
575
|
return [`Implement this feature in this project: ${idea.title}`, '', idea.summary, ...(idea.why ? ['', `Why: ${idea.why}`] : []), '',
|
|
528
|
-
'First read the related code. Then plan the smallest change that delivers this feature, with the checks that prove it works.'
|
|
576
|
+
plan ? 'First read the related code. Then plan the smallest change that delivers this feature, with the checks that prove it works.'
|
|
577
|
+
: 'First read the related code. Then make the smallest change that delivers this feature, and run the checks that prove it works.'].join('\n');
|
|
529
578
|
}
|
|
530
579
|
|
|
531
580
|
/** The chat's own project folder, from its session header (the browser never sends a path). */
|
|
@@ -539,20 +588,21 @@ async function chatFolder(sessionQuery, sessionId, signal) {
|
|
|
539
588
|
|
|
540
589
|
/** Start a new chat in the same project (the same sidebar group as the source chat) for one accepted idea, and enter plan
|
|
541
590
|
* mode with the idea as the request. When the profile has no /plan command, or /plan fails, the idea goes as an ordinary
|
|
542
|
-
* message.
|
|
543
|
-
|
|
591
|
+
* message. `{ plan: false }` (Build now) skips plan mode: the idea goes as an ordinary message that asks for the change.
|
|
592
|
+
* `idea` can be a list (Plan all): one chat plans all of them. Returns the new chat, whether it plans, and the project folder. */
|
|
593
|
+
export async function startIdeaChat({ sessionController, sessionQuery, commands, workspaces }, sessionId, idea, signal = new AbortController().signal, { plan: wantPlan = true } = {}) {
|
|
544
594
|
const cwd = await chatFolder(sessionQuery, sessionId, signal);
|
|
545
595
|
let workspace;
|
|
546
596
|
try { workspace = workspaces?.list?.().find((w) => w.sessionIds.includes(sessionId)); } catch { workspace = undefined; }
|
|
547
597
|
const { sessionId: created } = await sessionController.create(workspace === undefined ? { cwd } : { workspaceId: workspace.id });
|
|
548
598
|
const found = await sessionController.resolveAgent(created);
|
|
549
599
|
if ('error' in found) throw new Error('the new chat could not start');
|
|
550
|
-
const text = ideaPrompt(idea);
|
|
600
|
+
const text = ideaPrompt(idea, wantPlan);
|
|
551
601
|
let planned;
|
|
552
|
-
try { planned = commands?.find?.(found.agent, 'plan') === undefined ? undefined : await commands.execute(found.agent, `/plan ${text}`, [], signal); } catch { planned = undefined; }
|
|
602
|
+
try { planned = !wantPlan || commands?.find?.(found.agent, 'plan') === undefined ? undefined : await commands.execute(found.agent, `/plan ${text}`, [], signal); } catch { planned = undefined; }
|
|
553
603
|
const plan = planned?.result?.kind === 'success';
|
|
554
604
|
if (!plan) await sessionController.prompt({ requestId: randomUUID(), sessionId: created, mode: 'queue', content: [{ type: 'text', text }] }, signal);
|
|
555
|
-
return { sessionId: created, plan };
|
|
605
|
+
return { sessionId: created, plan, cwd };
|
|
556
606
|
}
|
|
557
607
|
|
|
558
608
|
/** Ideas saved for later: at most this many per project. A full list refuses a new idea; it never drops an old one. */
|
|
@@ -591,7 +641,10 @@ export function laterStore(file) {
|
|
|
591
641
|
if (list.length >= LATER_MAX) throw Object.assign(new Error(`a project keeps at most ${LATER_MAX} ideas for later`), { status: 409 });
|
|
592
642
|
return [{ id: randomUUID(), ...idea, at: Date.now() }, ...list];
|
|
593
643
|
}),
|
|
594
|
-
|
|
644
|
+
/** One id, or a list of ids in one write. */
|
|
645
|
+
remove: (cwd, id) => update(cwd, (list) => list.filter((i) => (Array.isArray(id) ? !id.includes(i.id) : i.id !== id))),
|
|
646
|
+
/** Any change to one project's list, in order with the others (the idea history uses it). */
|
|
647
|
+
change: update,
|
|
595
648
|
};
|
|
596
649
|
}
|
|
597
650
|
|
|
@@ -1208,14 +1261,22 @@ function installContentSearch(ctx, home) {
|
|
|
1208
1261
|
});
|
|
1209
1262
|
}
|
|
1210
1263
|
|
|
1211
|
-
/** Feature ideas: `POST {sessionId, locale?}` asks the chat's model for ideas
|
|
1212
|
-
*
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1264
|
+
/** Feature ideas: `POST {sessionId, locale?}` asks the chat's model for ideas (never one from the project's history or
|
|
1265
|
+
* Later list); `POST {sessionId, idea}` starts a chat for one idea.
|
|
1266
|
+
* Ideas saved for later: `GET ?sessionId` lists the project's ideas; `POST {sessionId, add | remove | start | startAll}`
|
|
1267
|
+
* changes the list (`start` starts a chat for the stored idea and removes it once the chat runs).
|
|
1268
|
+
* Idea history: `GET ?sessionId` lists the project's earlier ideas; `POST {sessionId, remove | clear}` forgets one or all.
|
|
1269
|
+
* All act only on the chat's own project folder; the browser never sends a path. */
|
|
1215
1270
|
function installIdeas(ctx, home) {
|
|
1216
1271
|
const get = (name) => { try { return ctx.get?.(name); } catch { return undefined; } };
|
|
1217
1272
|
const status = (error) => (Number.isInteger(error?.status) ? error.status : 502);
|
|
1218
1273
|
const services = () => ({ sessionController: ctx.sessionController, sessionQuery: ctx.sessionQuery, commands: get('commands'), workspaces: get('workspaceRegistry') });
|
|
1274
|
+
const later = laterStore(join(home, 'storages', 'dsh-composer-plus', 'later.json'));
|
|
1275
|
+
const history = ideaHistory(laterStore(join(home, 'storages', 'dsh-composer-plus', 'idea-history.json')));
|
|
1276
|
+
// ponytail: the history marks a start best-effort; the chat already runs, so a failed write only loses the mark.
|
|
1277
|
+
const markStarted = async (cwd, ideas) => { try { await history.started(cwd, ideas); } catch { /* best-effort */ } };
|
|
1278
|
+
// ponytail: a broken saved file never blocks Suggest; its titles are just not avoided (the file itself is kept as it is).
|
|
1279
|
+
const titles = async (store, cwd) => { try { return (await store.list(cwd)).map((i) => i.title); } catch { return []; } };
|
|
1219
1280
|
ctx.connection.fetch.register({
|
|
1220
1281
|
path: IDEAS_PATH, methods: ['POST'], requestBody: 'buffered',
|
|
1221
1282
|
fetch: async (request) => {
|
|
@@ -1226,17 +1287,36 @@ function installIdeas(ctx, home) {
|
|
|
1226
1287
|
const idea = ideaFrom(body.idea);
|
|
1227
1288
|
if (idea === undefined) return json({ error: 'bad request' }, 400);
|
|
1228
1289
|
try {
|
|
1229
|
-
|
|
1290
|
+
const { cwd, ...started } = await startIdeaChat(services(), body.sessionId, { title: idea.title, summary: idea.summary, why: idea.why }, request.signal, { plan: body.plan !== false });
|
|
1291
|
+
await markStarted(cwd, [idea]);
|
|
1292
|
+
return json(started);
|
|
1230
1293
|
} catch (error) { return json({ error: String(error?.message ?? error) }, status(error)); }
|
|
1231
1294
|
}
|
|
1232
1295
|
const llm = get('llm');
|
|
1233
1296
|
if (llm === undefined) return json({ error: 'this dsh profile cannot call a model' }, 409);
|
|
1234
1297
|
try {
|
|
1235
|
-
|
|
1298
|
+
// Not again: every earlier suggestion of this project (skipped, started, saved) and the Later list.
|
|
1299
|
+
const avoid = async (cwd) => [...(await titles(history, cwd)), ...(await titles(later, cwd))];
|
|
1300
|
+
return json(await suggestIdeas({ llm, sessionQuery: ctx.sessionQuery, subprocess: ctx.subprocess, defaultModel: get('agentDefaultModel'), avoid, record: history.record },
|
|
1301
|
+
body.sessionId, request.signal, body.locale));
|
|
1236
1302
|
} catch (error) { return json({ error: String(error?.message ?? error) }, status(error)); }
|
|
1237
1303
|
},
|
|
1238
1304
|
});
|
|
1239
|
-
|
|
1305
|
+
ctx.connection.fetch.register({
|
|
1306
|
+
path: HISTORY_PATH, methods: ['GET', 'POST'], requestBody: 'buffered',
|
|
1307
|
+
fetch: async (request) => {
|
|
1308
|
+
let body;
|
|
1309
|
+
if (request.method === 'GET') body = { sessionId: new URL(request.url).searchParams.get('sessionId') };
|
|
1310
|
+
else try { body = await request.json(); } catch { return json({ error: 'bad request' }, 400); }
|
|
1311
|
+
// POST: `remove` one entry (it may be suggested again) or `clear` the project's history.
|
|
1312
|
+
if (!isText(body?.sessionId) || (request.method === 'POST' && !isText(body.remove) && body.clear !== true)) return json({ error: 'bad request' }, 400);
|
|
1313
|
+
try {
|
|
1314
|
+
const cwd = await chatFolder(ctx.sessionQuery, body.sessionId, request.signal);
|
|
1315
|
+
if (request.method === 'GET') return json({ ideas: await history.list(cwd) });
|
|
1316
|
+
return json({ ideas: body.clear === true ? await history.clear(cwd) : await history.remove(cwd, body.remove) });
|
|
1317
|
+
} catch (error) { return json({ error: String(error?.message ?? error) }, Number.isInteger(error?.status) ? error.status : 500); }
|
|
1318
|
+
},
|
|
1319
|
+
});
|
|
1240
1320
|
const starting = new Set(); // `${cwd}\0${id}` of the stored ideas whose chat is starting now
|
|
1241
1321
|
ctx.connection.fetch.register({
|
|
1242
1322
|
path: LATER_PATH, methods: ['GET', 'POST'], requestBody: 'buffered',
|
|
@@ -1246,21 +1326,30 @@ function installIdeas(ctx, home) {
|
|
|
1246
1326
|
else try { body = await request.json(); } catch { return json({ error: 'bad request' }, 400); }
|
|
1247
1327
|
if (!isText(body?.sessionId)) return json({ error: 'bad request' }, 400);
|
|
1248
1328
|
const add = body.add === undefined ? undefined : ideaFrom(body.add);
|
|
1249
|
-
|
|
1250
|
-
|
|
1329
|
+
// `startAll`: the ids the user saw (2 or more, no repeats), so an idea that another window added meanwhile stays.
|
|
1330
|
+
const all = Array.isArray(body.startAll) && body.startAll.length >= 2 && body.startAll.length <= LATER_MAX && body.startAll.every((id) => isText(id))
|
|
1331
|
+
&& new Set(body.startAll).size === body.startAll.length ? body.startAll : undefined;
|
|
1332
|
+
const action = request.method === 'GET' ? 'list' : body.add !== undefined ? 'add' : isText(body.remove) ? 'remove' : isText(body.start) ? 'start'
|
|
1333
|
+
: body.startAll !== undefined ? 'startAll' : undefined;
|
|
1334
|
+
if (action === undefined || (action === 'add' && add === undefined) || (action === 'startAll' && all === undefined)) return json({ error: 'bad request' }, 400);
|
|
1251
1335
|
try {
|
|
1252
1336
|
const cwd = await chatFolder(ctx.sessionQuery, body.sessionId, request.signal);
|
|
1253
1337
|
if (action === 'list') return json({ ideas: await later.list(cwd) });
|
|
1254
1338
|
if (action === 'add') return json({ ideas: await later.add(cwd, add) });
|
|
1255
1339
|
if (action === 'remove') return json({ ideas: await later.remove(cwd, body.remove) });
|
|
1256
|
-
const
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1340
|
+
const ids = action === 'start' ? [body.start] : all;
|
|
1341
|
+
const list = await later.list(cwd);
|
|
1342
|
+
const ideas = ids.map((id) => list.find((i) => i.id === id));
|
|
1343
|
+
const keys = ids.map((id) => `${cwd}\0${id}`);
|
|
1344
|
+
if (ideas.includes(undefined) || keys.some((key) => starting.has(key))) return json({ error: 'this idea is no longer in the list' }, 409);
|
|
1345
|
+
for (const key of keys) starting.add(key);
|
|
1260
1346
|
try {
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1263
|
-
|
|
1347
|
+
const brief = ideas.map((i) => ({ title: i.title, summary: i.summary, why: i.why }));
|
|
1348
|
+
// Plan all always plans: many features at once without a plan would be a large unreviewed change.
|
|
1349
|
+
const { cwd: _, ...started } = await startIdeaChat(services(), body.sessionId, action === 'start' ? brief[0] : brief, request.signal, { plan: action === 'startAll' || body.plan !== false });
|
|
1350
|
+
await markStarted(cwd, brief);
|
|
1351
|
+
return json({ ...started, ideas: await later.remove(cwd, ids) });
|
|
1352
|
+
} finally { for (const key of keys) starting.delete(key); }
|
|
1264
1353
|
} catch (error) { return json({ error: String(error?.message ?? error) }, Number.isInteger(error?.status) ? error.status : 500); }
|
|
1265
1354
|
},
|
|
1266
1355
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-composer-plus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "DeepSeek Harness helpers: 11 extra UI languages (Türkçe, Español, Français, Deutsch, 日本語…), Changes panel, swipe-card feature ideas, sidebar search in message text, turn rollback with git file restore, delete chats to the Trash, 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",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"assets/features/*.webp"
|
|
74
74
|
],
|
|
75
75
|
"scripts": {
|
|
76
|
-
"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/content-search.test.mjs && node test/turn-rollback.test.mjs && node test/turn-rollback-client.test.mjs && node test/delete-chat.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",
|
|
76
|
+
"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/content-search.test.mjs && node test/turn-rollback.test.mjs && node test/turn-rollback-client.test.mjs && node test/delete-chat.test.mjs && node test/project-name.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 && node test/readme.test.mjs",
|
|
77
77
|
"build:locales": "node scripts/build-locales.mjs",
|
|
78
78
|
"prepublishOnly": "npm test"
|
|
79
79
|
},
|