dsh-composer-plus 1.5.0 → 1.6.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 +17 -9
- package/assets/features/resumeAfterRestart.webp +0 -0
- package/client.js +43 -23
- package/index.js +171 -1
- 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 +5 -3
package/index.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* through dsh's own plugin manager).
|
|
8
8
|
* - Mobile connect (Tether): runs the iPhone gateway (tether/gateway/server.mjs) in this process while the user switches it
|
|
9
9
|
* on; the switch is saved under the dsh home and is off by default.
|
|
10
|
+
* - Resume after restart: remembers the chats that were running when dsh quit and continues them at the next start; off by
|
|
11
|
+
* default (it starts model turns without a click).
|
|
10
12
|
*/
|
|
11
13
|
import { readFileSync } from 'node:fs';
|
|
12
14
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
@@ -25,8 +27,9 @@ export const CONTINUE_PATH = '/api/composer-plus.continue';
|
|
|
25
27
|
export const UPDATE_PATH = '/api/composer-plus.update';
|
|
26
28
|
export const SHOT_PATH = '/api/composer-plus.shot';
|
|
27
29
|
export const MOBILE_PATH = '/api/composer-plus.mobile';
|
|
30
|
+
export const RESUME_PATH = '/api/composer-plus.resume';
|
|
28
31
|
/** Feature screenshots shipped in assets/features/<id>.webp; only these ids are served (no path input). */
|
|
29
|
-
export const SHOTS = ['changesPanel', 'projectStatus', 'recentProjects', 'collapseProjects', 'statusOrder', 'notifications', 'allowForChat', 'continueButton', 'inputHistory', 'planToggle', 'keepFocus',
|
|
32
|
+
export const SHOTS = ['changesPanel', 'projectStatus', 'recentProjects', 'collapseProjects', 'statusOrder', 'notifications', 'allowForChat', 'continueButton', 'resumeAfterRestart', 'inputHistory', 'planToggle', 'keepFocus',
|
|
30
33
|
'queueReorder', 'mobileConnect', 'copyDeeplink', 'copyProjectPath', 'language', 'autoUpdate'];
|
|
31
34
|
const PACKAGE = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
32
35
|
const NPM_NAME = 'dsh-composer-plus';
|
|
@@ -251,6 +254,127 @@ export function mobileGateway({ upstreamUrl, dir, make = createGateway, retryMs
|
|
|
251
254
|
};
|
|
252
255
|
}
|
|
253
256
|
|
|
257
|
+
/** Resume after restart: `{ on, sessions: [{ id, at }] }` in one owner-only JSON file. A missing or broken file is off. */
|
|
258
|
+
export function resumeStore(file) {
|
|
259
|
+
let queue = Promise.resolve();
|
|
260
|
+
return {
|
|
261
|
+
async read() {
|
|
262
|
+
try {
|
|
263
|
+
const data = JSON.parse(await readFile(file, 'utf8'));
|
|
264
|
+
const sessions = Array.isArray(data?.sessions) ? data.sessions.filter((s) => isText(s?.id) && Number.isFinite(s?.at)) : [];
|
|
265
|
+
return { on: data?.on === true, sessions };
|
|
266
|
+
} catch { return { on: false, sessions: [] }; }
|
|
267
|
+
},
|
|
268
|
+
/** Writes run in order, each replacing the file atomically, so a quit in the middle never leaves a half-written file. */
|
|
269
|
+
write(data) {
|
|
270
|
+
const done = queue.then(async () => {
|
|
271
|
+
await mkdir(dirname(file), { recursive: true });
|
|
272
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
273
|
+
await writeFile(tmp, JSON.stringify(data), { mode: 0o600 });
|
|
274
|
+
await rename(tmp, file);
|
|
275
|
+
});
|
|
276
|
+
queue = done.catch(() => {});
|
|
277
|
+
return done;
|
|
278
|
+
},
|
|
279
|
+
flush: () => queue,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** At most this many chats continue at one start (newest first): a safety cap, not a queue. */
|
|
284
|
+
export const RESUME_MAX = 20;
|
|
285
|
+
/** A turn end that dsh caused, not the user: a quit (`aborted`, cause `disposed`; the goal driver's own shutdown cancels a
|
|
286
|
+
* goal round with cause `parent`) or a crash (`interrupted`, written when dsh opens the chat again). */
|
|
287
|
+
export const stoppedByDsh = (end) => end?.kind === 'interrupted' || (end?.kind === 'aborted' && ['disposed', 'parent'].includes(end.reason?.kind));
|
|
288
|
+
|
|
289
|
+
/** Resume after restart. `status()` and `turnEnd()` follow the top-level chats; `wake()` continues, once per start, the chats
|
|
290
|
+
* that were running when dsh stopped. A chat stays listed while it runs and when dsh stops its turn; any other turn end
|
|
291
|
+
* (done, failed, stopped by you) removes it. The list is saved at each change, so a crash keeps it too. */
|
|
292
|
+
export function resumeTracker({ store, isRoot, running = () => [], wakeOne, log = () => {}, now = Date.now }) {
|
|
293
|
+
let on = false;
|
|
294
|
+
let closed = false;
|
|
295
|
+
let carried = []; // the previous run's chats, until wake() takes them
|
|
296
|
+
const live = new Map(); // id → time it started running
|
|
297
|
+
const ends = new Map(); // id → newest turn end of this run; null while a turn is open
|
|
298
|
+
const list = () => [...carried.filter((s) => !live.has(s.id)), ...[...live].map(([id, at]) => ({ id, at }))];
|
|
299
|
+
const save = () => { if (!closed) store.write({ on, sessions: list() }).catch((error) => log(`resume list not saved: ${error?.message ?? error}`)); };
|
|
300
|
+
// Chats already running (switched on mid-turn, or a plugin reload) are tracked from now on.
|
|
301
|
+
const adopt = () => {
|
|
302
|
+
try { for (const id of running()) if (!live.has(id)) { live.set(id, now()); ends.set(id, null); } } catch { /* services gone */ }
|
|
303
|
+
};
|
|
304
|
+
const ready = store.read().then((data) => { on = data.on; carried = data.on ? data.sessions : []; if (on) adopt(); });
|
|
305
|
+
return {
|
|
306
|
+
ready,
|
|
307
|
+
isOn: () => on,
|
|
308
|
+
/** Save the switch; a failed save changes nothing. Switching off forgets the list. */
|
|
309
|
+
async setOn(next) {
|
|
310
|
+
await ready;
|
|
311
|
+
const before = { on, carried, live: [...live] };
|
|
312
|
+
on = next;
|
|
313
|
+
if (next) adopt(); else { carried = []; live.clear(); }
|
|
314
|
+
try { await store.write({ on, sessions: list() }); } catch (error) {
|
|
315
|
+
on = before.on; carried = before.carried; live.clear(); for (const [id, at] of before.live) live.set(id, at);
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
status(agent, status) {
|
|
320
|
+
const end = ends.get(agent.id);
|
|
321
|
+
if (status !== 'running') ends.delete(agent.id);
|
|
322
|
+
if (!on || closed || !isRoot(agent)) return;
|
|
323
|
+
if (status === 'running') { if (!live.has(agent.id)) { live.set(agent.id, now()); save(); } }
|
|
324
|
+
// An open turn without an end: the shutdown could not write it (dsh 0.2.0-rc.2 closes it as `interrupted` at the next
|
|
325
|
+
// start). Keep the chat; wakeSession() reads the saved log before it continues anything.
|
|
326
|
+
else if (end !== null && !stoppedByDsh(end) && live.delete(agent.id)) save();
|
|
327
|
+
},
|
|
328
|
+
turnStart(id) { ends.set(id, null); },
|
|
329
|
+
turnEnd(id, reason) { ends.set(id, reason); },
|
|
330
|
+
/** Plugin unload: no more writes (dsh's shutdown makes every chat idle); waits for the last write. */
|
|
331
|
+
close() { closed = true; return store.flush(); },
|
|
332
|
+
async wake() {
|
|
333
|
+
await ready;
|
|
334
|
+
const queue = [...carried].sort((a, b) => b.at - a.at).slice(0, RESUME_MAX);
|
|
335
|
+
if (queue.length === 0) return [];
|
|
336
|
+
carried = [];
|
|
337
|
+
save(); // forget them first: a crash while waking never continues the same chats twice
|
|
338
|
+
const results = [];
|
|
339
|
+
for (const { id } of queue) {
|
|
340
|
+
if (closed || !on) break;
|
|
341
|
+
try { results.push({ id, result: await wakeOne(id) }); } catch (error) { results.push({ id, result: 'failed' }); log(`resume ${id} failed: ${error?.message ?? error}`); }
|
|
342
|
+
}
|
|
343
|
+
return results;
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Continue one chat after a restart, if dsh (not the user) stopped its last turn: the chat gets the same hidden "continue"
|
|
349
|
+
* step as One-click Continue. When the stopped turn was a goal round, the goal is armed again instead (dsh disarms goals at
|
|
350
|
+
* every start and waits for `/goal resume`), and dsh's goal driver starts the next round, so the model can still mark the
|
|
351
|
+
* goal complete. A goal is never armed for an ordinary turn: dsh does not keep whether it was armed before the quit. */
|
|
352
|
+
export async function wakeSession({ sessionController, sessionQuery, goals, archived = () => false }, id) {
|
|
353
|
+
if (archived(id)) return 'archived';
|
|
354
|
+
const found = await sessionController.resolveAgent(id); // opens the chat; a crashed turn is closed as `interrupted` here
|
|
355
|
+
if ('error' in found) return 'unavailable';
|
|
356
|
+
const { agent } = found;
|
|
357
|
+
if (agent.status !== 'idle') return 'busy';
|
|
358
|
+
const observation = await sessionQuery.observeSession(id, { projectionMode: 'none' });
|
|
359
|
+
let start, end, goalRound;
|
|
360
|
+
try {
|
|
361
|
+
const events = observation.events;
|
|
362
|
+
end = events.findLast((e) => e.type === 'turn/start' || e.type === 'turn/end');
|
|
363
|
+
start = events.findLast((e) => e.type === 'turn/start');
|
|
364
|
+
goalRound = start !== undefined && events.some((e) => e.seq > start.seq && e.type === 'user/message' && e.data?.source?.kind === 'goal');
|
|
365
|
+
} finally { observation[Symbol.dispose]?.(); }
|
|
366
|
+
if (end?.type !== 'turn/end' || !stoppedByDsh(end.data?.reason)) return 'skipped';
|
|
367
|
+
let goal;
|
|
368
|
+
try { goal = goalRound ? goals?.get(agent) : undefined; } catch { goal = undefined; }
|
|
369
|
+
// A pause during the stopped turn would have ended it as stopped by the user; so a goal paused after that turn started was
|
|
370
|
+
// paused by dsh's shutdown (its goal driver pauses a round it cancels).
|
|
371
|
+
if (goal !== undefined && ((goal.phase === 'active' && goal.activation !== 'armed') || (goal.phase === 'paused' && goal.updatedAt >= start.time))) {
|
|
372
|
+
try { goals.resume(agent, { id: goal.id, revision: goal.revision }); return 'goal'; } catch { /* no goal rounds left: continue the turn */ }
|
|
373
|
+
}
|
|
374
|
+
agent.steer({ id: randomUUID(), role: 'user', content: [], source: { kind: CONTINUE_SOURCE } });
|
|
375
|
+
return 'continued';
|
|
376
|
+
}
|
|
377
|
+
|
|
254
378
|
const json = (value, status = 200) => Response.json(value, { status, headers: { 'cache-control': 'no-store' } });
|
|
255
379
|
const isText = (v, max = 512) => typeof v === 'string' && v.length > 0 && v.length <= max;
|
|
256
380
|
|
|
@@ -402,4 +526,50 @@ export function apply(ctx) {
|
|
|
402
526
|
return json({ on, available: mobile !== undefined, ...(on && mobile !== undefined ? mobile.state() : { running: false }) });
|
|
403
527
|
},
|
|
404
528
|
});
|
|
529
|
+
installResume(ctx, home);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Resume after restart: follow the top-level chats, and once dsh is ready, continue the ones its last shutdown stopped.
|
|
533
|
+
* Services that a profile may not have (`agents`, `goals`, `workspaceRegistry`, `appReady`) are read with `ctx.get`. */
|
|
534
|
+
function installResume(ctx, home) {
|
|
535
|
+
const get = (name) => { try { return ctx.get?.(name); } catch { return undefined; } };
|
|
536
|
+
const isRoot = (agent) => {
|
|
537
|
+
const agents = get('agents');
|
|
538
|
+
return agents?.roots?.().includes(agent) === true && agent.session?.header?.origin !== 'subagent';
|
|
539
|
+
};
|
|
540
|
+
const tracker = resumeTracker({
|
|
541
|
+
store: resumeStore(join(home, 'storages', 'dsh-composer-plus', 'resume.json')),
|
|
542
|
+
isRoot,
|
|
543
|
+
running: () => (get('agents')?.roots?.() ?? []).filter((a) => a.status === 'running' && isRoot(a)).map((a) => a.id),
|
|
544
|
+
// ponytail: an unreadable archive list counts as "not archived"; dsh's own archived-session gate still blocks the step.
|
|
545
|
+
wakeOne: (id) => wakeSession({ sessionController: ctx.sessionController, sessionQuery: ctx.sessionQuery, goals: get('goals'),
|
|
546
|
+
archived: (sid) => { try { return get('workspaceRegistry')?.archivedSessionIds?.includes(sid) === true; } catch { return false; } } }, id),
|
|
547
|
+
log: (text) => ctx.logger?.warn?.(`dsh-composer-plus: ${text}`),
|
|
548
|
+
});
|
|
549
|
+
ctx.on('agent/status', ({ agent, status }) => tracker.status(agent, status));
|
|
550
|
+
ctx.on('session/event', (session, event) => {
|
|
551
|
+
if (event.type === 'turn/start') tracker.turnStart(session.id);
|
|
552
|
+
else if (event.type === 'turn/end') tracker.turnEnd(session.id, event.data?.reason);
|
|
553
|
+
});
|
|
554
|
+
// Wake after dsh's own startup (web server, plugins) is done; a profile without the ready signal waits a moment instead.
|
|
555
|
+
ctx.effect?.(() => {
|
|
556
|
+
let done = false;
|
|
557
|
+
const run = () => { if (!done) { done = true; tracker.wake().catch(() => {}); } };
|
|
558
|
+
const ready = get('appReady');
|
|
559
|
+
const cancel = ready?.onReady !== undefined ? ready.onReady(run) : (() => { const t = setTimeout(run, 3000); return () => clearTimeout(t); })();
|
|
560
|
+
return () => { done = true; cancel?.(); tracker.close(); };
|
|
561
|
+
}, 'dsh-composer-plus: resume after restart');
|
|
562
|
+
ctx.connection.fetch.register({
|
|
563
|
+
path: RESUME_PATH, methods: ['GET', 'POST'], requestBody: 'buffered',
|
|
564
|
+
fetch: async (request) => {
|
|
565
|
+
if (request.method === 'POST') {
|
|
566
|
+
let body;
|
|
567
|
+
try { body = await request.json(); } catch { return json({ error: 'bad request' }, 400); }
|
|
568
|
+
if (typeof body?.on !== 'boolean') return json({ error: 'bad request' }, 400);
|
|
569
|
+
try { await tracker.setOn(body.on); } catch (error) { return json({ error: String(error?.message ?? error) }, 500); }
|
|
570
|
+
}
|
|
571
|
+
await tracker.ready;
|
|
572
|
+
return json({ on: tracker.isOn() });
|
|
573
|
+
},
|
|
574
|
+
});
|
|
405
575
|
}
|
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, Warteschlange per Drag-and-drop, Links, Benachrichtigungen, iPhone-Zugriff (Tether), 11 Sprachen und automatische Updates."
|
|
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."
|
|
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, queue drag-and-drop, deeplinks, notifications, iPhone access (Tether), 11 UI languages and automatic updates."
|
|
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."
|
|
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, reordenar la cola arrastrando, enlaces, notificaciones, acceso desde iPhone (Tether), 11 idiomas y actualizaciones automáticas."
|
|
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."
|
|
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, 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, 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, urutkan antrean dengan seret, tautan, notifikasi, akses dari iPhone (Tether), 11 bahasa, dan pembaruan otomatis."
|
|
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."
|
|
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": "変更パネル、入力履歴、Shift+Tab で /plan
|
|
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, reordenar a fila arrastando, links, notificações, acesso pelo iPhone (Tether), 11 idiomas e atualizações automáticas."
|
|
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."
|
|
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, 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, 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, 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, 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
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, queue drag-and-drop, deeplinks, Auto Review allow-for-chat, notifications, iPhone access with the Tether app, automatic updates",
|
|
3
|
+
"version": "1.6.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",
|
|
5
5
|
"icon": "./assets/icon.svg",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"queue",
|
|
39
39
|
"recent-projects",
|
|
40
40
|
"remote-control",
|
|
41
|
+
"restart",
|
|
42
|
+
"resume",
|
|
41
43
|
"sort-by-status",
|
|
42
44
|
"tailscale",
|
|
43
45
|
"tether",
|
|
@@ -64,7 +66,7 @@
|
|
|
64
66
|
"assets/features/*.webp"
|
|
65
67
|
],
|
|
66
68
|
"scripts": {
|
|
67
|
-
"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/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",
|
|
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",
|
|
68
70
|
"build:locales": "node scripts/build-locales.mjs",
|
|
69
71
|
"prepublishOnly": "npm test"
|
|
70
72
|
},
|