dsh-composer-plus 1.19.1 → 1.20.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/index.js CHANGED
@@ -6436,8 +6436,9 @@ export function installBackup(ctx, home, { graceMs = AUTO_BACKUP_GRACE_MS, retry
6436
6436
  /** Task board (Host half): a kanban board of dsh's real chats. The client draws the cards from dsh's own stores (the chat
6437
6437
  * list, each chat's live state and the projects); the Host keeps the switch and the record of the chats that the board
6438
6438
  * started: `storages/dsh-composer-plus/board-chats.json`, `{ chats: { [sessionId]: { prompt, tags, at } } }`, owner-only.
6439
- * A board start makes a new chat in a dsh project with the user's prompt (dsh's own title service names the chat); one
6440
- * short call to the new chat's model writes its tags. The old notebook board (`board.json`) is not read or changed. */
6439
+ * New task opens dsh's own new chat screen; the client sends `{sessionId}` of that chat. When its first message of its own
6440
+ * arrives (the user's, from dsh's composer: attachments, commands and the model menu as usual), the Host keeps its text
6441
+ * as the card's prompt and one short call to the chat's model writes its tags. The old `board.json` is not read. */
6441
6442
  export const BOARD_PATH = '/api/composer-plus.board';
6442
6443
  export const BOARD_CHATS_PATH = '/api/composer-plus.board-chats';
6443
6444
  export const BOARD_LIMITS = { prompt: 20000, tags: 3, tag: 24, chats: 2000 };
@@ -6445,8 +6446,6 @@ const BOARD_TAG_INPUT = 4000;
6445
6446
  const BOARD_TAG_TOKENS = 200;
6446
6447
  const BOARD_TAG_TIMEOUT_MS = 60000;
6447
6448
  export const BOARD_TAG_SYSTEM = `You label a coding task for a kanban board. Read the user's request and answer with only a JSON array of 1 to ${BOARD_LIMITS.tags} short tags, for example ["frontend","bug"]. Each tag is one or two lower-case words, at most ${BOARD_LIMITS.tag} characters, in the language of the request. No other text.`;
6448
- const BRANCH_DIR = /^(.+?)[\\/]\.worktrees[\\/][^\\/]+(?:[\\/].*)?$/;
6449
- const trimSlash = (p) => (p.length > 1 ? p.replace(/[\\/]+$/, '') : p);
6450
6449
  const boardLine = (raw) => String(raw ?? '').replace(/\s+/g, ' ').trim();
6451
6450
 
6452
6451
  /** The model's tags: the first JSON array of strings, each one line, lower case, at most BOARD_LIMITS.tag characters, no
@@ -6510,63 +6509,94 @@ export function boardChatsStore(file) {
6510
6509
  };
6511
6510
  }
6512
6511
 
6513
- /** The dsh project of a board start, by its workspace id: a registered workspace that is not a Branch per chat folder. */
6514
- export function boardProject(registry, id) {
6515
- if (!isText(id)) return undefined;
6516
- let list;
6517
- try { list = registry?.list?.() ?? []; } catch { list = []; }
6518
- return list.find((w) => w?.id === id && isText(w.path, 4096) && !BRANCH_DIR.test(trimSlash(w.path)));
6512
+ /** The text of a chat's first message of its own: the first `user/message` from the user at or after `from` (a fork's
6513
+ * inherited events come first, rule 10) that no rollback withdrew (Composer Plus's and dsh-rewind-plugin's markers, rule 9),
6514
+ * at most BOARD_LIMITS.prompt characters; undefined when there is none. */
6515
+ export function boardPromptOf(events, from = 0) {
6516
+ const spans = rollbackSpans(events, [ROLLBACK_SOURCE, REWIND_SOURCE]);
6517
+ for (const e of events) {
6518
+ if (e?.type !== 'user/message' || e.data?.source?.kind !== 'user' || !Number.isSafeInteger(e.seq) || e.seq < from) continue;
6519
+ if (spans.some(([a, b]) => e.seq >= a && e.seq <= b)) continue;
6520
+ const text = textBlocks(e.data?.content).trim();
6521
+ if (text !== '') return text.slice(0, BOARD_LIMITS.prompt);
6522
+ }
6523
+ return undefined;
6519
6524
  }
6520
6525
 
6521
- /** One short call to `route` for the tags of a prompt; [] when the call fails or answers nothing usable. */
6522
- export async function boardTagsFor({ llm, route }, prompt, signal) {
6523
- if (llm?.prepareCall === undefined || route === undefined) return [];
6524
- const timeout = AbortSignal.timeout(BOARD_TAG_TIMEOUT_MS);
6526
+ /** One short background call to `route` with `system` and one user message: the answer's text. The lowest reasoning effort
6527
+ * keeps it fast; the output cap never goes above the model's own default. Throws when the call ends with an error. */
6528
+ async function boardCall({ llm, route }, system, text, { tokens, timeoutMs }, signal) {
6529
+ const timeout = AbortSignal.timeout(timeoutMs);
6525
6530
  const callSignal = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
6526
6531
  const call = await llm.prepareCall(route, callSignal, (controls, model) => {
6527
- // The lowest reasoning effort keeps it fast; the output cap never goes above the model's own default.
6528
6532
  const first = model?.reasoning?.efforts?.[0];
6529
- const cap = Number.isSafeInteger(model?.defaultMaxTokens) && model.defaultMaxTokens > 0 ? Math.min(BOARD_TAG_TOKENS, model.defaultMaxTokens) : BOARD_TAG_TOKENS;
6533
+ const cap = Number.isSafeInteger(model?.defaultMaxTokens) && model.defaultMaxTokens > 0 ? Math.min(tokens, model.defaultMaxTokens) : tokens;
6530
6534
  return { ...controls, maxTokens: cap, ...(first === undefined ? {} : { reasoningEffort: first.id }) };
6531
6535
  });
6532
- let text = '';
6536
+ let out = '';
6533
6537
  let finish;
6534
- for await (const chunk of call.stream({ ...call.config, system: BOARD_TAG_SYSTEM, signal: callSignal,
6535
- messages: [{ role: 'user', content: [{ type: 'text', text: String(prompt).slice(0, BOARD_TAG_INPUT) }] }] })) {
6536
- if (chunk.type === 'text-delta') text += chunk.text;
6538
+ for await (const chunk of call.stream({ ...call.config, system, signal: callSignal, messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {
6539
+ if (chunk.type === 'text-delta') out += chunk.text;
6537
6540
  else if (chunk.type === 'finish') finish = chunk.reason;
6538
6541
  }
6539
6542
  if (finish?.kind === 'error' || finish?.kind === 'aborted') throw new Error(finish.failure?.message || `the model call ended: ${finish.kind}`);
6540
- return boardTags(text);
6541
- }
6542
-
6543
- /** Start a board task: a new chat in the project's dsh workspace with `prompt` as its first message (a `/command` that dsh
6544
- * knows runs as that command), then the record. All or none: when the prompt cannot be sent, the new chat is archived and
6545
- * the error says so. The prompt is sent with its own timeout, not the request's signal, so a closed request does not leave
6546
- * an empty chat. Returns `{ sessionId, agent }`. */
6547
- export async function startBoardChat({ sessionController, commands, registry, store }, project, prompt) {
6548
- const text = String(prompt ?? '').trim();
6549
- if (text === '' || text.length > BOARD_LIMITS.prompt) throw refused(`a task needs a prompt of at most ${BOARD_LIMITS.prompt} characters`, 400);
6550
- const workspace = boardProject(registry, project);
6551
- if (workspace === undefined) throw refused('this is not a dsh project', 404);
6552
- const { sessionId } = await sessionController.create({ workspaceId: workspace.id });
6553
- let agent;
6554
- try {
6555
- const found = await sessionController.resolveAgent(sessionId);
6556
- if ('error' in found) throw new Error('the new chat could not start');
6557
- agent = found.agent;
6558
- const name = /^\/([\w:.-]+)(?:\s|$)/.exec(text)?.[1];
6559
- let done;
6560
- if (name !== undefined && commands?.find?.(agent, name) !== undefined) {
6561
- done = await commands.execute(agent, text, [], AbortSignal.timeout(STOP_WAIT_MS));
6562
- if (done?.result?.kind !== 'success') throw new Error(`/${name} failed${done?.result?.text ? `: ${done.result.text}` : ''}`);
6563
- } else await sessionController.prompt({ requestId: randomUUID(), sessionId, mode: 'queue', content: [{ type: 'text', text }] }, AbortSignal.timeout(STOP_WAIT_MS));
6564
- await store.change((chats) => { chats[sessionId] = { prompt: text, tags: [], at: Date.now() }; });
6565
- } catch (error) {
6566
- const undone = await Promise.resolve(registry?.archiveSession?.(sessionId, { stopActivity: true })).then(() => registry?.archiveSession !== undefined, () => false);
6567
- throw Object.assign(new Error(`${error?.message ?? error}; ${undone ? 'the new chat was archived' : `the new chat ${sessionId} stays empty`}`), { status: Number.isInteger(error?.status) ? error.status : 502 });
6543
+ return out;
6544
+ }
6545
+
6546
+ /** One short call to `route` for the tags of a prompt; [] when it answers nothing usable. */
6547
+ export async function boardTagsFor({ llm, route }, prompt, signal) {
6548
+ if (llm?.prepareCall === undefined || route === undefined) return [];
6549
+ return boardTags(await boardCall({ llm, route }, BOARD_TAG_SYSTEM, String(prompt).slice(0, BOARD_TAG_INPUT), { tokens: BOARD_TAG_TOKENS, timeoutMs: BOARD_TAG_TIMEOUT_MS }, signal));
6550
+ }
6551
+
6552
+ /* Progress on hover: what a running chat does now. "fast" (the default) reads the chat's log only; "ai" also asks the
6553
+ * chat's model for a one- or two-sentence summary (Settings → Task board → Progress on hover). */
6554
+ export const BOARD_PROGRESS_PATH = '/api/composer-plus.board-progress';
6555
+ export const BOARD_PROGRESS_MODES = ['fast', 'ai'];
6556
+ export const BOARD_PROGRESS = { detail: 120, said: 200, summary: 240, input: 6000, tokens: 200, timeoutMs: 30000, gapMs: 20000, kept: 200 };
6557
+ export const BOARD_PROGRESS_SYSTEM = `You write the progress note of a running coding task on a kanban card. Read the newest part of the task's chat. Answer with one or two short sentences, at most ${BOARD_PROGRESS.summary} characters: what the agent does now and how far the task is. Use the language of the user's messages. Plain text only: no Markdown, no other text.`;
6558
+ /** Tool calls that the progress does not show as "the step": the todo list has its own line, a plan is not work. */
6559
+ const BOARD_PROGRESS_QUIET = new Set(['todo_write', 'plan_next_steps']);
6560
+ /** A tool call in a few words: its description (bash, a subagent), else its command's first line, a path or a pattern. */
6561
+ const toolDetail = (args) => {
6562
+ if (!plainObject(args)) return '';
6563
+ const pick = [args.description, args.command, args.file_path, args.path, args.pattern, args.url, args.query].find((v) => typeof v === 'string' && v.trim() !== '');
6564
+ return pick === undefined ? '' : cutEnd(boardLine(pick.trim().split('\n')[0]), BOARD_PROGRESS.detail);
6565
+ };
6566
+
6567
+ /** What a running chat does now, from its own events (from `from`: a fork's inherited events come first, rule 10; steps that
6568
+ * a rollback withdrew are left out, rule 9): `since` (the start of the newest turn, epoch ms), `todos` (`{done, total,
6569
+ * current?}` of the newest todo list), `tool` (`{name, detail, running}`: the newest tool call of that turn, running while it
6570
+ * has no result) and `said` (the last line of the agent's newest text in that turn). */
6571
+ export function boardProgressOf(events, from = 0) {
6572
+ const hidden = withdrawn(events);
6573
+ const out = {};
6574
+ let call;
6575
+ for (const [i, e] of events.entries()) {
6576
+ if (i < from || hidden(e.seq)) continue;
6577
+ if (e.type === 'turn/start') { out.since = Number.isFinite(e.time) ? e.time : undefined; call = undefined; delete out.said; }
6578
+ else if (e.type === 'todo/write' && Array.isArray(e.data?.todos)) {
6579
+ const todos = e.data.todos.filter((x) => typeof x?.content === 'string' && x.content.trim() !== '');
6580
+ const current = todos.find((x) => x.status === 'in_progress');
6581
+ out.todos = todos.length === 0 ? undefined
6582
+ : { done: todos.filter((x) => x.status === 'completed').length, total: todos.length, ...(current === undefined ? {} : { current: cutEnd(boardLine(current.content), BOARD_PROGRESS.detail) }) };
6583
+ } else if (e.type === 'tool/call' && isText(e.data?.name, 200) && !BOARD_PROGRESS_QUIET.has(e.data.name)) {
6584
+ call = { id: e.data.callId, name: e.data.name, detail: toolDetail(argsOf(e.data.arguments)), running: true };
6585
+ } else if (e.type === 'tool/result' && call !== undefined && e.data?.message?.source?.callId === call.id) call.running = false;
6586
+ else if (e.type === 'assistant/message') {
6587
+ const line = textBlocks(e.data?.message?.content).split('\n').map(boardLine).filter(Boolean).at(-1);
6588
+ if (line !== undefined) out.said = cutEnd(line, BOARD_PROGRESS.said);
6589
+ }
6568
6590
  }
6569
- return { sessionId, agent };
6591
+ if (call !== undefined) { const { id: _, ...tool } = call; out.tool = tool; }
6592
+ return JSON.parse(JSON.stringify(out));
6593
+ }
6594
+
6595
+ /** One short call for the progress summary of a chat's newest part; '' when it answers nothing. */
6596
+ export async function boardSummaryFor({ llm, route }, transcript, signal) {
6597
+ if (llm?.prepareCall === undefined || route === undefined) return '';
6598
+ const text = await boardCall({ llm, route }, BOARD_PROGRESS_SYSTEM, `The task's chat (its newest part):\n\n${transcript}\n\nThe progress note:`, { tokens: BOARD_PROGRESS.tokens, timeoutMs: BOARD_PROGRESS.timeoutMs }, signal);
6599
+ return cutEnd(boardLine(text.replace(/<think>[\s\S]*?<\/think>/g, '')), BOARD_PROGRESS.summary);
6570
6600
  }
6571
6601
 
6572
6602
  /** Task board (Host half): the switch route and, while it is on, the board's chat route. */
@@ -6577,8 +6607,13 @@ export function installBoard(ctx, home, dir = join(home, 'storages', 'dsh-compos
6577
6607
  let on = true;
6578
6608
  const ready = sw.read().then((v) => { on = v; }, () => {});
6579
6609
  let closed = false;
6580
- const tagging = new Set(); // background tag calls; unload stops them
6610
+ const tagging = new Set(); // background tag and summary calls; unload stops them
6581
6611
  ctx.effect?.(() => () => { closed = true; for (const c of tagging) c.abort(); }, 'dsh-composer-plus: task board');
6612
+ /** The chat's own model (its selection), else dsh's default. */
6613
+ const chatRoute = async (agent) => {
6614
+ const current = agent === undefined ? undefined : ctx.sessionController.agents?.selectionFor?.(agent)?.current;
6615
+ return isText(current?.provider) && isText(current?.model) ? { provider: current.provider, model: current.model } : routeOf(ctx, get);
6616
+ };
6582
6617
  /** The tags of a new chat, in the background: the chat's own model (its selection), else dsh's default. */
6583
6618
  const tag = async (sessionId, agent, prompt) => {
6584
6619
  const llm = get('llm');
@@ -6586,13 +6621,27 @@ export function installBoard(ctx, home, dir = join(home, 'storages', 'dsh-compos
6586
6621
  const mine = new AbortController();
6587
6622
  tagging.add(mine);
6588
6623
  try {
6589
- const current = ctx.sessionController.agents?.selectionFor?.(agent)?.current;
6590
- const route = isText(current?.provider) && isText(current?.model) ? { provider: current.provider, model: current.model } : await routeOf(ctx, get);
6591
- const tags = await boardTagsFor({ llm, route }, prompt, mine.signal);
6624
+ const tags = await boardTagsFor({ llm, route: await chatRoute(agent) }, prompt, mine.signal);
6592
6625
  if (tags.length > 0 && !closed) await store.change((chats) => { if (chats[sessionId] !== undefined) chats[sessionId] = { ...chats[sessionId], tags }; });
6593
6626
  } catch (error) { if (!closed) ctx.logger?.warn?.(`dsh-composer-plus: task board tags failed: ${String(error?.message ?? error).slice(0, 300)}`); }
6594
6627
  finally { tagging.delete(mine); }
6595
6628
  };
6629
+ /** A board chat that has no prompt yet gets its first message of its own as its prompt, and its tags. Once per chat: the
6630
+ * record changes one at a time and sets the prompt only while it is empty, so a second message, or the route and the
6631
+ * event at the same time, do not write it again or call the model again. */
6632
+ const name = async (sessionId, agent, prompt) => {
6633
+ if (prompt === undefined || closed || store.list()[sessionId]?.prompt !== '') return;
6634
+ try {
6635
+ let set = false;
6636
+ await store.change((chats) => { if (chats[sessionId]?.prompt === '') { chats[sessionId] = { ...chats[sessionId], prompt }; set = true; } });
6637
+ if (set) await tag(sessionId, agent, prompt);
6638
+ } catch (error) { ctx.logger?.warn?.(`dsh-composer-plus: task board record failed: ${String(error?.message ?? error).slice(0, 300)}`); }
6639
+ };
6640
+ ctx.on?.('session/event', (session, event) => {
6641
+ if (!on || closed || event?.type !== 'user/message') return;
6642
+ const from = Number.isSafeInteger(session.inheritedEventCount) && session.inheritedEventCount > 0 ? session.inheritedEventCount : 0;
6643
+ name(session.id, get('agents')?.get?.(session.id), boardPromptOf([event], from));
6644
+ });
6596
6645
  let switching = ready;
6597
6646
  ctx.connection.fetch.register({
6598
6647
  path: BOARD_PATH, methods: ['GET', 'POST'], requestBody: 'buffered',
@@ -6611,7 +6660,9 @@ export function installBoard(ctx, home, dir = join(home, 'storages', 'dsh-compos
6611
6660
  return json({ on });
6612
6661
  },
6613
6662
  });
6614
- /** GET `?rev=N`: `{rev, chats}` (`{rev, same}` when nothing changed since rev). POST `{project, prompt}`: start a chat. */
6663
+ /** GET `?rev=N`: `{rev, chats}` (`{rev, same}` when nothing changed since rev). POST `{sessionId}`: New task opened this
6664
+ * dsh chat; it goes on the board (Todo until its first message). A message that came before the POST is read from the
6665
+ * chat's log, so a fast first message still names the card. */
6615
6666
  ctx.connection.fetch.register({
6616
6667
  path: BOARD_CHATS_PATH, methods: ['GET', 'POST'], requestBody: 'buffered',
6617
6668
  fetch: async (request) => {
@@ -6626,14 +6677,70 @@ export function installBoard(ctx, home, dir = join(home, 'storages', 'dsh-compos
6626
6677
  }
6627
6678
  let body;
6628
6679
  try { body = await request.json(); } catch { return json({ error: 'bad request' }, 400); }
6629
- if (!plainObject(body) || !isText(body.project) || typeof body.prompt !== 'string' || body.prompt.length > BOARD_LIMITS.prompt + 1000) return json({ error: 'bad request' }, 400);
6630
- const { sessionId, agent } = await startBoardChat({ sessionController: ctx.sessionController, commands: get('commands'), registry: get('workspaceRegistry'), store }, body.project, body.prompt);
6631
- tag(sessionId, agent, body.prompt.trim());
6680
+ if (!plainObject(body) || Object.keys(body).join() !== 'sessionId' || !isText(body.sessionId, 128) || body.sessionId === '__proto__') return json({ error: 'bad request' }, 400);
6681
+ const { sessionId } = body;
6682
+ const found = await ctx.sessionController.resolveAgent(sessionId);
6683
+ if (found === undefined || 'error' in found) return json({ error: 'this is not a dsh chat' }, 404);
6684
+ if (store.list()[sessionId] === undefined) await store.change((chats) => { chats[sessionId] ??= { prompt: '', tags: [], at: Date.now() }; });
6685
+ const session = found.agent?.session;
6686
+ const from = Number.isSafeInteger(session?.inheritedEventCount) && session.inheritedEventCount > 0 ? session.inheritedEventCount : 0;
6687
+ await name(sessionId, found.agent, boardPromptOf(session?.snapshotEvents?.() ?? [], from));
6632
6688
  return json({ rev: store.rev(), sessionId });
6633
6689
  } catch (error) { return json({ error: String(error?.message ?? error) }, Number.isInteger(error?.status) ? error.status : 500); }
6634
6690
  },
6635
6691
  });
6636
- return { store, ready, tag, isOn: () => on };
6692
+ /** Progress on hover. The mode is a setting (`board-progress.json`, owner-only). In "ai" mode one summary call per chat
6693
+ * runs at a time, in the background: a new one only when the chat moved on and BOARD_PROGRESS.gapMs after the last one
6694
+ * started; the answer gives the newest summary that is ready (`pending` while a call runs). */
6695
+ const progress = settingsStore(join(dir, 'board-progress.json'), (v) => ({ mode: v?.mode === 'ai' ? 'ai' : 'fast' }));
6696
+ const summaries = new Map(); // sessionId → { key, at, text?, busy }
6697
+ const summaryOf = (sessionId, agent, events, from) => {
6698
+ const key = events.at(-1)?.seq;
6699
+ const kept = summaries.get(sessionId);
6700
+ const llm = get('llm');
6701
+ if (!closed && llm?.prepareCall !== undefined && kept?.busy !== true && kept?.key !== key && Date.now() - (kept?.at ?? 0) >= BOARD_PROGRESS.gapMs) {
6702
+ const entry = { key, at: Date.now(), text: kept?.text, busy: true };
6703
+ summaries.delete(sessionId); summaries.set(sessionId, entry);
6704
+ if (summaries.size > BOARD_PROGRESS.kept) summaries.delete(summaries.keys().next().value);
6705
+ const mine = new AbortController();
6706
+ tagging.add(mine);
6707
+ (async () => {
6708
+ try { const text = await boardSummaryFor({ llm, route: await chatRoute(agent) }, goalTranscript(events, from, BOARD_PROGRESS.input), mine.signal); if (text !== '') entry.text = text; }
6709
+ catch (error) { if (!closed) ctx.logger?.warn?.(`dsh-composer-plus: task board summary failed: ${String(error?.message ?? error).slice(0, 300)}`); }
6710
+ finally { entry.busy = false; tagging.delete(mine); }
6711
+ })();
6712
+ }
6713
+ const now = summaries.get(sessionId);
6714
+ return { ...(now?.text ? { summary: now.text } : {}), ...(now?.busy ? { pending: true } : {}) };
6715
+ };
6716
+ /** GET `?sessionId=`: `{mode, since?, todos?, tool?, said?, summary?, pending?}` of a chat that dsh has open (404
6717
+ * otherwise; 409 while the board is off). GET: `{mode}`. POST `{mode}`: "fast" or "ai" (Settings only). */
6718
+ ctx.connection.fetch.register({
6719
+ path: BOARD_PROGRESS_PATH, methods: ['GET', 'POST'], requestBody: 'buffered',
6720
+ fetch: async (request) => {
6721
+ await ready;
6722
+ try {
6723
+ if (request.method === 'POST') {
6724
+ let body;
6725
+ try { body = await request.json(); } catch { return json({ error: 'bad request' }, 400); }
6726
+ if (!plainObject(body) || Object.keys(body).join() !== 'mode' || !BOARD_PROGRESS_MODES.includes(body.mode)) return json({ error: 'bad request' }, 400);
6727
+ return json(await progress.change(() => ({ mode: body.mode })));
6728
+ }
6729
+ const { mode } = await progress.read();
6730
+ const sessionId = new URL(request.url).searchParams.get('sessionId');
6731
+ if (sessionId === null) return json({ mode });
6732
+ if (!on) return json({ error: 'Task board is off' }, 409);
6733
+ if (!isText(sessionId, 128)) return json({ error: 'bad request' }, 400);
6734
+ const agent = get('agents')?.get?.(sessionId);
6735
+ const session = agent?.session;
6736
+ if (typeof session?.snapshotEvents !== 'function') return json({ error: 'dsh has no open chat with this id' }, 404);
6737
+ const events = session.snapshotEvents();
6738
+ const from = Number.isSafeInteger(session.inheritedEventCount) && session.inheritedEventCount > 0 ? session.inheritedEventCount : 0;
6739
+ return json({ mode, ...boardProgressOf(events, from), ...(mode === 'ai' ? summaryOf(sessionId, agent, events, from) : {}) });
6740
+ } catch (error) { return json({ error: String(error?.message ?? error) }, 500); }
6741
+ },
6742
+ });
6743
+ return { store, ready, tag, name, isOn: () => on, progress };
6637
6744
  }
6638
6745
 
6639
6746
  /* Planned steps, after HarnessMM (arXiv 2610.32813, "guarded decision reuse"): the model plans its next commands with the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-composer-plus",
3
- "version": "1.19.1",
3
+ "version": "1.20.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",
@@ -1,18 +1,18 @@
1
1
  {
2
- "version": "1.19.1",
2
+ "version": "1.20.0",
3
3
  "notes": {
4
- "en": "## New\n\n- **Task board** has a search box and a filter above its project list. Type in **Search projects** to find a project by its name (case and accents do not matter, `Esc` empties the box). Click **Active in the last 2 hours** to show only projects with a chat that waits, runs, finished unseen or changed in the last 2 hours. This choice stays on this device. In Tether, tap the board's title to open the same project list.\n\n## Changes\n\n- **Task board** cards in a narrow window: a title breaks only between words, not inside a word. The time moved to the end of the tag row, so the title has the full width of the card.",
5
- "zh": "## 新功能\n\n- **任务看板** 的项目列表上方新增了搜索框和筛选器。在 **搜索项目** 中输入,即可按名称查找项目(不区分大小写和重音符号,按 `Esc` 清空搜索框)。点击 **最近 2 小时内活跃**,只显示有对话正在等待、正在运行、已结束但尚未查看,或在最近 2 小时内有变化的项目。此选择保存在此设备上。在 Tether 中,点按看板的标题即可打开同样的项目列表。\n\n## 变更\n\n- 窄窗口中的 **任务看板** 卡片:标题只在词与词之间换行,不会在词的中间断开。时间移到了标签行的末尾,因此标题可以使用卡片的全部宽度。",
6
- "tr": "## Yeni\n\n- **Görev panosu**, proje listesinin üstünde bir arama kutusu ve bir filtre gösterir. Bir projeyi adıyla bulmak için **Projelerde ara** kutusuna yazın (büyük/küçük harf ve aksanlar fark etmez, `Esc` kutuyu boşaltır). Yalnızca bekleyen, çalışan, siz görmeden biten ya da son 2 saatte değişen bir sohbeti olan projeleri göstermek için **Son 2 saatte aktif olanlar** seçeneğine tıklayın. Bu seçim bu cihazda kalır. Tether'da aynı proje listesini açmak için panonun başlığına dokunun.\n\n## Değişiklikler\n\n- Dar bir penceredeki **Görev panosu** kartları: başlık yalnızca sözcükler arasında bölünür, bir sözcüğün ortasında bölünmez. Zaman, etiket satırının sonuna taşındı; böylece başlık kartın tüm genişliğini kullanır.",
7
- "es": "## Novedades\n\n- El **Tablero de tareas** tiene un cuadro de búsqueda y un filtro encima de su lista de proyectos. Escribe en **Buscar proyectos** para encontrar un proyecto por su nombre (no importan las mayúsculas ni los acentos; `Esc` vacía el cuadro). Haz clic en **Activos en las últimas 2 horas** para mostrar solo los proyectos con un chat que espera, está en ejecución, terminó sin que lo vieras o cambió en las últimas 2 horas. Esta elección se guarda en este dispositivo. En Tether, toca el título del tablero para abrir la misma lista de proyectos.\n\n## Cambios\n\n- Tarjetas del **Tablero de tareas** en una ventana estrecha: un título solo se corta entre palabras, no dentro de una palabra. La hora pasó al final de la fila de etiquetas, así que el título tiene todo el ancho de la tarjeta.",
8
- "pt-BR": "## Novidades\n\n- O **Quadro de tarefas** tem uma caixa de busca e um filtro acima da lista de projetos. Digite em **Buscar projetos** para encontrar um projeto pelo nome (maiúsculas e acentos não importam; `Esc` esvazia a caixa). Clique em **Ativos nas últimas 2 horas** para mostrar só os projetos com um chat que espera, está em execução, terminou sem que você visse ou mudou nas últimas 2 horas. Essa escolha fica guardada neste dispositivo. No Tether, toque no título do quadro para abrir a mesma lista de projetos.\n\n## Alterações\n\n- Cartões do **Quadro de tarefas** em uma janela estreita: o título quebra só entre palavras, não no meio de uma palavra. O horário foi para o fim da linha de etiquetas, então o título tem toda a largura do cartão.",
9
- "fr": "## Nouveautés\n\n- Le **Tableau des tâches** a une zone de recherche et un filtre au-dessus de sa liste de projets. Tapez dans **Rechercher des projets** pour trouver un projet par son nom (la casse et les accents ne comptent pas, `Esc` vide la zone). Cliquez sur **Actifs au cours des 2 dernières heures** pour n’afficher que les projets dont une discussion attend, s’exécute, s’est terminée sans que vous l’ayez vue ou a changé au cours des 2 dernières heures. Ce choix reste enregistré sur cet appareil. Dans Tether, touchez le titre du tableau pour ouvrir la même liste de projets.\n\n## Modifications\n\n- Cartes du **Tableau des tâches** dans une fenêtre étroite : un titre ne se coupe qu’entre deux mots, jamais au milieu d’un mot. L’heure est passée à la fin de la ligne des étiquettes : le titre occupe donc toute la largeur de la carte.",
10
- "de": "## Neu\n\n- Die **Aufgabentafel** hat über ihrer Projektliste ein Suchfeld und einen Filter. Tippe in **Projekte durchsuchen**, um ein Projekt nach seinem Namen zu finden (Groß- und Kleinschreibung und Akzente spielen keine Rolle, `Esc` leert das Feld). Klicke auf **In den letzten 2 Stunden aktiv**, um nur Projekte mit einem Chat zu zeigen, der wartet, läuft, ungesehen beendet wurde oder sich in den letzten 2 Stunden geändert hat. Diese Auswahl bleibt auf diesem Gerät gespeichert. Tippe in Tether auf den Titel der Tafel, um dieselbe Projektliste zu öffnen.\n\n## Änderungen\n\n- Karten der **Aufgabentafel** in einem schmalen Fenster: Ein Titel bricht nur zwischen Wörtern um, nicht innerhalb eines Wortes. Die Uhrzeit steht jetzt am Ende der Schlagwortzeile, sodass der Titel die volle Breite der Karte hat.",
11
- "ru": "## Новое\n\n- **Доска задач** получила поле поиска и фильтр над списком проектов. Введите текст в поле **Поиск проектов**, чтобы найти проект по названию (регистр и диакритические знаки не важны, `Esc` очищает поле). Нажмите **Активные за последние 2 часа**, чтобы показать только проекты с чатом, который ждёт, работает, завершился, но ещё не просмотрен, или изменился за последние 2 часа. Этот выбор сохраняется на этом устройстве. В Tether нажмите на заголовок доски, чтобы открыть тот же список проектов.\n\n## Изменения\n\n- **Доска задач** в узком окне: заголовок карточки переносится только между словами, а не внутри слова. Время перенесено в конец строки тегов, поэтому заголовок занимает всю ширину карточки.",
12
- "ja": "## 新機能\n\n- **タスクボード** のプロジェクト一覧の上に、検索ボックスとフィルターが付きました。**プロジェクトを検索** に入力すると、名前でプロジェクトを探せます(大文字と小文字、アクセント記号は区別しません。`Esc` でボックスが空になります)。**過去 2 時間以内にアクティブ** をクリックすると、待機中、実行中、終了してまだ見ていない、または過去 2 時間以内に変化があったチャットを含むプロジェクトだけを表示します。この選択はこのデバイスに保存されます。Tether では、ボードのタイトルをタップすると同じプロジェクト一覧が開きます。\n\n## 変更点\n\n- 狭いウィンドウでの **タスクボード** のカード:タイトルは単語の間でだけ改行し、単語の途中では改行しません。時刻をタグ行の末尾に移したため、タイトルにカードの幅全体を使えます。",
13
- "ko": "## 새 기능\n\n- **작업 보드**의 프로젝트 목록 위에 검색 상자와 필터가 생겼습니다. **프로젝트 검색**에 입력하면 이름으로 프로젝트를 찾을 수 있습니다(대소문자와 악센트는 구분하지 않으며, `Esc`를 누르면 상자가 비워집니다). **최근 2시간 내 활성**을 클릭하면 대기 중이거나, 실행 중이거나, 끝났지만 아직 보지 않았거나, 최근 2시간 내에 바뀐 채팅이 있는 프로젝트만 표시합니다. 이 선택은 이 기기에 저장됩니다. Tether에서는 보드 제목을 탭하면 같은 프로젝트 목록이 열립니다.\n\n## 변경 사항\n\n- 좁은 창의 **작업 보드** 카드: 제목은 단어 사이에서만 줄바꿈되고 단어 중간에서는 줄바꿈되지 않습니다. 시간이 태그 행의 끝으로 옮겨져서 제목이 카드의 전체 너비를 사용합니다.",
14
- "id": "## Baru\n\n- **Papan tugas** memiliki kotak pencarian dan filter di atas daftar proyeknya. Ketik di **Cari proyek** untuk menemukan proyek berdasarkan namanya (huruf besar/kecil dan aksen tidak berpengaruh, `Esc` mengosongkan kotak). Klik **Aktif dalam 2 jam terakhir** untuk hanya menampilkan proyek yang memiliki obrolan yang menunggu, berjalan, selesai tetapi belum Anda lihat, atau berubah dalam 2 jam terakhir. Pilihan ini tetap tersimpan di perangkat ini. Di Tether, ketuk judul papan untuk membuka daftar proyek yang sama.\n\n## Perubahan\n\n- Kartu **Papan tugas** di jendela sempit: judul hanya dipotong di antara kata, tidak di tengah kata. Waktu dipindahkan ke akhir baris label, sehingga judul memakai seluruh lebar kartu.",
15
- "vi": "## Mới\n\n- **Bảng tác vụ** có ô tìm kiếm và bộ lọc phía trên danh sách dự án. Nhập vào **Tìm dự án** để tìm một dự án theo tên (không phân biệt chữ hoa, chữ thường và dấu; `Esc` xóa trống ô). Nhấp **Hoạt động trong 2 giờ qua** để chỉ hiện các dự án có cuộc trò chuyện đang chờ, đang chạy, đã xong nhưng bạn chưa xem, hoặc đã thay đổi trong 2 giờ qua. Lựa chọn này được lưu trên thiết bị này. Trong Tether, chạm vào tiêu đề của bảng để mở cùng danh sách dự án đó.\n\n## Thay đổi\n\n- Thẻ của **Bảng tác vụ** trong cửa sổ hẹp: tiêu đề chỉ xuống dòng giữa các từ, không ngắt ở giữa một từ. Thời gian được chuyển về cuối dòng nhãn, nên tiêu đề có toàn bộ chiều rộng của thẻ.",
16
- "hi": "## नया\n\n- **टास्क बोर्ड** में प्रोजेक्ट सूची के ऊपर एक खोज बॉक्स और एक फ़िल्टर है। किसी प्रोजेक्ट को उसके नाम से ढूँढने के लिए **प्रोजेक्ट खोजें** में टाइप करें (बड़े-छोटे अक्षरों और एक्सेंट चिह्नों से फ़र्क नहीं पड़ता, `Esc` बॉक्स खाली कर देता है)। **पिछले 2 घंटों में सक्रिय** पर क्लिक करें, ताकि सिर्फ़ वे प्रोजेक्ट दिखें जिनमें कोई चैट इंतज़ार कर रही है, चल रही है, आपके देखे बिना खत्म हुई है या पिछले 2 घंटों में बदली है। यह चुनाव इस डिवाइस पर सेव रहता है। Tether में, वही प्रोजेक्ट सूची खोलने के लिए बोर्ड के शीर्षक पर टैप करें।\n\n## बदलाव\n\n- संकरी विंडो में **टास्क बोर्ड** के कार्ड: शीर्षक सिर्फ़ शब्दों के बीच टूटता है, किसी शब्द के अंदर नहीं। समय अब टैग वाली पंक्ति के आख़िर में है, इसलिए शीर्षक को कार्ड की पूरी चौड़ाई मिलती है।"
4
+ "en": "## New\n\n- **Task board**: point at a card in **Doing** (or move the keyboard focus to it) to see its progress: how long it runs, the step of its todo list, the tool that runs now and the agent's last line. In Settings → Composer Plus → Task board, **Progress on hover** picks **Fast (no AI)** (the default: it reads the chat only) or **AI summary** (one short call to the chat's model also writes one or two sentences). Tether shows one progress line under each running card.\n- **New task** now opens dsh's own new chat in the project, so the first message has dsh's composer (files, commands, model menu). After you send it, the board opens again. Until then the chat waits in **Todo**, and **Todo** shows only then.\n\n## Changes\n\n- **Task board** has no project colors: the only colors are the states (orange waits for you, blue runs, green is finished).\n- A project with a chat that waits for you has an orange dot in the board's project list. Tether opens the board on **Review** when a chat waits for you.\n- Only the project list scrolls: the title, **Search projects** and **Active in the last 2 hours** stay in place. **Search projects** has the height of a project row.\n- In a narrow board, the header stays on one line: **Hide finished** shows only its icon.",
5
+ "zh": "## 新功能\n\n- **任务看板**:将指针指向 **进行中** 里的卡片(或把键盘焦点移到卡片上),即可看到它的进度:运行了多久、待办列表中的当前步骤、正在运行的工具,以及智能体的最后一行。在设置 → Composer Plus → 任务看板中,**悬停显示进度** 可选择 **快速(不用 AI)**(默认:只读取对话)或 **AI 摘要**(再向对话的模型发送一次简短调用,额外写出一两句话)。Tether 会在每张运行中的卡片下方显示一行进度。\n- **新建任务** 现在会在项目中打开 dsh 自己的新对话,因此写第一条消息时可以使用 dsh 的输入框(文件、命令、模型菜单)。发送后,看板会再次打开。在此之前,该对话在 **待办** 中等待,**待办** 也只在这时显示。\n\n## 变更\n\n- **任务看板** 不使用项目颜色:颜色只表示状态(橙色表示在等你,蓝色表示正在运行,绿色表示已结束)。\n- 有对话在等你的项目,在看板的项目列表中带橙色圆点。有对话在等你时,Tether 会在 **审核** 列打开看板。\n- 只有项目列表会滚动:标题、**搜索项目** 和 **最近 2 小时内活跃** 固定不动。**搜索项目** 与项目行同高。\n- 看板较窄时,顶栏仍保持在一行内:**隐藏已完成** 只显示图标。",
6
+ "tr": "## Yeni\n\n- **Görev panosu**: ilerlemesini görmek için **Yapılıyor** sütunundaki bir kartın üzerine gelin (ya da klavye odağını karta taşıyın): sohbetin ne kadar süredir çalıştığı, yapılacaklar listesindeki adımı, şu an çalışan araç ve ajanın son satırı. Ayarlar → Composer Plus → Görev panosu bölümünde **Üzerine gelince ilerleme** ile **Hızlı (AI’sız)** (varsayılan: yalnız sohbeti okur) ya da **AI özeti** (sohbetin modeline yapılan kısa bir çağrı bir iki cümle de yazar) seçilir. Tether, çalışan her kartın altında bir ilerleme satırı gösterir.\n- **Yeni görev** artık projede dsh’nin kendi yeni sohbetini açar; böylece ilk mesajı dsh’nin mesaj alanında (dosyalar, komutlar, model menüsü) yazarsınız. Mesajı gönderdikten sonra pano yeniden açılır. O zamana kadar sohbet **Yapılacak** sütununda bekler ve **Yapılacak** yalnız bu sırada görünür.\n\n## Değişiklikler\n\n- **Görev panosu** proje renklerini kullanmaz: renkler yalnız durumları gösterir (turuncu: sizi bekliyor, mavi: çalışıyor, yeşil: bitti).\n- Sizi bekleyen bir sohbeti olan projenin yanında, panonun proje listesinde turuncu bir nokta görünür. Bir sohbet sizi beklerken Tether panoyu **İncelemede** sütununda açar.\n- Yalnız proje listesi kayar: başlık, **Projelerde ara** ve **Son 2 saatte aktif olanlar** yerinde kalır. **Projelerde ara**, bir proje satırı yüksekliğindedir.\n- Pano darken üst çubuk tek satırda kalır: **Bitenleri gizle** yalnız simgesini gösterir.",
7
+ "es": "## Novedades\n\n- **Tablero de tareas**: apunta a una tarjeta de **En curso** (o mueve el foco del teclado a ella) para ver su progreso: cuánto tiempo lleva en ejecución, el paso de su lista de tareas pendientes, la herramienta que se ejecuta ahora y la última línea del agente. En Configuración → Composer Plus → Tablero de tareas, **Progreso al pasar el cursor** elige **Rápido (sin IA)** (el predeterminado: solo lee el chat) o **Resumen con IA** (una llamada corta al modelo del chat también escribe una o dos frases). Tether muestra una línea de progreso debajo de cada tarjeta en ejecución.\n- **Nueva tarea** ahora abre el propio chat nuevo de dsh en el proyecto, así que el primer mensaje tiene el editor de dsh (archivos, comandos, menú de modelos). Después de enviarlo, el tablero se abre de nuevo. Hasta entonces, el chat espera en **Por hacer**, y **Por hacer** solo se muestra entonces.\n\n## Cambios\n\n- El **Tablero de tareas** no tiene colores de proyecto: los únicos colores son los de los estados (naranja: te espera; azul: en ejecución; verde: terminado).\n- Un proyecto con un chat que te espera tiene un punto naranja en la lista de proyectos del tablero. Tether abre el tablero en **En revisión** cuando un chat te espera.\n- Solo se desplaza la lista de proyectos: el título, **Buscar proyectos** y **Activos en las últimas 2 horas** se quedan en su sitio. **Buscar proyectos** tiene la altura de una fila de proyecto.\n- En un tablero estrecho, la cabecera se queda en una sola línea: **Ocultar terminados** solo muestra su icono.",
8
+ "pt-BR": "## Novidades\n\n- **Quadro de tarefas**: aponte para um cartão em **Fazendo** (ou mova o foco do teclado até ele) para ver o progresso: há quanto tempo ele está em execução, a etapa da sua lista de tarefas pendentes, a ferramenta que está em execução agora e a última linha do agente. Em Configurações → Composer Plus → Quadro de tarefas, **Progresso ao passar o cursor** escolhe **Rápido (sem IA)** (o padrão: só lê o chat) ou **Resumo com IA** (uma chamada curta ao modelo do chat também escreve uma ou duas frases). O Tether mostra uma linha de progresso abaixo de cada cartão em execução.\n- **Nova tarefa** agora abre o próprio novo chat do dsh no projeto, então a primeira mensagem tem o editor de mensagens do dsh (arquivos, comandos, menu de modelos). Depois que você a envia, o quadro abre de novo. Até lá, o chat espera em **A fazer**, e **A fazer** só aparece nesse momento.\n\n## Alterações\n\n- O **Quadro de tarefas** não tem cores de projeto: as únicas cores são as dos estados (laranja: espera por você; azul: em execução; verde: terminado).\n- Um projeto com um chat que espera por você tem um ponto laranja na lista de projetos do quadro. O Tether abre o quadro em **Em revisão** quando um chat espera por você.\n- Só a lista de projetos rola: o título, **Buscar projetos** e **Ativos nas últimas 2 horas** ficam no lugar. **Buscar projetos** tem a altura de uma linha de projeto.\n- Em um quadro estreito, o cabeçalho fica em uma só linha: **Ocultar concluídos** mostra só o ícone.",
9
+ "fr": "## Nouveautés\n\n- **Tableau des tâches** : pointez une carte dans **En cours** (ou placez-y le focus du clavier) pour voir sa progression : depuis combien de temps la discussion s’exécute, l’étape de sa liste de tâches, l’outil qui s’exécute en ce moment et la dernière ligne de l’agent. Dans Réglages → Composer Plus → Tableau des tâches, **Progression au survol** choisit **Rapide (sans IA)** (par défaut : lit seulement la discussion) ou **Résumé par IA** (un court appel au modèle de la discussion écrit aussi une ou deux phrases). Tether affiche une ligne de progression sous chaque carte en cours d’exécution.\n- **Nouvelle tâche** ouvre maintenant la nouvelle discussion de dsh lui-même dans le projet : le premier message dispose donc de la zone de saisie de dsh (fichiers, commandes, menu des modèles). Après l’envoi, le tableau s’ouvre de nouveau. D’ici là, la discussion attend dans **À faire**, et **À faire** ne s’affiche qu’à ce moment-là.\n\n## Modifications\n\n- Le **Tableau des tâches** n’a pas de couleurs de projet : les seules couleurs sont celles des états (orange : vous attend, bleu : s’exécute, vert : terminé).\n- Un projet dont une discussion vous attend a un point orange dans la liste des projets du tableau. Tether ouvre le tableau sur **En revue** quand une discussion vous attend.\n- Seule la liste des projets défile : le titre, **Rechercher des projets** et **Actifs au cours des 2 dernières heures** restent en place. **Rechercher des projets** a la hauteur d’une ligne de projet.\n- Dans un tableau étroit, l’en-tête reste sur une seule ligne : **Masquer les terminées** n’affiche que son icône.",
10
+ "de": "## Neu\n\n- **Aufgabentafel**: Zeige auf eine Karte in **In Arbeit** (oder setze den Tastaturfokus darauf), um ihren Fortschritt zu sehen: wie lange der Chat läuft, den Schritt seiner To-do-Liste, das Tool, das gerade läuft, und die letzte Zeile des Agenten. Unter Einstellungen → Composer Plus → Aufgabentafel wählt **Fortschritt beim Darüberfahren** zwischen **Schnell (ohne KI)** (Standard: liest nur den Chat) und **KI-Zusammenfassung** (ein kurzer Aufruf an das Modell des Chats schreibt zusätzlich ein oder zwei Sätze). Tether zeigt unter jeder laufenden Karte eine Fortschrittszeile.\n- **Neue Aufgabe** öffnet jetzt den eigenen neuen Chat von dsh im Projekt, sodass die erste Nachricht das Eingabefeld von dsh hat (Dateien, Befehle, Modellmenü). Nachdem du sie gesendet hast, öffnet sich die Tafel wieder. Bis dahin wartet der Chat in **Zu erledigen**, und **Zu erledigen** erscheint nur dann.\n\n## Änderungen\n\n- Die **Aufgabentafel** hat keine Projektfarben: Farben gibt es nur für die Zustände (Orange: wartet auf dich, Blau: läuft, Grün: fertig).\n- Ein Projekt mit einem Chat, der auf dich wartet, hat in der Projektliste der Tafel einen orangen Punkt. Tether öffnet die Tafel auf **In Prüfung**, wenn ein Chat auf dich wartet.\n- Nur die Projektliste scrollt: Der Titel, **Projekte durchsuchen** und **In den letzten 2 Stunden aktiv** bleiben an ihrem Platz. **Projekte durchsuchen** hat die Höhe einer Projektzeile.\n- In einer schmalen Tafel bleibt die Kopfleiste einzeilig: **Beendete ausblenden** zeigt nur sein Symbol.",
11
+ "ru": "## Новое\n\n- **Доска задач**: наведите указатель на карточку в **В работе** (или переведите на неё фокус клавиатуры), чтобы увидеть ход работы: сколько времени работает чат, шаг его списка задач, инструмент, который работает сейчас, и последнюю строку агента. В разделе Настройки → Composer Plus → Доска задач параметр **Ход работы при наведении** выбирает **Быстро (без ИИ)** (по умолчанию: только читает чат) или **Сводка ИИ** (один короткий вызов модели чата дополнительно пишет одно-два предложения). Tether показывает одну строку хода работы под каждой работающей карточкой.\n- **Новая задача** теперь открывает собственный новый чат dsh в проекте, поэтому для первого сообщения доступно поле ввода dsh (файлы, команды, меню моделей). После отправки доска снова открывается. До этого чат ждёт в **К выполнению**, и **К выполнению** показывается только в это время.\n\n## Изменения\n\n- **Доска задач** не использует цвета проектов: цвета есть только у состояний (оранжевый — ждёт вас, синий — работает, зелёный — завершён).\n- У проекта с чатом, который ждёт вас, в списке проектов доски есть оранжевая точка. Когда чат ждёт вас, Tether открывает доску на столбце **На проверке**.\n- Прокручивается только список проектов: заголовок, **Поиск проектов** и **Активные за последние 2 часа** остаются на месте. Поле **Поиск проектов** по высоте равно строке проекта.\n- На узкой доске шапка остаётся в одну строку: кнопка **Скрыть завершённые** показывает только свой значок.",
12
+ "ja": "## 新機能\n\n- **タスクボード**:**作業中** のカードにポインターを合わせる(またはキーボードのフォーカスを移す)と、その進行状況が表示されます。実行時間、ToDo リストのステップ、いま実行中のツール、エージェントの最後の行です。設定 → Composer Plus → タスクボード の **ホバーで進行状況を表示** で、**高速(AI なし)**(既定:チャットを読むだけです)か **AI の要約**(チャットのモデルを短く 1 回呼び出し、1〜2 文も書きます)を選びます。Tether は、実行中の各カードの下に進行状況を 1 行表示します。\n- **新しいタスク** は、プロジェクトで dsh 自身の新しいチャットを開くようになりました。そのため、最初のメッセージで dsh の入力欄(ファイル、コマンド、モデルメニュー)を使えます。送信すると、ボードがまた開きます。それまで、そのチャットは **未着手** で待ち、**未着手** はこのときだけ表示されます。\n\n## 変更点\n\n- **タスクボード** ではプロジェクトの色を使いません。色は状態だけを示します(オレンジはあなたの対応待ち、青は実行中、緑は終了)。\n- あなたを待っているチャットがあるプロジェクトには、ボードのプロジェクト一覧でオレンジの点が付きます。あなたを待っているチャットがあると、Tether はボードを **レビュー** の列で開きます。\n- スクロールするのはプロジェクト一覧だけです。タイトル、**プロジェクトを検索**、**過去 2 時間以内にアクティブ** は動きません。**プロジェクトを検索** の高さはプロジェクトの行と同じです。\n- 幅の狭いボードでも、ヘッダーは 1 行のままです。**完了を非表示** はアイコンだけになります。",
13
+ "ko": "## 새 기능\n\n- **작업 보드**: **진행 중**의 카드를 가리키면(또는 키보드 포커스를 옮기면) 진행 상황을 볼 수 있습니다. 실행된 시간, 할 일 목록의 단계, 지금 실행 중인 도구, 에이전트의 마지막 줄이 표시됩니다. 설정 → Composer Plus → 작업 보드의 **마우스를 올리면 진행 상황 표시**에서 **빠름(AI 없음)**(기본값: 채팅만 읽음) 또는 **AI 요약**(채팅의 모델을 한 번 짧게 호출해 한두 문장도 씀)을 고릅니다. Tether는 실행 중인 각 카드 아래에 진행 상황을 한 줄 표시합니다.\n- 이제 **새 작업**은 프로젝트에서 dsh 자체의 새 채팅을 엽니다. 그래서 첫 메시지에 dsh의 입력란(파일, 명령, 모델 메뉴)을 쓸 수 있습니다. 메시지를 보내면 보드가 다시 열립니다. 그때까지 채팅은 **할 일**에서 기다리며, **할 일**은 이때만 표시됩니다.\n\n## 변경 사항\n\n- **작업 보드**에는 프로젝트 색이 없습니다. 색은 상태만 나타냅니다(주황색은 응답 대기, 파란색은 실행 중, 초록색은 끝남).\n- 응답을 기다리는 채팅이 있는 프로젝트에는 보드의 프로젝트 목록에서 주황색 점이 붙습니다. 응답을 기다리는 채팅이 있으면 Tether는 보드를 **검토** 열에서 엽니다.\n- 프로젝트 목록만 스크롤됩니다. 제목, **프로젝트 검색**, **최근 2시간 내 활성**은 제자리에 있습니다. **프로젝트 검색**의 높이는 프로젝트 행과 같습니다.\n- 보드가 좁아도 헤더는 한 줄로 유지됩니다. **끝난 채팅 숨기기**는 아이콘만 표시합니다.",
14
+ "id": "## Baru\n\n- **Papan tugas**: arahkan penunjuk ke kartu di **Sedang dikerjakan** (atau pindahkan fokus keyboard ke kartu itu) untuk melihat progresnya: berapa lama obrolan berjalan, langkah dari daftar tugasnya, alat yang sedang berjalan, dan baris terakhir agen. Di Pengaturan → Composer Plus → Papan tugas, **Progres saat kursor di atas** memilih **Cepat (tanpa AI)** (default: hanya membaca obrolan) atau **Ringkasan AI** (satu panggilan singkat ke model obrolan juga menulis satu atau dua kalimat). Tether menampilkan satu baris progres di bawah setiap kartu yang berjalan.\n- **Tugas baru** kini membuka obrolan baru milik dsh sendiri di proyek, jadi pesan pertama memakai kotak pesan dsh (file, perintah, menu model). Setelah Anda mengirimnya, papan terbuka lagi. Sampai saat itu, obrolan menunggu di **Akan dikerjakan**, dan **Akan dikerjakan** hanya muncul saat itu.\n\n## Perubahan\n\n- **Papan tugas** tidak memakai warna proyek: satu-satunya warna adalah warna status (oranye menunggu Anda, biru berjalan, hijau selesai).\n- Proyek yang punya obrolan yang menunggu Anda memiliki titik oranye di daftar proyek papan. Tether membuka papan di **Ditinjau** saat ada obrolan yang menunggu Anda.\n- Hanya daftar proyek yang bergulir: judul, **Cari proyek**, dan **Aktif dalam 2 jam terakhir** tetap di tempatnya. **Cari proyek** setinggi satu baris proyek.\n- Di papan yang sempit, header tetap dalam satu baris: **Sembunyikan yang selesai** hanya menampilkan ikonnya.",
15
+ "vi": "## Mới\n\n- **Bảng tác vụ**: trỏ vào một thẻ ở **Đang làm** (hoặc chuyển tiêu điểm bàn phím đến thẻ đó) để xem tiến độ: cuộc trò chuyện đã chạy bao lâu, bước trong danh sách việc cần làm, công cụ đang chạy và dòng cuối của tác tử. Trong Cài đặt → Composer Plus → Bảng tác vụ, **Tiến độ khi di chuột** chọn **Nhanh (không dùng AI)** (mặc định: chỉ đọc cuộc trò chuyện) hoặc **Tóm tắt bằng AI** (một lệnh gọi ngắn tới mô hình của cuộc trò chuyện viết thêm một hoặc hai câu). Tether hiện một dòng tiến độ dưới mỗi thẻ đang chạy.\n- **Tác vụ mới** giờ mở cuộc trò chuyện mới của chính dsh trong dự án, nên tin nhắn đầu tiên có khung soạn thảo của dsh (tệp, lệnh, menu mô hình). Sau khi bạn gửi, bảng mở lại. Trước đó, cuộc trò chuyện chờ ở **Cần làm**, và **Cần làm** chỉ hiện vào lúc đó.\n\n## Thay đổi\n\n- **Bảng tác vụ** không dùng màu theo dự án: màu chỉ dành cho trạng thái (cam là đang chờ bạn, xanh dương là đang chạy, xanh lá là đã xong).\n- Dự án có cuộc trò chuyện đang chờ bạn có một chấm cam trong danh sách dự án của bảng. Tether mở bảng ở **Đang duyệt** khi có cuộc trò chuyện đang chờ bạn.\n- Chỉ danh sách dự án cuộn: tiêu đề, **Tìm dự án** và **Hoạt động trong 2 giờ qua** giữ nguyên vị trí. **Tìm dự án** cao bằng một hàng dự án.\n- Khi bảng hẹp, phần đầu bảng vẫn nằm trên một dòng: **Ẩn mục đã xong** chỉ hiện biểu tượng.",
16
+ "hi": "## नया\n\n- **टास्क बोर्ड**: **प्रगति में** के किसी कार्ड पर पॉइंटर रखें (या कीबोर्ड फ़ोकस उस पर ले जाएं), तो उसकी प्रगति दिखती है: चैट कितनी देर से चल रही है, उसकी करने वाले कामों की सूची का चरण, अभी चल रहा टूल और एजेंट की आखिरी लाइन। सेटिंग → Composer Plus → टास्क बोर्ड में **होवर पर प्रगति** से **तेज़ (AI के बिना)** (डिफ़ॉल्ट: सिर्फ़ चैट पढ़ता है) या **AI सारांश** (चैट के मॉडल को एक छोटी कॉल एक या दो वाक्य भी लिखती है) चुनें। Tether हर चल रहे कार्ड के नीचे प्रगति की एक लाइन दिखाता है।\n- **नया टास्क** अब प्रोजेक्ट में dsh की अपनी नई चैट खोलता है, इसलिए पहले संदेश के लिए dsh का कंपोज़र (फ़ाइलें, कमांड, मॉडल मेन्यू) मिलता है। संदेश भेजने के बाद बोर्ड फिर से खुल जाता है। तब तक चैट **करना है** में इंतज़ार करती है, और **करना है** सिर्फ़ तभी दिखता है।\n\n## बदलाव\n\n- **टास्क बोर्ड** में प्रोजेक्ट के रंग नहीं हैं: रंग सिर्फ़ स्थितियों के हैं (नारंगी: आपका इंतज़ार, नीला: चल रहा है, हरा: खत्म)।\n- जिस प्रोजेक्ट में कोई चैट आपका इंतज़ार कर रही है, बोर्ड की प्रोजेक्ट सूची में उसके आगे नारंगी बिंदु दिखता है। जब कोई चैट आपका इंतज़ार कर रही हो, तो Tether बोर्ड को **समीक्षा** पर खोलता है।\n- सिर्फ़ प्रोजेक्ट सूची स्क्रॉल होती है: शीर्षक, **प्रोजेक्ट खोजें** और **पिछले 2 घंटों में सक्रिय** अपनी जगह पर रहते हैं। **प्रोजेक्ट खोजें** की ऊंचाई एक प्रोजेक्ट पंक्ति जितनी है।\n- संकरे बोर्ड में हेडर एक ही लाइन में रहता है: **खत्म हुई चैट छिपाएं** सिर्फ़ अपना आइकन दिखाता है।"
17
17
  }
18
18
  }
@@ -48,7 +48,7 @@ export const PLUS = new Map([
48
48
  ['rollback', ['GET', 'POST']], ['delete-chat', ['POST']], ['ideas', ['POST']], ['later', ['GET', 'POST']], ['idea-history', ['GET', 'POST']],
49
49
  ['queue-move', ['POST']], ['queue-edit', ['POST']], ['branches', ['GET']], ['predict', ['POST']], ['complete', ['POST']], ['improve', ['POST']], ['unread', ['GET', 'POST']],
50
50
  ['worktrees', ['GET', 'POST']], ['new-session-workspace', ['POST']], ['memory-edit', ['GET', 'POST']], ['handoff', ['POST']], ['context', ['GET']],
51
- ['board-chats', ['GET', 'POST']],
51
+ ['board-chats', ['GET', 'POST']], ['board-progress', ['GET']],
52
52
  ])
53
53
  /** Allow for this chat: an Auto Review ask (`approval/request` with this reason) for a tool that the chat granted. */
54
54
  export const AUTO_REVIEW_ASK = 'Auto review denied tool '