@gajae-code/coding-agent 0.4.5 → 0.5.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/CHANGELOG.md +62 -0
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +7 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +30 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +8 -1
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/harness-control-plane/types.d.ts +4 -0
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +3 -1
- package/dist/types/session/blob-store.d.ts +59 -4
- package/dist/types/session/session-manager.d.ts +24 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +7 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/tools/subagent.d.ts +6 -0
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli.ts +9 -4
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +43 -5
- package/src/commands/launch.ts +2 -2
- package/src/commands/session.ts +3 -1
- package/src/config/file-lock-gc.ts +181 -0
- package/src/config/file-lock.ts +14 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +264 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +513 -26
- package/src/cursor.ts +16 -2
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/export/html/index.ts +13 -9
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +58 -7
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +46 -29
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +211 -8
- package/src/gjc-runtime/tmux-common.ts +29 -0
- package/src/gjc-runtime/tmux-gc.ts +176 -0
- package/src/gjc-runtime/tmux-sessions.ts +68 -12
- package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +89 -27
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +93 -0
- package/src/harness-control-plane/types.ts +4 -0
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +14 -8
- package/src/main.ts +7 -2
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +370 -181
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +34 -42
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +187 -39
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/sdk.ts +46 -5
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +179 -25
- package/src/session/blob-store.ts +148 -6
- package/src/session/session-manager.ts +311 -60
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +78 -6
- package/src/task/receipt.ts +5 -0
- package/src/task/render.ts +21 -1
- package/src/task/types.ts +8 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/ask.ts +56 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +9 -0
- package/src/tools/subagent.ts +26 -2
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/tool-choice.ts +45 -16
|
@@ -66,7 +66,7 @@ function classifyProjectDir(pwd: string): { scratch: boolean; relative: string |
|
|
|
66
66
|
const gajaeSegment: StatusLineSegment = {
|
|
67
67
|
id: "gajae",
|
|
68
68
|
render(_ctx) {
|
|
69
|
-
return { content: theme.fg("accent", "
|
|
69
|
+
return { content: theme.fg("accent", "🦞"), visible: true };
|
|
70
70
|
},
|
|
71
71
|
};
|
|
72
72
|
|
|
@@ -152,6 +152,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
152
152
|
isError?: boolean;
|
|
153
153
|
details?: any;
|
|
154
154
|
};
|
|
155
|
+
#textOutputCache?: { content: unknown; showImages: boolean; terminalImageProtocol: unknown; output: string };
|
|
155
156
|
// Edit preview state
|
|
156
157
|
#editMode?: EditMode;
|
|
157
158
|
#editDiffPreview?: PerFileDiffPreview[];
|
|
@@ -197,9 +198,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
197
198
|
|
|
198
199
|
this.addChild(new Spacer(1));
|
|
199
200
|
|
|
200
|
-
// Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
// Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins.
|
|
202
|
+
// Vertical padding is 0: block separation comes solely from the leading Spacer
|
|
203
|
+
// (1 blank line above each block), matching reference TUIs (083.2).
|
|
204
|
+
this.#contentBox = new Box(1, 0, (text: string) => theme.bg("toolPendingBg", text));
|
|
205
|
+
this.#contentText = new Text("", 1, 0, (text: string) => theme.bg("toolPendingBg", text));
|
|
203
206
|
|
|
204
207
|
// Use Box for custom tools or built-in tools that have renderers
|
|
205
208
|
const hasRenderer = toolName in toolRenderers;
|
|
@@ -295,6 +298,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
295
298
|
isPartial = false,
|
|
296
299
|
_toolCallId?: string,
|
|
297
300
|
): void {
|
|
301
|
+
this.#textOutputCache = undefined;
|
|
298
302
|
this.#result = result;
|
|
299
303
|
this.#isPartial = isPartial;
|
|
300
304
|
// When tool is complete, ensure args are marked complete so spinner stops
|
|
@@ -397,6 +401,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
397
401
|
|
|
398
402
|
setShowImages(show: boolean): void {
|
|
399
403
|
this.#showImages = show;
|
|
404
|
+
this.#textOutputCache = undefined;
|
|
400
405
|
this.#updateDisplay();
|
|
401
406
|
}
|
|
402
407
|
|
|
@@ -514,7 +519,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
514
519
|
const fileBgFn = fileResult.isError
|
|
515
520
|
? (text: string) => theme.bg("toolErrorBg", text)
|
|
516
521
|
: (text: string) => theme.bg("toolSuccessBg", text);
|
|
517
|
-
const fileBox = new Box(1,
|
|
522
|
+
const fileBox = new Box(1, 0, fileBgFn);
|
|
518
523
|
try {
|
|
519
524
|
const resultComponent = renderer.renderResult(
|
|
520
525
|
{ content: [], details: fileResult, isError: fileResult.isError },
|
|
@@ -540,7 +545,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
540
545
|
const pendingSpacer = new Spacer(1);
|
|
541
546
|
this.#multiFileBoxes.push(pendingSpacer);
|
|
542
547
|
this.addChild(pendingSpacer);
|
|
543
|
-
const pendingBox = new Box(1,
|
|
548
|
+
const pendingBox = new Box(1, 0, (text: string) => theme.bg("toolPendingBg", text));
|
|
544
549
|
const pendingText = renderStatusLine(
|
|
545
550
|
{
|
|
546
551
|
icon: "pending",
|
|
@@ -723,16 +728,27 @@ export class ToolExecutionComponent extends Container {
|
|
|
723
728
|
#getTextOutput(): string {
|
|
724
729
|
if (!this.#result) return "";
|
|
725
730
|
|
|
726
|
-
const
|
|
727
|
-
const
|
|
731
|
+
const content = this.#result.content;
|
|
732
|
+
const terminalImageProtocol = TERMINAL.imageProtocol;
|
|
733
|
+
const cached = this.#textOutputCache;
|
|
734
|
+
if (
|
|
735
|
+
cached?.content === content &&
|
|
736
|
+
cached.showImages === this.#showImages &&
|
|
737
|
+
cached.terminalImageProtocol === terminalImageProtocol
|
|
738
|
+
) {
|
|
739
|
+
return cached.output;
|
|
740
|
+
}
|
|
728
741
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
.
|
|
742
|
+
const textParts: string[] = [];
|
|
743
|
+
for (const block of content ?? []) {
|
|
744
|
+
if (block.type !== "text") continue;
|
|
745
|
+
const text = block.text || "";
|
|
746
|
+
textParts.push(sanitizeWithOptionalSixelPassthrough(text, sanitizeText));
|
|
747
|
+
}
|
|
748
|
+
let output = textParts.join("\n");
|
|
734
749
|
|
|
735
|
-
|
|
750
|
+
const imageBlocks = this.#getAllImageBlocks();
|
|
751
|
+
if (imageBlocks.length > 0 && (!terminalImageProtocol || !this.#showImages)) {
|
|
736
752
|
const imageIndicators = imageBlocks
|
|
737
753
|
.map((img: any) => {
|
|
738
754
|
const dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;
|
|
@@ -742,6 +758,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
742
758
|
output = output ? `${output}\n${imageIndicators}` : imageIndicators;
|
|
743
759
|
}
|
|
744
760
|
|
|
761
|
+
this.#textOutputCache = { content, showImages: this.#showImages, terminalImageProtocol, output };
|
|
745
762
|
return output;
|
|
746
763
|
}
|
|
747
764
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@gajae-code/tui";
|
|
14
14
|
import { formatDuration, Snowflake, setProjectDir } from "@gajae-code/utils";
|
|
15
15
|
import { $ } from "bun";
|
|
16
|
+
import { jobElapsedMs } from "../../async";
|
|
16
17
|
import { reset as resetCapabilities } from "../../capability";
|
|
17
18
|
import { clearClaudePluginRootsCache } from "../../discovery/helpers";
|
|
18
19
|
import { loadCustomShare } from "../../export/custom-share";
|
|
@@ -1279,7 +1280,7 @@ const BAR_WIDTH_MAX = 24;
|
|
|
1279
1280
|
const BAR_WIDTH_MIN = 4;
|
|
1280
1281
|
|
|
1281
1282
|
function renderJobLine(job: AsyncJobSnapshotItem, now: number): string {
|
|
1282
|
-
const duration = formatDuration(
|
|
1283
|
+
const duration = formatDuration(jobElapsedMs(job, now));
|
|
1283
1284
|
const status = formatJobStatus(job.status);
|
|
1284
1285
|
return `${theme.fg("dim", job.id)} ${theme.fg("dim", `[${job.type}]`)} ${status} ${theme.fg("dim", `(${duration})`)}`;
|
|
1285
1286
|
}
|
|
@@ -1538,7 +1539,7 @@ function resolveColumnWidth(count: number, available: number, trailing: number):
|
|
|
1538
1539
|
return ideal;
|
|
1539
1540
|
}
|
|
1540
1541
|
|
|
1541
|
-
function renderUsageReports(
|
|
1542
|
+
export function renderUsageReports(
|
|
1542
1543
|
reports: UsageReport[],
|
|
1543
1544
|
uiTheme: typeof theme,
|
|
1544
1545
|
nowMs: number,
|
|
@@ -1592,17 +1593,35 @@ function renderUsageReports(
|
|
|
1592
1593
|
|
|
1593
1594
|
lines.push(uiTheme.bold(uiTheme.fg("accent", providerName)));
|
|
1594
1595
|
|
|
1596
|
+
// Stable account column order shared across every window group, so each
|
|
1597
|
+
// account keeps the same column in the 5h / 7d / ... rows. Rank accounts
|
|
1598
|
+
// by total usage across their windows (a per-account value identical for
|
|
1599
|
+
// every group), tie-broken by label for determinism.
|
|
1600
|
+
const rankedAccounts = providerReports
|
|
1601
|
+
.map(report => {
|
|
1602
|
+
const [firstLimit] = report.limits;
|
|
1603
|
+
return {
|
|
1604
|
+
report,
|
|
1605
|
+
total: report.limits.reduce((sum, limit) => sum + (resolveFraction(limit) ?? 0), 0),
|
|
1606
|
+
label: firstLimit ? formatAccountLabel(firstLimit, report, 0) : formatUnlimitedReportLabel(report, 0),
|
|
1607
|
+
};
|
|
1608
|
+
})
|
|
1609
|
+
.sort((a, b) => (a.total !== b.total ? b.total - a.total : a.label.localeCompare(b.label)));
|
|
1610
|
+
const accountRank = new Map<UsageReport, number>();
|
|
1611
|
+
rankedAccounts.forEach((entry, rank) => {
|
|
1612
|
+
accountRank.set(entry.report, rank);
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1595
1615
|
const renderableGroups = Array.from(limitGroups.values()).map(group => {
|
|
1596
1616
|
const entries = group.limits.map((limit, index) => ({
|
|
1597
1617
|
limit,
|
|
1598
1618
|
report: group.reports[index],
|
|
1599
|
-
fraction: resolveFraction(limit),
|
|
1600
1619
|
index,
|
|
1601
1620
|
}));
|
|
1602
1621
|
entries.sort((a, b) => {
|
|
1603
|
-
const
|
|
1604
|
-
const
|
|
1605
|
-
if (
|
|
1622
|
+
const aRank = accountRank.get(a.report) ?? Number.MAX_SAFE_INTEGER;
|
|
1623
|
+
const bRank = accountRank.get(b.report) ?? Number.MAX_SAFE_INTEGER;
|
|
1624
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
1606
1625
|
return a.index - b.index;
|
|
1607
1626
|
});
|
|
1608
1627
|
const sortedLimits = entries.map(entry => entry.limit);
|
|
@@ -645,6 +645,9 @@ export class ExtensionUiController {
|
|
|
645
645
|
timeout: dialogOptions?.timeout,
|
|
646
646
|
onTimeout: dialogOptions?.onTimeout,
|
|
647
647
|
tui: this.ctx.ui,
|
|
648
|
+
// Share the main prompt editor's autocomplete provider so the
|
|
649
|
+
// inline "Other (type your own)" editor supports `@` file links.
|
|
650
|
+
autocompleteProvider: dialogOptions?.customInput ? this.ctx.editor.getAutocompleteProvider() : undefined,
|
|
648
651
|
outline: dialogOptions?.outline,
|
|
649
652
|
wrapFocused: dialogOptions?.wrapFocused,
|
|
650
653
|
scrollTitleRows,
|
|
@@ -5,6 +5,7 @@ import type { Component, OverlayHandle } from "@gajae-code/tui";
|
|
|
5
5
|
import { Input, Loader, Spacer, Text } from "@gajae-code/tui";
|
|
6
6
|
import { getAgentDbPath, getProjectDir } from "@gajae-code/utils";
|
|
7
7
|
import { activateModelProfile } from "../../config/model-profile-activation";
|
|
8
|
+
import { recommendModelProfileForProvider } from "../../config/model-profiles";
|
|
8
9
|
import { settings } from "../../config/settings";
|
|
9
10
|
import { DebugSelectorComponent } from "../../debug";
|
|
10
11
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
@@ -45,7 +46,7 @@ import { CustomProviderWizardComponent, type CustomProviderWizardSubmit } from "
|
|
|
45
46
|
import { ExtensionDashboard } from "../components/extensions";
|
|
46
47
|
import { HistorySearchComponent } from "../components/history-search";
|
|
47
48
|
import { JobsOverlayComponent } from "../components/jobs-overlay";
|
|
48
|
-
import { ModelSelectorComponent
|
|
49
|
+
import { ModelSelectorComponent } from "../components/model-selector";
|
|
49
50
|
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
|
50
51
|
import { PluginSelectorComponent } from "../components/plugin-selector";
|
|
51
52
|
import {
|
|
@@ -69,6 +70,7 @@ const CALLBACK_SERVER_PROVIDERS = new Set<string>([
|
|
|
69
70
|
"google-gemini-cli",
|
|
70
71
|
"google-antigravity",
|
|
71
72
|
"xai",
|
|
73
|
+
"grok-build",
|
|
72
74
|
]);
|
|
73
75
|
|
|
74
76
|
const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
|
|
@@ -537,12 +539,6 @@ export class SelectorController {
|
|
|
537
539
|
this.ctx.session.scopedModels,
|
|
538
540
|
async selection => {
|
|
539
541
|
try {
|
|
540
|
-
if (selection.kind === "preset") {
|
|
541
|
-
await this.#applyModelAssignmentPreset(selection);
|
|
542
|
-
done();
|
|
543
|
-
this.ctx.ui.requestRender();
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
542
|
if (selection.kind === "profile") {
|
|
547
543
|
await activateModelProfile(
|
|
548
544
|
{
|
|
@@ -609,45 +605,12 @@ export class SelectorController {
|
|
|
609
605
|
done();
|
|
610
606
|
this.ctx.ui.requestRender();
|
|
611
607
|
},
|
|
612
|
-
options,
|
|
608
|
+
{ ...options, sessionId: this.ctx.session.sessionId },
|
|
613
609
|
);
|
|
614
610
|
return { component: selector, focus: selector };
|
|
615
611
|
});
|
|
616
612
|
}
|
|
617
613
|
|
|
618
|
-
async #applyModelAssignmentPreset(selection: Extract<ModelSelectorSelection, { kind: "preset" }>): Promise<void> {
|
|
619
|
-
const { assignments, model, preset, selector } = selection;
|
|
620
|
-
const apiKey = await this.ctx.session.modelRegistry.getApiKey(model, this.ctx.session.sessionId);
|
|
621
|
-
if (!apiKey) {
|
|
622
|
-
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const defaultThinkingLevel = assignments.default;
|
|
626
|
-
await this.ctx.session.setModel(model, "default", {
|
|
627
|
-
selector,
|
|
628
|
-
thinkingLevel: defaultThinkingLevel,
|
|
629
|
-
});
|
|
630
|
-
if (defaultThinkingLevel && defaultThinkingLevel !== ThinkingLevel.Inherit) {
|
|
631
|
-
this.ctx.session.setThinkingLevel(defaultThinkingLevel);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const overrides = this.ctx.settings.get("task.agentModelOverrides");
|
|
635
|
-
const nextOverrides = { ...overrides };
|
|
636
|
-
for (const [targetRole, presetThinkingLevel] of Object.entries(assignments) as [
|
|
637
|
-
keyof Extract<ModelSelectorSelection, { kind: "preset" }>["assignments"],
|
|
638
|
-
ThinkingLevel,
|
|
639
|
-
][]) {
|
|
640
|
-
if (!targetRole || targetRole === "default") continue;
|
|
641
|
-
nextOverrides[targetRole] =
|
|
642
|
-
presetThinkingLevel === ThinkingLevel.Inherit ? selector : `${selector}:${presetThinkingLevel}`;
|
|
643
|
-
}
|
|
644
|
-
this.ctx.settings.set("task.agentModelOverrides", nextOverrides);
|
|
645
|
-
this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
646
|
-
this.ctx.statusLine.invalidate();
|
|
647
|
-
this.ctx.updateEditorBorderColor();
|
|
648
|
-
this.ctx.showStatus(`${preset.label}: ${selector}`);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
614
|
async showPluginSelector(mode: "install" | "uninstall" = "install"): Promise<void> {
|
|
652
615
|
const mgr = new MarketplaceManager({
|
|
653
616
|
marketplacesRegistryPath: getMarketplacesRegistryPath(),
|
|
@@ -1034,6 +997,34 @@ export class SelectorController {
|
|
|
1034
997
|
await this.showSessionSelector();
|
|
1035
998
|
}
|
|
1036
999
|
|
|
1000
|
+
async #handlePostLoginModelProfileRecommendation(providerId: string): Promise<void> {
|
|
1001
|
+
const recommendedProfile = recommendModelProfileForProvider(
|
|
1002
|
+
providerId,
|
|
1003
|
+
this.ctx.session.modelRegistry.getModelProfiles(),
|
|
1004
|
+
);
|
|
1005
|
+
if (!recommendedProfile) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const activeProfile = this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default");
|
|
1010
|
+
if (activeProfile) {
|
|
1011
|
+
this.ctx.showStatus(`Preset ${recommendedProfile.name} is available in /model.`);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const confirmed = await this.ctx.showHookConfirm(`Apply ${recommendedProfile.name} now?`, "");
|
|
1016
|
+
if (!confirmed) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
await activateModelProfile({
|
|
1021
|
+
session: this.ctx.session,
|
|
1022
|
+
modelRegistry: this.ctx.session.modelRegistry,
|
|
1023
|
+
settings: this.ctx.settings,
|
|
1024
|
+
profileName: recommendedProfile.name,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1037
1028
|
async #handleOAuthLogin(providerId: string): Promise<void> {
|
|
1038
1029
|
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
1039
1030
|
const manualInput = this.ctx.oauthManualInput;
|
|
@@ -1090,6 +1081,7 @@ export class SelectorController {
|
|
|
1090
1081
|
new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0),
|
|
1091
1082
|
);
|
|
1092
1083
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0));
|
|
1084
|
+
await this.#handlePostLoginModelProfileRecommendation(providerId);
|
|
1093
1085
|
this.ctx.ui.requestRender();
|
|
1094
1086
|
} catch (error: unknown) {
|
|
1095
1087
|
this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1120,7 +1112,7 @@ export class SelectorController {
|
|
|
1120
1112
|
async showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
|
|
1121
1113
|
if (providerId) {
|
|
1122
1114
|
const oauthProvider = getOAuthProviders().find(provider => provider.id === providerId);
|
|
1123
|
-
if (!oauthProvider) {
|
|
1115
|
+
if (!oauthProvider && !this.ctx.session.modelRegistry.getModelProfiles().has(providerId)) {
|
|
1124
1116
|
this.ctx.showError(`Unknown OAuth provider: ${providerId}`);
|
|
1125
1117
|
return;
|
|
1126
1118
|
}
|
|
@@ -12,6 +12,7 @@ import type { SessionStats } from "../../session/agent-session";
|
|
|
12
12
|
import type {
|
|
13
13
|
RpcCommand,
|
|
14
14
|
RpcExtensionUIRequest,
|
|
15
|
+
RpcGetStateInclude,
|
|
15
16
|
RpcHandoffResult,
|
|
16
17
|
RpcHostToolCallRequest,
|
|
17
18
|
RpcHostToolCancelRequest,
|
|
@@ -442,8 +443,8 @@ export class RpcClient {
|
|
|
442
443
|
/**
|
|
443
444
|
* Get current session state.
|
|
444
445
|
*/
|
|
445
|
-
async getState(): Promise<RpcSessionState> {
|
|
446
|
-
const response = await this.#send({ type: "get_state" });
|
|
446
|
+
async getState(include?: RpcGetStateInclude[]): Promise<RpcSessionState> {
|
|
447
|
+
const response = await this.#send(include ? { type: "get_state", include } : { type: "get_state" });
|
|
447
448
|
return this.#getData(response);
|
|
448
449
|
}
|
|
449
450
|
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* - Events: AgentSessionEvent objects streamed as they occur
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from "node:fs/promises";
|
|
13
15
|
import * as path from "node:path";
|
|
14
|
-
import { $
|
|
16
|
+
import { $pickenv, readLines, Snowflake } from "@gajae-code/utils";
|
|
15
17
|
import type {
|
|
16
18
|
ExtensionUIContext,
|
|
17
19
|
ExtensionUIDialogOptions,
|
|
@@ -23,6 +25,7 @@ import { initializeExtensions } from "../runtime-init";
|
|
|
23
25
|
import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
|
|
24
26
|
import { AgentWireFrameSequencer, toAgentWireEventFrame } from "../shared/agent-wire/event-envelope";
|
|
25
27
|
import { rpcError as error } from "../shared/agent-wire/responses";
|
|
28
|
+
import { registerRpcSession, unregisterRpcSession } from "../shared/agent-wire/session-registry";
|
|
26
29
|
import { defaultAuditPath, UnattendedAuditLog } from "../shared/agent-wire/unattended-audit";
|
|
27
30
|
import { UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
|
|
28
31
|
import { FileGateStore } from "../shared/agent-wire/workflow-gate-broker";
|
|
@@ -70,13 +73,15 @@ function parseValueDialogResponse(
|
|
|
70
73
|
return undefined;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
function
|
|
74
|
-
const raw = $
|
|
76
|
+
export function shouldEmitRpcTitlesForTest(): boolean {
|
|
77
|
+
const raw = $pickenv("GJC_RPC_EMIT_TITLE", "PI_RPC_EMIT_TITLE");
|
|
75
78
|
if (!raw) return false;
|
|
76
79
|
const normalized = raw.trim().toLowerCase();
|
|
77
80
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
const shouldEmitRpcTitles = shouldEmitRpcTitlesForTest;
|
|
84
|
+
|
|
80
85
|
function auditOutcomeFor(event: string): "accepted" | "rejected" | "denied" | "exceeded" | "aborted" | "info" {
|
|
81
86
|
if (event.includes("denied")) return "denied";
|
|
82
87
|
if (event.includes("exceeded")) return "exceeded";
|
|
@@ -156,6 +161,7 @@ export function requestRpcEditor(
|
|
|
156
161
|
export async function runRpcMode(
|
|
157
162
|
session: AgentSession,
|
|
158
163
|
setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
|
|
164
|
+
options?: { listen?: string },
|
|
159
165
|
): Promise<never> {
|
|
160
166
|
// Signal to RPC clients that the server is ready to accept commands
|
|
161
167
|
// Suppress terminal notifications: they write \x07 (BEL) or OSC sequences directly to
|
|
@@ -164,11 +170,20 @@ export async function runRpcMode(
|
|
|
164
170
|
// may write there.
|
|
165
171
|
process.env.PI_NOTIFICATIONS = "off";
|
|
166
172
|
|
|
167
|
-
|
|
173
|
+
// Frames go to a swappable sink: stdout for stdio, the active client socket for a
|
|
174
|
+
// persistent --listen (UDS) server. Defaults to stdout, so the stdio path is unchanged.
|
|
175
|
+
let frameSink = (line: string): void => {
|
|
176
|
+
process.stdout.write(line);
|
|
177
|
+
};
|
|
168
178
|
const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
|
|
169
|
-
|
|
179
|
+
frameSink(`${JSON.stringify(obj)}\n`);
|
|
170
180
|
};
|
|
181
|
+
// stdio announces readiness immediately; the UDS server announces it per client connection.
|
|
182
|
+
if (!options?.listen) {
|
|
183
|
+
output({ type: "ready" });
|
|
184
|
+
}
|
|
171
185
|
const emitRpcTitles = shouldEmitRpcTitles();
|
|
186
|
+
const decodeError = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
172
187
|
|
|
173
188
|
const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
|
|
174
189
|
const hostToolBridge = new RpcHostToolBridge(output);
|
|
@@ -229,6 +244,37 @@ export async function runRpcMode(
|
|
|
229
244
|
|
|
230
245
|
// Shutdown request flag (wrapped in object to allow mutation with const)
|
|
231
246
|
const shutdownState = { requested: false };
|
|
247
|
+
let shutdownStarted = false;
|
|
248
|
+
// Tracks in-flight non-blocking command handlers so shutdown can drain them.
|
|
249
|
+
const inFlightCommands = new Set<Promise<void>>();
|
|
250
|
+
async function shutdown(exitCode: number, reason: string): Promise<never> {
|
|
251
|
+
if (shutdownStarted) {
|
|
252
|
+
process.exit(exitCode);
|
|
253
|
+
}
|
|
254
|
+
shutdownStarted = true;
|
|
255
|
+
// Let in-flight non-blocking commands (bash/compact/handoff) finish and emit
|
|
256
|
+
// their responses before teardown, bounded so a never-resolving login cannot
|
|
257
|
+
// wedge shutdown (issue 13).
|
|
258
|
+
if (inFlightCommands.size > 0) {
|
|
259
|
+
await Promise.race([Promise.allSettled([...inFlightCommands]), Bun.sleep(5000)]);
|
|
260
|
+
}
|
|
261
|
+
await unregisterRpcSession(session.sessionId).catch(() => {});
|
|
262
|
+
hostToolBridge.rejectAllPending(`${reason} before host tool execution completed`);
|
|
263
|
+
hostUriBridge.clear(`${reason} before host URI request completed`);
|
|
264
|
+
try {
|
|
265
|
+
await session.sessionManager.ensureOnDisk();
|
|
266
|
+
} catch (err) {
|
|
267
|
+
output(error(undefined, "shutdown", decodeError(err)));
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
await session.dispose();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
output(error(undefined, "shutdown", decodeError(err)));
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
process.exit(exitCode);
|
|
277
|
+
}
|
|
232
278
|
|
|
233
279
|
/**
|
|
234
280
|
* Extension UI context that uses the RPC protocol.
|
|
@@ -376,7 +422,7 @@ export async function runRpcMode(
|
|
|
376
422
|
}
|
|
377
423
|
|
|
378
424
|
setTitle(title: string): void {
|
|
379
|
-
// Title updates are low-value noise for most RPC hosts; opt in via
|
|
425
|
+
// Title updates are low-value noise for most RPC hosts; opt in via GJC_RPC_EMIT_TITLE=1.
|
|
380
426
|
if (!emitRpcTitles) return;
|
|
381
427
|
this.output({
|
|
382
428
|
type: "extension_ui_request",
|
|
@@ -491,62 +537,164 @@ export async function runRpcMode(
|
|
|
491
537
|
unattendedControlPlane,
|
|
492
538
|
});
|
|
493
539
|
|
|
540
|
+
// Cancellation commands must interrupt in-flight work, so they bypass the ordered
|
|
541
|
+
// queue and run immediately. Everything else runs through a serial chain so causal
|
|
542
|
+
// order is preserved (e.g. `get_state` after `bash` still observes the bash result)
|
|
543
|
+
// while the read loop itself never blocks — that is what lets a cancellation command
|
|
544
|
+
// reach a long-running `bash`/`compact`/`handoff`/`login` instead of being
|
|
545
|
+
// head-of-line-blocked behind it (issue 13).
|
|
546
|
+
const CANCELLATION_COMMANDS = new Set<RpcCommand["type"]>(["abort", "abort_bash", "abort_retry"]);
|
|
547
|
+
let orderedChain: Promise<void> = Promise.resolve();
|
|
548
|
+
const runCommand = async (command: RpcCommand): Promise<void> => {
|
|
549
|
+
try {
|
|
550
|
+
output(await handleCommand(command));
|
|
551
|
+
} catch (err) {
|
|
552
|
+
output(error(command.id, command.type, decodeError(err)));
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
const trackCommand = (task: Promise<void>): void => {
|
|
556
|
+
inFlightCommands.add(task);
|
|
557
|
+
void task.finally(() => inFlightCommands.delete(task));
|
|
558
|
+
};
|
|
559
|
+
const dispatchCommand = (command: RpcCommand): void => {
|
|
560
|
+
if (CANCELLATION_COMMANDS.has(command.type)) {
|
|
561
|
+
trackCommand(runCommand(command));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
orderedChain = orderedChain.then(() => runCommand(command));
|
|
565
|
+
trackCommand(orderedChain);
|
|
566
|
+
};
|
|
567
|
+
|
|
494
568
|
/**
|
|
495
569
|
* Check if shutdown was requested and perform shutdown if so.
|
|
496
570
|
* Called after handling each command when waiting for the next command.
|
|
497
571
|
*/
|
|
498
572
|
async function checkShutdownRequested(): Promise<void> {
|
|
499
573
|
if (!shutdownState.requested) return;
|
|
500
|
-
|
|
501
|
-
if (session.extensionRunner?.hasHandlers("session_shutdown")) {
|
|
502
|
-
await session.extensionRunner.emit({ type: "session_shutdown" });
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
process.exit(0);
|
|
574
|
+
await shutdown(0, "RPC shutdown requested");
|
|
506
575
|
}
|
|
507
576
|
|
|
508
|
-
//
|
|
509
|
-
|
|
577
|
+
// Parse + route a single inbound JSONL frame. Shared by the stdio reader and the
|
|
578
|
+
// persistent UDS server so both transports use the same command surface.
|
|
579
|
+
const inputDecoder = new TextDecoder("utf-8", { fatal: false });
|
|
580
|
+
async function handleInboundLine(text: string): Promise<void> {
|
|
581
|
+
let parsed: unknown;
|
|
582
|
+
try {
|
|
583
|
+
parsed = JSON.parse(text);
|
|
584
|
+
} catch (err) {
|
|
585
|
+
output(error(undefined, "parse", `Failed to parse command: ${decodeError(err)}`));
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
510
588
|
try {
|
|
511
|
-
// Handle extension UI responses
|
|
512
589
|
if ((parsed as RpcExtensionUIResponse).type === "extension_ui_response") {
|
|
513
590
|
const response = parsed as RpcExtensionUIResponse;
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
pending.resolve(response);
|
|
517
|
-
}
|
|
518
|
-
continue;
|
|
591
|
+
pendingExtensionRequests.get(response.id)?.resolve(response);
|
|
592
|
+
return;
|
|
519
593
|
}
|
|
520
|
-
|
|
521
594
|
if (isRpcHostToolResult(parsed)) {
|
|
522
595
|
hostToolBridge.handleResult(parsed);
|
|
523
|
-
|
|
596
|
+
return;
|
|
524
597
|
}
|
|
525
|
-
|
|
526
598
|
if (isRpcHostToolUpdate(parsed)) {
|
|
527
599
|
hostToolBridge.handleUpdate(parsed);
|
|
528
|
-
|
|
600
|
+
return;
|
|
529
601
|
}
|
|
530
|
-
|
|
531
602
|
if (isRpcHostUriResult(parsed)) {
|
|
532
603
|
hostUriBridge.handleResult(parsed);
|
|
533
|
-
|
|
604
|
+
return;
|
|
534
605
|
}
|
|
535
|
-
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
output(response);
|
|
540
|
-
|
|
541
|
-
// Check for deferred shutdown request (idle between commands)
|
|
606
|
+
// Ordered commands run through a serial chain to preserve causal order; the
|
|
607
|
+
// reader never blocks, so cancellation commands stay responsive even while a
|
|
608
|
+
// long command is in flight (issue 13).
|
|
609
|
+
dispatchCommand(parsed as RpcCommand);
|
|
542
610
|
await checkShutdownRequested();
|
|
543
|
-
} catch (
|
|
544
|
-
output(error(undefined, "parse", `Failed to parse command: ${
|
|
611
|
+
} catch (err) {
|
|
612
|
+
output(error(undefined, "parse", `Failed to parse command: ${decodeError(err)}`));
|
|
545
613
|
}
|
|
546
614
|
}
|
|
547
615
|
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
616
|
+
// Persistent UDS server (issue 09): keep the AgentSession alive across client
|
|
617
|
+
// reconnects instead of exiting on stdin EOF. Frames route to the active client
|
|
618
|
+
// socket; while no client is connected they are dropped (clients resync via
|
|
619
|
+
// get_state/get_messages on reconnect).
|
|
620
|
+
if (options?.listen) {
|
|
621
|
+
const socketPath = options.listen;
|
|
622
|
+
await fs.mkdir(path.dirname(socketPath), { recursive: true }).catch(() => {});
|
|
623
|
+
await fs.rm(socketPath, { force: true }).catch(() => {});
|
|
624
|
+
await registerRpcSession({
|
|
625
|
+
sessionId: session.sessionId,
|
|
626
|
+
pid: process.pid,
|
|
627
|
+
transport: "socket",
|
|
628
|
+
cwd: session.sessionManager.getCwd(),
|
|
629
|
+
model: session.model?.id,
|
|
630
|
+
startedAt: new Date().toISOString(),
|
|
631
|
+
endpoint: socketPath,
|
|
632
|
+
}).catch(() => {});
|
|
633
|
+
|
|
634
|
+
const noopSink = (_line: string): void => {};
|
|
635
|
+
let currentSocket: object | undefined;
|
|
636
|
+
let buf = "";
|
|
637
|
+
Bun.listen({
|
|
638
|
+
unix: socketPath,
|
|
639
|
+
socket: {
|
|
640
|
+
open(socket) {
|
|
641
|
+
currentSocket = socket;
|
|
642
|
+
buf = "";
|
|
643
|
+
frameSink = (line: string) => {
|
|
644
|
+
socket.write(line);
|
|
645
|
+
};
|
|
646
|
+
output({ type: "ready" });
|
|
647
|
+
},
|
|
648
|
+
data(socket, data) {
|
|
649
|
+
if (socket !== currentSocket) return;
|
|
650
|
+
buf += inputDecoder.decode(data);
|
|
651
|
+
while (true) {
|
|
652
|
+
const nl = buf.indexOf("\n");
|
|
653
|
+
if (nl < 0) break;
|
|
654
|
+
const text = buf.slice(0, nl).trim();
|
|
655
|
+
buf = buf.slice(nl + 1);
|
|
656
|
+
if (text) void handleInboundLine(text);
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
close(socket) {
|
|
660
|
+
if (socket === currentSocket) {
|
|
661
|
+
currentSocket = undefined;
|
|
662
|
+
frameSink = noopSink;
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
error() {},
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const onSignal = (): void => {
|
|
670
|
+
void shutdown(0, "RPC socket server signal");
|
|
671
|
+
};
|
|
672
|
+
process.on("SIGINT", onSignal);
|
|
673
|
+
process.on("SIGTERM", onSignal);
|
|
674
|
+
// Block until an explicit shutdown (signal/extension) calls process.exit.
|
|
675
|
+
await new Promise<never>(() => {});
|
|
676
|
+
throw new Error("RPC socket server returned unexpectedly");
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Register this stdio RPC session so other processes can discover it (issue 10).
|
|
680
|
+
await registerRpcSession({
|
|
681
|
+
sessionId: session.sessionId,
|
|
682
|
+
pid: process.pid,
|
|
683
|
+
transport: "stdio",
|
|
684
|
+
cwd: session.sessionManager.getCwd(),
|
|
685
|
+
model: session.model?.id,
|
|
686
|
+
startedAt: new Date().toISOString(),
|
|
687
|
+
}).catch(() => {});
|
|
688
|
+
|
|
689
|
+
// Listen for JSONL input using Bun's stdin. Parse frame-by-frame so a malformed
|
|
690
|
+
// command reports a parse error without poisoning the whole long-lived RPC session.
|
|
691
|
+
for await (const line of readLines(Bun.stdin.stream())) {
|
|
692
|
+
const text = inputDecoder.decode(line).trim();
|
|
693
|
+
if (!text) continue;
|
|
694
|
+
await handleInboundLine(text);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// stdin closed — RPC client is gone, flush durable state and exit cleanly
|
|
698
|
+
await shutdown(0, "RPC client disconnected");
|
|
699
|
+
throw new Error("RPC shutdown returned unexpectedly");
|
|
552
700
|
}
|