@norman-else/dsh-claude 0.1.42 → 0.1.43

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/lib/index.mjs CHANGED
@@ -2876,20 +2876,7 @@ var ClaudeSupervisor = class {
2876
2876
  ...result.usage.outputTokens === void 0 ? {} : { outputTokens: result.usage.outputTokens }
2877
2877
  };
2878
2878
  }
2879
- async #completeProgressSegment(active, result, recordUsage = true) {
2880
- if (recordUsage && (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0)) {
2881
- await this.#appendSafely(active, {
2882
- kind: "usage",
2883
- phase: "completed",
2884
- title: "Claude usage",
2885
- summary: usageSummary(result.usage),
2886
- usage: this.#timedUsage(active, result.usage)
2887
- });
2888
- active.output.push({
2889
- type: "usage",
2890
- usage: this.#reportedUsage(active, result)
2891
- });
2892
- }
2879
+ async #completeProgressSegment(active, result) {
2893
2880
  if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
2894
2881
  active.text = result.text;
2895
2882
  active.transcriptText = result.text;
@@ -2909,6 +2896,25 @@ var ClaudeSupervisor = class {
2909
2896
  this.#closeTranscriptTextSegment(active);
2910
2897
  active.thinking = "";
2911
2898
  }
2899
+ /** The turn's accounting, recorded once the turn is actually over.
2900
+ *
2901
+ * A turn that hands off to background tasks passes through the same result
2902
+ * handling on its way to `waiting-tasks`, and recording there drew a closing
2903
+ * total under a turn that was still running. */
2904
+ async #recordTurnUsage(active, result) {
2905
+ if (result.usage.inputTokens === void 0 && result.usage.outputTokens === void 0 && result.usage.cumulativeCostUsd === void 0) return;
2906
+ await this.#appendSafely(active, {
2907
+ kind: "usage",
2908
+ phase: "completed",
2909
+ title: "Claude usage",
2910
+ summary: usageSummary(result.usage),
2911
+ usage: this.#timedUsage(active, result.usage)
2912
+ });
2913
+ active.output.push({
2914
+ type: "usage",
2915
+ usage: this.#reportedUsage(active, result)
2916
+ });
2917
+ }
2912
2918
  async #completeTurn(entry, active, result) {
2913
2919
  if (entry.active !== active) return;
2914
2920
  if (active.aborted) {
@@ -2930,19 +2936,6 @@ var ClaudeSupervisor = class {
2930
2936
  this.#scheduleLimitReconciliation();
2931
2937
  return;
2932
2938
  }
2933
- if (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0) {
2934
- await this.#appendSafely(active, {
2935
- kind: "usage",
2936
- phase: "completed",
2937
- title: "Claude usage",
2938
- summary: usageSummary(result.usage),
2939
- usage: this.#timedUsage(active, result.usage)
2940
- });
2941
- active.output.push({
2942
- type: "usage",
2943
- usage: this.#reportedUsage(active, result)
2944
- });
2945
- }
2946
2939
  const unmatchedDenials = (result.permissionDenials ?? []).filter((denial) => !active.deniedToolUseIds.has(denial.toolUseId));
2947
2940
  if (unmatchedDenials.length > 0) await this.#appendSafely(active, {
2948
2941
  kind: "permission",
@@ -2951,6 +2944,7 @@ var ClaudeSupervisor = class {
2951
2944
  summary: unmatchedDenials.map((denial) => denial.toolName).join(", ")
2952
2945
  });
2953
2946
  if (!result.success) {
2947
+ await this.#recordTurnUsage(active, result);
2954
2948
  if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
2955
2949
  active.text = result.text;
2956
2950
  active.transcriptText = result.text;
@@ -2989,9 +2983,10 @@ var ClaudeSupervisor = class {
2989
2983
  phase: "updated",
2990
2984
  title: "Claude Code is waiting for background tasks"
2991
2985
  });
2992
- await this.#completeProgressSegment(active, result, false);
2986
+ await this.#completeProgressSegment(active, result);
2993
2987
  return;
2994
2988
  }
2989
+ await this.#recordTurnUsage(active, result);
2995
2990
  await this.#appendSafely(active, {
2996
2991
  kind: "status",
2997
2992
  phase: "completed",
@@ -4328,6 +4323,7 @@ const GIT_TIMEOUT_MS$3 = 5e3;
4328
4323
  const GH_TIMEOUT_MS$1 = 8e3;
4329
4324
  const CACHE_TTL_MS = 5e3;
4330
4325
  const MAX_TEXT_CHARS = 1024;
4326
+ const MAX_CONFLICT_PATHS = 100;
4331
4327
  function bounded(value) {
4332
4328
  return value.trim().slice(0, MAX_TEXT_CHARS);
4333
4329
  }
@@ -4365,6 +4361,7 @@ function parseGitStatus(output) {
4365
4361
  let upstream = false;
4366
4362
  let ahead;
4367
4363
  let behind;
4364
+ const conflicts = [];
4368
4365
  for (const line of output.split(/\r?\n/u)) {
4369
4366
  if (line.startsWith("# branch.head ")) {
4370
4367
  const head = bounded(line.slice(14));
@@ -4384,6 +4381,10 @@ function parseGitStatus(output) {
4384
4381
  }
4385
4382
  continue;
4386
4383
  }
4384
+ if (line.startsWith("u ")) {
4385
+ const path = line.split(" ").slice(10).join(" ");
4386
+ if (path.length > 0 && conflicts.length < MAX_CONFLICT_PATHS) conflicts.push(bounded(path));
4387
+ }
4387
4388
  if (line.length > 0 && !line.startsWith("# ")) dirty = true;
4388
4389
  }
4389
4390
  return {
@@ -4392,9 +4393,36 @@ function parseGitStatus(output) {
4392
4393
  dirty,
4393
4394
  upstream,
4394
4395
  ...ahead === void 0 ? {} : { ahead },
4395
- ...behind === void 0 ? {} : { behind }
4396
+ ...behind === void 0 ? {} : { behind },
4397
+ ...conflicts.length === 0 ? {} : { conflicts }
4396
4398
  };
4397
4399
  }
4400
+ /** Ordered so the rebase directories win: a conflicted rebase also writes
4401
+ * MERGE_HEAD-like state, and the two are resumed by different commands. */
4402
+ const OPERATION_MARKERS = [
4403
+ ["rebase-merge", "rebase"],
4404
+ ["rebase-apply", "rebase"],
4405
+ ["MERGE_HEAD", "merge"],
4406
+ ["CHERRY_PICK_HEAD", "cherry-pick"],
4407
+ ["REVERT_HEAD", "revert"]
4408
+ ];
4409
+ /** Reads the in-progress operation out of the worktree's own git dir -- linked
4410
+ * worktrees keep their own, so this must not be handed the common dir. */
4411
+ async function detectRepositoryOperation(gitDir) {
4412
+ for (const [marker, operation] of OPERATION_MARKERS) {
4413
+ const path = join(gitDir, marker);
4414
+ try {
4415
+ await stat(path);
4416
+ } catch {
4417
+ continue;
4418
+ }
4419
+ const branch = bounded(operation === "rebase" ? await readFile(join(path, "head-name"), "utf8").catch(() => "") : "").replace(/^refs\/heads\//u, "");
4420
+ return {
4421
+ operation,
4422
+ ...branch.length === 0 || branch.includes(" ") ? {} : { branch }
4423
+ };
4424
+ }
4425
+ }
4398
4426
  function parseDiffNumstat(value) {
4399
4427
  let additions = 0;
4400
4428
  let deletions = 0;
@@ -4601,13 +4629,15 @@ var RepositoryStatusService = class {
4601
4629
  cwd: safeCwd
4602
4630
  };
4603
4631
  const status = parseGitStatus(statusResult.stdout);
4632
+ const operation = await detectRepositoryOperation(gitDir);
4633
+ const branch = operation?.branch ?? status.branch;
4604
4634
  const remoteResult = await run(this.#runtime, git, [
4605
4635
  "remote",
4606
4636
  "get-url",
4607
4637
  "origin"
4608
4638
  ], cwd, GIT_TIMEOUT_MS$3);
4609
4639
  const remote = remoteResult.exitCode === 0 ? parseGitHubRemote(remoteResult.stdout) : void 0;
4610
- const pullRequest = status.branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, status.branch);
4640
+ const pullRequest = branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, branch);
4611
4641
  const diffBase = pullRequest?.baseBranch === void 0 ? "HEAD" : await this.#mergeBase(cwd, git, pullRequest.baseBranch) ?? "HEAD";
4612
4642
  const diff = status.dirty || diffBase !== "HEAD" ? await this.#diff(cwd, git, diffBase, signal) : {
4613
4643
  additions: 0,
@@ -4621,6 +4651,8 @@ var RepositoryStatusService = class {
4621
4651
  cwd: safeCwd,
4622
4652
  root,
4623
4653
  ...status,
4654
+ ...branch === void 0 ? {} : { branch },
4655
+ ...operation === void 0 ? {} : { operation: operation.operation },
4624
4656
  worktree: normalizedPath(gitDir) !== normalizedPath(commonDir),
4625
4657
  ...remote === void 0 ? {} : { remote },
4626
4658
  ...pullRequest === void 0 ? {} : { pullRequest },
@@ -5496,6 +5528,10 @@ function parseRepositoryActionStatus(output) {
5496
5528
  }
5497
5529
  return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
5498
5530
  }
5531
+ function conflictPaths(result) {
5532
+ if (result.exitCode !== 0 || result.lossy) return [];
5533
+ return result.stdout.split(/\r?\n/u).filter((line) => line.length > 0).slice(0, 100);
5534
+ }
5499
5535
  function fallbackCommitMessage(files) {
5500
5536
  if (files.length === 1) return `Update ${files[0]?.path ?? "repository files"}`;
5501
5537
  return `Update ${files.length} repository files`;
@@ -5560,6 +5596,7 @@ var RepositoryActionService = class {
5560
5596
  return operation;
5561
5597
  }
5562
5598
  async #execute(cwd, request) {
5599
+ if (request.action === "resolve-continue" || request.action === "resolve-abort") return this.#resolve(cwd, request.action, request.push === true);
5563
5600
  const before = await this.#preview(cwd);
5564
5601
  if (before.fingerprint !== request.fingerprint) throw new RepositoryActionError("repository-changed", "Repository changes have changed. Refresh the commit panel.");
5565
5602
  if (request.action === "push") {
@@ -5622,13 +5659,12 @@ var RepositoryActionService = class {
5622
5659
  `origin/${base}`
5623
5660
  ], before.root, REMOTE_TIMEOUT_MS);
5624
5661
  if (merged.exitCode !== 0 || merged.lossy) {
5625
- const conflicted = await this.#run(git, [
5662
+ const conflicts = conflictPaths(await this.#run(git, [
5626
5663
  "diff",
5627
5664
  "--name-only",
5628
5665
  "--diff-filter=U",
5629
5666
  "--"
5630
- ], before.root, GIT_TIMEOUT_MS$1);
5631
- const conflicts = conflicted.exitCode === 0 && !conflicted.lossy ? conflicted.stdout.split(/\r?\n/u).filter((line) => line.length > 0).slice(0, 100) : [];
5667
+ ], before.root, GIT_TIMEOUT_MS$1));
5632
5668
  if (conflicts.length === 0) {
5633
5669
  await this.#run(git, [method, "--abort"], before.root, GIT_TIMEOUT_MS$1).catch(() => void 0);
5634
5670
  throw new RepositoryActionError("merge-failed", `Git could not ${method} the base branch.`);
@@ -5727,6 +5763,87 @@ var RepositoryActionService = class {
5727
5763
  pullRequestUrl: url
5728
5764
  };
5729
5765
  }
5766
+ /** Finishes or discards the merge, rebase, cherry-pick or revert git is
5767
+ * waiting on. The operation is read from the git dir rather than taken from
5768
+ * the caller: `--continue` and `--abort` are only safe against the one that
5769
+ * is actually in progress. */
5770
+ async #resolve(cwd, action, push) {
5771
+ const git = await this.#git();
5772
+ const [rootValue, gitDirValue] = (await this.#mustRun(git, [
5773
+ "rev-parse",
5774
+ "--path-format=absolute",
5775
+ "--show-toplevel",
5776
+ "--absolute-git-dir"
5777
+ ], cwd, GIT_TIMEOUT_MS$1, "not-repository", "The session directory is not a Git repository.")).stdout.split(/\r?\n/u);
5778
+ const root = (rootValue ?? "").trim();
5779
+ const gitDir = (gitDirValue ?? "").trim();
5780
+ if (root.length === 0 || gitDir.length === 0) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
5781
+ const state = await detectRepositoryOperation(gitDir);
5782
+ if (state === void 0) throw new RepositoryActionError("no-operation", "No merge, rebase, cherry-pick or revert is in progress.");
5783
+ const operation = state.operation;
5784
+ if (action === "resolve-abort") {
5785
+ await this.#mustRun(git, [operation, "--abort"], root, GIT_TIMEOUT_MS$1, "abort-failed", `Git could not abort the ${operation}.`);
5786
+ this.#invalidate(root);
5787
+ return {
5788
+ commit: await this.#head(git, root),
5789
+ pushed: false
5790
+ };
5791
+ }
5792
+ if (conflictPaths(await this.#run(git, [
5793
+ "diff",
5794
+ "--name-only",
5795
+ "--diff-filter=U",
5796
+ "--"
5797
+ ], root, GIT_TIMEOUT_MS$1)).length > 0) throw new RepositoryActionError("unresolved-conflicts", "Resolve and stage every conflicted file before continuing.");
5798
+ const continued = await this.#run(git, [
5799
+ "-c",
5800
+ "core.editor=true",
5801
+ operation,
5802
+ "--continue"
5803
+ ], root, REMOTE_TIMEOUT_MS);
5804
+ this.#invalidate(root);
5805
+ if (continued.exitCode !== 0 || continued.lossy) {
5806
+ const next = conflictPaths(await this.#run(git, [
5807
+ "diff",
5808
+ "--name-only",
5809
+ "--diff-filter=U",
5810
+ "--"
5811
+ ], root, GIT_TIMEOUT_MS$1));
5812
+ if (next.length > 0) return {
5813
+ commit: await this.#head(git, root),
5814
+ pushed: false,
5815
+ conflicts: next
5816
+ };
5817
+ const reason = continued.stderr.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).at(-1);
5818
+ throw new RepositoryActionError("continue-failed", reason === void 0 || reason.length === 0 ? `Git could not continue the ${operation}.` : reason);
5819
+ }
5820
+ const head = await this.#head(git, root);
5821
+ if (!push) return {
5822
+ commit: head,
5823
+ pushed: false
5824
+ };
5825
+ const branch = await this.#run(git, [
5826
+ "symbolic-ref",
5827
+ "--quiet",
5828
+ "--short",
5829
+ "HEAD"
5830
+ ], root, GIT_TIMEOUT_MS$1);
5831
+ if (branch.exitCode !== 0 || branch.lossy) throw new RepositoryActionError("detached-head", "The finished operation left a detached HEAD, so nothing was pushed.", head);
5832
+ try {
5833
+ await this.#push(git, root, branch.stdout.trim(), operation === "rebase");
5834
+ } catch (error) {
5835
+ throw new RepositoryActionError("push-failed", error instanceof Error ? error.message : "Git push failed.", head);
5836
+ }
5837
+ this.#invalidate(root);
5838
+ return {
5839
+ commit: head,
5840
+ pushed: true
5841
+ };
5842
+ }
5843
+ async #head(git, root) {
5844
+ const head = await this.#run(git, ["rev-parse", "HEAD"], root, GIT_TIMEOUT_MS$1);
5845
+ return head.exitCode === 0 && !head.lossy ? head.stdout.trim() : "";
5846
+ }
5730
5847
  async #preview(cwd) {
5731
5848
  const git = await this.#git();
5732
5849
  const root = (await this.#mustRun(git, [
@@ -6012,7 +6129,17 @@ const ACTIONS = /* @__PURE__ */ new Set([
6012
6129
  "push",
6013
6130
  "create-pr",
6014
6131
  "merge-pr",
6015
- "update-branch"
6132
+ "update-branch",
6133
+ "resolve-continue",
6134
+ "resolve-abort"
6135
+ ]);
6136
+ /** Actions that commit nothing of their own, so the panel sends no message. */
6137
+ const MESSAGELESS = /* @__PURE__ */ new Set([
6138
+ "push",
6139
+ "merge-pr",
6140
+ "update-branch",
6141
+ "resolve-continue",
6142
+ "resolve-abort"
6016
6143
  ]);
6017
6144
  function record$9(value) {
6018
6145
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -6051,7 +6178,7 @@ function actionRequest(input) {
6051
6178
  return {
6052
6179
  action,
6053
6180
  fingerprint: string$1(input, "fingerprint"),
6054
- message: action === "push" || action === "merge-pr" || action === "update-branch" ? optionalString(input, "message") ?? "" : string$1(input, "message"),
6181
+ message: MESSAGELESS.has(action) ? optionalString(input, "message") ?? "" : string$1(input, "message"),
6055
6182
  includeUnstaged: input.includeUnstaged,
6056
6183
  ...optionalString(input, "prTitle") === void 0 ? {} : { prTitle: optionalString(input, "prTitle") },
6057
6184
  ...optionalString(input, "prBody") === void 0 ? {} : { prBody: optionalString(input, "prBody") },
@@ -6059,6 +6186,9 @@ function actionRequest(input) {
6059
6186
  ...input.draft === void 0 ? {} : typeof input.draft === "boolean" ? { draft: input.draft } : (() => {
6060
6187
  throw new RepositoryActionError("invalid-request", "The draft field must be a boolean.");
6061
6188
  })(),
6189
+ ...input.push === void 0 ? {} : typeof input.push === "boolean" ? { push: input.push } : (() => {
6190
+ throw new RepositoryActionError("invalid-request", "The push field must be a boolean.");
6191
+ })(),
6062
6192
  ...input.mergeMethod === void 0 ? {} : input.mergeMethod === "merge" || input.mergeMethod === "squash" || input.mergeMethod === "rebase" ? { mergeMethod: input.mergeMethod } : (() => {
6063
6193
  throw new RepositoryActionError("invalid-request", "The mergeMethod field is invalid.");
6064
6194
  })()