@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.
- package/dist/app-sidecar/app-settings.d.ts +6 -2
- package/dist/app-sidecar/app-settings.js +39 -20
- package/dist/app-sidecar/app-settings.js.map +1 -1
- package/dist/app-sidecar/git-target.d.ts +17 -0
- package/dist/app-sidecar/git-target.js +31 -0
- package/dist/app-sidecar/git-target.js.map +1 -0
- package/dist/app-sidecar/goal-routes.d.ts +26 -0
- package/dist/app-sidecar/goal-routes.js +445 -0
- package/dist/app-sidecar/goal-routes.js.map +1 -0
- package/dist/app-sidecar/history-routes.d.ts +13 -0
- package/dist/app-sidecar/history-routes.js +36 -25
- package/dist/app-sidecar/history-routes.js.map +1 -1
- package/dist/app-sidecar/mcp-routes.js +1 -2
- package/dist/app-sidecar/mcp-routes.js.map +1 -1
- package/dist/app-sidecar/route-context.d.ts +12 -0
- package/dist/app-sidecar/run-control-routes.js +2 -1
- package/dist/app-sidecar/run-control-routes.js.map +1 -1
- package/dist/app-sidecar/session-routes.js +41 -0
- package/dist/app-sidecar/session-routes.js.map +1 -1
- package/dist/app-sidecar/session-types.d.ts +2 -0
- package/dist/app-sidecar/workspace-routes.js +13 -95
- package/dist/app-sidecar/workspace-routes.js.map +1 -1
- package/dist/app-sidecar.js +213 -60
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli/shared.js +1 -1
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/core/acceptance-checks.js +17 -4
- package/dist/core/acceptance-checks.js.map +1 -1
- package/dist/core/agent-notifications.d.ts +1 -1
- package/dist/core/agent-session.d.ts +76 -5
- package/dist/core/agent-session.js +118 -22
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/ask-user.d.ts +11 -0
- package/dist/core/ask-user.js +6 -1
- package/dist/core/ask-user.js.map +1 -1
- package/dist/core/auth-providers.d.ts +2 -80
- package/dist/core/auth-providers.js +4 -183
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/context-files.d.ts +8 -0
- package/dist/core/context-files.js +46 -0
- package/dist/core/context-files.js.map +1 -0
- package/dist/core/env-delta.d.ts +9 -1
- package/dist/core/env-delta.js +20 -0
- package/dist/core/env-delta.js.map +1 -1
- package/dist/core/goals-store.d.ts +208 -0
- package/dist/core/goals-store.js +520 -0
- package/dist/core/goals-store.js.map +1 -0
- package/dist/core/local-endpoint-store.js +15 -11
- package/dist/core/local-endpoint-store.js.map +1 -1
- package/dist/core/project-discovery.d.ts +7 -0
- package/dist/core/project-discovery.js +8 -2
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/radio.d.ts +2 -3
- package/dist/core/radio.js.map +1 -1
- package/dist/core/session-manager.d.ts +7 -1
- package/dist/core/session-manager.js +17 -7
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-storage.d.ts +3 -0
- package/dist/core/session-storage.js +8 -2
- package/dist/core/session-storage.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +51 -0
- package/dist/core/subagent-manager.js +95 -15
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-store.js +2 -1
- package/dist/core/subagent-store.js.map +1 -1
- package/dist/goals-agent/goal-team.d.ts +2 -0
- package/dist/goals-agent/goal-team.js +82 -0
- package/dist/goals-agent/goal-team.js.map +1 -0
- package/dist/goals-agent/goals-agent.d.ts +90 -0
- package/dist/goals-agent/goals-agent.js +246 -0
- package/dist/goals-agent/goals-agent.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interactive.js +4 -3
- package/dist/interactive.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts +12 -0
- package/dist/modes/subagent-worker-mode.js +22 -1
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/system-prompt.d.ts +8 -0
- package/dist/system-prompt.js +21 -9
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/bash-read-evidence.d.ts +13 -0
- package/dist/tools/bash-read-evidence.js +48 -1
- package/dist/tools/bash-read-evidence.js.map +1 -1
- package/dist/tools/index.d.ts +7 -1
- package/dist/tools/index.js +10 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read-tracker.d.ts +6 -12
- package/dist/tools/read-tracker.js +21 -36
- package/dist/tools/read-tracker.js.map +1 -1
- package/dist/tools/read.js +2 -2
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/subagent-control.js +22 -3
- package/dist/tools/subagent-control.js.map +1 -1
- package/dist/tools/write.js +110 -8
- package/dist/tools/write.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.d.ts +5 -5
- package/dist/ui/components/InputArea.d.ts +2 -2
- package/dist/ui/components/InputArea.js +3 -3
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/ToolExecution.d.ts +2 -2
- package/dist/utils/format.d.ts +0 -4
- package/dist/utils/format.js +0 -7
- package/dist/utils/format.js.map +1 -1
- package/package.json +4 -4
package/dist/app-sidecar.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
let
|
|
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
|
-
|
|
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(
|
|
1443
|
-
isGitRepo(
|
|
1444
|
-
getGitDirtyFileCount(
|
|
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
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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
|
-
|
|
1790
|
-
gitHubPRs = counts.prs;
|
|
1864
|
+
gitDirtyFileCount = next;
|
|
1791
1865
|
broadcast("extras", footerExtras());
|
|
1792
|
-
})
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
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
|
-
|
|
1881
|
+
gitHubCI = next;
|
|
1806
1882
|
broadcast("extras", footerExtras());
|
|
1807
|
-
})
|
|
1808
|
-
|
|
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
|
-
|
|
2044
|
-
|
|
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) => {
|