dsh-composer-plus 1.14.0 → 1.15.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
@@ -3672,8 +3672,10 @@ async function excludeWorktrees(subprocess, { root, common }) {
3672
3672
  const worktreeStarts = new Set(); // main roots with a "New branch" start running now
3673
3673
  /** "New branch": a new branch and worktree `<main root>/.worktrees/<name>` from the chat's HEAD, the included ignored files
3674
3674
  * copied, the worktree registered as a dsh workspace, and a new chat there (the source chat's preset and model) that gets
3675
- * `text`. One start per repository at a time. A failure after the worktree exists keeps it on disk and names its path. */
3676
- export async function startOnBranch({ subprocess, sessionController, sessionQuery, registry, llm, route }, sessionId, text, signal) {
3675
+ * `text` and `attachments` (dsh's prompt parts: `{type:'image', mediaType, data}` as is; `{type:'file', receiptId}` is
3676
+ * an upload of the source chat, which is uploaded again to the new chat). One start per repository at a time. A failure
3677
+ * after the worktree exists keeps it on disk and names its path. */
3678
+ export async function startOnBranch({ subprocess, sessionController, sessionQuery, registry, llm, route, agents, fileUploads, store }, sessionId, text, signal, attachments = []) {
3677
3679
  const observation = await sessionQuery.observeSession(sessionId, { signal, projectionMode: 'all' });
3678
3680
  let cwd, origin, preset, model;
3679
3681
  try {
@@ -3689,11 +3691,20 @@ export async function startOnBranch({ subprocess, sessionController, sessionQuer
3689
3691
  if (repo === undefined) throw refused('this chat\'s folder is not in a git repository');
3690
3692
  if (repo.root === undefined) throw refused('a bare repository or a submodule has no main folder for worktrees');
3691
3693
  if (!repo.hasCommit) throw refused('this repository has no commit yet: make the first commit, then start a branch');
3694
+ // The files are found before git starts: a file that is gone makes no worktree.
3695
+ const source = attachments.some((a) => a.type === 'file') ? agents?.get?.(sessionId) : undefined;
3696
+ const files = attachments.filter((a) => a.type === 'file').map((a) => {
3697
+ if (fileUploads?.uploadStream === undefined || store?.readFileStream === undefined) throw refused('this dsh cannot give files to a new chat');
3698
+ const ref = source === undefined ? undefined : fileUploads.resolve?.(source, a.receiptId);
3699
+ if (ref === undefined) throw refused('a file of this message is not uploaded any more: add it again');
3700
+ return ref;
3701
+ });
3692
3702
  if (worktreeStarts.has(repo.root)) throw refused('a new branch of this repository is already starting');
3693
3703
  worktreeStarts.add(repo.root);
3694
3704
  try {
3695
3705
  // Only the naming stops with the request: once git starts, the steps run to the end (no half-made worktree or chat).
3696
- const { name: base, fromModel } = await branchName({ llm, route }, text, signal);
3706
+ // A message of files only gets the dated name.
3707
+ const { name: base, fromModel } = text.trim() === '' ? { name: fallbackBranch(''), fromModel: false } : await branchName({ llm, route }, text, signal);
3697
3708
  signal?.throwIfAborted();
3698
3709
  // `.worktrees` must be a real folder (or absent): a committed symlink would put the checkout and the copied secrets
3699
3710
  // outside the repository (lstat does not follow it).
@@ -3713,9 +3724,19 @@ export async function startOnBranch({ subprocess, sessionController, sessionQuer
3713
3724
  const { sessionId: created } = await sessionController.create({ workspaceId: workspace.id, ...(preset === undefined ? {} : { agentPreset: preset }) });
3714
3725
  // ponytail: a model that the new chat cannot take leaves dsh's default; the chat still starts.
3715
3726
  if (model !== undefined) await sessionController.selectModel({ sessionId: created, ...model }).catch(() => {});
3727
+ // dsh's upload receipt belongs to one chat: each file is stored again for the new chat (the same content-addressed
3728
+ // file). The parts keep dsh's order: attachments, then the text.
3729
+ const content = [];
3730
+ let next = 0;
3731
+ for (const a of attachments) {
3732
+ if (a.type !== 'file') { content.push(a); continue; }
3733
+ const ref = files[next++];
3734
+ content.push({ type: 'file', receiptId: (await fileUploads.uploadStream({ sessionId: created, data: store.readFileStream(ref), name: ref.name })).receiptId });
3735
+ }
3736
+ if (text !== '') content.push({ type: 'text', text });
3716
3737
  // dsh's `prompt(request, signal)` calls `signal.throwIfAborted()`. Not the request's signal: a closed request must not
3717
3738
  // leave the new chat without its message; the timeout only ends a stuck admission.
3718
- await sessionController.prompt({ requestId: randomUUID(), sessionId: created, mode: 'queue', content: [{ type: 'text', text }] }, AbortSignal.timeout(STOP_WAIT_MS));
3739
+ await sessionController.prompt({ requestId: randomUUID(), sessionId: created, mode: 'queue', content }, AbortSignal.timeout(STOP_WAIT_MS));
3719
3740
  return { sessionId: created, branch: name, path, workspaceId: workspace.id, copied, skipped, named: fromModel };
3720
3741
  } catch (error) {
3721
3742
  throw Object.assign(new Error(`${error?.message ?? error}; the new worktree stays at ${path}`), Number.isInteger(error?.status) ? { status: error.status } : {});
@@ -3733,7 +3754,7 @@ export async function openWorktree({ subprocess, sessionQuery, registry }, sessi
3733
3754
  }
3734
3755
 
3735
3756
  /** Worktree chats (Host half): `GET ?sessionId[&list=1]` answers the chat's branch status (and the repository's
3736
- * worktrees); `POST {action:'start', sessionId, text}` starts a chat on a new branch; `POST {action:'open', sessionId,
3757
+ * worktrees); `POST {action:'start', sessionId, text, attachments?}` starts a chat on a new branch; `POST {action:'open', sessionId,
3737
3758
  * path}` registers one listed worktree as a workspace. The browser never gives a path that git did not list. */
3738
3759
  function installWorktrees(ctx) {
3739
3760
  const get = (name) => { try { return ctx.get?.(name); } catch { return undefined; } };
@@ -3759,12 +3780,19 @@ function installWorktrees(ctx) {
3759
3780
  if (registry?.create === undefined) return json({ error: 'this dsh cannot add a workspace' }, 409);
3760
3781
  return json(await openWorktree({ subprocess: ctx.subprocess, sessionQuery: ctx.sessionQuery, registry }, body.sessionId, body.path, request.signal));
3761
3782
  }
3762
- if (body.action !== 'start' || typeof body.text !== 'string' || body.text.trim() === '' || body.text.length > 100000) return json({ error: 'bad request' }, 400);
3783
+ // dsh's own prompt parts (dsh checks the image bytes and limits when the new chat takes the message).
3784
+ const attachments = body.attachments ?? [];
3785
+ const part = (a) => (a?.type === 'image' && Object.keys(a).every((k) => ['type', 'mediaType', 'data', 'name'].includes(k)) && isText(a.mediaType, 64)
3786
+ && isText(a.data, 400e6) && (a.name === undefined || isText(a.name, 1024)))
3787
+ || (a?.type === 'file' && Object.keys(a).length === 2 && isText(a.receiptId, 128));
3788
+ if (body.action !== 'start' || typeof body.text !== 'string' || body.text.length > 100000 || !Array.isArray(attachments) || attachments.length > 50
3789
+ || !attachments.every(part) || (body.text.trim() === '' && attachments.length === 0)) return json({ error: 'bad request' }, 400);
3763
3790
  if (registry?.create === undefined) return json({ error: 'this dsh cannot add a workspace' }, 409);
3764
3791
  // No model (or no catalog): the branch gets the message's own slug.
3765
3792
  const llm = get('llm');
3766
3793
  const route = llm?.prepareCall === undefined ? undefined : await routeOf(ctx, get).catch(() => undefined);
3767
- return json(await startOnBranch({ subprocess: ctx.subprocess, sessionController: ctx.sessionController, sessionQuery: ctx.sessionQuery, registry, llm, route }, body.sessionId, body.text, request.signal));
3794
+ return json(await startOnBranch({ subprocess: ctx.subprocess, sessionController: ctx.sessionController, sessionQuery: ctx.sessionQuery, registry, llm, route,
3795
+ agents: get('agents'), fileUploads: get('fileUploads'), store: get('attachments') }, body.sessionId, body.text, request.signal, attachments));
3768
3796
  } catch (error) { return json({ error: String(error?.message ?? error) }, Number.isInteger(error?.status) ? error.status : 500); }
3769
3797
  },
3770
3798
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-composer-plus",
3
- "version": "1.14.0",
3
+ "version": "1.15.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",