@kenkaiiii/ggcoder 5.27.0 → 5.29.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 (120) hide show
  1. package/dist/app-sidecar-auth-broadcast.test.d.ts +2 -0
  2. package/dist/app-sidecar-auth-broadcast.test.d.ts.map +1 -0
  3. package/dist/app-sidecar-auth-broadcast.test.js +271 -0
  4. package/dist/app-sidecar-auth-broadcast.test.js.map +1 -0
  5. package/dist/app-sidecar-sessions.d.ts +7 -2
  6. package/dist/app-sidecar-sessions.d.ts.map +1 -1
  7. package/dist/app-sidecar-sessions.js +36 -8
  8. package/dist/app-sidecar-sessions.js.map +1 -1
  9. package/dist/app-sidecar-sessions.test.js +95 -0
  10. package/dist/app-sidecar-sessions.test.js.map +1 -1
  11. package/dist/app-sidecar.js +151 -10
  12. package/dist/app-sidecar.js.map +1 -1
  13. package/dist/core/agent-session-checkpoint-flush.test.d.ts +2 -0
  14. package/dist/core/agent-session-checkpoint-flush.test.d.ts.map +1 -0
  15. package/dist/core/agent-session-checkpoint-flush.test.js +201 -0
  16. package/dist/core/agent-session-checkpoint-flush.test.js.map +1 -0
  17. package/dist/core/agent-session-crash-durability.test.d.ts +2 -0
  18. package/dist/core/agent-session-crash-durability.test.d.ts.map +1 -0
  19. package/dist/core/agent-session-crash-durability.test.js +95 -0
  20. package/dist/core/agent-session-crash-durability.test.js.map +1 -0
  21. package/dist/core/agent-session.d.ts +51 -1
  22. package/dist/core/agent-session.d.ts.map +1 -1
  23. package/dist/core/agent-session.js +114 -12
  24. package/dist/core/agent-session.js.map +1 -1
  25. package/dist/core/event-bus.d.ts +4 -0
  26. package/dist/core/event-bus.d.ts.map +1 -1
  27. package/dist/core/event-bus.js +3 -0
  28. package/dist/core/event-bus.js.map +1 -1
  29. package/dist/core/lsp/client.d.ts +15 -0
  30. package/dist/core/lsp/client.d.ts.map +1 -1
  31. package/dist/core/lsp/client.js +39 -1
  32. package/dist/core/lsp/client.js.map +1 -1
  33. package/dist/core/lsp/manager.d.ts +3 -0
  34. package/dist/core/lsp/manager.d.ts.map +1 -1
  35. package/dist/core/lsp/manager.js +33 -1
  36. package/dist/core/lsp/manager.js.map +1 -1
  37. package/dist/core/lsp/manager.test.js +18 -0
  38. package/dist/core/lsp/manager.test.js.map +1 -1
  39. package/dist/core/mcp/client-session-recovery.test.d.ts +2 -0
  40. package/dist/core/mcp/client-session-recovery.test.d.ts.map +1 -0
  41. package/dist/core/mcp/client-session-recovery.test.js +231 -0
  42. package/dist/core/mcp/client-session-recovery.test.js.map +1 -0
  43. package/dist/core/mcp/client.d.ts +57 -0
  44. package/dist/core/mcp/client.d.ts.map +1 -1
  45. package/dist/core/mcp/client.js +146 -8
  46. package/dist/core/mcp/client.js.map +1 -1
  47. package/dist/core/mcp/elicitation-bridge.d.ts +41 -0
  48. package/dist/core/mcp/elicitation-bridge.d.ts.map +1 -0
  49. package/dist/core/mcp/elicitation-bridge.js +52 -0
  50. package/dist/core/mcp/elicitation-bridge.js.map +1 -0
  51. package/dist/core/mcp/elicitation.test.d.ts +2 -0
  52. package/dist/core/mcp/elicitation.test.d.ts.map +1 -0
  53. package/dist/core/mcp/elicitation.test.js +158 -0
  54. package/dist/core/mcp/elicitation.test.js.map +1 -0
  55. package/dist/core/mcp/index.d.ts +3 -1
  56. package/dist/core/mcp/index.d.ts.map +1 -1
  57. package/dist/core/mcp/index.js +1 -0
  58. package/dist/core/mcp/index.js.map +1 -1
  59. package/dist/core/process-manager-notifications.test.js +102 -0
  60. package/dist/core/process-manager-notifications.test.js.map +1 -1
  61. package/dist/core/process-manager.d.ts +14 -4
  62. package/dist/core/process-manager.d.ts.map +1 -1
  63. package/dist/core/process-manager.js +83 -21
  64. package/dist/core/process-manager.js.map +1 -1
  65. package/dist/core/project-discovery.d.ts +19 -0
  66. package/dist/core/project-discovery.d.ts.map +1 -1
  67. package/dist/core/project-discovery.js +100 -0
  68. package/dist/core/project-discovery.js.map +1 -1
  69. package/dist/core/run-journal.test.d.ts +2 -0
  70. package/dist/core/run-journal.test.d.ts.map +1 -0
  71. package/dist/core/run-journal.test.js +195 -0
  72. package/dist/core/run-journal.test.js.map +1 -0
  73. package/dist/core/run-lifecycle.d.ts +22 -3
  74. package/dist/core/run-lifecycle.d.ts.map +1 -1
  75. package/dist/core/run-lifecycle.js +12 -3
  76. package/dist/core/run-lifecycle.js.map +1 -1
  77. package/dist/core/session-export.d.ts.map +1 -1
  78. package/dist/core/session-export.js +5 -0
  79. package/dist/core/session-export.js.map +1 -1
  80. package/dist/core/session-history.d.ts +10 -0
  81. package/dist/core/session-history.d.ts.map +1 -1
  82. package/dist/core/session-history.js +17 -2
  83. package/dist/core/session-history.js.map +1 -1
  84. package/dist/core/session-history.test.js +26 -1
  85. package/dist/core/session-history.test.js.map +1 -1
  86. package/dist/core/session-manager.d.ts +63 -1
  87. package/dist/core/session-manager.d.ts.map +1 -1
  88. package/dist/core/session-manager.js +94 -1
  89. package/dist/core/session-manager.js.map +1 -1
  90. package/dist/core/session-notification-visibility.test.d.ts +2 -0
  91. package/dist/core/session-notification-visibility.test.d.ts.map +1 -0
  92. package/dist/core/session-notification-visibility.test.js +79 -0
  93. package/dist/core/session-notification-visibility.test.js.map +1 -0
  94. package/dist/core/session-preview.d.ts +4 -2
  95. package/dist/core/session-preview.d.ts.map +1 -1
  96. package/dist/core/session-preview.js +8 -3
  97. package/dist/core/session-preview.js.map +1 -1
  98. package/dist/core/session-storage.test.js +6 -1
  99. package/dist/core/session-storage.test.js.map +1 -1
  100. package/dist/core/settings-manager.d.ts +2 -2
  101. package/dist/core/slash-commands.d.ts +0 -2
  102. package/dist/core/slash-commands.d.ts.map +1 -1
  103. package/dist/core/slash-commands.js +0 -17
  104. package/dist/core/slash-commands.js.map +1 -1
  105. package/dist/core/slash-commands.test.js +0 -1
  106. package/dist/core/slash-commands.test.js.map +1 -1
  107. package/dist/tools/subagent.d.ts.map +1 -1
  108. package/dist/tools/subagent.js +17 -3
  109. package/dist/tools/subagent.js.map +1 -1
  110. package/dist/tools/subagent.test.js +12 -0
  111. package/dist/tools/subagent.test.js.map +1 -1
  112. package/dist/utils/image.d.ts +26 -3
  113. package/dist/utils/image.d.ts.map +1 -1
  114. package/dist/utils/image.js +83 -14
  115. package/dist/utils/image.js.map +1 -1
  116. package/dist/utils/image.test.d.ts +2 -0
  117. package/dist/utils/image.test.d.ts.map +1 -0
  118. package/dist/utils/image.test.js +139 -0
  119. package/dist/utils/image.test.js.map +1 -0
  120. package/package.json +4 -4
@@ -65,7 +65,7 @@ import { enrichProcessPath } from "./core/shell-path.js";
65
65
  import { downscaleForPreview, shrinkToFit, validateVisionImage } from "./utils/image.js";
66
66
  import { startServeMode } from "./modes/serve-mode.js";
67
67
  import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
68
- import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, } from "./core/mcp/index.js";
68
+ import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, createElicitationBridge, } from "./core/mcp/index.js";
69
69
  import { buildSnapshot, levelForXp, rankForLevel } from "./core/progress/ranks.js";
70
70
  import { loadProgress, peekProgress, updateProgress } from "./core/progress/store.js";
71
71
  import { awardPrompt, awardCommits } from "./core/progress/engine.js";
@@ -575,6 +575,20 @@ async function main() {
575
575
  // request to its window's session via the `x-gg-session` header (and the
576
576
  // `?session=` query for the SSE /events stream).
577
577
  const sessions = new Map();
578
+ /**
579
+ * Fan one frame out to every window.
580
+ *
581
+ * For state that is genuinely global rather than per-session — `~/.gg/auth.json`
582
+ * is shared by all windows, so connecting a provider in one must refresh the
583
+ * model picker in all of them. Mirrors the memory/jiwa/progress fan-outs.
584
+ */
585
+ const broadcastAll = (type, data) => {
586
+ for (const ctx of sessions.values())
587
+ ctx.broadcast(type, data);
588
+ };
589
+ // Providers currently mid-OAuth in some window. Daemon-level so two windows
590
+ // cannot race two browser flows for the same provider into one auth file.
591
+ const oauthInFlightProviders = new Set();
578
592
  const memoryStore = new MemoryStore({
579
593
  onChange: ({ memories }) => {
580
594
  for (const ctx of sessions.values()) {
@@ -770,7 +784,15 @@ async function main() {
770
784
  const sessionPath = typeof body.sessionPath === "string" && body.sessionPath ? body.sessionPath : undefined;
771
785
  const id = randomUUID();
772
786
  try {
773
- const ctx = await createSession({ auth, paths, progress, memoryStore, jiwaStore }, { id, mode, chatAgent, cwd: sessionCwd, sessionPath });
787
+ const ctx = await createSession({
788
+ auth,
789
+ paths,
790
+ progress,
791
+ memoryStore,
792
+ jiwaStore,
793
+ broadcastAll,
794
+ oauthInFlightProviders,
795
+ }, { id, mode, chatAgent, cwd: sessionCwd, sessionPath });
774
796
  sessions.set(id, ctx);
775
797
  log("INFO", "app-sidecar", "session created", {
776
798
  id,
@@ -1046,7 +1068,7 @@ async function createProgressManager(agentDir, broadcastAll) {
1046
1068
  * logger, PATH, shared auth file, and radio live at the daemon level.
1047
1069
  */
1048
1070
  async function createSession(deps, opts) {
1049
- const { auth, progress, memoryStore, jiwaStore } = deps;
1071
+ const { auth, progress, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders } = deps;
1050
1072
  const paths = deps.paths;
1051
1073
  const mode = opts.mode;
1052
1074
  let chatAgent = opts.chatAgent;
@@ -1182,6 +1204,17 @@ async function createSession(deps, opts) {
1182
1204
  })
1183
1205
  .catch(() => { });
1184
1206
  }
1207
+ // ── MCP elicitation bridge ─────────────────────────────────
1208
+ // An MCP server can ask for user input in the middle of a tool call. The
1209
+ // bridge parks the promise; we broadcast the prompt over SSE and resolve it
1210
+ // when the webview POSTs /mcp/elicit/:id.
1211
+ const elicitations = createElicitationBridge({
1212
+ broadcast: (prompt) => broadcast("mcp_elicit", prompt),
1213
+ onTimeout: (prompt) => log("WARN", "app-sidecar", "MCP elicitation timed out", {
1214
+ id: prompt.id,
1215
+ server: prompt.server,
1216
+ }),
1217
+ });
1185
1218
  // The session file path to resume (passed by the daemon's POST /session);
1186
1219
  // empty/unset starts a fresh session.
1187
1220
  const resumeSessionPath = opts.sessionPath;
@@ -1195,6 +1228,7 @@ async function createSession(deps, opts) {
1195
1228
  signal: abort.signal,
1196
1229
  // Keep MCP startup off the readiness path in both modes.
1197
1230
  backgroundMcpConnect: true,
1231
+ onMcpElicit: elicitations.onElicit,
1198
1232
  // Keep restore-time auto-compaction off the readiness path too: its summary
1199
1233
  // LLM call (30s timeout) used to freeze waitForReady — and with it the whole
1200
1234
  // window (project picker, session list) — whenever a resumed session was
@@ -1566,6 +1600,21 @@ async function createSession(deps, opts) {
1566
1600
  running = runState !== "idle";
1567
1601
  if (runState === "cancelling")
1568
1602
  broadcast("run_cancelling", { runState });
1603
+ },
1604
+ // Durable run journal. Fire-and-forget on purpose: an unwritten journal
1605
+ // entry is a missed crash hint, while a journal write that throws inside
1606
+ // begin()/settle() would break run ownership itself.
1607
+ {
1608
+ started: (generation) => void session.persistRunStarted(generation).catch((err) => {
1609
+ log("WARN", "app-sidecar", "failed to journal run start", {
1610
+ error: err instanceof Error ? err.message : String(err),
1611
+ });
1612
+ }),
1613
+ finished: (generation, outcome) => void session.persistRunFinished(generation, outcome).catch((err) => {
1614
+ log("WARN", "app-sidecar", "failed to journal run finish", {
1615
+ error: err instanceof Error ? err.message : String(err),
1616
+ });
1617
+ }),
1569
1618
  });
1570
1619
  const cancelledRunEndGenerations = new Set();
1571
1620
  let pendingCancelDrain = null;
@@ -1791,6 +1840,10 @@ async function createSession(deps, opts) {
1791
1840
  function abortOwnedWork() {
1792
1841
  cancelGeneration++;
1793
1842
  abort.abort();
1843
+ // An MCP tool call parked on user input is not cancelled by the signal —
1844
+ // the promise lives in the bridge. Release it, or the aborted turn's tool
1845
+ // call never returns.
1846
+ elicitations.cancelAll();
1794
1847
  // Stop a run-all sweep and every async child through AgentSession's signal.
1795
1848
  taskRunAll = false;
1796
1849
  autopilotCancelled = true;
@@ -1802,9 +1855,9 @@ async function createSession(deps, opts) {
1802
1855
  kenAutoAbort = new AbortController();
1803
1856
  kenAutoSession?.setSignal(kenAutoAbort.signal);
1804
1857
  }
1805
- function finishOwnedGeneration(generation, emitCancelledFallback) {
1858
+ function finishOwnedGeneration(generation, emitCancelledFallback, outcome = "completed") {
1806
1859
  const cancelled = runLifecycle.isCancellationRequested(generation);
1807
- const settlement = runLifecycle.settle(generation);
1860
+ const settlement = runLifecycle.settle(generation, outcome);
1808
1861
  if (!settlement.settled)
1809
1862
  return cancelled;
1810
1863
  // A replacement signal is safe only after the provider-backed owner settled.
@@ -1882,8 +1935,9 @@ async function createSession(deps, opts) {
1882
1935
  });
1883
1936
  }
1884
1937
  }
1885
- if (ownsGeneration)
1886
- finishOwnedGeneration(generation, false);
1938
+ if (ownsGeneration) {
1939
+ finishOwnedGeneration(generation, false, runSucceeded ? "completed" : "failed");
1940
+ }
1887
1941
  // A cancelled injected run is still owned by the surrounding autopilot
1888
1942
  // cycle; its outer finalizer emits the one terminal cancelled run_end.
1889
1943
  if (!(cancelled && !ownsGeneration)) {
@@ -2762,6 +2816,21 @@ async function createSession(deps, opts) {
2762
2816
  },
2763
2817
  });
2764
2818
  }
2819
+ else if (marker.kind === "interrupted_run") {
2820
+ // Rendered as an error row: the run's tools already changed the
2821
+ // repo, so the user needs to see it and decide what to do. We
2822
+ // never replay it — that would duplicate those changes.
2823
+ history.push({
2824
+ role: "assistant",
2825
+ text: "",
2826
+ error: {
2827
+ scope: "interrupted_run",
2828
+ headline: "A run was interrupted",
2829
+ message: "GG Coder stopped mid-run, so this turn is incomplete. Any files its tools already changed are still on disk.",
2830
+ guidance: "Review the working tree, then re-send the request if you still want it.",
2831
+ },
2832
+ });
2833
+ }
2765
2834
  }
2766
2835
  };
2767
2836
  let nonSystemCount = 0;
@@ -2842,7 +2911,14 @@ async function createSession(deps, opts) {
2842
2911
  // Autopilot injected this turn — live showed only the Ken-tinted
2843
2912
  // marker for it, never a user bubble. Emitting one here would print
2844
2913
  // the injected instruction a second time, unstyled.
2845
- if (!restored.autopilotInjected && (text.trim() || restored.images.length > 0)) {
2914
+ //
2915
+ // Pushed background-status updates are skipped for the same reason:
2916
+ // the live run rendered no bubble for them, so showing them here
2917
+ // would fill a reopened session with machine-facing status lines
2918
+ // the user never saw while working.
2919
+ if (!restored.autopilotInjected &&
2920
+ !restored.notification &&
2921
+ (text.trim() || restored.images.length > 0)) {
2846
2922
  history.push({
2847
2923
  role: "user",
2848
2924
  text: command ?? text,
@@ -3799,7 +3875,17 @@ async function createSession(deps, opts) {
3799
3875
  ...(baseUrl ? { baseUrl } : {}),
3800
3876
  };
3801
3877
  await auth.setCredentials(storageKey, creds);
3802
- broadcast("auth_done", { provider });
3878
+ // auth.json is shared by every window, so this is a global change:
3879
+ // close their login modals and refresh their provider lists too.
3880
+ broadcastAll("auth_done", { provider });
3881
+ // `auth_done` means "a login succeeded" (modals close on it).
3882
+ // `auth_change` means "auth.json changed" — which a DISCONNECT also is,
3883
+ // so connection state has one signal that covers both directions.
3884
+ broadcastAll("auth_change", { provider });
3885
+ // A newly connected provider unlocks its models. `/models` filters on
3886
+ // who is logged in, so every window's picker is now stale — without
3887
+ // this the new models don't appear until the session is reopened.
3888
+ broadcastAll("models_change", {});
3803
3889
  json(res, 200, { ok: true });
3804
3890
  });
3805
3891
  return;
@@ -3825,7 +3911,18 @@ async function createSession(deps, opts) {
3825
3911
  json(res, 409, { error: "a login is already in progress" });
3826
3912
  return;
3827
3913
  }
3914
+ // A login writes the shared ~/.gg/auth.json, so two windows racing the
3915
+ // same provider means two browser tabs and two token exchanges whose
3916
+ // writes clobber each other. The per-session flag above cannot see
3917
+ // that — guard the provider daemon-wide as well.
3918
+ if (oauthInFlightProviders.has(provider)) {
3919
+ json(res, 409, {
3920
+ error: `a ${meta.label} login is already in progress in another window`,
3921
+ });
3922
+ return;
3923
+ }
3828
3924
  oauthInFlight = true;
3925
+ oauthInFlightProviders.add(provider);
3829
3926
  json(res, 202, { accepted: true });
3830
3927
  void (async () => {
3831
3928
  const cb = authCallbacks();
@@ -3846,10 +3943,19 @@ async function createSession(deps, opts) {
3846
3943
  throw new Error(`OAuth not implemented for ${provider}`);
3847
3944
  }
3848
3945
  await auth.setCredentials(storageKey, creds);
3849
- broadcast("auth_done", { provider });
3946
+ // Terminal outcome of a GLOBAL change: every window's login modal
3947
+ // should close and its provider list refresh, not just the one
3948
+ // that started the flow.
3949
+ broadcastAll("auth_done", { provider });
3950
+ broadcastAll("auth_change", { provider });
3951
+ // The OAuth provider's models just became selectable everywhere.
3952
+ broadcastAll("models_change", {});
3850
3953
  }
3851
3954
  catch (err) {
3852
3955
  captureSidecarError(err, "app-sidecar.auth.oauth", { provider });
3956
+ // Deliberately session-scoped: this is the outcome of ONE window's
3957
+ // attempt. Another window that never pressed Connect has nothing to
3958
+ // show an error about, and its modal correctly still offers login.
3853
3959
  broadcast("auth_error", {
3854
3960
  provider,
3855
3961
  message: err instanceof Error ? err.message : String(err),
@@ -3857,6 +3963,7 @@ async function createSession(deps, opts) {
3857
3963
  }
3858
3964
  finally {
3859
3965
  oauthInFlight = false;
3966
+ oauthInFlightProviders.delete(provider);
3860
3967
  pendingCode = null;
3861
3968
  }
3862
3969
  })();
@@ -3885,6 +3992,39 @@ async function createSession(deps, opts) {
3885
3992
  });
3886
3993
  return;
3887
3994
  }
3995
+ if (method === "POST" && url.startsWith("/mcp/elicit/")) {
3996
+ const id = decodeURIComponent(url.slice("/mcp/elicit/".length));
3997
+ void readBody(req, res).then((raw) => {
3998
+ if (raw === null)
3999
+ return;
4000
+ let result;
4001
+ try {
4002
+ const parsed = JSON.parse(raw);
4003
+ if (parsed.action !== "accept" &&
4004
+ parsed.action !== "decline" &&
4005
+ parsed.action !== "cancel") {
4006
+ json(res, 400, { error: "action must be accept, decline, or cancel" });
4007
+ return;
4008
+ }
4009
+ result =
4010
+ parsed.action === "accept"
4011
+ ? { action: "accept", content: parsed.content ?? {} }
4012
+ : { action: parsed.action };
4013
+ }
4014
+ catch {
4015
+ json(res, 400, { error: "invalid JSON body" });
4016
+ return;
4017
+ }
4018
+ // Unknown id means it already timed out or was cancelled by an abort —
4019
+ // the tool call has moved on, so the answer has nowhere to go.
4020
+ if (!elicitations.settle(id, result)) {
4021
+ json(res, 409, { error: "no elicitation is awaiting a response" });
4022
+ return;
4023
+ }
4024
+ json(res, 200, { ok: true });
4025
+ });
4026
+ return;
4027
+ }
3888
4028
  if (method === "POST" && url === "/auth/logout") {
3889
4029
  void readBody(req, res).then(async (raw) => {
3890
4030
  if (raw === null)
@@ -4270,6 +4410,7 @@ async function createSession(deps, opts) {
4270
4410
  json(res, 404, { error: "not found" });
4271
4411
  }
4272
4412
  async function dispose() {
4413
+ elicitations.cancelAll();
4273
4414
  tasksPollStopped = true;
4274
4415
  if (tasksPoll)
4275
4416
  clearTimeout(tasksPoll);