@kenkaiiii/ggcoder 5.50.0 → 5.51.1

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.
@@ -71,6 +71,8 @@ import { downscaleForPreview, shrinkToFit, validateVisionImage } from "./utils/i
71
71
  import { startServeMode } from "./modes/serve-mode.js";
72
72
  import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
73
73
  import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, createElicitationBridge, } from "./core/mcp/index.js";
74
+ import { createAskUserBridge } from "./core/ask-user.js";
75
+ import { createAskUserTool } from "./tools/ask-user.js";
74
76
  import { buildSnapshot, levelForXp, rankForLevel } from "./core/progress/ranks.js";
75
77
  import { loadProgress, peekProgress, updateProgress } from "./core/progress/store.js";
76
78
  import { awardPrompt, awardCommits } from "./core/progress/engine.js";
@@ -1297,6 +1299,16 @@ async function createSession(deps, opts) {
1297
1299
  server: prompt.server,
1298
1300
  }),
1299
1301
  });
1302
+ // ── ask_user bridge ────────────────────────────────────────
1303
+ // The `ask_user` tool parks the turn on a human answer. Same shape as the
1304
+ // MCP bridge above: broadcast over SSE, resolved when the webview POSTs
1305
+ // /ask/:id. Registered ONLY here — a TUI/headless/subagent run has nobody to
1306
+ // answer, so the tool is absent there rather than hanging on a dead channel.
1307
+ const asks = createAskUserBridge({
1308
+ broadcast: (prompt) => broadcast("ask_user", prompt),
1309
+ onTimeout: (prompt) => log("WARN", "app-sidecar", "ask_user timed out", { id: prompt.id }),
1310
+ });
1311
+ const askUserTool = createAskUserTool(asks.park);
1300
1312
  // The session file path to resume (passed by the daemon's POST /session);
1301
1313
  // empty/unset starts a fresh session.
1302
1314
  const resumeSessionPath = opts.sessionPath;
@@ -1322,7 +1334,11 @@ async function createSession(deps, opts) {
1322
1334
  session = createChatAgent(chatAgent, {
1323
1335
  ...baseSessionOptions,
1324
1336
  sessionsDir: paths.sessionsDir,
1325
- additionalTools: [...buildMemoryTools(memoryStore), ...buildJiwaTools(jiwaStore)],
1337
+ additionalTools: [
1338
+ askUserTool,
1339
+ ...buildMemoryTools(memoryStore),
1340
+ ...buildJiwaTools(jiwaStore),
1341
+ ],
1326
1342
  getSystemPromptTail: () => `${memoryStore.renderForPrompt()}\n\n${jiwaStore.renderForPrompt()}`,
1327
1343
  onAgentChange: async (nextAgent) => {
1328
1344
  chatAgent = nextAgent;
@@ -1339,6 +1355,7 @@ async function createSession(deps, opts) {
1339
1355
  else {
1340
1356
  session = new AgentSession({
1341
1357
  ...baseSessionOptions,
1358
+ additionalTools: [askUserTool],
1342
1359
  // Plan mode belongs only to the coding agent.
1343
1360
  onEnterPlan: async (reason) => {
1344
1361
  deactivateApprovedPlan();
@@ -2152,8 +2169,9 @@ async function createSession(deps, opts) {
2152
2169
  abort.abort();
2153
2170
  // An MCP tool call parked on user input is not cancelled by the signal —
2154
2171
  // the promise lives in the bridge. Release it, or the aborted turn's tool
2155
- // call never returns.
2172
+ // call never returns. Same for a question parked on the user.
2156
2173
  elicitations.cancelAll();
2174
+ asks.cancelAll();
2157
2175
  // Stop a run-all sweep and every async child through AgentSession's signal.
2158
2176
  taskRunAll = false;
2159
2177
  autopilotCancelled = true;
@@ -4480,6 +4498,50 @@ async function createSession(deps, opts) {
4480
4498
  });
4481
4499
  return;
4482
4500
  }
4501
+ // Answer (or dismiss) an `ask_user` question band. The turn is blocked on
4502
+ // this, so both paths must land: "answer" carries the picked values,
4503
+ // "cancel" releases the tool call with no answer.
4504
+ if (method === "POST" && url.startsWith("/ask/")) {
4505
+ const id = decodeURIComponent(url.slice("/ask/".length));
4506
+ void readBody(req, res).then((raw) => {
4507
+ if (raw === null)
4508
+ return;
4509
+ let result;
4510
+ try {
4511
+ const parsed = JSON.parse(raw);
4512
+ if (parsed.action !== "answer" && parsed.action !== "cancel") {
4513
+ json(res, 400, { error: "action must be answer or cancel" });
4514
+ return;
4515
+ }
4516
+ if (parsed.action === "cancel") {
4517
+ result = { action: "cancel" };
4518
+ }
4519
+ else {
4520
+ // Only strings and string arrays are answers; anything else is a
4521
+ // malformed client, not a value to hand the model.
4522
+ const answers = {};
4523
+ for (const [key, value] of Object.entries(parsed.answers ?? {})) {
4524
+ if (typeof value === "string")
4525
+ answers[key] = value;
4526
+ else if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
4527
+ answers[key] = value;
4528
+ }
4529
+ }
4530
+ result = { action: "answer", answers };
4531
+ }
4532
+ }
4533
+ catch {
4534
+ json(res, 400, { error: "invalid JSON body" });
4535
+ return;
4536
+ }
4537
+ if (!asks.settle(id, result)) {
4538
+ json(res, 409, { error: "no question is awaiting an answer" });
4539
+ return;
4540
+ }
4541
+ json(res, 200, { ok: true });
4542
+ });
4543
+ return;
4544
+ }
4483
4545
  if (method === "POST" && url === "/auth/logout") {
4484
4546
  void readBody(req, res).then(async (raw) => {
4485
4547
  if (raw === null)
@@ -4958,6 +5020,7 @@ async function createSession(deps, opts) {
4958
5020
  }
4959
5021
  async function dispose() {
4960
5022
  elicitations.cancelAll();
5023
+ asks.cancelAll();
4961
5024
  tasksPollStopped = true;
4962
5025
  if (tasksPoll)
4963
5026
  clearTimeout(tasksPoll);