@dungsil/dsh-plugin-ui-vcs 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/dist/client.js +43 -43
  2. package/dist/index.js +182 -109
  3. package/package.json +2 -1
package/dist/client.js CHANGED
@@ -13474,6 +13474,7 @@ function apply$2(ctx) {
13474
13474
  function SessionRequest({ sessionId }) {
13475
13475
  (0, react.useSyncExternalStore)(subscribe, () => ctx.locale.getSnapshot().active);
13476
13476
  const copy = text();
13477
+ const current = (0, react.useSyncExternalStore)((listener) => ctx.sidebarRight.mounted.subscribe(listener), () => ctx.sidebarRight.mounted.getSnapshot() === sessionId);
13477
13478
  const running = (0, react.useSyncExternalStore)((listener) => sessions.list.subscribe(listener), () => sessions.list.getSnapshot().byId[sessionId]?.running ?? false);
13478
13479
  const cwd = (0, react.useSyncExternalStore)((listener) => sessions.list.subscribe(listener), () => sessions.list.getSnapshot().byId[sessionId]?.cwd);
13479
13480
  const anchor = (0, react.useRef)(null);
@@ -13491,12 +13492,21 @@ function apply$2(ctx) {
13491
13492
  return () => node.remove();
13492
13493
  }, [sessionId]);
13493
13494
  (0, react.useEffect)(() => {
13494
- let active;
13495
13495
  setView(void 0);
13496
+ }, [
13497
+ sessionId,
13498
+ current,
13499
+ cwd
13500
+ ]);
13501
+ (0, react.useEffect)(() => {
13502
+ if (!current || running) return;
13503
+ let disposed = false;
13504
+ let active;
13496
13505
  const refresh = async () => {
13497
- active?.abort();
13498
- active = new AbortController();
13499
- const { signal } = active;
13506
+ if (disposed || active || document.visibilityState === "hidden") return;
13507
+ const controller = new AbortController();
13508
+ active = controller;
13509
+ const { signal } = controller;
13500
13510
  try {
13501
13511
  const result = await connection.rpc.call(channel$1, method, {
13502
13512
  sessionId,
@@ -13505,22 +13515,28 @@ function apply$2(ctx) {
13505
13515
  if (!signal.aborted) setView(result.ok ? result.value : void 0);
13506
13516
  } catch {
13507
13517
  if (!signal.aborted) setView(void 0);
13518
+ } finally {
13519
+ if (active === controller) active = void 0;
13508
13520
  }
13509
13521
  };
13510
13522
  refresh();
13511
13523
  window.addEventListener("focus", refresh);
13524
+ document.addEventListener("visibilitychange", refresh);
13512
13525
  const interval = window.setInterval(refresh, 6e4);
13513
13526
  return () => {
13527
+ disposed = true;
13514
13528
  active?.abort();
13515
13529
  window.removeEventListener("focus", refresh);
13530
+ document.removeEventListener("visibilitychange", refresh);
13516
13531
  window.clearInterval(interval);
13517
13532
  };
13518
13533
  }, [
13519
13534
  sessionId,
13535
+ current,
13520
13536
  running,
13521
13537
  cwd
13522
13538
  ]);
13523
- const item = view?.request;
13539
+ const item = current ? view?.request : void 0;
13524
13540
  const state = item?.state === "open" && item.draft ? "draft" : item?.state;
13525
13541
  const icon = state === "merged" ? git_merge_default : state === "closed" ? git_pull_request_closed_default : state === "draft" ? git_pull_request_draft_default : git_pull_request_default;
13526
13542
  const label = item && state ? `${copy.title} ${view?.provider === "gitlab" ? "!" : "#"}${item.number}: ${copy[state]}` : "";
@@ -14279,7 +14295,6 @@ const copy$1 = {
14279
14295
  invalid: "이름과 올바른 이메일을 함께 입력하거나 두 항목을 모두 비우세요.",
14280
14296
  cli: "CLI 인증 상태",
14281
14297
  refresh: "새로고침",
14282
- selectSession: "CLI 상태를 확인하려면 세션을 먼저 여세요.",
14283
14298
  authenticated: "인증됨",
14284
14299
  unauthenticated: "인증되지 않음",
14285
14300
  unavailable: "설치되지 않음",
@@ -14303,7 +14318,6 @@ const copy$1 = {
14303
14318
  invalid: "Enter both a name and a valid email, or leave both fields empty.",
14304
14319
  cli: "CLI authentication",
14305
14320
  refresh: "Refresh",
14306
- selectSession: "Open a session to check CLI status.",
14307
14321
  authenticated: "Authenticated",
14308
14322
  unauthenticated: "Not authenticated",
14309
14323
  unavailable: "Not installed",
@@ -14327,7 +14341,6 @@ const copy$1 = {
14327
14341
  invalid: "请输入姓名和有效邮箱,或将两个字段都留空。",
14328
14342
  cli: "CLI 认证状态",
14329
14343
  refresh: "刷新",
14330
- selectSession: "请先打开会话以检查 CLI 状态。",
14331
14344
  authenticated: "已认证",
14332
14345
  unauthenticated: "未认证",
14333
14346
  unavailable: "未安装",
@@ -14343,7 +14356,6 @@ function VcsSettings({ ctx }) {
14343
14356
  const text = localized(active);
14344
14357
  const id = (0, react.useId)();
14345
14358
  const connection = ctx.get("connection");
14346
- const sessionId = (0, react.useSyncExternalStore)((listener) => ctx.sidebarRight.mounted.subscribe(listener), () => ctx.sidebarRight.mounted.getSnapshot());
14347
14359
  const [view, setView] = (0, react.useState)();
14348
14360
  const [draft, setDraft] = (0, react.useState)();
14349
14361
  const [busy, setBusy] = (0, react.useState)(false);
@@ -14378,21 +14390,14 @@ function VcsSettings({ ctx }) {
14378
14390
  const controller = new AbortController();
14379
14391
  lifetime.current = controller;
14380
14392
  load(controller.signal);
14393
+ check(controller.signal);
14381
14394
  return () => controller.abort();
14382
14395
  }, []);
14383
- (0, react.useEffect)(() => {
14384
- const controller = new AbortController();
14385
- setCli(void 0);
14386
- setCliError(false);
14387
- setChecking(false);
14388
- if (sessionId) check(controller.signal);
14389
- return () => controller.abort();
14390
- }, [sessionId]);
14391
14396
  const check = async (signal) => {
14392
14397
  setChecking(true);
14393
14398
  setCliError(false);
14394
14399
  try {
14395
- const result = await connection.rpc.call(channel, "dungsil-vcs/cli-status", { sessionId }, signal);
14400
+ const result = await connection.rpc.call(channel, "dungsil-vcs/cli-status", {}, signal);
14396
14401
  if (signal.aborted) return;
14397
14402
  if (!result.ok) throw new Error("unavailable");
14398
14403
  setCli(result.value);
@@ -14452,7 +14457,6 @@ function VcsSettings({ ctx }) {
14452
14457
  className: "dsh-vcs-settings",
14453
14458
  "aria-label": text.nav,
14454
14459
  children: [
14455
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: text.scope }),
14456
14460
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
14457
14461
  onSubmit: (event) => {
14458
14462
  event.preventDefault();
@@ -14463,6 +14467,7 @@ function VcsSettings({ ctx }) {
14463
14467
  disabled: blocked,
14464
14468
  children: [
14465
14469
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: text.author }),
14470
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: text.scope }),
14466
14471
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: text.authorHint }),
14467
14472
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
14468
14473
  label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
@@ -14545,31 +14550,26 @@ function VcsSettings({ ctx }) {
14545
14550
  ]
14546
14551
  }),
14547
14552
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: text.cli }),
14548
- !sessionId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14553
+ checking && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14549
14554
  role: "status",
14550
- children: text.selectSession
14551
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
14552
- checking && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14553
- role: "status",
14554
- children: text.loading
14555
- }),
14556
- cliError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14557
- role: "alert",
14558
- children: text.error
14559
- }),
14560
- cli?.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14561
- className: "dsh-vcs-settings-cli",
14562
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
14563
- label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: item.cli }),
14564
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text[item.status === "error" ? "errorStatus" : item.status] })
14565
- }), item.accounts.map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
14566
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: account.host }),
14567
- " · ",
14568
- account.login,
14569
- account.method && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", text[account.method]] })
14570
- ] }, index))]
14571
- }, item.cli))
14572
- ] })
14555
+ children: text.loading
14556
+ }),
14557
+ cliError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14558
+ role: "alert",
14559
+ children: text.error
14560
+ }),
14561
+ cli?.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14562
+ className: "dsh-vcs-settings-cli",
14563
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
14564
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: item.cli }),
14565
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text[item.status === "error" ? "errorStatus" : item.status] })
14566
+ }), item.accounts.map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
14567
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: account.host }),
14568
+ " · ",
14569
+ account.login,
14570
+ account.method && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" · ", text[account.method]] })
14571
+ ] }, index))]
14572
+ }, item.cli))
14573
14573
  ]
14574
14574
  });
14575
14575
  }
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
3
- import { SessionId } from "@deepseek-ai/dsh-session";
3
+ import { Session, SessionId } from "@deepseek-ai/dsh-session";
4
4
  import { Buffer as Buffer$1 } from "node:buffer";
5
5
  import { Context, Service } from "@deepseek-ai/cordis";
6
6
  import { createHash } from "node:crypto";
7
+ import { homedir } from "node:os";
7
8
  // \0rolldown/runtime.js
8
9
  var __defProp = Object.defineProperty;
9
10
  var __exportAll = (all, no_symbols) => {
@@ -900,6 +901,69 @@ defineMethod("transform", [
900
901
  "callback",
901
902
  "preserve"
902
903
  ], ({ inner }, isInner) => inner.toString(isInner));
904
+ // src/inspect-session.ts
905
+ async function inspectSession(ctx, id, signal) {
906
+ signal.throwIfAborted();
907
+ const live = ctx.get("agents")?.get(id);
908
+ if (live) return live.session;
909
+ const inspection = await ctx.sessionController.inspect(id, signal);
910
+ signal.throwIfAborted();
911
+ return Session.create(id, inspection.events, inspection.meta, inspection.inheritedEventCount);
912
+ }
913
+ // ../../tools/async/src/index.ts
914
+ function createTaskQueue({ concurrency, maxPending }) {
915
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1 || !Number.isSafeInteger(maxPending) || maxPending < 0) throw new RangeError("Invalid task queue limits");
916
+ let active = 0;
917
+ const pending = /* @__PURE__ */ new Set();
918
+ const drain = () => {
919
+ while (active < concurrency && pending.size) pending.values().next().value.start();
920
+ };
921
+ return (signal, task) => {
922
+ if (signal.aborted) return Promise.reject(signal.reason);
923
+ if (active >= concurrency && pending.size >= maxPending) return Promise.reject(/* @__PURE__ */ new Error("busy"));
924
+ return new Promise((resolve, reject) => {
925
+ const entry = {
926
+ start() {
927
+ pending.delete(entry);
928
+ signal.removeEventListener("abort", entry.cancel);
929
+ active++;
930
+ (async () => {
931
+ try {
932
+ signal.throwIfAborted();
933
+ resolve(await task());
934
+ } catch (error) {
935
+ reject(error);
936
+ } finally {
937
+ active--;
938
+ drain();
939
+ }
940
+ })();
941
+ },
942
+ cancel() {
943
+ pending.delete(entry);
944
+ signal.removeEventListener("abort", entry.cancel);
945
+ reject(signal.reason);
946
+ }
947
+ };
948
+ pending.add(entry);
949
+ signal.addEventListener("abort", entry.cancel, { once: true });
950
+ drain();
951
+ });
952
+ };
953
+ }
954
+ // src/read-queue.ts
955
+ const queues = /* @__PURE__ */ new WeakMap();
956
+ function readQueue(provider) {
957
+ let queue = queues.get(provider);
958
+ if (!queue) {
959
+ queue = createTaskQueue({
960
+ concurrency: 2,
961
+ maxPending: 128
962
+ });
963
+ queues.set(provider, queue);
964
+ }
965
+ return queue;
966
+ }
903
967
  // src/pull-requests-protocol.ts
904
968
  const channel$1 = "/api";
905
969
  const method = "dungsil-vcs/pull-requests/list";
@@ -975,9 +1039,10 @@ function actionInput$1(value) {
975
1039
  function apply$3(ctx, config) {
976
1040
  const lifetime = new AbortController();
977
1041
  const actionsInFlight = /* @__PURE__ */ new Set();
1042
+ const read = readQueue(ctx.subprocess);
978
1043
  ctx.effect(() => () => lifetime.abort());
979
- const list = async (agent, signal, action, requested, summary = false) => {
980
- const cwd = ctx.workingDirectory.get(agent.session);
1044
+ const list = async (session, signal, action, requested, summary = false) => {
1045
+ const cwd = ctx.workingDirectory.get(session);
981
1046
  const run = async (executable, args, failure = "command_failed", allowed = [0], trim = true) => {
982
1047
  let resolved;
983
1048
  try {
@@ -986,7 +1051,7 @@ function apply$3(ctx, config) {
986
1051
  throw new Error(executable === "git" ? "repository" : "cli_missing");
987
1052
  }
988
1053
  const argv = [resolved, ...args];
989
- const policy = ctx.sandboxPolicy.resolve({ session: agent.session });
1054
+ const policy = ctx.sandboxPolicy.resolve({ session });
990
1055
  const command = policy.mode === "danger-full-access" ? argv : (await ctx.sandbox.confine(argv, {
991
1056
  ...policy,
992
1057
  mode: policy.mode
@@ -1119,7 +1184,7 @@ function apply$3(ctx, config) {
1119
1184
  if (requested.origin !== new URL(origin).origin || provider === "github" && repository.split("/").length !== 2 || !match || !Number.isSafeInteger(Number(match[2]))) throw new Error("unsupported");
1120
1185
  if (provider === "github" ? match[1].toLowerCase() !== prefix.toLowerCase() : match[1] !== prefix) {
1121
1186
  if (provider === "gitlab") throw new Error("unsupported");
1122
- const associated = await list(agent, signal, void 0, void 0, summary);
1187
+ const associated = await list(session, signal, void 0, void 0, summary);
1123
1188
  const linkedUrl = `${requested.origin}${match[1]}/${provider === "github" ? "pull" : forge ? "pulls" : "-/merge_requests"}/${match[2]}`;
1124
1189
  if ("request" in associated && associated.request && (provider === "github" ? associated.request.url.toLowerCase() === linkedUrl.toLowerCase() : associated.request.url === linkedUrl)) return associated;
1125
1190
  throw new Error("unsupported");
@@ -1714,14 +1779,24 @@ function apply$3(ctx, config) {
1714
1779
  inFlight = action.sessionId;
1715
1780
  }
1716
1781
  const requested = route === "dungsil-vcs/pull-requests/list" && input.requestUrl !== void 0 ? lookupUrl(input.requestUrl) : void 0;
1717
- const resolved = await ctx.sessionController.resolveAgent(SessionId(input.sessionId));
1718
- if ("error" in resolved) throw new Error("unavailable");
1719
1782
  const signal = AbortSignal.any([
1720
1783
  request.signal,
1721
1784
  lifetime.signal,
1722
1785
  AbortSignal.timeout(2e4)
1723
1786
  ]);
1724
- const value = await list(resolved.agent, signal, action, requested, input.summary === true);
1787
+ const sessionId = SessionId(input.sessionId);
1788
+ const execute = async () => {
1789
+ signal.throwIfAborted();
1790
+ let session;
1791
+ if (action) {
1792
+ const resolved = await ctx.sessionController.resolveAgent(sessionId);
1793
+ if ("error" in resolved) throw new Error("unavailable");
1794
+ signal.throwIfAborted();
1795
+ session = resolved.agent.session;
1796
+ } else session = await inspectSession(ctx, sessionId, signal);
1797
+ return list(session, signal, action, requested, input.summary === true);
1798
+ };
1799
+ const value = action ? await execute() : await read(signal, execute);
1725
1800
  if (action && !("completed" in value)) throw new Error("stale_request");
1726
1801
  return reply({
1727
1802
  ok: true,
@@ -5062,8 +5137,6 @@ const name$1 = "dungsil-vcs-settings";
5062
5137
  const inject$1 = [
5063
5138
  "connection",
5064
5139
  "configEditor",
5065
- "sessionController",
5066
- "workingDirectory",
5067
5140
  "subprocess",
5068
5141
  "sandbox",
5069
5142
  "sandboxPolicy"
@@ -5143,12 +5216,6 @@ function apply$1(ctx) {
5143
5216
  value: snapshot()
5144
5217
  };
5145
5218
  } else {
5146
- const sessionId = body.payload?.sessionId;
5147
- if (typeof sessionId !== "string" || !sessionId || sessionId.length > 256) throw new Error("invalid");
5148
- const resolved = await ctx.sessionController.resolveAgent(SessionId(sessionId));
5149
- if ("error" in resolved) throw new Error("unavailable");
5150
- const { agent } = resolved;
5151
- const cwd = await ctx.workingDirectory.ensure(agent, signal);
5152
5219
  const value = await inspectCliStatus(async (executable, args) => {
5153
5220
  const bounded = AbortSignal.any([signal, AbortSignal.timeout(1e4)]);
5154
5221
  let path;
@@ -5159,14 +5226,14 @@ function apply$1(ctx) {
5159
5226
  throw error;
5160
5227
  }
5161
5228
  const argv = [path, ...args];
5162
- const policy = ctx.sandboxPolicy.resolve({ session: agent.session });
5229
+ const policy = ctx.sandboxPolicy.resolve();
5163
5230
  const command = policy.mode === "danger-full-access" ? argv : (await ctx.sandbox.confine(argv, {
5164
5231
  ...policy,
5165
5232
  mode: policy.mode
5166
5233
  }, bounded)).argv;
5167
5234
  const child = ctx.subprocess.spawn({
5168
5235
  argv: command,
5169
- cwd,
5236
+ cwd: homedir(),
5170
5237
  signal: bounded,
5171
5238
  graceMs: 1e3,
5172
5239
  env: {
@@ -5205,7 +5272,6 @@ function apply$1(ctx) {
5205
5272
  }
5206
5273
  });
5207
5274
  signal.throwIfAborted();
5208
- if (ctx.workingDirectory.get(agent.session) !== cwd) throw new Error("conflict");
5209
5275
  result = {
5210
5276
  ok: true,
5211
5277
  value
@@ -5241,6 +5307,9 @@ const inject = [
5241
5307
  "workingDirectory"
5242
5308
  ];
5243
5309
  function apply(ctx, config) {
5310
+ const lifetime = new AbortController();
5311
+ const read = readQueue(ctx.subprocess);
5312
+ ctx.effect(() => () => lifetime.abort(), "ui-vcs: cancel context reads");
5244
5313
  ctx.plugin(pull_requests_exports, config);
5245
5314
  ctx.plugin(commands_exports, config);
5246
5315
  ctx.plugin(settings_exports);
@@ -5265,100 +5334,104 @@ function apply(ctx, config) {
5265
5334
  if (body.method !== "dungsil-vcs/describe") throw new Error("Invalid method");
5266
5335
  const sessionId = body.payload?.sessionId;
5267
5336
  if (typeof sessionId !== "string" || !sessionId) throw new Error("Invalid session");
5268
- const resolved = await ctx.sessionController.resolveAgent(SessionId(sessionId));
5269
- if ("error" in resolved) throw new Error("Session unavailable");
5270
- const { agent } = resolved;
5271
- const signal = AbortSignal.any([request.signal, AbortSignal.timeout(1e4)]);
5272
- const cwd = await ctx.workingDirectory.ensure(agent, signal);
5273
- const git = async (args) => {
5274
- const argv = [
5275
- await ctx.subprocess.resolveExecutable("git", void 0, signal),
5276
- "-c",
5277
- "core.fsmonitor=false",
5278
- "-c",
5279
- "core.hooksPath=/dev/null",
5280
- ...args
5281
- ];
5282
- const policy = ctx.sandboxPolicy.resolve({ session: agent.session });
5283
- const command = policy.mode === "danger-full-access" ? argv : (await ctx.sandbox.confine(argv, {
5284
- ...policy,
5285
- mode: policy.mode
5286
- }, signal)).argv;
5287
- const child = ctx.subprocess.spawn({
5288
- argv: command,
5289
- cwd,
5290
- signal,
5291
- graceMs: 1e3,
5292
- env: {
5293
- ...Object.fromEntries(Object.keys(process.env).filter((key) => key.startsWith("GIT_")).map((key) => [key, void 0])),
5294
- GIT_TERMINAL_PROMPT: "0",
5295
- GIT_OPTIONAL_LOCKS: "0",
5296
- GIT_NO_LAZY_FETCH: "1"
5297
- },
5298
- stdio: {
5299
- stdin: "ignore",
5300
- stdout: { maxBytes: 65536 },
5301
- stderr: { maxBytes: 4096 }
5302
- }
5303
- });
5304
- try {
5305
- const outcome = await child.done;
5306
- signal.throwIfAborted();
5307
- const output = child.collected.stdout.readFrom(0);
5308
- if (outcome.exitCode !== 0 || output.lossy) throw new Error("Git read failed");
5309
- return output.text.replace(/\r?\n$/, "");
5310
- } finally {
5311
- child.terminate();
5312
- await child.waitForExit();
5313
- }
5314
- };
5315
- const gitDir = await git([
5316
- "rev-parse",
5317
- "--path-format=absolute",
5318
- "--git-dir"
5319
- ]);
5320
- const commonDir = await git([
5321
- "rev-parse",
5322
- "--path-format=absolute",
5323
- "--git-common-dir"
5337
+ const signal = AbortSignal.any([
5338
+ request.signal,
5339
+ lifetime.signal,
5340
+ AbortSignal.timeout(1e4)
5324
5341
  ]);
5325
- let operation = "pr";
5326
- if (gitDir === commonDir) {
5327
- const branch = await git([
5328
- "symbolic-ref",
5329
- "--quiet",
5330
- "--short",
5331
- "HEAD"
5342
+ result = await read(signal, async () => {
5343
+ const session = await inspectSession(ctx, SessionId(sessionId), signal);
5344
+ const cwd = ctx.workingDirectory.get(session);
5345
+ const git = async (args) => {
5346
+ const argv = [
5347
+ await ctx.subprocess.resolveExecutable("git", void 0, signal),
5348
+ "-c",
5349
+ "core.fsmonitor=false",
5350
+ "-c",
5351
+ "core.hooksPath=/dev/null",
5352
+ ...args
5353
+ ];
5354
+ const policy = ctx.sandboxPolicy.resolve({ session });
5355
+ const command = policy.mode === "danger-full-access" ? argv : (await ctx.sandbox.confine(argv, {
5356
+ ...policy,
5357
+ mode: policy.mode
5358
+ }, signal)).argv;
5359
+ const child = ctx.subprocess.spawn({
5360
+ argv: command,
5361
+ cwd,
5362
+ signal,
5363
+ graceMs: 1e3,
5364
+ env: {
5365
+ ...Object.fromEntries(Object.keys(process.env).filter((key) => key.startsWith("GIT_")).map((key) => [key, void 0])),
5366
+ GIT_TERMINAL_PROMPT: "0",
5367
+ GIT_OPTIONAL_LOCKS: "0",
5368
+ GIT_NO_LAZY_FETCH: "1"
5369
+ },
5370
+ stdio: {
5371
+ stdin: "ignore",
5372
+ stdout: { maxBytes: 65536 },
5373
+ stderr: { maxBytes: 4096 }
5374
+ }
5375
+ });
5376
+ try {
5377
+ const outcome = await child.done;
5378
+ signal.throwIfAborted();
5379
+ const output = child.collected.stdout.readFrom(0);
5380
+ if (outcome.exitCode !== 0 || output.lossy) throw new Error("Git read failed");
5381
+ return output.text.replace(/\r?\n$/, "");
5382
+ } finally {
5383
+ child.terminate();
5384
+ await child.waitForExit();
5385
+ }
5386
+ };
5387
+ const gitDir = await git([
5388
+ "rev-parse",
5389
+ "--path-format=absolute",
5390
+ "--git-dir"
5332
5391
  ]);
5333
- const upstream = await git([
5334
- "for-each-ref",
5335
- "--format=%(upstream:remotename)",
5336
- `refs/heads/${branch}`
5392
+ const commonDir = await git([
5393
+ "rev-parse",
5394
+ "--path-format=absolute",
5395
+ "--git-common-dir"
5337
5396
  ]);
5338
- const remotes = (await git(["remote"])).split("\n").filter(Boolean);
5339
- const remote = upstream && upstream !== "." ? upstream : remotes.includes("origin") ? "origin" : remotes.length === 1 ? remotes[0] : void 0;
5340
- if (!remote) throw new Error("Default remote unknown");
5341
- operation = await git([
5342
- "symbolic-ref",
5343
- "--quiet",
5344
- `refs/remotes/${remote}/HEAD`
5345
- ]) === `refs/remotes/${remote}/${branch}` ? "push" : "pr";
5346
- }
5347
- const headSha = await git(["rev-parse", "HEAD"]);
5348
- const dirty = !!await git([
5349
- "status",
5350
- "--porcelain",
5351
- "--untracked-files=normal"
5352
- ]);
5353
- result = {
5354
- ok: true,
5355
- value: {
5356
- operation,
5357
- headSha,
5358
- dirty
5397
+ let operation = "pr";
5398
+ if (gitDir === commonDir) {
5399
+ const branch = await git([
5400
+ "symbolic-ref",
5401
+ "--quiet",
5402
+ "--short",
5403
+ "HEAD"
5404
+ ]);
5405
+ const upstream = await git([
5406
+ "for-each-ref",
5407
+ "--format=%(upstream:remotename)",
5408
+ `refs/heads/${branch}`
5409
+ ]);
5410
+ const remotes = (await git(["remote"])).split("\n").filter(Boolean);
5411
+ const remote = upstream && upstream !== "." ? upstream : remotes.includes("origin") ? "origin" : remotes.length === 1 ? remotes[0] : void 0;
5412
+ if (!remote) throw new Error("Default remote unknown");
5413
+ operation = await git([
5414
+ "symbolic-ref",
5415
+ "--quiet",
5416
+ `refs/remotes/${remote}/HEAD`
5417
+ ]) === `refs/remotes/${remote}/${branch}` ? "push" : "pr";
5359
5418
  }
5360
- };
5361
- if (ctx.workingDirectory.get(agent.session) !== cwd) throw new Error("Working directory changed");
5419
+ const headSha = await git(["rev-parse", "HEAD"]);
5420
+ const dirty = !!await git([
5421
+ "status",
5422
+ "--porcelain",
5423
+ "--untracked-files=normal"
5424
+ ]);
5425
+ if (ctx.workingDirectory.get(session) !== cwd) throw new Error("Working directory changed");
5426
+ return {
5427
+ ok: true,
5428
+ value: {
5429
+ operation,
5430
+ headSha,
5431
+ dirty
5432
+ }
5433
+ };
5434
+ });
5362
5435
  } catch {
5363
5436
  result = failure;
5364
5437
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dungsil/dsh-plugin-ui-vcs",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Git actions and PR/MR review for GitHub, GitLab, Gitea and Forgejo.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,6 +52,7 @@
52
52
  "@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.5",
53
53
  "@deepseek-ai/dsh-working-directory": "0.2.1-rc.5",
54
54
  "@deepseek-ai/schemastery": "~3.18.5",
55
+ "@dungsil/dsh-async": "0.0.0",
55
56
  "@dungsil/dsh-tsconfig": "0.0.0",
56
57
  "@dungsil/dsh-ui": "0.0.0",
57
58
  "@types/react": "18.3.31",