@supanadit/morse-web 0.9.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.mjs CHANGED
@@ -1706,11 +1706,11 @@ function promptDescription(raw) {
1706
1706
  }
1707
1707
  body = raw.slice(frontmatter[0].length);
1708
1708
  }
1709
- const firstLine = body.split("\n").find((line) => line.trim().length > 0);
1710
- if (firstLine === void 0) {
1709
+ const firstLine2 = body.split("\n").find((line) => line.trim().length > 0);
1710
+ if (firstLine2 === void 0) {
1711
1711
  return void 0;
1712
1712
  }
1713
- const trimmed = firstLine.trim();
1713
+ const trimmed = firstLine2.trim();
1714
1714
  return trimmed.length > 60 ? `${trimmed.slice(0, 60)}...` : trimmed;
1715
1715
  }
1716
1716
  function unquote(value) {
@@ -2383,6 +2383,11 @@ var SessionTranscriptStore = class {
2383
2383
  projectors = /* @__PURE__ */ new Map();
2384
2384
  cursors = /* @__PURE__ */ new Map();
2385
2385
  listeners = /* @__PURE__ */ new Set();
2386
+ /**
2387
+ * Agent events already projected, by instance (see `apply`). A weak set, so
2388
+ * a projected record never keeps itself alive after it has been fanned out.
2389
+ */
2390
+ applied = /* @__PURE__ */ new WeakSet();
2386
2391
  subscribe(listener) {
2387
2392
  this.listeners.add(listener);
2388
2393
  return () => {
@@ -2402,7 +2407,25 @@ var SessionTranscriptStore = class {
2402
2407
  setHistoryCursor(sessionKey, cursor) {
2403
2408
  this.cursors.set(sessionKey, cursor);
2404
2409
  }
2410
+ /**
2411
+ * Feeds one agent event in. Idempotent per event *instance*: the registry
2412
+ * fans a single tagged event out to every subscribed controller, and each of
2413
+ * them writes it here, so a host with more than one connected client would
2414
+ * otherwise project the same stream once per connection. The symptoms are
2415
+ * unmistakable — a delta doubled into "therethere", a second `message_end`
2416
+ * creating a duplicate prose row, and a second `tool-start` re-binding the
2417
+ * call id so the first tool item never receives its `tool-end` and stays
2418
+ * `running` forever ("Working for …" never settling).
2419
+ *
2420
+ * Identity is the right key because `SessionRegistry.emit` passes the same
2421
+ * event object to every listener: the first writer projects it, the rest are
2422
+ * no-ops, whatever the connection count.
2423
+ */
2405
2424
  apply(sessionKey, event) {
2425
+ if (this.applied.has(event)) {
2426
+ return;
2427
+ }
2428
+ this.applied.add(event);
2406
2429
  this.projectorFor(sessionKey).apply(event);
2407
2430
  }
2408
2431
  /** Replays a resumed session's history into an empty transcript. */
@@ -3253,7 +3276,7 @@ var HostSessionController = class {
3253
3276
  this.agentReady = false;
3254
3277
  this.agentError = event.message;
3255
3278
  this.agentFailure = hintOnly(this.options.agentHint);
3256
- this.options.transcripts.error(sessionKey, event.message, event.detail);
3279
+ this.options.transcripts.apply(sessionKey, event);
3257
3280
  this.emitState();
3258
3281
  return;
3259
3282
  default:
@@ -3936,6 +3959,169 @@ async function readGitDiff(cwd, requested) {
3936
3959
  );
3937
3960
  return { path: rel, diff: diff ?? "" };
3938
3961
  }
3962
+ async function readCommitFiles(cwd, requested) {
3963
+ const hash = requested.trim();
3964
+ if (!isCommitHash(hash)) {
3965
+ return { isRepo: false, hash, files: [] };
3966
+ }
3967
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
3968
+ if (inside?.trim() !== "true") {
3969
+ return { isRepo: false, hash, files: [] };
3970
+ }
3971
+ const raw = await gitText(
3972
+ ["show", "--name-status", "--format=", "-z", "--find-renames", "--relative", hash],
3973
+ cwd
3974
+ );
3975
+ return { isRepo: true, hash, files: parseNameStatus(raw ?? "") };
3976
+ }
3977
+ async function readCommitDiff(cwd, hash, requested) {
3978
+ const rel = toPosix2(relative3(cwd, resolveWithin(cwd, requested)));
3979
+ if (!isCommitHash(hash.trim())) {
3980
+ return { path: rel, diff: "" };
3981
+ }
3982
+ const diff = await gitText(
3983
+ ["show", "--format=", "--no-color", "--no-ext-diff", "-U3", "--relative", hash.trim(), "--", rel],
3984
+ cwd
3985
+ );
3986
+ return { path: rel, diff: diff ?? "" };
3987
+ }
3988
+ function isCommitHash(hash) {
3989
+ return /^[0-9a-fA-F]{4,40}$/.test(hash);
3990
+ }
3991
+ function parseNameStatus(raw) {
3992
+ const tokens = raw.split("\0");
3993
+ const files = [];
3994
+ for (let index = 0; index < tokens.length; index += 1) {
3995
+ const status = tokens[index];
3996
+ if (!status) {
3997
+ continue;
3998
+ }
3999
+ const letter = status[0] ?? "";
4000
+ const rename = letter === "R" || letter === "C";
4001
+ const path = tokens[index + (rename ? 2 : 1)];
4002
+ index += rename ? 2 : 1;
4003
+ if (path) {
4004
+ files.push({ path: toPosix2(path), status: `${letter} ` });
4005
+ }
4006
+ }
4007
+ return files;
4008
+ }
4009
+ async function stageGitPaths(cwd, requested) {
4010
+ const paths = resolvePaths(cwd, requested);
4011
+ if (paths.length > 0) {
4012
+ await gitRun(["add", "--", ...paths], cwd);
4013
+ }
4014
+ return readGitStatus(cwd);
4015
+ }
4016
+ async function unstageGitPaths(cwd, requested) {
4017
+ const paths = resolvePaths(cwd, requested);
4018
+ if (paths.length > 0) {
4019
+ const reset = await gitRun(["reset", "--quiet", "HEAD", "--", ...paths], cwd);
4020
+ if (!reset) {
4021
+ await gitRun(["rm", "--cached", "--quiet", "--", ...paths], cwd);
4022
+ }
4023
+ }
4024
+ return readGitStatus(cwd);
4025
+ }
4026
+ async function readGitSync(cwd) {
4027
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4028
+ if (inside?.trim() !== "true") {
4029
+ return { isRepo: false, ahead: 0, behind: 0 };
4030
+ }
4031
+ const [branch, upstream] = await Promise.all([
4032
+ gitText(["rev-parse", "--abbrev-ref", "HEAD"], cwd),
4033
+ gitText(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], cwd)
4034
+ ]);
4035
+ const name = branch?.trim() || void 0;
4036
+ const tracked = upstream?.trim() || void 0;
4037
+ if (tracked === void 0) {
4038
+ return { isRepo: true, branch: name, ahead: 0, behind: 0 };
4039
+ }
4040
+ const counts = await gitText(
4041
+ ["rev-list", "--left-right", "--count", `${tracked}...HEAD`],
4042
+ cwd
4043
+ );
4044
+ const [behind, ahead] = parseCounts(counts ?? "");
4045
+ return { isRepo: true, branch: name, upstream: tracked, ahead, behind };
4046
+ }
4047
+ async function pullGit(cwd) {
4048
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4049
+ if (inside?.trim() === "true") {
4050
+ await gitRun(["pull", "--no-edit"], cwd, 18e4);
4051
+ }
4052
+ return readGitSync(cwd);
4053
+ }
4054
+ async function pushGit(cwd) {
4055
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4056
+ if (inside?.trim() === "true") {
4057
+ await gitRun(["push", "--follow-tags"], cwd, 18e4);
4058
+ }
4059
+ return readGitSync(cwd);
4060
+ }
4061
+ async function readGitBranches(cwd) {
4062
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4063
+ if (inside?.trim() !== "true") {
4064
+ return { isRepo: false, local: [], remote: [], tags: [] };
4065
+ }
4066
+ const [current, local, remote, tags] = await Promise.all([
4067
+ gitText(["rev-parse", "--abbrev-ref", "HEAD"], cwd),
4068
+ gitText(["for-each-ref", "--format=%(refname:short)", "refs/heads"], cwd),
4069
+ gitText(["for-each-ref", "--format=%(refname:short)", "refs/remotes"], cwd),
4070
+ gitText(["for-each-ref", "--format=%(refname:short)", "refs/tags"], cwd)
4071
+ ]);
4072
+ return {
4073
+ isRepo: true,
4074
+ current: current?.trim() || void 0,
4075
+ local: nonEmptyLines(local),
4076
+ remote: nonEmptyLines(remote).filter((name) => !name.endsWith("/HEAD")),
4077
+ tags: nonEmptyLines(tags)
4078
+ };
4079
+ }
4080
+ async function commitGit(cwd, message) {
4081
+ const text = message.trim();
4082
+ if (text.length === 0) {
4083
+ return { ok: false, message: "A commit needs a message." };
4084
+ }
4085
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4086
+ if (inside?.trim() !== "true") {
4087
+ return { ok: false, message: "This project is not a git repository." };
4088
+ }
4089
+ const result = await gitExec(["commit", "-m", text], cwd, 6e4);
4090
+ return { ok: result.ok, message: result.ok ? void 0 : firstLine(result.output) };
4091
+ }
4092
+ async function checkoutGit(cwd, branch, create) {
4093
+ const name = branch.trim();
4094
+ if (!isBranchName(name)) {
4095
+ return { ok: false, message: "That is not a usable branch name." };
4096
+ }
4097
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4098
+ if (inside?.trim() !== "true") {
4099
+ return { ok: false, message: "This project is not a git repository." };
4100
+ }
4101
+ const result = await gitExec(
4102
+ create ? ["checkout", "-b", name] : ["checkout", name],
4103
+ cwd,
4104
+ 6e4
4105
+ );
4106
+ return { ok: result.ok, message: result.ok ? void 0 : firstLine(result.output) };
4107
+ }
4108
+ function isBranchName(name) {
4109
+ return name.length > 0 && !name.startsWith("-") && !name.includes("..") && !name.includes("@{") && !name.endsWith("/") && !/\s/.test(name) && !/[~^:?*\[\\]/.test(name);
4110
+ }
4111
+ function firstLine(output) {
4112
+ const line = output.split("\n").find((candidate) => candidate.trim().length > 0)?.trim();
4113
+ if (line === void 0) {
4114
+ return void 0;
4115
+ }
4116
+ return line.length > 200 ? `${line.slice(0, 197)}\u2026` : line;
4117
+ }
4118
+ function nonEmptyLines(output) {
4119
+ return (output ?? "").split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
4120
+ }
4121
+ function parseCounts(raw) {
4122
+ const [behind, ahead] = raw.trim().split(/\s+/).map((value) => Number.parseInt(value, 10));
4123
+ return [Number.isFinite(behind) ? behind : 0, Number.isFinite(ahead) ? ahead : 0];
4124
+ }
3939
4125
  function toPosix2(path) {
3940
4126
  return path.split("\\").join("/");
3941
4127
  }
@@ -3983,6 +4169,37 @@ function gitText(args, cwd) {
3983
4169
  );
3984
4170
  });
3985
4171
  }
4172
+ function resolvePaths(cwd, requested) {
4173
+ return requested.filter((path) => typeof path === "string" && path.length > 0).map((path) => toPosix2(relative3(cwd, resolveWithin(cwd, path))));
4174
+ }
4175
+ async function gitRun(args, cwd, timeout = 5e3) {
4176
+ return (await gitExec(args, cwd, timeout)).ok;
4177
+ }
4178
+ function gitExec(args, cwd, timeout = 5e3) {
4179
+ return new Promise((resolve6) => {
4180
+ execFile(
4181
+ "git",
4182
+ args,
4183
+ {
4184
+ cwd,
4185
+ timeout,
4186
+ maxBuffer: 8 * 1024 * 1024,
4187
+ windowsHide: true,
4188
+ env: {
4189
+ ...process.env,
4190
+ GIT_TERMINAL_PROMPT: "0",
4191
+ GIT_EDITOR: "true",
4192
+ GIT_MERGE_AUTOEDIT: "no"
4193
+ }
4194
+ },
4195
+ (error, stdout, stderr) => {
4196
+ resolve6(
4197
+ error === null ? { ok: true, output: stdout.trim() } : { ok: false, output: (stderr || error.message || "").trim() }
4198
+ );
4199
+ }
4200
+ );
4201
+ });
4202
+ }
3986
4203
 
3987
4204
  // ../server/src/internal/workspace/workspace-index.ts
3988
4205
  import { execFile as execFile2 } from "node:child_process";
@@ -4242,6 +4459,54 @@ var MorseSessionFactory = class {
4242
4459
  }
4243
4460
  return readGitDiff(cwd, path);
4244
4461
  }
4462
+ case "gitStage": {
4463
+ const cwd = this.requireWritableCwd(context);
4464
+ return stageGitPaths(cwd, gitPaths(args));
4465
+ }
4466
+ case "gitUnstage": {
4467
+ const cwd = this.requireWritableCwd(context);
4468
+ return unstageGitPaths(cwd, gitPaths(args));
4469
+ }
4470
+ case "gitCommitFiles": {
4471
+ const cwd = this.requireWritableCwd(context);
4472
+ const hash = typeof args?.hash === "string" ? args.hash : "";
4473
+ return readCommitFiles(cwd, hash);
4474
+ }
4475
+ case "gitCommitDiff": {
4476
+ const cwd = this.requireWritableCwd(context);
4477
+ const hash = typeof args?.hash === "string" ? args.hash : "";
4478
+ const path = typeof args?.path === "string" ? args.path : "";
4479
+ if (path.length === 0) {
4480
+ throw new UnsupportedByHostError('gitCommitDiff needs a "path" argument.');
4481
+ }
4482
+ return readCommitDiff(cwd, hash, path);
4483
+ }
4484
+ case "gitSync": {
4485
+ const cwd = this.requireWritableCwd(context);
4486
+ return readGitSync(cwd);
4487
+ }
4488
+ case "gitPull": {
4489
+ const cwd = this.requireWritableCwd(context);
4490
+ return pullGit(cwd);
4491
+ }
4492
+ case "gitPush": {
4493
+ const cwd = this.requireWritableCwd(context);
4494
+ return pushGit(cwd);
4495
+ }
4496
+ case "gitCommit": {
4497
+ const cwd = this.requireWritableCwd(context);
4498
+ const message = typeof args?.message === "string" ? args.message : "";
4499
+ return commitGit(cwd, message);
4500
+ }
4501
+ case "gitBranches": {
4502
+ const cwd = this.requireWritableCwd(context);
4503
+ return readGitBranches(cwd);
4504
+ }
4505
+ case "gitCheckout": {
4506
+ const cwd = this.requireWritableCwd(context);
4507
+ const branch = typeof args?.branch === "string" ? args.branch : "";
4508
+ return checkoutGit(cwd, branch, args?.create === true);
4509
+ }
4245
4510
  default:
4246
4511
  throw new UnsupportedByHostError(`This host does not support "${command}".`);
4247
4512
  }
@@ -4276,6 +4541,13 @@ MorseSessionFactory = __decorateClass([
4276
4541
  __decorateParam(3, Inject3(MORSE_LOGGER)),
4277
4542
  __decorateParam(4, Inject3(MORSE_CONFIG))
4278
4543
  ], MorseSessionFactory);
4544
+ function gitPaths(args) {
4545
+ const paths = args?.paths;
4546
+ if (!Array.isArray(paths)) {
4547
+ return [];
4548
+ }
4549
+ return paths.filter((path) => typeof path === "string" && path.length > 0);
4550
+ }
4279
4551
 
4280
4552
  // ../server/src/internal/ws/morse.gateway.ts
4281
4553
  var MorseGateway = class {