@supanadit/morse-web 0.9.5 → 0.11.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
@@ -38,7 +38,11 @@ var CLIENT_MESSAGE_TYPES = [
38
38
  "model/set",
39
39
  "thinking/set",
40
40
  "interaction/respond",
41
- "host/command"
41
+ "host/command",
42
+ "terminal/open",
43
+ "terminal/input",
44
+ "terminal/resize",
45
+ "terminal/close"
42
46
  ];
43
47
  function encodeWireMessage(message) {
44
48
  return JSON.stringify(message);
@@ -75,7 +79,7 @@ function hasKnownType(value, allowed) {
75
79
  }
76
80
 
77
81
  // ../protocol/dist/version.js
78
- var PROTOCOL_VERSION = 19;
82
+ var PROTOCOL_VERSION = 20;
79
83
  var FRONTEND_MANIFEST_FILE = "webview.manifest.json";
80
84
  var DEFAULT_WS_PATH = "/ws";
81
85
 
@@ -1706,11 +1710,11 @@ function promptDescription(raw) {
1706
1710
  }
1707
1711
  body = raw.slice(frontmatter[0].length);
1708
1712
  }
1709
- const firstLine = body.split("\n").find((line) => line.trim().length > 0);
1710
- if (firstLine === void 0) {
1713
+ const firstLine2 = body.split("\n").find((line) => line.trim().length > 0);
1714
+ if (firstLine2 === void 0) {
1711
1715
  return void 0;
1712
1716
  }
1713
- const trimmed = firstLine.trim();
1717
+ const trimmed = firstLine2.trim();
1714
1718
  return trimmed.length > 60 ? `${trimmed.slice(0, 60)}...` : trimmed;
1715
1719
  }
1716
1720
  function unquote(value) {
@@ -2667,6 +2671,14 @@ var HostSessionController = class {
2667
2671
  loadingOlderHistory = false;
2668
2672
  /** Discriminates history item ids across pages (see `historyItems`). */
2669
2673
  historyPageSeq = 0;
2674
+ /** The shells this client has open, keyed by the terminal id it named. */
2675
+ terminals = /* @__PURE__ */ new Map();
2676
+ /**
2677
+ * Bumped whenever a terminal id is opened or closed. A shell's late `exit`
2678
+ * (a replaced one, a killed one) compares its generation and stays silent, so
2679
+ * it cannot delete or report over the shell now holding the id.
2680
+ */
2681
+ terminalGenerations = /* @__PURE__ */ new Map();
2670
2682
  constructor(options) {
2671
2683
  this.options = options;
2672
2684
  }
@@ -2709,6 +2721,11 @@ var HostSessionController = class {
2709
2721
  return;
2710
2722
  }
2711
2723
  this.disposed = true;
2724
+ for (const process2 of this.terminals.values()) {
2725
+ process2.kill();
2726
+ }
2727
+ this.terminals.clear();
2728
+ this.terminalGenerations.clear();
2712
2729
  this.detachRegistry?.();
2713
2730
  this.detachRegistry = void 0;
2714
2731
  this.detachTranscripts?.();
@@ -2796,10 +2813,83 @@ var HostSessionController = class {
2796
2813
  case "host/command":
2797
2814
  await this.runHostCommand(message.payload.command, message.payload.args, message.payload.requestId);
2798
2815
  return;
2816
+ case "terminal/open":
2817
+ await this.openTerminal(message.payload);
2818
+ return;
2819
+ case "terminal/input":
2820
+ this.terminals.get(message.payload.terminalId)?.write(message.payload.data);
2821
+ return;
2822
+ case "terminal/resize":
2823
+ this.terminals.get(message.payload.terminalId)?.resize(message.payload.cols, message.payload.rows);
2824
+ return;
2825
+ case "terminal/close":
2826
+ this.closeTerminal(message.payload.terminalId);
2827
+ return;
2799
2828
  default:
2800
2829
  return;
2801
2830
  }
2802
2831
  }
2832
+ /**
2833
+ * Starts a shell for the bottom panel. The directory is the viewing session's
2834
+ * (or the draft's), never an arbitrary path the client names — the backend
2835
+ * still re-checks it against the host policy before spawning.
2836
+ */
2837
+ async openTerminal(payload) {
2838
+ if (this.options.terminal === void 0) {
2839
+ this.emitTerminalExit(payload.terminalId, void 0, "This host has no terminal.");
2840
+ return;
2841
+ }
2842
+ this.closeTerminal(payload.terminalId);
2843
+ const generation = this.terminalGenerations.get(payload.terminalId) ?? 0;
2844
+ const cwd = payload.cwd ?? this.currentState().workspace.cwd;
2845
+ const cols = clampGeometry(payload.cols, 80);
2846
+ const rows = clampGeometry(payload.rows, 24);
2847
+ let process2;
2848
+ try {
2849
+ process2 = await this.options.terminal.open({ cwd, cols, rows }, {
2850
+ output: (data) => {
2851
+ if (this.terminalGenerations.get(payload.terminalId) === generation) {
2852
+ this.emitTerminalOutput(payload.terminalId, data);
2853
+ }
2854
+ },
2855
+ exit: (code, error) => {
2856
+ if (this.terminalGenerations.get(payload.terminalId) !== generation) {
2857
+ return;
2858
+ }
2859
+ this.terminals.delete(payload.terminalId);
2860
+ this.emitTerminalExit(payload.terminalId, code, error);
2861
+ }
2862
+ });
2863
+ } catch (error) {
2864
+ this.options.logger.warn("Failed to open a terminal", error);
2865
+ this.emitTerminalExit(payload.terminalId, void 0, describeTerminalError(error));
2866
+ return;
2867
+ }
2868
+ if (!this.disposed && this.terminalGenerations.get(payload.terminalId) === generation) {
2869
+ this.terminals.set(payload.terminalId, process2);
2870
+ } else {
2871
+ process2.kill();
2872
+ }
2873
+ }
2874
+ closeTerminal(terminalId) {
2875
+ this.terminalGenerations.set(terminalId, (this.terminalGenerations.get(terminalId) ?? 0) + 1);
2876
+ const process2 = this.terminals.get(terminalId);
2877
+ if (process2 === void 0) {
2878
+ return;
2879
+ }
2880
+ this.terminals.delete(terminalId);
2881
+ process2.kill();
2882
+ }
2883
+ emitTerminalOutput(terminalId, data) {
2884
+ if (data.length > 0 && !this.disposed) {
2885
+ this.options.emit({ type: "terminal/output", payload: { terminalId, data } });
2886
+ }
2887
+ }
2888
+ emitTerminalExit(terminalId, code, error) {
2889
+ if (!this.disposed) {
2890
+ this.options.emit({ type: "terminal/exit", payload: { terminalId, code, error } });
2891
+ }
2892
+ }
2803
2893
  async openDefaultSession() {
2804
2894
  const existingKey = this.options.services.registry.activeKeyOf();
2805
2895
  if (existingKey) {
@@ -3227,15 +3317,15 @@ var HostSessionController = class {
3227
3317
  this.answerHostCommand(requestId, false, void 0, `This host does not support "${command}".`);
3228
3318
  return;
3229
3319
  }
3230
- const result = await this.guard(() => Promise.resolve(handler(command, args, { cwd: this.currentState().workspace.cwd })));
3231
- if (!requestId) {
3232
- return;
3233
- }
3234
- if (result.ok) {
3235
- this.answerHostCommand(requestId, true, result.value);
3320
+ let value;
3321
+ try {
3322
+ value = await handler(command, args, { cwd: this.currentState().workspace.cwd });
3323
+ } catch (error) {
3324
+ this.options.logger.error(`Morse command failed: ${describeError(error)}`, error);
3325
+ this.answerHostCommand(requestId, false, void 0, `"${command}" failed`);
3236
3326
  return;
3237
3327
  }
3238
- this.answerHostCommand(requestId, false, void 0, `"${command}" failed`);
3328
+ this.answerHostCommand(requestId, true, value);
3239
3329
  }
3240
3330
  /** Replies to a host command the frontend asked a value for. */
3241
3331
  answerHostCommand(requestId, ok, data, error) {
@@ -3543,6 +3633,15 @@ function describeError(error) {
3543
3633
  }
3544
3634
  return typeof error === "string" ? error : JSON.stringify(error);
3545
3635
  }
3636
+ function clampGeometry(value, fallback) {
3637
+ if (value === void 0 || !Number.isFinite(value)) {
3638
+ return fallback;
3639
+ }
3640
+ return Math.max(1, Math.min(1e3, Math.floor(value)));
3641
+ }
3642
+ function describeTerminalError(error) {
3643
+ return error instanceof MorseError ? error.message : describeError(error);
3644
+ }
3546
3645
  function describeAgentFailure(error, hint) {
3547
3646
  const code = agentErrorCode(error);
3548
3647
  const install = error instanceof AgentUnavailableError ? error.remedy?.install : void 0;
@@ -3667,8 +3766,75 @@ var ServerProjectPolicy = class {
3667
3766
  }
3668
3767
  };
3669
3768
 
3670
- // ../server/src/internal/registry.ts
3769
+ // ../server/src/internal/terminal/terminal.service.ts
3770
+ import { spawn as spawnPty } from "node-pty";
3671
3771
  import { Inject as Inject2, Injectable } from "@nestjs/common";
3772
+ var SIGKILL_AFTER_MS = 1500;
3773
+ function defaultShell() {
3774
+ const configured = process.env.SHELL?.trim();
3775
+ if (configured !== void 0 && configured.length > 0) {
3776
+ return configured;
3777
+ }
3778
+ return process.platform === "win32" ? "powershell.exe" : "/bin/bash";
3779
+ }
3780
+ var ServerTerminalBackend = class {
3781
+ constructor(policy, logger) {
3782
+ this.policy = policy;
3783
+ this.logger = logger;
3784
+ }
3785
+ open(options, sink) {
3786
+ if (!this.policy.canOpen(options.cwd)) {
3787
+ throw new UnsupportedByHostError(
3788
+ `Morse is not allowed to run a terminal in ${options.cwd}.`
3789
+ );
3790
+ }
3791
+ const shell = defaultShell();
3792
+ const pty = spawnPty(shell, [], {
3793
+ name: "xterm-256color",
3794
+ cols: options.cols,
3795
+ rows: options.rows,
3796
+ cwd: options.cwd,
3797
+ env: { ...process.env, TERM: "xterm-256color", MORSE_TERMINAL: "1" }
3798
+ });
3799
+ this.logger.info(`Terminal opened in ${options.cwd} (${shell}, pid ${pty.pid})`);
3800
+ let exited = false;
3801
+ const data = pty.onData((chunk) => sink.output(chunk));
3802
+ pty.onExit(({ exitCode }) => {
3803
+ exited = true;
3804
+ data.dispose();
3805
+ sink.exit(exitCode);
3806
+ });
3807
+ return {
3808
+ write: (input) => {
3809
+ if (!exited) {
3810
+ pty.write(input);
3811
+ }
3812
+ },
3813
+ resize: (cols, rows) => {
3814
+ if (!exited) {
3815
+ pty.resize(cols, rows);
3816
+ }
3817
+ },
3818
+ kill: () => {
3819
+ if (exited) {
3820
+ return;
3821
+ }
3822
+ exited = true;
3823
+ pty.kill();
3824
+ const timer = setTimeout(() => pty.kill("SIGKILL"), SIGKILL_AFTER_MS);
3825
+ timer.unref?.();
3826
+ }
3827
+ };
3828
+ }
3829
+ };
3830
+ ServerTerminalBackend = __decorateClass([
3831
+ Injectable(),
3832
+ __decorateParam(0, Inject2(MORSE_PROJECT_POLICY)),
3833
+ __decorateParam(1, Inject2(MORSE_LOGGER))
3834
+ ], ServerTerminalBackend);
3835
+
3836
+ // ../server/src/internal/registry.ts
3837
+ import { Inject as Inject3, Injectable as Injectable2 } from "@nestjs/common";
3672
3838
  function createSessionRegistry(adapter, config, logger) {
3673
3839
  return new SessionRegistry({
3674
3840
  factory: adapter.factory,
@@ -3689,19 +3855,19 @@ var MorseRegistryLifecycle = class {
3689
3855
  }
3690
3856
  };
3691
3857
  MorseRegistryLifecycle = __decorateClass([
3692
- Injectable(),
3693
- __decorateParam(0, Inject2(MORSE_SESSION_REGISTRY)),
3694
- __decorateParam(1, Inject2(MORSE_TRANSCRIPT_STORE))
3858
+ Injectable2(),
3859
+ __decorateParam(0, Inject3(MORSE_SESSION_REGISTRY)),
3860
+ __decorateParam(1, Inject3(MORSE_TRANSCRIPT_STORE))
3695
3861
  ], MorseRegistryLifecycle);
3696
3862
 
3697
3863
  // ../server/src/internal/ws/morse.gateway.ts
3698
3864
  import {
3699
3865
  WebSocketGateway
3700
3866
  } from "@nestjs/websockets";
3701
- import { Inject as Inject4 } from "@nestjs/common";
3867
+ import { Inject as Inject5 } from "@nestjs/common";
3702
3868
 
3703
3869
  // ../server/src/internal/ws/session-factory.service.ts
3704
- import { Inject as Inject3, Injectable as Injectable2 } from "@nestjs/common";
3870
+ import { Inject as Inject4, Injectable as Injectable3 } from "@nestjs/common";
3705
3871
 
3706
3872
  // ../server/src/internal/uploads/upload-store.ts
3707
3873
  import { existsSync } from "node:fs";
@@ -3959,6 +4125,169 @@ async function readGitDiff(cwd, requested) {
3959
4125
  );
3960
4126
  return { path: rel, diff: diff ?? "" };
3961
4127
  }
4128
+ async function readCommitFiles(cwd, requested) {
4129
+ const hash = requested.trim();
4130
+ if (!isCommitHash(hash)) {
4131
+ return { isRepo: false, hash, files: [] };
4132
+ }
4133
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4134
+ if (inside?.trim() !== "true") {
4135
+ return { isRepo: false, hash, files: [] };
4136
+ }
4137
+ const raw = await gitText(
4138
+ ["show", "--name-status", "--format=", "-z", "--find-renames", "--relative", hash],
4139
+ cwd
4140
+ );
4141
+ return { isRepo: true, hash, files: parseNameStatus(raw ?? "") };
4142
+ }
4143
+ async function readCommitDiff(cwd, hash, requested) {
4144
+ const rel = toPosix2(relative3(cwd, resolveWithin(cwd, requested)));
4145
+ if (!isCommitHash(hash.trim())) {
4146
+ return { path: rel, diff: "" };
4147
+ }
4148
+ const diff = await gitText(
4149
+ ["show", "--format=", "--no-color", "--no-ext-diff", "-U3", "--relative", hash.trim(), "--", rel],
4150
+ cwd
4151
+ );
4152
+ return { path: rel, diff: diff ?? "" };
4153
+ }
4154
+ function isCommitHash(hash) {
4155
+ return /^[0-9a-fA-F]{4,40}$/.test(hash);
4156
+ }
4157
+ function parseNameStatus(raw) {
4158
+ const tokens = raw.split("\0");
4159
+ const files = [];
4160
+ for (let index = 0; index < tokens.length; index += 1) {
4161
+ const status = tokens[index];
4162
+ if (!status) {
4163
+ continue;
4164
+ }
4165
+ const letter = status[0] ?? "";
4166
+ const rename = letter === "R" || letter === "C";
4167
+ const path = tokens[index + (rename ? 2 : 1)];
4168
+ index += rename ? 2 : 1;
4169
+ if (path) {
4170
+ files.push({ path: toPosix2(path), status: `${letter} ` });
4171
+ }
4172
+ }
4173
+ return files;
4174
+ }
4175
+ async function stageGitPaths(cwd, requested) {
4176
+ const paths = resolvePaths(cwd, requested);
4177
+ if (paths.length > 0) {
4178
+ await gitRun(["add", "--", ...paths], cwd);
4179
+ }
4180
+ return readGitStatus(cwd);
4181
+ }
4182
+ async function unstageGitPaths(cwd, requested) {
4183
+ const paths = resolvePaths(cwd, requested);
4184
+ if (paths.length > 0) {
4185
+ const reset = await gitRun(["reset", "--quiet", "HEAD", "--", ...paths], cwd);
4186
+ if (!reset) {
4187
+ await gitRun(["rm", "--cached", "--quiet", "--", ...paths], cwd);
4188
+ }
4189
+ }
4190
+ return readGitStatus(cwd);
4191
+ }
4192
+ async function readGitSync(cwd) {
4193
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4194
+ if (inside?.trim() !== "true") {
4195
+ return { isRepo: false, ahead: 0, behind: 0 };
4196
+ }
4197
+ const [branch, upstream] = await Promise.all([
4198
+ gitText(["rev-parse", "--abbrev-ref", "HEAD"], cwd),
4199
+ gitText(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], cwd)
4200
+ ]);
4201
+ const name = branch?.trim() || void 0;
4202
+ const tracked = upstream?.trim() || void 0;
4203
+ if (tracked === void 0) {
4204
+ return { isRepo: true, branch: name, ahead: 0, behind: 0 };
4205
+ }
4206
+ const counts = await gitText(
4207
+ ["rev-list", "--left-right", "--count", `${tracked}...HEAD`],
4208
+ cwd
4209
+ );
4210
+ const [behind, ahead] = parseCounts(counts ?? "");
4211
+ return { isRepo: true, branch: name, upstream: tracked, ahead, behind };
4212
+ }
4213
+ async function pullGit(cwd) {
4214
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4215
+ if (inside?.trim() === "true") {
4216
+ await gitRun(["pull", "--no-edit"], cwd, 18e4);
4217
+ }
4218
+ return readGitSync(cwd);
4219
+ }
4220
+ async function pushGit(cwd) {
4221
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4222
+ if (inside?.trim() === "true") {
4223
+ await gitRun(["push", "--follow-tags"], cwd, 18e4);
4224
+ }
4225
+ return readGitSync(cwd);
4226
+ }
4227
+ async function readGitBranches(cwd) {
4228
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4229
+ if (inside?.trim() !== "true") {
4230
+ return { isRepo: false, local: [], remote: [], tags: [] };
4231
+ }
4232
+ const [current, local, remote, tags] = await Promise.all([
4233
+ gitText(["rev-parse", "--abbrev-ref", "HEAD"], cwd),
4234
+ gitText(["for-each-ref", "--format=%(refname:short)", "refs/heads"], cwd),
4235
+ gitText(["for-each-ref", "--format=%(refname:short)", "refs/remotes"], cwd),
4236
+ gitText(["for-each-ref", "--format=%(refname:short)", "refs/tags"], cwd)
4237
+ ]);
4238
+ return {
4239
+ isRepo: true,
4240
+ current: current?.trim() || void 0,
4241
+ local: nonEmptyLines(local),
4242
+ remote: nonEmptyLines(remote).filter((name) => !name.endsWith("/HEAD")),
4243
+ tags: nonEmptyLines(tags)
4244
+ };
4245
+ }
4246
+ async function commitGit(cwd, message) {
4247
+ const text = message.trim();
4248
+ if (text.length === 0) {
4249
+ return { ok: false, message: "A commit needs a message." };
4250
+ }
4251
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4252
+ if (inside?.trim() !== "true") {
4253
+ return { ok: false, message: "This project is not a git repository." };
4254
+ }
4255
+ const result = await gitExec(["commit", "-m", text], cwd, 6e4);
4256
+ return { ok: result.ok, message: result.ok ? void 0 : firstLine(result.output) };
4257
+ }
4258
+ async function checkoutGit(cwd, branch, create) {
4259
+ const name = branch.trim();
4260
+ if (!isBranchName(name)) {
4261
+ return { ok: false, message: "That is not a usable branch name." };
4262
+ }
4263
+ const inside = await gitText(["rev-parse", "--is-inside-work-tree"], cwd);
4264
+ if (inside?.trim() !== "true") {
4265
+ return { ok: false, message: "This project is not a git repository." };
4266
+ }
4267
+ const result = await gitExec(
4268
+ create ? ["checkout", "-b", name] : ["checkout", name],
4269
+ cwd,
4270
+ 6e4
4271
+ );
4272
+ return { ok: result.ok, message: result.ok ? void 0 : firstLine(result.output) };
4273
+ }
4274
+ function isBranchName(name) {
4275
+ return name.length > 0 && !name.startsWith("-") && !name.includes("..") && !name.includes("@{") && !name.endsWith("/") && !/\s/.test(name) && !/[~^:?*\[\\]/.test(name);
4276
+ }
4277
+ function firstLine(output) {
4278
+ const line = output.split("\n").find((candidate) => candidate.trim().length > 0)?.trim();
4279
+ if (line === void 0) {
4280
+ return void 0;
4281
+ }
4282
+ return line.length > 200 ? `${line.slice(0, 197)}\u2026` : line;
4283
+ }
4284
+ function nonEmptyLines(output) {
4285
+ return (output ?? "").split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
4286
+ }
4287
+ function parseCounts(raw) {
4288
+ const [behind, ahead] = raw.trim().split(/\s+/).map((value) => Number.parseInt(value, 10));
4289
+ return [Number.isFinite(behind) ? behind : 0, Number.isFinite(ahead) ? ahead : 0];
4290
+ }
3962
4291
  function toPosix2(path) {
3963
4292
  return path.split("\\").join("/");
3964
4293
  }
@@ -4006,6 +4335,37 @@ function gitText(args, cwd) {
4006
4335
  );
4007
4336
  });
4008
4337
  }
4338
+ function resolvePaths(cwd, requested) {
4339
+ return requested.filter((path) => typeof path === "string" && path.length > 0).map((path) => toPosix2(relative3(cwd, resolveWithin(cwd, path))));
4340
+ }
4341
+ async function gitRun(args, cwd, timeout = 5e3) {
4342
+ return (await gitExec(args, cwd, timeout)).ok;
4343
+ }
4344
+ function gitExec(args, cwd, timeout = 5e3) {
4345
+ return new Promise((resolve6) => {
4346
+ execFile(
4347
+ "git",
4348
+ args,
4349
+ {
4350
+ cwd,
4351
+ timeout,
4352
+ maxBuffer: 8 * 1024 * 1024,
4353
+ windowsHide: true,
4354
+ env: {
4355
+ ...process.env,
4356
+ GIT_TERMINAL_PROMPT: "0",
4357
+ GIT_EDITOR: "true",
4358
+ GIT_MERGE_AUTOEDIT: "no"
4359
+ }
4360
+ },
4361
+ (error, stdout, stderr) => {
4362
+ resolve6(
4363
+ error === null ? { ok: true, output: stdout.trim() } : { ok: false, output: (stderr || error.message || "").trim() }
4364
+ );
4365
+ }
4366
+ );
4367
+ });
4368
+ }
4009
4369
 
4010
4370
  // ../server/src/internal/workspace/workspace-index.ts
4011
4371
  import { execFile as execFile2 } from "node:child_process";
@@ -4140,12 +4500,13 @@ function toPosix3(path) {
4140
4500
 
4141
4501
  // ../server/src/internal/ws/session-factory.service.ts
4142
4502
  var MorseSessionFactory = class {
4143
- constructor(registry, transcripts, policy, logger, config) {
4503
+ constructor(registry, transcripts, policy, logger, config, terminal) {
4144
4504
  this.registry = registry;
4145
4505
  this.transcripts = transcripts;
4146
4506
  this.policy = policy;
4147
4507
  this.logger = logger;
4148
4508
  this.config = config;
4509
+ this.terminal = terminal;
4149
4510
  }
4150
4511
  capabilities() {
4151
4512
  return {
@@ -4167,6 +4528,10 @@ var MorseSessionFactory = class {
4167
4528
  // The same reasoning for history: VS Code has Source Control, so the
4168
4529
  // browser host is the one that answers `gitLog` for its own git panel.
4169
4530
  gitPanel: true,
4531
+ // VS Code already has an integrated terminal; the browser host has none, so
4532
+ // the bottom panel's terminal is this host's. It runs in the viewing
4533
+ // session's directory and is gated by `ProjectPolicy`, like a session.
4534
+ terminal: true,
4170
4535
  // A browser cannot hand a dragged file's path to pi, so the host takes the
4171
4536
  // bytes and writes them next to the session; the frontend then `@mentions`
4172
4537
  // the path it gets back.
@@ -4205,6 +4570,8 @@ var MorseSessionFactory = class {
4205
4570
  // A page load must not create a session; the first prompt opens one.
4206
4571
  autoOpen: false,
4207
4572
  onHostCommand: (command, args, context) => this.runHostCommand(command, args, context),
4573
+ // The bottom panel's shell: this host can spawn one on its own machine.
4574
+ terminal: this.terminal,
4208
4575
  agentHint: "Install the pi CLI and make sure it is on PATH, or start the server with MORSE_PI_PATH set."
4209
4576
  });
4210
4577
  }
@@ -4265,6 +4632,54 @@ var MorseSessionFactory = class {
4265
4632
  }
4266
4633
  return readGitDiff(cwd, path);
4267
4634
  }
4635
+ case "gitStage": {
4636
+ const cwd = this.requireWritableCwd(context);
4637
+ return stageGitPaths(cwd, gitPaths(args));
4638
+ }
4639
+ case "gitUnstage": {
4640
+ const cwd = this.requireWritableCwd(context);
4641
+ return unstageGitPaths(cwd, gitPaths(args));
4642
+ }
4643
+ case "gitCommitFiles": {
4644
+ const cwd = this.requireWritableCwd(context);
4645
+ const hash = typeof args?.hash === "string" ? args.hash : "";
4646
+ return readCommitFiles(cwd, hash);
4647
+ }
4648
+ case "gitCommitDiff": {
4649
+ const cwd = this.requireWritableCwd(context);
4650
+ const hash = typeof args?.hash === "string" ? args.hash : "";
4651
+ const path = typeof args?.path === "string" ? args.path : "";
4652
+ if (path.length === 0) {
4653
+ throw new UnsupportedByHostError('gitCommitDiff needs a "path" argument.');
4654
+ }
4655
+ return readCommitDiff(cwd, hash, path);
4656
+ }
4657
+ case "gitSync": {
4658
+ const cwd = this.requireWritableCwd(context);
4659
+ return readGitSync(cwd);
4660
+ }
4661
+ case "gitPull": {
4662
+ const cwd = this.requireWritableCwd(context);
4663
+ return pullGit(cwd);
4664
+ }
4665
+ case "gitPush": {
4666
+ const cwd = this.requireWritableCwd(context);
4667
+ return pushGit(cwd);
4668
+ }
4669
+ case "gitCommit": {
4670
+ const cwd = this.requireWritableCwd(context);
4671
+ const message = typeof args?.message === "string" ? args.message : "";
4672
+ return commitGit(cwd, message);
4673
+ }
4674
+ case "gitBranches": {
4675
+ const cwd = this.requireWritableCwd(context);
4676
+ return readGitBranches(cwd);
4677
+ }
4678
+ case "gitCheckout": {
4679
+ const cwd = this.requireWritableCwd(context);
4680
+ const branch = typeof args?.branch === "string" ? args.branch : "";
4681
+ return checkoutGit(cwd, branch, args?.create === true);
4682
+ }
4268
4683
  default:
4269
4684
  throw new UnsupportedByHostError(`This host does not support "${command}".`);
4270
4685
  }
@@ -4292,13 +4707,21 @@ var MorseSessionFactory = class {
4292
4707
  }
4293
4708
  };
4294
4709
  MorseSessionFactory = __decorateClass([
4295
- Injectable2(),
4296
- __decorateParam(0, Inject3(MORSE_SESSION_REGISTRY)),
4297
- __decorateParam(1, Inject3(MORSE_TRANSCRIPT_STORE)),
4298
- __decorateParam(2, Inject3(MORSE_PROJECT_POLICY)),
4299
- __decorateParam(3, Inject3(MORSE_LOGGER)),
4300
- __decorateParam(4, Inject3(MORSE_CONFIG))
4710
+ Injectable3(),
4711
+ __decorateParam(0, Inject4(MORSE_SESSION_REGISTRY)),
4712
+ __decorateParam(1, Inject4(MORSE_TRANSCRIPT_STORE)),
4713
+ __decorateParam(2, Inject4(MORSE_PROJECT_POLICY)),
4714
+ __decorateParam(3, Inject4(MORSE_LOGGER)),
4715
+ __decorateParam(4, Inject4(MORSE_CONFIG)),
4716
+ __decorateParam(5, Inject4(ServerTerminalBackend))
4301
4717
  ], MorseSessionFactory);
4718
+ function gitPaths(args) {
4719
+ const paths = args?.paths;
4720
+ if (!Array.isArray(paths)) {
4721
+ return [];
4722
+ }
4723
+ return paths.filter((path) => typeof path === "string" && path.length > 0);
4724
+ }
4302
4725
 
4303
4726
  // ../server/src/internal/ws/morse.gateway.ts
4304
4727
  var MorseGateway = class {
@@ -4343,8 +4766,8 @@ var MorseGateway = class {
4343
4766
  };
4344
4767
  MorseGateway = __decorateClass([
4345
4768
  WebSocketGateway({ path: DEFAULT_WS_PATH }),
4346
- __decorateParam(0, Inject4(MorseSessionFactory)),
4347
- __decorateParam(1, Inject4(MORSE_LOGGER))
4769
+ __decorateParam(0, Inject5(MorseSessionFactory)),
4770
+ __decorateParam(1, Inject5(MORSE_LOGGER))
4348
4771
  ], MorseGateway);
4349
4772
 
4350
4773
  // ../server/src/app/config.ts
@@ -4476,6 +4899,7 @@ AppModule = __decorateClass([
4476
4899
  inject: [MORSE_CONFIG]
4477
4900
  },
4478
4901
  MorseRegistryLifecycle,
4902
+ ServerTerminalBackend,
4479
4903
  MorseSessionFactory,
4480
4904
  MorseGateway
4481
4905
  ]