@gajae-code/coding-agent 0.15.6 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +10 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +77 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/state-version.d.ts +4 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/index.d.ts +6 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/session-runtime.d.ts +52 -0
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +6 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +77 -11
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +1 -5
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +23 -0
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +8 -1
- package/src/config/settings.ts +5 -0
- package/src/coordinator-mcp/projection-scan.ts +21 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +94 -20
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +79 -6
- package/src/gjc-runtime/session-state-sidecar.ts +929 -23
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +57 -2
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +605 -156
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/input-controller.ts +28 -0
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/prompts/system/master-mode.md +28 -0
- package/src/sdk/broker/broker.ts +1636 -26
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +186 -41
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +548 -0
- package/src/sdk/broker/state-version.ts +26 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +11 -8
- package/src/sdk/cli/session-cli.ts +264 -5
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +99 -38
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/host.ts +27 -2
- package/src/sdk/host/session-runtime.test.ts +210 -6
- package/src/sdk/host/session-runtime.ts +291 -49
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +13 -3
- package/src/sdk/router/session-router.ts +67 -59
- package/src/sdk/session.ts +357 -155
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/agent-session.ts +640 -103
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +337 -24
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/bash.ts +256 -42
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +4 -0
package/src/tools/bash.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
3
4
|
import type { Component } from "@gajae-code/tui";
|
|
4
5
|
import { getKeybindings, ImageProtocol, TERMINAL, Text, visibleWidth } from "@gajae-code/tui";
|
|
@@ -17,6 +18,7 @@ import { InternalUrlRouter } from "../internal-urls";
|
|
|
17
18
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
18
19
|
import { highlightCode, type Theme } from "../modes/theme/theme";
|
|
19
20
|
import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
21
|
+
import { renderSpawnTable, runSdkSpawn, type SdkSpawnArgs } from "../sdk/cli/master-cli";
|
|
20
22
|
import type { ArtifactManager } from "../session/artifacts";
|
|
21
23
|
import type {
|
|
22
24
|
ClientBridgeTerminalExitStatus,
|
|
@@ -87,6 +89,7 @@ export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
|
87
89
|
const BASH_ERROR_MAX_BYTES = 4096;
|
|
88
90
|
const ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
|
|
89
91
|
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
92
|
+
const MASTER_CAPABILITY_ENV = "GJC_MASTER_CAPABILITY";
|
|
90
93
|
const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
|
91
94
|
const ACP_RELEASE_TIMEOUT_MS = 1_000;
|
|
92
95
|
const READ_ONLY_BASH_ENV: Record<string, string> = {
|
|
@@ -601,6 +604,107 @@ function normalizeBashEnv(env: Record<string, string> | undefined): Record<strin
|
|
|
601
604
|
return normalized;
|
|
602
605
|
}
|
|
603
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Return true only for a direct, shell-syntax-free `gjc sdk spawn` command.
|
|
609
|
+
*
|
|
610
|
+
* Master authority is process-local and must not become ambient state for an
|
|
611
|
+
* arbitrary shell pipeline. Keep a tiny lexer here instead of a prefix check:
|
|
612
|
+
* shell quoting is allowed for ordinary argument values, while operators,
|
|
613
|
+
* expansions, escapes, globbing, and line breaks all refuse the privileged
|
|
614
|
+
* environment injection path.
|
|
615
|
+
*/
|
|
616
|
+
function strictDirectSdkSpawnTokens(command: string): string[] | undefined {
|
|
617
|
+
if (command.length === 0) return undefined;
|
|
618
|
+
const tokens: string[] = [];
|
|
619
|
+
let token = "";
|
|
620
|
+
let tokenStarted = false;
|
|
621
|
+
let quote: "'" | '"' | undefined;
|
|
622
|
+
for (const character of command) {
|
|
623
|
+
if (quote !== undefined) {
|
|
624
|
+
if (character === quote) {
|
|
625
|
+
quote = undefined;
|
|
626
|
+
tokenStarted = true;
|
|
627
|
+
} else {
|
|
628
|
+
token += character;
|
|
629
|
+
tokenStarted = true;
|
|
630
|
+
}
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (";&|<>()$`\\*?[]{}#!\n\r".includes(character)) return undefined;
|
|
634
|
+
if (character === "'" || character === '"') {
|
|
635
|
+
quote = character;
|
|
636
|
+
tokenStarted = true;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (/\s/u.test(character)) {
|
|
640
|
+
if (tokenStarted) {
|
|
641
|
+
tokens.push(token);
|
|
642
|
+
token = "";
|
|
643
|
+
tokenStarted = false;
|
|
644
|
+
}
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
token += character;
|
|
648
|
+
tokenStarted = true;
|
|
649
|
+
}
|
|
650
|
+
if (quote !== undefined) return undefined;
|
|
651
|
+
if (tokenStarted) tokens.push(token);
|
|
652
|
+
return tokens.length >= 3 && tokens[0] === "gjc" && tokens[1] === "sdk" && tokens[2] === "spawn"
|
|
653
|
+
? tokens
|
|
654
|
+
: undefined;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function isStrictDirectSdkSpawnCommand(command: string): boolean {
|
|
658
|
+
return strictDirectSdkSpawnTokens(command) !== undefined;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function parseDirectSdkSpawnArgs(command: string): SdkSpawnArgs {
|
|
662
|
+
const tokens = strictDirectSdkSpawnTokens(command);
|
|
663
|
+
if (tokens === undefined) throw new ToolError("Master spawn command is not a direct shell-safe invocation.");
|
|
664
|
+
const args: SdkSpawnArgs = {};
|
|
665
|
+
for (let index = 3; index < tokens.length; index++) {
|
|
666
|
+
const token = tokens[index]!;
|
|
667
|
+
if (token === "--json") {
|
|
668
|
+
args.json = true;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const equals = token.indexOf("=");
|
|
672
|
+
const name = equals < 0 ? token : token.slice(0, equals);
|
|
673
|
+
const value = equals < 0 ? tokens[++index] : token.slice(equals + 1);
|
|
674
|
+
if (value === undefined || value.length === 0) throw new ToolError(`Master spawn flag ${name} requires a value.`);
|
|
675
|
+
switch (name) {
|
|
676
|
+
case "--cwd":
|
|
677
|
+
args.cwd = value;
|
|
678
|
+
break;
|
|
679
|
+
case "--prompt":
|
|
680
|
+
args.prompt = value;
|
|
681
|
+
break;
|
|
682
|
+
case "--model":
|
|
683
|
+
args.model = value;
|
|
684
|
+
break;
|
|
685
|
+
case "--profile":
|
|
686
|
+
args.profile = value;
|
|
687
|
+
break;
|
|
688
|
+
case "--idempotency-key":
|
|
689
|
+
args.idempotencyKey = value;
|
|
690
|
+
break;
|
|
691
|
+
case "--agent-dir":
|
|
692
|
+
throw new ToolError("Master spawn cannot override the session agent directory.");
|
|
693
|
+
default:
|
|
694
|
+
throw new ToolError(`Unsupported master spawn flag: ${name}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return args;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function masterCommandEnvOverrides(
|
|
701
|
+
env: Record<string, string> | undefined,
|
|
702
|
+
directMasterSpawn: boolean,
|
|
703
|
+
): Record<string, string> {
|
|
704
|
+
if (directMasterSpawn) return {};
|
|
705
|
+
return Object.fromEntries(Object.entries(env ?? {}).filter(([key]) => key !== MASTER_CAPABILITY_ENV));
|
|
706
|
+
}
|
|
707
|
+
|
|
604
708
|
function escapeBashEnvValueForDisplay(value: string): string {
|
|
605
709
|
return value
|
|
606
710
|
.replaceAll("\\", "\\\\")
|
|
@@ -874,6 +978,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
874
978
|
notices?: readonly string[];
|
|
875
979
|
|
|
876
980
|
resolvedEnv?: Record<string, string>;
|
|
981
|
+
directMasterSpawn: boolean;
|
|
877
982
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
|
|
878
983
|
startBackgrounded: boolean;
|
|
879
984
|
onCompletion?: () => void;
|
|
@@ -885,7 +990,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
885
990
|
throw new ToolError("Background job manager unavailable for this session.");
|
|
886
991
|
}
|
|
887
992
|
|
|
888
|
-
const label = options.
|
|
993
|
+
const label = options.directMasterSpawn
|
|
994
|
+
? "gjc sdk spawn"
|
|
995
|
+
: options.command.length > 120
|
|
996
|
+
? `${options.command.slice(0, 117)}...`
|
|
997
|
+
: options.command;
|
|
889
998
|
let latestText = "";
|
|
890
999
|
let backgrounded = options.startBackgrounded;
|
|
891
1000
|
const runningDetails = (jobId: string): Record<string, unknown> | undefined =>
|
|
@@ -908,37 +1017,37 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
908
1017
|
|
|
909
1018
|
let executionResult: BashResult | BashInteractiveResult | undefined;
|
|
910
1019
|
try {
|
|
911
|
-
const result =
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1020
|
+
const result = options.directMasterSpawn
|
|
1021
|
+
? await this.#runDirectMasterSpawn(
|
|
1022
|
+
options.command,
|
|
1023
|
+
options.commandCwd,
|
|
1024
|
+
options.timeoutMs,
|
|
1025
|
+
runSignal,
|
|
1026
|
+
options.resolvedEnv,
|
|
1027
|
+
)
|
|
1028
|
+
: await executeBash(options.command, {
|
|
1029
|
+
cwd: options.commandCwd,
|
|
1030
|
+
settings: this.session.settings,
|
|
1031
|
+
sessionKey: `${this.session.getSessionId?.() ?? ""}:async:${jobId}`,
|
|
1032
|
+
timeout: options.timeoutMs,
|
|
1033
|
+
signal: runSignal,
|
|
1034
|
+
env: options.resolvedEnv,
|
|
1035
|
+
artifactPath,
|
|
1036
|
+
artifactId,
|
|
1037
|
+
artifactPublisher,
|
|
1038
|
+
spillThreshold,
|
|
1039
|
+
headBytes,
|
|
1040
|
+
oneShot: true,
|
|
1041
|
+
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only",
|
|
1042
|
+
disableShellSnapshot: this.session.bashRestrictionProfile === "read-only",
|
|
1043
|
+
onChunk: chunk => {
|
|
1044
|
+
tailBuffer.append(chunk);
|
|
1045
|
+
latestText = tailBuffer.text();
|
|
1046
|
+
void reportProgress(latestText, runningDetails(jobId));
|
|
1047
|
+
},
|
|
1048
|
+
onRawChunk: chunk => manager.appendOutput(jobId, chunk),
|
|
1049
|
+
onMinimizedSave: async originalText => saveBashOriginalArtifact(this.session, originalText),
|
|
1050
|
+
});
|
|
942
1051
|
executionResult = result;
|
|
943
1052
|
const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
|
|
944
1053
|
requestedTimeoutSec: options.requestedTimeoutSec,
|
|
@@ -1061,6 +1170,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1061
1170
|
command: string;
|
|
1062
1171
|
commandCwd: string;
|
|
1063
1172
|
resolvedEnv: Record<string, string>;
|
|
1173
|
+
directMasterSpawn: boolean;
|
|
1064
1174
|
requestedTimeoutSec: number;
|
|
1065
1175
|
timeoutSec: number;
|
|
1066
1176
|
timeoutMs: number;
|
|
@@ -1206,14 +1316,26 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1206
1316
|
// silently ignore the caller's override.
|
|
1207
1317
|
const explicitAgentDirOverride =
|
|
1208
1318
|
expandedEnv?.GJC_CODING_AGENT_DIR !== undefined || expandedEnv?.PI_CODING_AGENT_DIR !== undefined;
|
|
1319
|
+
// The master capability is a one-shot authority for the direct spawn CLI,
|
|
1320
|
+
// never ambient state for ordinary Bash or a chained/pipelined descendant.
|
|
1321
|
+
// Check both spellings so cwd normalization or URL expansion cannot turn a
|
|
1322
|
+
// command that contained shell syntax into a privileged one.
|
|
1323
|
+
const directMasterSpawn = isStrictDirectSdkSpawnCommand(rawCommand) && isStrictDirectSdkSpawnCommand(command);
|
|
1324
|
+
const masterCapability = directMasterSpawn ? this.session.getMasterBashCapability?.() : undefined;
|
|
1325
|
+
const masterOwnerSessionId = directMasterSpawn ? this.session.getMasterOwnerSessionId?.() : undefined;
|
|
1326
|
+
const commandEnvOverrides = masterCommandEnvOverrides(expandedEnv, directMasterSpawn);
|
|
1209
1327
|
const resolvedEnv = {
|
|
1210
1328
|
...buildGjcRuntimeSessionEnv({
|
|
1211
1329
|
sessionFile: null,
|
|
1212
1330
|
sessionId: this.session.getSessionId?.(),
|
|
1213
1331
|
cwd: this.session.cwd,
|
|
1214
1332
|
}),
|
|
1215
|
-
...(sessionAgentDir && !explicitAgentDirOverride
|
|
1216
|
-
|
|
1333
|
+
...(sessionAgentDir && (!explicitAgentDirOverride || directMasterSpawn)
|
|
1334
|
+
? { GJC_CODING_AGENT_DIR: sessionAgentDir }
|
|
1335
|
+
: {}),
|
|
1336
|
+
...commandEnvOverrides,
|
|
1337
|
+
...(masterCapability ? { [MASTER_CAPABILITY_ENV]: masterCapability } : {}),
|
|
1338
|
+
...(masterOwnerSessionId ? { GJC_MASTER_OWNER_SESSION_ID: masterOwnerSessionId } : {}),
|
|
1217
1339
|
...(this.session.bashRestrictionProfile === "read-only" ? READ_ONLY_BASH_ENV : {}),
|
|
1218
1340
|
...(allowedPrefixes && allowedPrefixes.length > 0 ? { [GJC_RESTRICTED_ROLE_AGENT_BASH_ENV]: "1" } : {}),
|
|
1219
1341
|
};
|
|
@@ -1245,7 +1367,80 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1245
1367
|
const sleepAdvisory = longSleepAdvisory(command, timeoutSec);
|
|
1246
1368
|
if (sleepAdvisory) notices.push(sleepAdvisory);
|
|
1247
1369
|
|
|
1248
|
-
return {
|
|
1370
|
+
return {
|
|
1371
|
+
command,
|
|
1372
|
+
commandCwd,
|
|
1373
|
+
resolvedEnv,
|
|
1374
|
+
directMasterSpawn,
|
|
1375
|
+
requestedTimeoutSec,
|
|
1376
|
+
timeoutSec,
|
|
1377
|
+
timeoutMs,
|
|
1378
|
+
notices,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
async #runDirectMasterSpawn(
|
|
1383
|
+
command: string,
|
|
1384
|
+
commandCwd: string,
|
|
1385
|
+
timeoutMs: number,
|
|
1386
|
+
signal: AbortSignal | undefined,
|
|
1387
|
+
resolvedEnv: Record<string, string> | undefined,
|
|
1388
|
+
): Promise<BashResult> {
|
|
1389
|
+
const masterCapability = resolvedEnv?.[MASTER_CAPABILITY_ENV];
|
|
1390
|
+
const sessionAgentDir = resolvedEnv?.GJC_CODING_AGENT_DIR;
|
|
1391
|
+
if (masterCapability === undefined || sessionAgentDir === undefined)
|
|
1392
|
+
throw new ToolError("Master spawn authority context is unavailable.");
|
|
1393
|
+
const args = parseDirectSdkSpawnArgs(command);
|
|
1394
|
+
args.agentDir = sessionAgentDir;
|
|
1395
|
+
if (args.cwd !== undefined && !path.isAbsolute(args.cwd)) args.cwd = path.resolve(commandCwd, args.cwd);
|
|
1396
|
+
const request = runSdkSpawn(args, {
|
|
1397
|
+
env: {
|
|
1398
|
+
...process.env,
|
|
1399
|
+
GJC_MASTER_CAPABILITY: masterCapability,
|
|
1400
|
+
GJC_SESSION_ID: resolvedEnv?.GJC_MASTER_OWNER_SESSION_ID ?? this.session.getSessionId?.() ?? undefined,
|
|
1401
|
+
GJC_CODING_AGENT_DIR: sessionAgentDir,
|
|
1402
|
+
},
|
|
1403
|
+
});
|
|
1404
|
+
const timeout = Bun.sleep(timeoutMs).then(() => {
|
|
1405
|
+
throw new ToolError(`Command timed out after ${Math.ceil(timeoutMs / 1000)} seconds`);
|
|
1406
|
+
});
|
|
1407
|
+
const aborted = Promise.withResolvers<never>();
|
|
1408
|
+
const onAbort = (): void => aborted.reject(new ToolAbortError("Command aborted"));
|
|
1409
|
+
if (signal?.aborted) onAbort();
|
|
1410
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1411
|
+
let result: Awaited<typeof request>;
|
|
1412
|
+
try {
|
|
1413
|
+
result = await Promise.race([request, timeout, aborted.promise]);
|
|
1414
|
+
} finally {
|
|
1415
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1416
|
+
}
|
|
1417
|
+
const output = args.json ? JSON.stringify(result.rendered) : renderSpawnTable(result.rendered);
|
|
1418
|
+
const outputBytes = Buffer.byteLength(output, "utf8");
|
|
1419
|
+
const outputLines = output.split("\n").length;
|
|
1420
|
+
return {
|
|
1421
|
+
output,
|
|
1422
|
+
exitCode: result.exitCode,
|
|
1423
|
+
cancelled: false,
|
|
1424
|
+
truncated: false,
|
|
1425
|
+
totalLines: outputLines,
|
|
1426
|
+
totalBytes: outputBytes,
|
|
1427
|
+
outputLines,
|
|
1428
|
+
outputBytes,
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
async #executeDirectMasterSpawn(
|
|
1433
|
+
command: string,
|
|
1434
|
+
commandCwd: string,
|
|
1435
|
+
timeoutSec: number,
|
|
1436
|
+
timeoutMs: number,
|
|
1437
|
+
requestedTimeoutSec: number,
|
|
1438
|
+
notices: readonly string[],
|
|
1439
|
+
signal: AbortSignal | undefined,
|
|
1440
|
+
resolvedEnv: Record<string, string>,
|
|
1441
|
+
): Promise<AgentToolResult<BashToolDetails>> {
|
|
1442
|
+
const result = await this.#runDirectMasterSpawn(command, commandCwd, timeoutMs, signal, resolvedEnv);
|
|
1443
|
+
return this.#buildCompletedResult(result, timeoutSec, { requestedTimeoutSec, notices });
|
|
1249
1444
|
}
|
|
1250
1445
|
|
|
1251
1446
|
/**
|
|
@@ -1343,8 +1538,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1343
1538
|
spillThreshold,
|
|
1344
1539
|
headBytes,
|
|
1345
1540
|
oneShot: true,
|
|
1346
|
-
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only",
|
|
1347
|
-
disableShellSnapshot:
|
|
1541
|
+
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only" || prepared.directMasterSpawn,
|
|
1542
|
+
disableShellSnapshot:
|
|
1543
|
+
this.session.bashRestrictionProfile === "read-only" || prepared.directMasterSpawn,
|
|
1348
1544
|
onChunk: chunk => {
|
|
1349
1545
|
tailBuffer.append(chunk);
|
|
1350
1546
|
void reportProgress(tailBuffer.text(), {
|
|
@@ -1416,11 +1612,25 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1416
1612
|
command,
|
|
1417
1613
|
commandCwd,
|
|
1418
1614
|
resolvedEnv,
|
|
1615
|
+
directMasterSpawn,
|
|
1419
1616
|
requestedTimeoutSec,
|
|
1420
1617
|
timeoutSec,
|
|
1421
1618
|
timeoutMs,
|
|
1422
1619
|
notices: pendingNotices,
|
|
1423
1620
|
} = prepared;
|
|
1621
|
+
const masterCapability = directMasterSpawn ? resolvedEnv[MASTER_CAPABILITY_ENV] : undefined;
|
|
1622
|
+
if (directMasterSpawn && masterCapability !== undefined && !asyncRequested) {
|
|
1623
|
+
return await this.#executeDirectMasterSpawn(
|
|
1624
|
+
command,
|
|
1625
|
+
commandCwd,
|
|
1626
|
+
timeoutSec,
|
|
1627
|
+
timeoutMs,
|
|
1628
|
+
requestedTimeoutSec,
|
|
1629
|
+
pendingNotices,
|
|
1630
|
+
signal,
|
|
1631
|
+
resolvedEnv,
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1424
1634
|
|
|
1425
1635
|
if (asyncRequested) {
|
|
1426
1636
|
// Availability is endpoint-first: a concurrent top-level session
|
|
@@ -1440,6 +1650,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1440
1650
|
notices: pendingNotices,
|
|
1441
1651
|
|
|
1442
1652
|
resolvedEnv,
|
|
1653
|
+
directMasterSpawn,
|
|
1443
1654
|
onUpdate,
|
|
1444
1655
|
startBackgrounded: true,
|
|
1445
1656
|
toolCallId,
|
|
@@ -1457,7 +1668,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1457
1668
|
// Skip when pty=true (PTY needs the local terminal UI).
|
|
1458
1669
|
const clientBridge =
|
|
1459
1670
|
this.session.bashRestrictionProfile === "read-only" ? undefined : this.session.getClientBridge?.();
|
|
1460
|
-
const clientTerminalActive = Boolean(
|
|
1671
|
+
const clientTerminalActive = Boolean(
|
|
1672
|
+
!directMasterSpawn && clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty,
|
|
1673
|
+
);
|
|
1461
1674
|
|
|
1462
1675
|
// Run non-PTY bash through the managed job path so Ctrl+B-twice fold-on-demand works
|
|
1463
1676
|
// even when auto-background is disabled. When a client terminal will handle the
|
|
@@ -1487,6 +1700,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1487
1700
|
notices: pendingNotices,
|
|
1488
1701
|
|
|
1489
1702
|
resolvedEnv,
|
|
1703
|
+
directMasterSpawn,
|
|
1490
1704
|
onUpdate,
|
|
1491
1705
|
startBackgrounded,
|
|
1492
1706
|
onCompletion: () => {
|
|
@@ -2200,7 +2414,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2200
2414
|
const artifactPublisher = createBashArtifactPublisher(this.session);
|
|
2201
2415
|
|
|
2202
2416
|
const interactiveUi =
|
|
2203
|
-
this.session.bashRestrictionProfile === "read-only"
|
|
2417
|
+
this.session.bashRestrictionProfile === "read-only" || directMasterSpawn
|
|
2204
2418
|
? undefined
|
|
2205
2419
|
: canUseInteractiveBashPty(pty, ctx)
|
|
2206
2420
|
? ctx?.ui
|
|
@@ -2368,7 +2582,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2368
2582
|
cwd: commandCwd,
|
|
2369
2583
|
settings: this.session.settings,
|
|
2370
2584
|
sessionKey: this.session.getSessionId?.() ?? undefined,
|
|
2371
|
-
oneShot: this.session.bashRestrictionProfile === "read-only",
|
|
2585
|
+
oneShot: this.session.bashRestrictionProfile === "read-only" || directMasterSpawn,
|
|
2372
2586
|
timeout: timeoutMs,
|
|
2373
2587
|
signal,
|
|
2374
2588
|
env: resolvedEnv,
|
|
@@ -2379,8 +2593,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2379
2593
|
headBytes,
|
|
2380
2594
|
onChunk: streamTailUpdates(tailBuffer, onUpdate),
|
|
2381
2595
|
onMinimizedSave: async originalText => saveBashOriginalArtifact(this.session, originalText),
|
|
2382
|
-
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only",
|
|
2383
|
-
disableShellSnapshot: this.session.bashRestrictionProfile === "read-only",
|
|
2596
|
+
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only" || directMasterSpawn,
|
|
2597
|
+
disableShellSnapshot: this.session.bashRestrictionProfile === "read-only" || directMasterSpawn,
|
|
2384
2598
|
});
|
|
2385
2599
|
ptyFoldUnregister?.();
|
|
2386
2600
|
// Folded: the foreground was settled by the fold, so return the background
|
package/src/tools/gh.ts
CHANGED
|
@@ -2806,6 +2806,14 @@ export interface PrDiffFile {
|
|
|
2806
2806
|
changeType: "modified" | "added" | "deleted" | "renamed" | "binary";
|
|
2807
2807
|
/** Pre-image path for renames/deletes; same as `path` otherwise. */
|
|
2808
2808
|
oldPath?: string;
|
|
2809
|
+
/**
|
|
2810
|
+
* Set when `path` kept its C-style escaped source form because the quoted
|
|
2811
|
+
* token held invalid UTF-8 or a malformed escape; distinguishes it from a
|
|
2812
|
+
* valid path whose decoded text looks identical.
|
|
2813
|
+
*/
|
|
2814
|
+
pathEscaped?: boolean;
|
|
2815
|
+
/** Same as {@link PrDiffFile.pathEscaped}, for `oldPath`. */
|
|
2816
|
+
oldPathEscaped?: boolean;
|
|
2809
2817
|
/** Byte offset of the section's `diff --git` line in the unified diff. */
|
|
2810
2818
|
startOffset: number;
|
|
2811
2819
|
/** Byte offset of the next section (or end-of-text). */
|
|
@@ -2861,6 +2869,8 @@ export function parsePrUnifiedDiff(text: string): PrDiffPayload {
|
|
|
2861
2869
|
interface ParsedDiffHeaderToken {
|
|
2862
2870
|
value: string;
|
|
2863
2871
|
nextIndex: number;
|
|
2872
|
+
/** Set when the token kept its C-style escaped source form. */
|
|
2873
|
+
escaped?: boolean;
|
|
2864
2874
|
}
|
|
2865
2875
|
|
|
2866
2876
|
function skipDiffHeaderSpaces(text: string, index: number): number {
|
|
@@ -2869,63 +2879,87 @@ function skipDiffHeaderSpaces(text: string, index: number): number {
|
|
|
2869
2879
|
return i;
|
|
2870
2880
|
}
|
|
2871
2881
|
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2882
|
+
interface ParsedDiffQuotedEscape {
|
|
2883
|
+
byte: number;
|
|
2884
|
+
nextIndex: number;
|
|
2885
|
+
}
|
|
2875
2886
|
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
while (end < text.length && end < slashIndex + 4) {
|
|
2879
|
-
const digit = text.charAt(end);
|
|
2880
|
-
if (digit < "0" || digit > "7") break;
|
|
2881
|
-
end += 1;
|
|
2882
|
-
}
|
|
2883
|
-
return {
|
|
2884
|
-
value: String.fromCharCode(Number.parseInt(text.slice(slashIndex + 1, end), 8)),
|
|
2885
|
-
nextIndex: end,
|
|
2886
|
-
};
|
|
2887
|
-
}
|
|
2887
|
+
const DIFF_PATH_ENCODER = new TextEncoder();
|
|
2888
|
+
const DIFF_PATH_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
2888
2889
|
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2890
|
+
const DIFF_NAMED_ESCAPE_BYTES: Record<string, number> = {
|
|
2891
|
+
a: 0x07,
|
|
2892
|
+
b: 0x08,
|
|
2893
|
+
f: 0x0c,
|
|
2894
|
+
n: 0x0a,
|
|
2895
|
+
r: 0x0d,
|
|
2896
|
+
t: 0x09,
|
|
2897
|
+
v: 0x0b,
|
|
2898
|
+
"\\": 0x5c,
|
|
2899
|
+
'"': 0x22,
|
|
2900
|
+
};
|
|
2901
|
+
|
|
2902
|
+
/**
|
|
2903
|
+
* Git quotes paths C-style: its named escapes plus exactly three octal digits
|
|
2904
|
+
* per byte (leading digit 0-3), so a multi-byte UTF-8 character arrives as
|
|
2905
|
+
* consecutive octal escapes. Anything else is malformed for this grammar.
|
|
2906
|
+
*/
|
|
2907
|
+
function parseDiffQuotedEscape(text: string, slashIndex: number): ParsedDiffQuotedEscape | undefined {
|
|
2908
|
+
const first = text.charAt(slashIndex + 1);
|
|
2909
|
+
if (first >= "0" && first <= "3") {
|
|
2910
|
+
const second = text.charAt(slashIndex + 2);
|
|
2911
|
+
const third = text.charAt(slashIndex + 3);
|
|
2912
|
+
if (second < "0" || second > "7" || third < "0" || third > "7") return undefined;
|
|
2913
|
+
return { byte: Number.parseInt(text.slice(slashIndex + 1, slashIndex + 4), 8), nextIndex: slashIndex + 4 };
|
|
2909
2914
|
}
|
|
2915
|
+
const named = DIFF_NAMED_ESCAPE_BYTES[first];
|
|
2916
|
+
if (named === undefined) return undefined;
|
|
2917
|
+
return { byte: named, nextIndex: slashIndex + 2 };
|
|
2910
2918
|
}
|
|
2911
2919
|
|
|
2920
|
+
/**
|
|
2921
|
+
* Escaped bytes are decoded as strict UTF-8. A token holding invalid UTF-8 or
|
|
2922
|
+
* a malformed escape keeps its escaped source form, so distinct non-UTF-8
|
|
2923
|
+
* byte sequences never collapse into one replacement-character path.
|
|
2924
|
+
*/
|
|
2912
2925
|
function parseDiffQuotedToken(text: string, startIndex: number): ParsedDiffHeaderToken | undefined {
|
|
2913
2926
|
if (text.charAt(startIndex) !== '"') return undefined;
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2927
|
+
const bytes: number[] = [];
|
|
2928
|
+
let malformed = false;
|
|
2929
|
+
for (let i = startIndex + 1; i < text.length; ) {
|
|
2930
|
+
const codePoint = text.codePointAt(i) as number;
|
|
2931
|
+
const ch = String.fromCodePoint(codePoint);
|
|
2932
|
+
if (ch === '"') {
|
|
2933
|
+
if (!malformed) {
|
|
2934
|
+
try {
|
|
2935
|
+
return { value: DIFF_PATH_DECODER.decode(Uint8Array.from(bytes)), nextIndex: i + 1 };
|
|
2936
|
+
} catch {}
|
|
2937
|
+
}
|
|
2938
|
+
return { value: text.slice(startIndex + 1, i), nextIndex: i + 1, escaped: true };
|
|
2939
|
+
}
|
|
2918
2940
|
if (ch !== "\\") {
|
|
2919
|
-
|
|
2941
|
+
bytes.push(...DIFF_PATH_ENCODER.encode(ch));
|
|
2942
|
+
i += ch.length;
|
|
2920
2943
|
continue;
|
|
2921
2944
|
}
|
|
2922
2945
|
const escaped = parseDiffQuotedEscape(text, i);
|
|
2923
|
-
|
|
2924
|
-
|
|
2946
|
+
if (escaped === undefined) {
|
|
2947
|
+
malformed = true;
|
|
2948
|
+
i += 2;
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
bytes.push(escaped.byte);
|
|
2952
|
+
i = escaped.nextIndex;
|
|
2925
2953
|
}
|
|
2926
2954
|
return undefined;
|
|
2927
2955
|
}
|
|
2928
2956
|
|
|
2957
|
+
function parseDiffPathLine(line: string, prefix: string): { value: string; escaped?: boolean } {
|
|
2958
|
+
const rest = line.slice(prefix.length);
|
|
2959
|
+
const token = parseDiffQuotedToken(rest, 0);
|
|
2960
|
+
return token ? { value: token.value, escaped: token.escaped } : { value: rest };
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2929
2963
|
function parseDiffHeaderToken(text: string, startIndex: number): ParsedDiffHeaderToken | undefined {
|
|
2930
2964
|
const start = skipDiffHeaderSpaces(text, startIndex);
|
|
2931
2965
|
if (start >= text.length) return undefined;
|
|
@@ -2940,7 +2974,14 @@ function stripPrDiffPathPrefix(value: string, prefix: "a/" | "b/"): string | und
|
|
|
2940
2974
|
return value.startsWith(prefix) ? value.slice(prefix.length) : undefined;
|
|
2941
2975
|
}
|
|
2942
2976
|
|
|
2943
|
-
|
|
2977
|
+
interface PrDiffHeaderPaths {
|
|
2978
|
+
oldPath?: string;
|
|
2979
|
+
newPath?: string;
|
|
2980
|
+
oldPathEscaped?: boolean;
|
|
2981
|
+
newPathEscaped?: boolean;
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
function parsePrDiffHeaderPaths(header: string): PrDiffHeaderPaths {
|
|
2944
2985
|
const trail = header.slice("diff --git ".length);
|
|
2945
2986
|
if (trail.startsWith('"')) {
|
|
2946
2987
|
const oldToken = parseDiffQuotedToken(trail, 0);
|
|
@@ -2950,6 +2991,8 @@ function parsePrDiffHeaderPaths(header: string): { oldPath?: string; newPath?: s
|
|
|
2950
2991
|
return {
|
|
2951
2992
|
oldPath: stripPrDiffPathPrefix(oldToken.value, "a/"),
|
|
2952
2993
|
newPath: stripPrDiffPathPrefix(newToken.value, "b/"),
|
|
2994
|
+
oldPathEscaped: oldToken.escaped,
|
|
2995
|
+
newPathEscaped: newToken.escaped,
|
|
2953
2996
|
};
|
|
2954
2997
|
}
|
|
2955
2998
|
|
|
@@ -2980,6 +3023,8 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
|
|
|
2980
3023
|
const headerPaths = parsePrDiffHeaderPaths(header);
|
|
2981
3024
|
let oldPath = headerPaths.oldPath;
|
|
2982
3025
|
let newPath = headerPaths.newPath;
|
|
3026
|
+
let oldPathEscaped = headerPaths.oldPathEscaped;
|
|
3027
|
+
let newPathEscaped = headerPaths.newPathEscaped;
|
|
2983
3028
|
|
|
2984
3029
|
let changeType: PrDiffFile["changeType"] = "modified";
|
|
2985
3030
|
let isBinary = false;
|
|
@@ -2999,11 +3044,11 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
|
|
|
2999
3044
|
}
|
|
3000
3045
|
if (line.startsWith("rename from ")) {
|
|
3001
3046
|
changeType = "renamed";
|
|
3002
|
-
oldPath = line
|
|
3047
|
+
({ value: oldPath, escaped: oldPathEscaped } = parseDiffPathLine(line, "rename from "));
|
|
3003
3048
|
continue;
|
|
3004
3049
|
}
|
|
3005
3050
|
if (line.startsWith("rename to ")) {
|
|
3006
|
-
newPath = line
|
|
3051
|
+
({ value: newPath, escaped: newPathEscaped } = parseDiffPathLine(line, "rename to "));
|
|
3007
3052
|
continue;
|
|
3008
3053
|
}
|
|
3009
3054
|
if (line.startsWith("Binary files ") && line.endsWith(" differ")) {
|
|
@@ -3028,8 +3073,15 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
|
|
|
3028
3073
|
deletions = 0;
|
|
3029
3074
|
}
|
|
3030
3075
|
|
|
3031
|
-
const
|
|
3032
|
-
|
|
3076
|
+
const preferOld = changeType === "deleted";
|
|
3077
|
+
const displayPath = preferOld ? (oldPath ?? newPath ?? "(unknown)") : (newPath ?? oldPath ?? "(unknown)");
|
|
3078
|
+
const displayEscaped = preferOld
|
|
3079
|
+
? oldPath !== undefined
|
|
3080
|
+
? oldPathEscaped
|
|
3081
|
+
: newPathEscaped
|
|
3082
|
+
: newPath !== undefined
|
|
3083
|
+
? newPathEscaped
|
|
3084
|
+
: oldPathEscaped;
|
|
3033
3085
|
const file: PrDiffFile = {
|
|
3034
3086
|
path: displayPath,
|
|
3035
3087
|
additions,
|
|
@@ -3038,8 +3090,12 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
|
|
|
3038
3090
|
startOffset,
|
|
3039
3091
|
endOffset,
|
|
3040
3092
|
};
|
|
3041
|
-
if (
|
|
3093
|
+
if (displayEscaped) {
|
|
3094
|
+
file.pathEscaped = true;
|
|
3095
|
+
}
|
|
3096
|
+
if (oldPath && (oldPath !== displayPath || oldPathEscaped !== displayEscaped)) {
|
|
3042
3097
|
file.oldPath = oldPath;
|
|
3098
|
+
if (oldPathEscaped) file.oldPathEscaped = true;
|
|
3043
3099
|
}
|
|
3044
3100
|
return file;
|
|
3045
3101
|
}
|
|
@@ -103,7 +103,7 @@ export function openDb(): Database | null {
|
|
|
103
103
|
// than running an in-place ALTER dance.
|
|
104
104
|
const userVersion = (db.prepare("PRAGMA user_version").get() as { user_version?: number } | undefined)
|
|
105
105
|
?.user_version;
|
|
106
|
-
if (userVersion !== undefined && userVersion <
|
|
106
|
+
if (userVersion !== undefined && userVersion < 4) {
|
|
107
107
|
db.run("DROP TABLE IF EXISTS github_view_cache");
|
|
108
108
|
}
|
|
109
109
|
db.run(`
|
|
@@ -120,7 +120,7 @@ export function openDb(): Database | null {
|
|
|
120
120
|
PRIMARY KEY (auth_key, repo, kind, number, include_comments)
|
|
121
121
|
);
|
|
122
122
|
CREATE INDEX IF NOT EXISTS idx_github_view_cache_fetched ON github_view_cache(fetched_at);
|
|
123
|
-
PRAGMA user_version =
|
|
123
|
+
PRAGMA user_version = 4;
|
|
124
124
|
`);
|
|
125
125
|
protectDbFiles(dbPath);
|
|
126
126
|
cachedDb = db;
|