@kenkaiiii/ggcoder 5.78.0 → 5.79.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.
Files changed (107) hide show
  1. package/dist/app-sidecar/app-settings.d.ts +6 -2
  2. package/dist/app-sidecar/app-settings.js +39 -20
  3. package/dist/app-sidecar/app-settings.js.map +1 -1
  4. package/dist/app-sidecar/git-target.d.ts +17 -0
  5. package/dist/app-sidecar/git-target.js +31 -0
  6. package/dist/app-sidecar/git-target.js.map +1 -0
  7. package/dist/app-sidecar/goal-routes.d.ts +26 -0
  8. package/dist/app-sidecar/goal-routes.js +445 -0
  9. package/dist/app-sidecar/goal-routes.js.map +1 -0
  10. package/dist/app-sidecar/history-routes.d.ts +13 -0
  11. package/dist/app-sidecar/history-routes.js +36 -25
  12. package/dist/app-sidecar/history-routes.js.map +1 -1
  13. package/dist/app-sidecar/mcp-routes.js +1 -2
  14. package/dist/app-sidecar/mcp-routes.js.map +1 -1
  15. package/dist/app-sidecar/route-context.d.ts +12 -0
  16. package/dist/app-sidecar/run-control-routes.js +2 -1
  17. package/dist/app-sidecar/run-control-routes.js.map +1 -1
  18. package/dist/app-sidecar/session-routes.js +41 -0
  19. package/dist/app-sidecar/session-routes.js.map +1 -1
  20. package/dist/app-sidecar/session-types.d.ts +2 -0
  21. package/dist/app-sidecar/workspace-routes.js +13 -95
  22. package/dist/app-sidecar/workspace-routes.js.map +1 -1
  23. package/dist/app-sidecar.js +213 -60
  24. package/dist/app-sidecar.js.map +1 -1
  25. package/dist/cli/shared.js +1 -1
  26. package/dist/cli/shared.js.map +1 -1
  27. package/dist/cli.js +1 -1
  28. package/dist/cli.js.map +1 -1
  29. package/dist/core/acceptance-checks.js +17 -4
  30. package/dist/core/acceptance-checks.js.map +1 -1
  31. package/dist/core/agent-notifications.d.ts +1 -1
  32. package/dist/core/agent-session.d.ts +76 -5
  33. package/dist/core/agent-session.js +118 -22
  34. package/dist/core/agent-session.js.map +1 -1
  35. package/dist/core/ask-user.d.ts +11 -0
  36. package/dist/core/ask-user.js +6 -1
  37. package/dist/core/ask-user.js.map +1 -1
  38. package/dist/core/auth-providers.d.ts +2 -80
  39. package/dist/core/auth-providers.js +4 -183
  40. package/dist/core/auth-providers.js.map +1 -1
  41. package/dist/core/context-files.d.ts +8 -0
  42. package/dist/core/context-files.js +46 -0
  43. package/dist/core/context-files.js.map +1 -0
  44. package/dist/core/env-delta.d.ts +9 -1
  45. package/dist/core/env-delta.js +20 -0
  46. package/dist/core/env-delta.js.map +1 -1
  47. package/dist/core/goals-store.d.ts +208 -0
  48. package/dist/core/goals-store.js +520 -0
  49. package/dist/core/goals-store.js.map +1 -0
  50. package/dist/core/local-endpoint-store.js +15 -11
  51. package/dist/core/local-endpoint-store.js.map +1 -1
  52. package/dist/core/project-discovery.d.ts +7 -0
  53. package/dist/core/project-discovery.js +8 -2
  54. package/dist/core/project-discovery.js.map +1 -1
  55. package/dist/core/radio.d.ts +2 -3
  56. package/dist/core/radio.js.map +1 -1
  57. package/dist/core/session-manager.d.ts +7 -1
  58. package/dist/core/session-manager.js +17 -7
  59. package/dist/core/session-manager.js.map +1 -1
  60. package/dist/core/session-storage.d.ts +3 -0
  61. package/dist/core/session-storage.js +8 -2
  62. package/dist/core/session-storage.js.map +1 -1
  63. package/dist/core/subagent-manager.d.ts +51 -0
  64. package/dist/core/subagent-manager.js +95 -15
  65. package/dist/core/subagent-manager.js.map +1 -1
  66. package/dist/core/subagent-store.js +2 -1
  67. package/dist/core/subagent-store.js.map +1 -1
  68. package/dist/goals-agent/goal-team.d.ts +2 -0
  69. package/dist/goals-agent/goal-team.js +82 -0
  70. package/dist/goals-agent/goal-team.js.map +1 -0
  71. package/dist/goals-agent/goals-agent.d.ts +90 -0
  72. package/dist/goals-agent/goals-agent.js +246 -0
  73. package/dist/goals-agent/goals-agent.js.map +1 -0
  74. package/dist/index.js +1 -1
  75. package/dist/index.js.map +1 -1
  76. package/dist/interactive.js +4 -3
  77. package/dist/interactive.js.map +1 -1
  78. package/dist/modes/subagent-worker-mode.d.ts +12 -0
  79. package/dist/modes/subagent-worker-mode.js +22 -1
  80. package/dist/modes/subagent-worker-mode.js.map +1 -1
  81. package/dist/system-prompt.d.ts +8 -0
  82. package/dist/system-prompt.js +21 -9
  83. package/dist/system-prompt.js.map +1 -1
  84. package/dist/tools/bash-read-evidence.d.ts +13 -0
  85. package/dist/tools/bash-read-evidence.js +48 -1
  86. package/dist/tools/bash-read-evidence.js.map +1 -1
  87. package/dist/tools/index.d.ts +7 -1
  88. package/dist/tools/index.js +10 -2
  89. package/dist/tools/index.js.map +1 -1
  90. package/dist/tools/read-tracker.d.ts +6 -12
  91. package/dist/tools/read-tracker.js +21 -36
  92. package/dist/tools/read-tracker.js.map +1 -1
  93. package/dist/tools/read.js +2 -2
  94. package/dist/tools/read.js.map +1 -1
  95. package/dist/tools/subagent-control.js +22 -3
  96. package/dist/tools/subagent-control.js.map +1 -1
  97. package/dist/tools/write.js +110 -8
  98. package/dist/tools/write.js.map +1 -1
  99. package/dist/ui/components/ActivityIndicator.d.ts +5 -5
  100. package/dist/ui/components/InputArea.d.ts +2 -2
  101. package/dist/ui/components/InputArea.js +3 -3
  102. package/dist/ui/components/InputArea.js.map +1 -1
  103. package/dist/ui/components/ToolExecution.d.ts +2 -2
  104. package/dist/utils/format.d.ts +0 -4
  105. package/dist/utils/format.js +0 -7
  106. package/dist/utils/format.js.map +1 -1
  107. package/package.json +4 -4
@@ -42,7 +42,6 @@ import { SettingsManager } from "./core/settings-manager.js";
42
42
  import { getModel, getDefaultThinkingLevel, getContextWindow, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
43
43
  import { resolveStartOrFallback } from "./core/resolve-start.js";
44
44
  import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
45
- import { getGitHubRepoSlug } from "./utils/github.js";
46
45
  import { scoreProjectHealth } from "./core/project-health-score.js";
47
46
  import { createRepoPolls } from "./app-sidecar/repo-polls.js";
48
47
  import { extractPlanSteps } from "./utils/plan-steps.js";
@@ -55,7 +54,7 @@ import { installTerminationHandlers } from "./core/shutdown.js";
55
54
  import { stopRadio } from "./core/radio.js";
56
55
  import { enrichProcessPath } from "./core/shell-path.js";
57
56
  import { createElicitationBridge } from "./core/mcp/index.js";
58
- import { askSoftDeadlineMs, createAskUserBridge, deliverLateAnswer } from "./core/ask-user.js";
57
+ import { askSoftDeadlineMs, createAskUserBridge, deliverLateAnswer, } from "./core/ask-user.js";
59
58
  import { createAskUserTool } from "./tools/ask-user.js";
60
59
  import { createUsageService } from "./app-sidecar/usage.js";
61
60
  import { daemonReadBody, daemonJson, json } from "./app-sidecar/http.js";
@@ -65,6 +64,10 @@ import { ALL_PROVIDERS, loadProjectModelPrefs, loadKenModelPref, loadAutopilot,
65
64
  import { KEN_ALLOWED_TOOLS } from "./app-sidecar/ken-context.js";
66
65
  import { createProgressManager } from "./app-sidecar/progress-manager.js";
67
66
  import { handleSessionRoutes } from "./app-sidecar/session-routes.js";
67
+ import { handleGoalRoutes } from "./app-sidecar/goal-routes.js";
68
+ import { readGitState, resolveGitCwd } from "./app-sidecar/git-target.js";
69
+ import { goalPhase, goalsRoot, isGoalDir, startGoalOnAnswer, readGoal, } from "./core/goals-store.js";
70
+ import { createGoalsAgentSession, goalCritterPick, loadGoalBriefExtras, } from "./goals-agent/goals-agent.js";
68
71
  import { handleWorkspaceRoutes } from "./app-sidecar/workspace-routes.js";
69
72
  import { handleHistoryRoutes } from "./app-sidecar/history-routes.js";
70
73
  import { handlePromptRoutes } from "./app-sidecar/prompt-routes.js";
@@ -177,6 +180,18 @@ async function main() {
177
180
  for (const ctx of sessions.values())
178
181
  ctx.broadcast(type, data);
179
182
  };
183
+ const sessionIdsAt = (cwd) => {
184
+ const target = path.resolve(cwd);
185
+ return [...sessions.values()]
186
+ .filter((ctx) => path.resolve(ctx.cwd) === target)
187
+ .map((ctx) => ctx.id)
188
+ .sort();
189
+ };
190
+ /** Whether any live window session in `cwd` is working right now. */
191
+ const busyAt = (cwd) => {
192
+ const target = path.resolve(cwd);
193
+ return [...sessions.values()].some((ctx) => path.resolve(ctx.cwd) === target && ctx.isBusy());
194
+ };
180
195
  // Providers currently mid-OAuth in some window. Daemon-level so two windows
181
196
  // cannot race two browser flows for the same provider into one auth file.
182
197
  const oauthInFlightProviders = new Set();
@@ -271,6 +286,8 @@ async function main() {
271
286
  broadcastAll,
272
287
  oauthInFlightProviders,
273
288
  repoPolls,
289
+ sessionIdsAt,
290
+ busyAt,
274
291
  }, { id, mode, chatAgent, cwd: sessionCwd, sessionPath });
275
292
  sessions.set(id, ctx);
276
293
  log("INFO", "app-sidecar", "session created", {
@@ -279,7 +296,13 @@ async function main() {
279
296
  chatAgent,
280
297
  cwd: sessionCwd,
281
298
  });
282
- daemonJson(res, 200, { sessionId: id });
299
+ // The conversation file too, so the app can reopen this exact
300
+ // session if the daemon restarts (a new session has no path yet
301
+ // on the app's side).
302
+ daemonJson(res, 200, {
303
+ sessionId: id,
304
+ sessionPath: ctx.session.getState().sessionPath ?? null,
305
+ });
283
306
  }
284
307
  catch (err) {
285
308
  const message = err instanceof Error ? err.message : String(err);
@@ -436,7 +459,7 @@ async function main() {
436
459
  * logger, PATH, shared auth file, and radio live at the daemon level.
437
460
  */
438
461
  async function createSession(deps, opts) {
439
- const { auth, progress, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders, repoPolls, } = deps;
462
+ const { auth, progress, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders, repoPolls, sessionIdsAt, busyAt, } = deps;
440
463
  const paths = deps.paths;
441
464
  const mode = opts.mode;
442
465
  let chatAgent = opts.chatAgent;
@@ -445,6 +468,10 @@ async function createSession(deps, opts) {
445
468
  // root; create it on first use so a fresh install can start a video at once.
446
469
  if (mode === "motion")
447
470
  await fs.mkdir(cwd, { recursive: true });
471
+ // A coding session whose cwd is a Goal folder is that goal's coordinator.
472
+ let goal = mode === "code" && isGoalDir(goalsRoot(paths.agentDir), cwd)
473
+ ? await readGoal(cwd).then((record) => (record ? { dir: cwd, record } : null))
474
+ : null;
448
475
  // Base host for parsing request-URL query params (value is irrelevant to
449
476
  // parsing); the daemon owns the real listen host.
450
477
  const host = "127.0.0.1";
@@ -590,7 +617,9 @@ async function createSession(deps, opts) {
590
617
  onClosed: (ids) => broadcast("ask_user_closed", { ids }),
591
618
  onLateAnswer: (late) => {
592
619
  log("INFO", "app-sidecar", "ask_user late answer queued", { id: late.prompt.id });
593
- deliverLateAnswer(late, {
620
+ // A late "Start the goal now" starts the goal before GG Goals hears it,
621
+ // so the run it wakes into may already hand out work.
622
+ void startGoalFrom({ action: "answer", answers: late.answers }, late.prompt.questions).then(() => deliverLateAnswer(late, {
594
623
  queueMessage: (text) => session.queueMessage(text),
595
624
  isBusy: () => running || runClaim.active || autopilotActive,
596
625
  onQueued: () => broadcast("queued", {
@@ -598,13 +627,44 @@ async function createSession(deps, opts) {
598
627
  messages: session.listQueuedMessages(),
599
628
  }),
600
629
  startIdleRun: () => void runStrandedQueue(),
601
- });
630
+ }));
602
631
  },
603
632
  });
604
- const askUserTool = createAskUserTool(asks.park);
633
+ // A goal starts when the user picks "Start the goal now" from GG Goals'
634
+ // question: answered while it waits, or late, after its deadline (see
635
+ // onLateAnswer). Recorded once, and announced so the window can switch to
636
+ // the goal's world view. Never throws: a failed write leaves it briefing.
637
+ const startGoalFrom = async (result, questions) => {
638
+ const current = goal;
639
+ if (!current)
640
+ return;
641
+ const started = await startGoalOnAnswer(current.dir, current.record, result, new Date(), questions).catch((err) => {
642
+ log("WARN", "app-sidecar", "couldn't record goal start", {
643
+ error: err instanceof Error ? err.message : String(err),
644
+ });
645
+ return null;
646
+ });
647
+ if (started) {
648
+ goal = { dir: current.dir, record: started };
649
+ broadcast("goal_state", { id: started.id, phase: goalPhase(started) });
650
+ }
651
+ };
652
+ const askUserTool = createAskUserTool(async (request) => {
653
+ const result = await asks.park(request);
654
+ await startGoalFrom(result, request.questions);
655
+ return result;
656
+ });
605
657
  // The session file path to resume (passed by the daemon's POST /session);
606
- // empty/unset starts a fresh session.
607
- const resumeSessionPath = opts.sessionPath;
658
+ // empty/unset starts a fresh session. A file that's gone (retention cleaned
659
+ // it up, or its goal was deleted) starts fresh too: failing instead left
660
+ // the window with no session at all, so not even its goal list loaded.
661
+ const resumeSessionPath = opts.sessionPath &&
662
+ (await fs.access(opts.sessionPath).then(() => true, () => false))
663
+ ? opts.sessionPath
664
+ : undefined;
665
+ if (opts.sessionPath && !resumeSessionPath) {
666
+ log("WARN", "app-sidecar", "session file gone, starting fresh", { cwd });
667
+ }
608
668
  let abort = new AbortController();
609
669
  const baseSessionOptions = {
610
670
  provider,
@@ -651,6 +711,19 @@ async function createSession(deps, opts) {
651
711
  additionalTools: [askUserTool],
652
712
  });
653
713
  }
714
+ else if (goal) {
715
+ // A Goal window talks to GG Goals, the orchestrator, not the coding agent.
716
+ session = createGoalsAgentSession({
717
+ ...baseSessionOptions,
718
+ goal,
719
+ // Memory and the attached projects' rules, read once for this session.
720
+ extras: await loadGoalBriefExtras(goal),
721
+ // Read live: the goal turns active mid-session when the user starts it.
722
+ isActive: () => Boolean(goal?.record.startedAt),
723
+ getChildModel: () => goalCritterPick(goal?.record),
724
+ additionalTools: [askUserTool],
725
+ });
726
+ }
654
727
  else {
655
728
  session = new AgentSession({
656
729
  ...baseSessionOptions,
@@ -681,6 +754,18 @@ async function createSession(deps, opts) {
681
754
  });
682
755
  }
683
756
  await session.initialize();
757
+ // A goal's attached folders join its workspace so the coordinator can read
758
+ // them. A resumed session may already carry them; `addDirectory` skips those.
759
+ if (goal) {
760
+ for (const item of goal.record.context) {
761
+ if (item.kind !== "folder")
762
+ continue;
763
+ const added = await session.addDirectory(item.path);
764
+ if (!added.ok && !added.error.startsWith("Already in the workspace")) {
765
+ log("WARN", "app-sidecar", "goal context folder unavailable", { error: added.error });
766
+ }
767
+ }
768
+ }
684
769
  if (mode === "chat") {
685
770
  const restoredAgent = [...session.getAppMarkers()]
686
771
  .reverse()
@@ -794,20 +879,18 @@ async function createSession(deps, opts) {
794
879
  });
795
880
  }
796
881
  // Workspace extras (context window, git status, background tasks). Git state
797
- // is resolved once at startup and refreshed after every run; the context
798
- // window follows the active model.
799
- const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount, initialGitHubSlug] = await Promise.all([
800
- getGitBranch(cwd).catch(() => null),
801
- isGitRepo(cwd).catch(() => false),
802
- getGitDirtyFileCount(cwd).catch(() => 0),
803
- getGitHubRepoSlug(cwd).catch(() => null),
804
- ]);
805
- let gitBranch = initialGitBranch;
806
- let gitIsRepo = initialGitIsRepo;
807
- let gitDirtyFileCount = initialDirtyFileCount;
882
+ // is resolved at startup and refreshed after every run; the context window
883
+ // follows the active model. A Goal window reads git from its attached
884
+ // project folder, not the goal folder (see resolveGitCwd), and re-targets
885
+ // when the goal's folders change (`retargetGit`).
886
+ let gitCwd = await resolveGitCwd(cwd, goal?.record ?? null);
887
+ const initialGit = await readGitState(gitCwd);
888
+ let gitBranch = initialGit.branch;
889
+ let gitIsRepo = initialGit.isRepo;
890
+ let gitDirtyFileCount = initialGit.dirtyFileCount;
808
891
  // Open issue/PR counts for the origin repo's GitHub slug, via the `gh` CLI's
809
892
  // auth. null = unknown (gh missing/unauthed, non-GitHub origin) → chips hidden.
810
- const gitHubSlug = initialGitHubSlug;
893
+ let gitHubSlug = initialGit.gitHubSlug;
811
894
  let gitHubIssues = null;
812
895
  let gitHubPRs = null;
813
896
  let gitHubCI = null;
@@ -1439,15 +1522,15 @@ async function createSession(deps, opts) {
1439
1522
  // A run may have switched branches, changed files, or spawned/finished
1440
1523
  // background tasks. Refresh the workspace extras once it settles.
1441
1524
  [gitBranch, gitIsRepo, gitDirtyFileCount] = await Promise.all([
1442
- getGitBranch(cwd).catch(() => gitBranch),
1443
- isGitRepo(cwd).catch(() => gitIsRepo),
1444
- getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
1525
+ getGitBranch(gitCwd).catch(() => gitBranch),
1526
+ isGitRepo(gitCwd).catch(() => gitIsRepo),
1527
+ getGitDirtyFileCount(gitCwd).catch(() => gitDirtyFileCount),
1445
1528
  ]);
1446
1529
  // A run may have opened/closed issues or PRs — refresh fire-and-forget so
1447
1530
  // teardown isn't delayed by the network. Broadcasts itself on change.
1448
- gitHubCountsPoll?.refresh();
1449
- ciPoll.refresh();
1450
- healthPoll?.refresh();
1531
+ repoWatch.gitHubCounts?.refresh();
1532
+ repoWatch.ci.refresh();
1533
+ repoWatch.health?.refresh();
1451
1534
  // Serialize behind any marker/tool-triggered refresh so the terminal
1452
1535
  // progress snapshot uses the live plan file. Once every canonical step
1453
1536
  // is complete, remove the approved plan from future system prompts and
@@ -1773,39 +1856,82 @@ async function createSession(deps, opts) {
1773
1856
  // (they change outside the agent: editor saves, commits, teammates). Branch/
1774
1857
  // repo state already refreshes after agent runs. The pollers are shared with
1775
1858
  // every other window on the same repo; each window broadcasts only changes.
1776
- const repoKey = path.resolve(cwd);
1777
- const dirtyFilesPoll = repoPolls.dirtyFiles.subscribe(repoKey, (next) => {
1778
- if (next === gitDirtyFileCount)
1779
- return;
1780
- gitDirtyFileCount = next;
1781
- broadcast("extras", footerExtras());
1782
- });
1783
- // Failed checks publish nothing, so the chips keep their last-known numbers
1784
- // instead of flickering off on a timeout. None when origin isn't GitHub.
1785
- const gitHubCountsPoll = gitHubSlug
1786
- ? repoPolls.gitHubCounts.subscribe(gitHubSlug, (counts) => {
1787
- if (counts.issues === gitHubIssues && counts.prs === gitHubPRs)
1859
+ function watchRepo(dir) {
1860
+ const repoKey = path.resolve(dir);
1861
+ const dirtyFiles = repoPolls.dirtyFiles.subscribe(repoKey, (next) => {
1862
+ if (next === gitDirtyFileCount)
1788
1863
  return;
1789
- gitHubIssues = counts.issues;
1790
- gitHubPRs = counts.prs;
1864
+ gitDirtyFileCount = next;
1791
1865
  broadcast("extras", footerExtras());
1792
- })
1793
- : null;
1794
- const ciPoll = repoPolls.gitHubCI.subscribe(repoKey, (next) => {
1795
- if (JSON.stringify(next) === JSON.stringify(gitHubCI))
1796
- return;
1797
- gitHubCI = next;
1798
- broadcast("extras", footerExtras());
1799
- });
1800
- // Project Health is a code-workspace signal; chat and motion windows skip the scan.
1801
- const healthPoll = mode === "code"
1802
- ? repoPolls.projectHealth.subscribe(repoKey, (next) => {
1803
- if (JSON.stringify(next) === JSON.stringify(projectHealthScan))
1866
+ });
1867
+ // Failed checks publish nothing, so the chips keep their last-known numbers
1868
+ // instead of flickering off on a timeout. None when origin isn't GitHub.
1869
+ const gitHubCounts = gitHubSlug
1870
+ ? repoPolls.gitHubCounts.subscribe(gitHubSlug, (counts) => {
1871
+ if (counts.issues === gitHubIssues && counts.prs === gitHubPRs)
1872
+ return;
1873
+ gitHubIssues = counts.issues;
1874
+ gitHubPRs = counts.prs;
1875
+ broadcast("extras", footerExtras());
1876
+ })
1877
+ : null;
1878
+ const ci = repoPolls.gitHubCI.subscribe(repoKey, (next) => {
1879
+ if (JSON.stringify(next) === JSON.stringify(gitHubCI))
1804
1880
  return;
1805
- projectHealthScan = next;
1881
+ gitHubCI = next;
1806
1882
  broadcast("extras", footerExtras());
1807
- })
1808
- : null;
1883
+ });
1884
+ // Project Health is a code-workspace signal; chat and motion windows skip the scan.
1885
+ const health = mode === "code"
1886
+ ? repoPolls.projectHealth.subscribe(repoKey, (next) => {
1887
+ if (JSON.stringify(next) === JSON.stringify(projectHealthScan))
1888
+ return;
1889
+ projectHealthScan = next;
1890
+ broadcast("extras", footerExtras());
1891
+ })
1892
+ : null;
1893
+ return {
1894
+ gitHubCounts,
1895
+ ci,
1896
+ health,
1897
+ unsubscribe() {
1898
+ dirtyFiles.unsubscribe();
1899
+ gitHubCounts?.unsubscribe();
1900
+ ci.unsubscribe();
1901
+ health?.unsubscribe();
1902
+ },
1903
+ };
1904
+ }
1905
+ let repoWatch = watchRepo(gitCwd);
1906
+ let repoWatchStopped = false;
1907
+ let gitTargetGeneration = 0;
1908
+ /**
1909
+ * A goal's attached folders changed: point the titlebar's git state at the
1910
+ * new project folder. No-op when the target is unchanged; a newer call or
1911
+ * teardown supersedes one still reading git.
1912
+ */
1913
+ async function retargetGit() {
1914
+ const generation = ++gitTargetGeneration;
1915
+ const next = await resolveGitCwd(cwd, goal?.record ?? null);
1916
+ if (generation !== gitTargetGeneration || repoWatchStopped || next === gitCwd)
1917
+ return;
1918
+ const state = await readGitState(next);
1919
+ if (generation !== gitTargetGeneration || repoWatchStopped)
1920
+ return;
1921
+ repoWatch.unsubscribe();
1922
+ gitCwd = next;
1923
+ gitBranch = state.branch;
1924
+ gitIsRepo = state.isRepo;
1925
+ gitDirtyFileCount = state.dirtyFileCount;
1926
+ gitHubSlug = state.gitHubSlug;
1927
+ // The old repo's numbers don't apply; the new watchers fill these in.
1928
+ gitHubIssues = null;
1929
+ gitHubPRs = null;
1930
+ gitHubCI = null;
1931
+ projectHealthScan = null;
1932
+ repoWatch = watchRepo(gitCwd);
1933
+ broadcast("extras", footerExtras());
1934
+ }
1809
1935
  const routeCtx = {
1810
1936
  get session() {
1811
1937
  return session;
@@ -1857,6 +1983,23 @@ async function createSession(deps, opts) {
1857
1983
  cwd,
1858
1984
  asks,
1859
1985
  runClaim,
1986
+ sessionId: opts.id,
1987
+ sessionIdsAt,
1988
+ busyAt,
1989
+ get goal() {
1990
+ return goal;
1991
+ },
1992
+ set goal(v) {
1993
+ const foldersBefore = JSON.stringify(goal?.record.context ?? []);
1994
+ goal = v;
1995
+ if (JSON.stringify(goal?.record.context ?? []) !== foldersBefore) {
1996
+ retargetGit().catch((err) => {
1997
+ log("WARN", "app-sidecar", "goal git retarget failed", {
1998
+ message: err instanceof Error ? err.message : String(err),
1999
+ });
2000
+ });
2001
+ }
2002
+ },
1860
2003
  broadcast,
1861
2004
  get scheduledRunActive() {
1862
2005
  return scheduledRunActive;
@@ -2014,6 +2157,8 @@ async function createSession(deps, opts) {
2014
2157
  return;
2015
2158
  if (handleWorkspaceRoutes(routeCtx, req, res, url, method))
2016
2159
  return;
2160
+ if (handleGoalRoutes(routeCtx, req, res, url, method))
2161
+ return;
2017
2162
  if (handleHistoryRoutes(routeCtx, req, res, url, method))
2018
2163
  return;
2019
2164
  if (handlePromptRoutes(routeCtx, req, res, url, method))
@@ -2031,6 +2176,12 @@ async function createSession(deps, opts) {
2031
2176
  json(res, 404, { error: "not found" });
2032
2177
  }
2033
2178
  async function dispose() {
2179
+ // A window closed (or switched project) mid-run: stop the run itself, any
2180
+ // run-all sweep or autopilot turn, and a model download this window
2181
+ // started. Otherwise they keep streaming tokens and running tools on a
2182
+ // session that is being torn down.
2183
+ abortOwnedWork();
2184
+ cancelHfPull();
2034
2185
  releaseRunAwake?.();
2035
2186
  releaseRunAwake = null;
2036
2187
  releaseSubagentAwake?.();
@@ -2040,10 +2191,8 @@ async function createSession(deps, opts) {
2040
2191
  tasksPollStopped = true;
2041
2192
  if (tasksPoll)
2042
2193
  clearTimeout(tasksPoll);
2043
- dirtyFilesPoll.unsubscribe();
2044
- gitHubCountsPoll?.unsubscribe();
2045
- ciPoll.unsubscribe();
2046
- healthPoll?.unsubscribe();
2194
+ repoWatchStopped = true;
2195
+ repoWatch.unsubscribe();
2047
2196
  // Stop the Telegram serve loop + dispose its per-chat sessions.
2048
2197
  if (serveController)
2049
2198
  await serveController.stop().catch(() => { });
@@ -2074,6 +2223,10 @@ async function createSession(deps, opts) {
2074
2223
  handle,
2075
2224
  dispose,
2076
2225
  stopBackgroundProcesses,
2226
+ isBusy: () => running ||
2227
+ runClaim.active ||
2228
+ autopilotActive ||
2229
+ session.listSubAgents().some((w) => w.state === "running" || w.state === "starting"),
2077
2230
  };
2078
2231
  }
2079
2232
  main().catch(async (err) => {