@supanadit/morse-web 0.11.0 → 0.12.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
@@ -1710,11 +1710,11 @@ function promptDescription(raw) {
1710
1710
  }
1711
1711
  body = raw.slice(frontmatter[0].length);
1712
1712
  }
1713
- const firstLine2 = body.split("\n").find((line) => line.trim().length > 0);
1714
- if (firstLine2 === void 0) {
1713
+ const firstLine = body.split("\n").find((line) => line.trim().length > 0);
1714
+ if (firstLine === void 0) {
1715
1715
  return void 0;
1716
1716
  }
1717
- const trimmed = firstLine2.trim();
1717
+ const trimmed = firstLine.trim();
1718
1718
  return trimmed.length > 60 ? `${trimmed.slice(0, 60)}...` : trimmed;
1719
1719
  }
1720
1720
  function unquote(value) {
@@ -4253,7 +4253,10 @@ async function commitGit(cwd, message) {
4253
4253
  return { ok: false, message: "This project is not a git repository." };
4254
4254
  }
4255
4255
  const result = await gitExec(["commit", "-m", text], cwd, 6e4);
4256
- return { ok: result.ok, message: result.ok ? void 0 : firstLine(result.output) };
4256
+ if (result.ok) {
4257
+ return { ok: true };
4258
+ }
4259
+ return { ok: false, message: describeCommitFailure(result.output) };
4257
4260
  }
4258
4261
  async function checkoutGit(cwd, branch, create) {
4259
4262
  const name = branch.trim();
@@ -4269,13 +4272,21 @@ async function checkoutGit(cwd, branch, create) {
4269
4272
  cwd,
4270
4273
  6e4
4271
4274
  );
4272
- return { ok: result.ok, message: result.ok ? void 0 : firstLine(result.output) };
4275
+ return { ok: result.ok, message: result.ok ? void 0 : gitRefusal(result.output) };
4273
4276
  }
4274
4277
  function isBranchName(name) {
4275
4278
  return name.length > 0 && !name.startsWith("-") && !name.includes("..") && !name.includes("@{") && !name.endsWith("/") && !/\s/.test(name) && !/[~^:?*\[\\]/.test(name);
4276
4279
  }
4277
- function firstLine(output) {
4278
- const line = output.split("\n").find((candidate) => candidate.trim().length > 0)?.trim();
4280
+ function describeCommitFailure(output) {
4281
+ if (/nothing to commit|no changes added to commit|nothing added to commit/i.test(output)) {
4282
+ return "Nothing is staged to commit. Stage a change first.";
4283
+ }
4284
+ return gitRefusal(output);
4285
+ }
4286
+ function gitRefusal(output) {
4287
+ const line = output.split("\n").map((candidate) => candidate.trim()).find(
4288
+ (candidate) => candidate.length > 0 && !candidate.startsWith("Command failed:") && !/^On branch /.test(candidate) && !/^Your branch /.test(candidate)
4289
+ );
4279
4290
  if (line === void 0) {
4280
4291
  return void 0;
4281
4292
  }
@@ -4359,9 +4370,12 @@ function gitExec(args, cwd, timeout = 5e3) {
4359
4370
  }
4360
4371
  },
4361
4372
  (error, stdout, stderr) => {
4362
- resolve6(
4363
- error === null ? { ok: true, output: stdout.trim() } : { ok: false, output: (stderr || error.message || "").trim() }
4364
- );
4373
+ if (error === null) {
4374
+ resolve6({ ok: true, output: stdout.trim() });
4375
+ return;
4376
+ }
4377
+ const output = [stderr.trim(), stdout.trim()].filter((part) => part.length > 0).join("\n");
4378
+ resolve6({ ok: false, output: output.length > 0 ? output : error.message.trim() });
4365
4379
  }
4366
4380
  );
4367
4381
  });
@@ -4498,6 +4512,81 @@ function toPosix3(path) {
4498
4512
  return path.split("\\").join("/");
4499
4513
  }
4500
4514
 
4515
+ // ../server/src/internal/workspace/workbench-store.ts
4516
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
4517
+ import { join as join8 } from "node:path";
4518
+ var LAYOUT_FILE = "workbench.json";
4519
+ var DRAFTS_FILE = "drafts.json";
4520
+ var LAYOUT_MAX_BYTES = 256 * 1024;
4521
+ var DRAFTS_MAX_BYTES = 32 * 1024 * 1024;
4522
+ function readWorkbench(dataDir) {
4523
+ return readSlot(join8(dataDir, LAYOUT_FILE), LAYOUT_MAX_BYTES);
4524
+ }
4525
+ function saveWorkbench(dataDir, snapshot) {
4526
+ return saveSlot(dataDir, LAYOUT_FILE, snapshot, LAYOUT_MAX_BYTES);
4527
+ }
4528
+ function readDrafts(dataDir) {
4529
+ return readSlot(join8(dataDir, DRAFTS_FILE), DRAFTS_MAX_BYTES);
4530
+ }
4531
+ function saveDrafts(dataDir, snapshot) {
4532
+ return saveSlot(dataDir, DRAFTS_FILE, snapshot, DRAFTS_MAX_BYTES);
4533
+ }
4534
+ function readSlot(path, maxBytes) {
4535
+ let raw;
4536
+ try {
4537
+ raw = readFileSync(path, "utf8");
4538
+ } catch {
4539
+ return void 0;
4540
+ }
4541
+ if (raw.length > maxBytes) {
4542
+ return void 0;
4543
+ }
4544
+ let parsed;
4545
+ try {
4546
+ parsed = JSON.parse(raw);
4547
+ } catch {
4548
+ return void 0;
4549
+ }
4550
+ return asSnapshot(parsed);
4551
+ }
4552
+ function saveSlot(dataDir, file, snapshot, maxBytes) {
4553
+ const valid = asSnapshot(snapshot);
4554
+ if (valid === void 0) {
4555
+ throw new UnsupportedByHostError(`Saving ${file} needs a { version, data } snapshot.`);
4556
+ }
4557
+ const serialized = JSON.stringify(valid);
4558
+ if (Buffer.byteLength(serialized, "utf8") > maxBytes) {
4559
+ throw new UnsupportedByHostError(`That ${file} snapshot is too large to save.`);
4560
+ }
4561
+ const target = join8(dataDir, file);
4562
+ const temp = `${target}.${process.pid}.tmp`;
4563
+ try {
4564
+ mkdirSync(dataDir, { recursive: true });
4565
+ writeFileSync(temp, serialized);
4566
+ renameSync(temp, target);
4567
+ return true;
4568
+ } catch {
4569
+ try {
4570
+ writeFileSync(target, serialized);
4571
+ return true;
4572
+ } catch {
4573
+ rmSync(temp, { force: true });
4574
+ return false;
4575
+ }
4576
+ }
4577
+ }
4578
+ function asSnapshot(value) {
4579
+ if (typeof value !== "object" || value === null) {
4580
+ return void 0;
4581
+ }
4582
+ const candidate = value;
4583
+ const version = candidate["version"];
4584
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
4585
+ return void 0;
4586
+ }
4587
+ return { version, data: candidate["data"] };
4588
+ }
4589
+
4501
4590
  // ../server/src/internal/ws/session-factory.service.ts
4502
4591
  var MorseSessionFactory = class {
4503
4592
  constructor(registry, transcripts, policy, logger, config, terminal) {
@@ -4532,6 +4621,10 @@ var MorseSessionFactory = class {
4532
4621
  // the bottom panel's terminal is this host's. It runs in the viewing
4533
4622
  // session's directory and is gated by `ProjectPolicy`, like a session.
4534
4623
  terminal: true,
4624
+ // The browser host restores the tabs, panel, terminals and half-written
4625
+ // prompts the reader left open, kept under `<MORSE_HOME>`. VS Code has its
4626
+ // own tab restoration and leaves this off.
4627
+ workbench: true,
4535
4628
  // A browser cannot hand a dragged file's path to pi, so the host takes the
4536
4629
  // bytes and writes them next to the session; the frontend then `@mentions`
4537
4630
  // the path it gets back.
@@ -4595,13 +4688,25 @@ var MorseSessionFactory = class {
4595
4688
  });
4596
4689
  return { ...listing, canOpen: this.policy.canOpen(listing.path) };
4597
4690
  }
4691
+ case "readWorkbench": {
4692
+ return readWorkbench(this.config.dataDir) ?? null;
4693
+ }
4694
+ case "saveWorkbench": {
4695
+ return { ok: saveWorkbench(this.config.dataDir, args) };
4696
+ }
4697
+ case "readDrafts": {
4698
+ return readDrafts(this.config.dataDir) ?? null;
4699
+ }
4700
+ case "saveDrafts": {
4701
+ return { ok: saveDrafts(this.config.dataDir, args) };
4702
+ }
4598
4703
  case "listFiles": {
4599
4704
  const cwd = this.requireWritableCwd(context);
4600
4705
  const files = await workspaceFiles(cwd, this.logger, { fresh: args?.fresh === true });
4601
4706
  return { files };
4602
4707
  }
4603
4708
  case "readFile": {
4604
- const cwd = this.requireWritableCwd(context);
4709
+ const cwd = this.commandCwd(context, args);
4605
4710
  const path = typeof args?.path === "string" ? args.path : "";
4606
4711
  if (path.length === 0) {
4607
4712
  throw new UnsupportedByHostError('readFile needs a "path" argument.');
@@ -4625,7 +4730,7 @@ var MorseSessionFactory = class {
4625
4730
  return readGitStatus(cwd);
4626
4731
  }
4627
4732
  case "gitDiff": {
4628
- const cwd = this.requireWritableCwd(context);
4733
+ const cwd = this.commandCwd(context, args);
4629
4734
  const path = typeof args?.path === "string" ? args.path : "";
4630
4735
  if (path.length === 0) {
4631
4736
  throw new UnsupportedByHostError('gitDiff needs a "path" argument.');
@@ -4646,7 +4751,7 @@ var MorseSessionFactory = class {
4646
4751
  return readCommitFiles(cwd, hash);
4647
4752
  }
4648
4753
  case "gitCommitDiff": {
4649
- const cwd = this.requireWritableCwd(context);
4754
+ const cwd = this.commandCwd(context, args);
4650
4755
  const hash = typeof args?.hash === "string" ? args.hash : "";
4651
4756
  const path = typeof args?.path === "string" ? args.path : "";
4652
4757
  if (path.length === 0) {
@@ -4699,6 +4804,22 @@ var MorseSessionFactory = class {
4699
4804
  }
4700
4805
  return cwd;
4701
4806
  }
4807
+ /**
4808
+ * The directory a file command works in. A restored preview tab carries the
4809
+ * project it was read from (`args.cwd`), because the host may still be on
4810
+ * another session when that tab is loaded; the policy checks it exactly like a
4811
+ * terminal's directory. Without one, the viewing session's directory is used.
4812
+ */
4813
+ commandCwd(context, args) {
4814
+ const requested = args?.cwd;
4815
+ if (typeof requested === "string" && requested.length > 0) {
4816
+ if (!this.policy.canOpen(requested)) {
4817
+ throw new UnsupportedByHostError(`Morse is not allowed to work in ${requested}.`);
4818
+ }
4819
+ return requested;
4820
+ }
4821
+ return this.requireWritableCwd(context);
4822
+ }
4702
4823
  /** The cwd of the session this connection is showing. */
4703
4824
  activeCwd() {
4704
4825
  const key = this.registry.activeKeyOf();
@@ -4771,18 +4892,19 @@ MorseGateway = __decorateClass([
4771
4892
  ], MorseGateway);
4772
4893
 
4773
4894
  // ../server/src/app/config.ts
4774
- import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute6, join as join8 } from "node:path";
4775
- import { existsSync as existsSync2, readFileSync } from "node:fs";
4895
+ import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute6, join as join9 } from "node:path";
4896
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
4897
+ import { homedir as homedir4 } from "node:os";
4776
4898
  import { fileURLToPath } from "node:url";
4777
4899
  function loadConfig(env = process.env) {
4778
4900
  const workspacePath = resolveWorkspacePath(env);
4779
4901
  const configuredUiDir = env.MORSE_UI_DIR;
4780
4902
  const moduleDir = dirname2(fileURLToPath(import.meta.url));
4781
- const bundledUiDir = join8(moduleDir, "ui");
4782
- const monorepoUiDir = join8(moduleDir, "..", "..", "..", "ui-angular", "dist");
4783
- const defaultUiDir = existsSync2(join8(bundledUiDir, "index.html")) ? bundledUiDir : monorepoUiDir;
4903
+ const bundledUiDir = join9(moduleDir, "ui");
4904
+ const monorepoUiDir = join9(moduleDir, "..", "..", "..", "ui-angular", "dist");
4905
+ const defaultUiDir = existsSync2(join9(bundledUiDir, "index.html")) ? bundledUiDir : monorepoUiDir;
4784
4906
  const defaultHost = "127.0.0.1";
4785
- const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir : join8(process.cwd(), configuredUiDir) : defaultUiDir;
4907
+ const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir : join9(process.cwd(), configuredUiDir) : defaultUiDir;
4786
4908
  return {
4787
4909
  host: env.MORSE_HOST ?? defaultHost,
4788
4910
  port: parsePositiveInt(env.MORSE_PORT, 4399),
@@ -4797,6 +4919,7 @@ function loadConfig(env = process.env) {
4797
4919
  hotSessions: parsePositiveInt(env.MORSE_HOT_SESSIONS, 4),
4798
4920
  projects: parseList(env.MORSE_PROJECTS),
4799
4921
  uploadDir: env.MORSE_UPLOAD_DIR?.trim() || DEFAULT_UPLOAD_DIR,
4922
+ dataDir: resolveDataDir(env),
4800
4923
  updateCheck: !isOff(env.MORSE_UPDATE_CHECK),
4801
4924
  instance: env.MORSE_INSTANCE?.trim() || void 0
4802
4925
  };
@@ -4804,12 +4927,16 @@ function loadConfig(env = process.env) {
4804
4927
  function readFrontendIdentity(uiDir) {
4805
4928
  try {
4806
4929
  return frontendIdentity(
4807
- parseFrontendManifest(readFileSync(join8(uiDir, FRONTEND_MANIFEST_FILE), "utf8"))
4930
+ parseFrontendManifest(readFileSync2(join9(uiDir, FRONTEND_MANIFEST_FILE), "utf8"))
4808
4931
  );
4809
4932
  } catch {
4810
4933
  return void 0;
4811
4934
  }
4812
4935
  }
4936
+ function resolveDataDir(env) {
4937
+ const explicit = env.MORSE_HOME?.trim();
4938
+ return explicit && explicit.length > 0 ? explicit : join9(homedir4(), ".morse");
4939
+ }
4813
4940
  function parsePositiveInt(value, fallback) {
4814
4941
  if (!value) {
4815
4942
  return fallback;
@@ -4835,7 +4962,7 @@ function resolveWorkspacePath(env) {
4835
4962
  const start = process.cwd();
4836
4963
  let directory = start;
4837
4964
  for (; ; ) {
4838
- const manifest = join8(directory, "package.json");
4965
+ const manifest = join9(directory, "package.json");
4839
4966
  if (existsSync2(manifest) && declaresWorkspaces(manifest)) {
4840
4967
  return directory;
4841
4968
  }
@@ -4848,7 +4975,7 @@ function resolveWorkspacePath(env) {
4848
4975
  }
4849
4976
  function declaresWorkspaces(manifestPath) {
4850
4977
  try {
4851
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
4978
+ const manifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
4852
4979
  return manifest.workspaces !== void 0;
4853
4980
  } catch {
4854
4981
  return false;