@oh-my-pi/pi-coding-agent 15.2.1 → 15.2.3
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 +34 -1
- package/dist/types/cli/worktree-cli.d.ts +26 -0
- package/dist/types/commands/worktree.d.ts +34 -0
- package/dist/types/config/settings-schema.d.ts +23 -0
- package/dist/types/modes/theme/shimmer.d.ts +15 -7
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/yield-queue.d.ts +24 -0
- package/dist/types/slash-commands/helpers/format.d.ts +1 -1
- package/dist/types/task/worktree.d.ts +0 -1
- package/dist/types/tools/browser/launch.d.ts +2 -0
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +7 -7
- package/src/autoresearch/storage.ts +14 -2
- package/src/cli/worktree-cli.ts +291 -0
- package/src/cli.ts +1 -0
- package/src/commands/worktree.ts +56 -0
- package/src/config/settings-schema.ts +16 -0
- package/src/discovery/builtin.ts +30 -0
- package/src/modes/components/mcp-add-wizard.ts +4 -3
- package/src/modes/components/settings-selector.ts +23 -10
- package/src/modes/components/welcome.ts +77 -35
- package/src/modes/controllers/mcp-command-controller.ts +4 -3
- package/src/modes/theme/shimmer.ts +161 -30
- package/src/modes/utils/ui-helpers.ts +31 -13
- package/src/prompts/tools/async-result.md +5 -2
- package/src/sdk.ts +95 -21
- package/src/session/agent-session.ts +28 -0
- package/src/session/yield-queue.ts +155 -0
- package/src/slash-commands/helpers/format.ts +4 -1
- package/src/task/worktree.ts +2 -7
- package/src/tools/browser/launch.ts +63 -51
- package/src/tools/gh.ts +35 -32
- package/src/utils/git.ts +4 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<system-notice>
|
|
2
|
-
|
|
2
|
+
{{#if multiple}}{{jobs.length}} background jobs have completed. Resume your work using the results below.
|
|
3
3
|
|
|
4
|
-
{{
|
|
4
|
+
{{else}}Background job {{jobs.[0].jobId}} has completed. Resume your work using the result below.
|
|
5
|
+
{{/if}}{{#each jobs}}{{#if @root.multiple}}── Job {{this.jobId}}{{#if this.label}} ({{this.label}}){{/if}} ──
|
|
6
|
+
{{/if}}{{this.result}}{{#unless @last}}
|
|
7
|
+
{{/unless}}{{/each}}
|
|
5
8
|
</system-notice>
|
package/src/sdk.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
Snowflake,
|
|
32
32
|
} from "@oh-my-pi/pi-utils";
|
|
33
33
|
import chalk from "chalk";
|
|
34
|
-
import { AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
|
|
34
|
+
import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
|
|
35
35
|
import { createAutoresearchExtension } from "./autoresearch";
|
|
36
36
|
import { loadCapability } from "./capability";
|
|
37
37
|
import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
|
|
@@ -101,7 +101,7 @@ import {
|
|
|
101
101
|
import { AgentSession } from "./session/agent-session";
|
|
102
102
|
import { resolveAuthBrokerConfig } from "./session/auth-broker-config";
|
|
103
103
|
import { AuthBrokerClient, AuthStorage, RemoteAuthCredentialStore } from "./session/auth-storage";
|
|
104
|
-
import { convertToLlm } from "./session/messages";
|
|
104
|
+
import { type CustomMessage, convertToLlm } from "./session/messages";
|
|
105
105
|
import { SessionManager } from "./session/session-manager";
|
|
106
106
|
import { closeAllConnections } from "./ssh/connection-manager";
|
|
107
107
|
import { unmountAll } from "./ssh/sshfs-mount";
|
|
@@ -152,6 +152,83 @@ import { EventBus } from "./utils/event-bus";
|
|
|
152
152
|
import { buildNamedToolChoice } from "./utils/tool-choice";
|
|
153
153
|
import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
|
154
154
|
|
|
155
|
+
type AsyncResultEntry = {
|
|
156
|
+
jobId: string;
|
|
157
|
+
result: string;
|
|
158
|
+
job: AsyncJob | undefined;
|
|
159
|
+
durationMs: number | undefined;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
type AsyncResultJobDetails = {
|
|
163
|
+
jobId: string;
|
|
164
|
+
type?: "bash" | "task";
|
|
165
|
+
label?: string;
|
|
166
|
+
durationMs?: number;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
type AsyncResultDetails = {
|
|
170
|
+
jobs: AsyncResultJobDetails[];
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
type McpNotificationEntry = {
|
|
174
|
+
serverName: string;
|
|
175
|
+
uri: string;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
function buildAsyncResultBatchMessage(entries: AsyncResultEntry[]): CustomMessage<AsyncResultDetails> | null {
|
|
179
|
+
if (entries.length === 0) return null;
|
|
180
|
+
const jobs = entries.map(entry => ({
|
|
181
|
+
jobId: entry.jobId,
|
|
182
|
+
result: entry.result,
|
|
183
|
+
type: entry.job?.type,
|
|
184
|
+
label: entry.job?.label,
|
|
185
|
+
durationMs: entry.durationMs,
|
|
186
|
+
}));
|
|
187
|
+
const details: AsyncResultDetails = {
|
|
188
|
+
jobs: jobs.map(job => ({
|
|
189
|
+
jobId: job.jobId,
|
|
190
|
+
type: job.type,
|
|
191
|
+
label: job.label,
|
|
192
|
+
durationMs: job.durationMs,
|
|
193
|
+
})),
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
role: "custom",
|
|
197
|
+
customType: "async-result",
|
|
198
|
+
content: prompt.render(asyncResultTemplate, {
|
|
199
|
+
multiple: jobs.length > 1,
|
|
200
|
+
jobs,
|
|
201
|
+
}),
|
|
202
|
+
display: true,
|
|
203
|
+
attribution: "agent",
|
|
204
|
+
details,
|
|
205
|
+
timestamp: Date.now(),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function buildMcpNotificationBatchMessage(entries: McpNotificationEntry[]): AgentMessage | null {
|
|
210
|
+
const resources: McpNotificationEntry[] = [];
|
|
211
|
+
const seen = new Set<string>();
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
const key = `${entry.serverName}\0${entry.uri}`;
|
|
214
|
+
if (seen.has(key)) continue;
|
|
215
|
+
seen.add(key);
|
|
216
|
+
resources.push(entry);
|
|
217
|
+
}
|
|
218
|
+
if (resources.length === 0) return null;
|
|
219
|
+
const lines = [`[MCP notification] ${resources.length} resource(s) updated:`];
|
|
220
|
+
for (const resource of resources) {
|
|
221
|
+
lines.push(`- server="${resource.serverName}" uri=${resource.uri}`);
|
|
222
|
+
}
|
|
223
|
+
lines.push('Use read(path="mcp://<uri>") to inspect if relevant.');
|
|
224
|
+
return {
|
|
225
|
+
role: "user",
|
|
226
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
227
|
+
attribution: "agent",
|
|
228
|
+
timestamp: Date.now(),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
155
232
|
// Types
|
|
156
233
|
export interface CreateAgentSessionOptions {
|
|
157
234
|
/** Working directory for project-local discovery. Default: getProjectDir() */
|
|
@@ -1035,23 +1112,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1035
1112
|
const formattedResult = await formatAsyncResultForFollowUp(result);
|
|
1036
1113
|
if (asyncJobManager!.isDeliverySuppressed(jobId)) return;
|
|
1037
1114
|
|
|
1038
|
-
const message = prompt.render(asyncResultTemplate, { jobId, result: formattedResult });
|
|
1039
1115
|
const durationMs = job ? Math.max(0, Date.now() - job.startTime) : undefined;
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
details: {
|
|
1047
|
-
jobId,
|
|
1048
|
-
type: job?.type,
|
|
1049
|
-
label: job?.label,
|
|
1050
|
-
durationMs,
|
|
1051
|
-
},
|
|
1052
|
-
},
|
|
1053
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
1054
|
-
);
|
|
1116
|
+
session.yieldQueue.enqueue<AsyncResultEntry>("async-result", {
|
|
1117
|
+
jobId,
|
|
1118
|
+
result: formattedResult,
|
|
1119
|
+
job,
|
|
1120
|
+
durationMs,
|
|
1121
|
+
});
|
|
1055
1122
|
},
|
|
1056
1123
|
})
|
|
1057
1124
|
: undefined;
|
|
@@ -1902,6 +1969,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1902
1969
|
providerSessionId: options.providerSessionId,
|
|
1903
1970
|
});
|
|
1904
1971
|
hasSession = true;
|
|
1972
|
+
if (asyncJobManager) {
|
|
1973
|
+
session.yieldQueue.register<AsyncResultEntry>("async-result", {
|
|
1974
|
+
isStale: entry => asyncJobManager.isDeliverySuppressed(entry.jobId),
|
|
1975
|
+
build: buildAsyncResultBatchMessage,
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
session.yieldQueue.register<McpNotificationEntry>("mcp-notification", {
|
|
1979
|
+
build: buildMcpNotificationBatchMessage,
|
|
1980
|
+
});
|
|
1905
1981
|
|
|
1906
1982
|
// Attach the live session to the pre-registered ref so peers can route IRC
|
|
1907
1983
|
// messages here. Refresh sessionFile in case it was unavailable at pre-register
|
|
@@ -2036,9 +2112,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2036
2112
|
notificationDebounceTimers.delete(key);
|
|
2037
2113
|
// Re-check: user may have disabled notifications during the debounce window
|
|
2038
2114
|
if (!settings.get("mcp.notifications")) return;
|
|
2039
|
-
|
|
2040
|
-
`[MCP notification] Server "${serverName}" reports resource \`${uri}\` was updated. Use read(path="mcp://${uri}") to inspect if relevant.`,
|
|
2041
|
-
);
|
|
2115
|
+
session.yieldQueue.enqueue<McpNotificationEntry>("mcp-notification", { serverName, uri });
|
|
2042
2116
|
}, debounceMs),
|
|
2043
2117
|
);
|
|
2044
2118
|
});
|
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
} from "@oh-my-pi/pi-ai";
|
|
77
77
|
import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
78
78
|
import {
|
|
79
|
+
extractRetryHint,
|
|
79
80
|
getAgentDbPath,
|
|
80
81
|
isEnoent,
|
|
81
82
|
isUnexpectedSocketCloseMessage,
|
|
@@ -204,6 +205,7 @@ import type {
|
|
|
204
205
|
} from "./session-manager";
|
|
205
206
|
import { getLatestCompactionEntry } from "./session-manager";
|
|
206
207
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
208
|
+
import { YieldQueue } from "./yield-queue";
|
|
207
209
|
|
|
208
210
|
/** Session-specific events that extend the core AgentEvent */
|
|
209
211
|
export type AgentSessionEvent =
|
|
@@ -734,6 +736,7 @@ export class AgentSession {
|
|
|
734
736
|
readonly agent: Agent;
|
|
735
737
|
readonly sessionManager: SessionManager;
|
|
736
738
|
readonly settings: Settings;
|
|
739
|
+
readonly yieldQueue: YieldQueue;
|
|
737
740
|
|
|
738
741
|
#powerAssertion: MacOSPowerAssertion | undefined;
|
|
739
742
|
|
|
@@ -1030,6 +1033,24 @@ export class AgentSession {
|
|
|
1030
1033
|
};
|
|
1031
1034
|
this.agent.setProviderResponseInterceptor(this.#onResponse);
|
|
1032
1035
|
this.agent.setRawSseEventInterceptor(this.#onSseEvent);
|
|
1036
|
+
this.yieldQueue = new YieldQueue({
|
|
1037
|
+
isStreaming: () => this.isStreaming,
|
|
1038
|
+
injectStreaming: message => this.agent.followUp(message),
|
|
1039
|
+
injectIdle: async messages => {
|
|
1040
|
+
const first = messages[0];
|
|
1041
|
+
if (!first) return;
|
|
1042
|
+
await this.agent.prompt(messages.length === 1 ? first : messages);
|
|
1043
|
+
},
|
|
1044
|
+
scheduleIdleFlush: run => {
|
|
1045
|
+
this.#schedulePostPromptTask(
|
|
1046
|
+
async () => {
|
|
1047
|
+
await run();
|
|
1048
|
+
},
|
|
1049
|
+
{ delayMs: 1 },
|
|
1050
|
+
);
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
1053
|
+
this.agent.setOnBeforeYield(() => this.yieldQueue.flush("streaming"));
|
|
1033
1054
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
1034
1055
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
1035
1056
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
@@ -2719,6 +2740,8 @@ export class AgentSession {
|
|
|
2719
2740
|
async dispose(): Promise<void> {
|
|
2720
2741
|
this.#isDisposed = true;
|
|
2721
2742
|
this.#pendingBackgroundExchanges = [];
|
|
2743
|
+
this.yieldQueue.clear();
|
|
2744
|
+
this.agent.setOnBeforeYield(undefined);
|
|
2722
2745
|
this.#evalExecutionDisposing = true;
|
|
2723
2746
|
try {
|
|
2724
2747
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
@@ -6949,6 +6972,11 @@ export class AgentSession {
|
|
|
6949
6972
|
}
|
|
6950
6973
|
}
|
|
6951
6974
|
|
|
6975
|
+
const retryHintMs = extractRetryHint(undefined, errorMessage);
|
|
6976
|
+
if (retryHintMs !== undefined) {
|
|
6977
|
+
return retryHintMs;
|
|
6978
|
+
}
|
|
6979
|
+
|
|
6952
6980
|
const resetMsMatch = /x-ratelimit-reset-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
|
|
6953
6981
|
if (resetMsMatch) {
|
|
6954
6982
|
const resetMs = Number(resetMsMatch[1]);
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
|
|
4
|
+
export interface YieldDispatcher<P> {
|
|
5
|
+
/** Drop entries already delivered through another path. Called per-entry at flush time. */
|
|
6
|
+
isStale?(entry: P): boolean;
|
|
7
|
+
/** Produce one batched AgentMessage from non-stale entries. Return null to skip. */
|
|
8
|
+
build(survivors: P[]): AgentMessage | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface YieldQueueOptions {
|
|
12
|
+
isStreaming: () => boolean;
|
|
13
|
+
injectStreaming(msg: AgentMessage): void;
|
|
14
|
+
injectIdle(messages: AgentMessage[]): Promise<void>;
|
|
15
|
+
scheduleIdleFlush(run: () => Promise<void>): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type YieldFlushMode = "streaming" | "idle";
|
|
19
|
+
|
|
20
|
+
interface StoredDispatcher {
|
|
21
|
+
isStale?: (entry: unknown) => boolean;
|
|
22
|
+
build: (survivors: unknown[]) => AgentMessage | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatError(error: unknown): string {
|
|
26
|
+
return error instanceof Error ? error.message : String(error);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class YieldQueue {
|
|
30
|
+
readonly #options: YieldQueueOptions;
|
|
31
|
+
readonly #dispatchers = new Map<string, StoredDispatcher>();
|
|
32
|
+
readonly #entries = new Map<string, unknown[]>();
|
|
33
|
+
#idleFlushPending = false;
|
|
34
|
+
|
|
35
|
+
constructor(options: YieldQueueOptions) {
|
|
36
|
+
this.#options = options;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
register<P>(kind: string, dispatcher: YieldDispatcher<P>): () => void {
|
|
40
|
+
const stored: StoredDispatcher = {
|
|
41
|
+
...(dispatcher.isStale ? { isStale: entry => dispatcher.isStale?.(entry as P) ?? false } : {}),
|
|
42
|
+
build: survivors => dispatcher.build(survivors as P[]),
|
|
43
|
+
};
|
|
44
|
+
this.#dispatchers.set(kind, stored);
|
|
45
|
+
return () => {
|
|
46
|
+
if (this.#dispatchers.get(kind) !== stored) return;
|
|
47
|
+
this.#dispatchers.delete(kind);
|
|
48
|
+
this.#entries.delete(kind);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
enqueue<P>(kind: string, entry: P): void {
|
|
53
|
+
if (!this.#dispatchers.has(kind)) {
|
|
54
|
+
logger.warn("Yield queue entry ignored for unregistered kind", { kind });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let entries = this.#entries.get(kind);
|
|
58
|
+
if (!entries) {
|
|
59
|
+
entries = [];
|
|
60
|
+
this.#entries.set(kind, entries);
|
|
61
|
+
}
|
|
62
|
+
entries.push(entry);
|
|
63
|
+
if (!this.#options.isStreaming()) {
|
|
64
|
+
this.#scheduleIdleFlush();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
has(kind?: string): boolean {
|
|
69
|
+
if (kind !== undefined) return (this.#entries.get(kind)?.length ?? 0) > 0;
|
|
70
|
+
for (const entries of this.#entries.values()) {
|
|
71
|
+
if (entries.length > 0) return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async flush(mode: YieldFlushMode): Promise<void> {
|
|
77
|
+
if (mode === "idle") {
|
|
78
|
+
this.#idleFlushPending = false;
|
|
79
|
+
}
|
|
80
|
+
const idleMessages: AgentMessage[] = [];
|
|
81
|
+
for (const [kind, dispatcher] of this.#dispatchers) {
|
|
82
|
+
const entries = this.#drain(kind);
|
|
83
|
+
if (entries.length === 0) continue;
|
|
84
|
+
const message = this.#build(kind, dispatcher, entries);
|
|
85
|
+
if (!message) continue;
|
|
86
|
+
if (mode === "streaming") {
|
|
87
|
+
try {
|
|
88
|
+
this.#options.injectStreaming(message);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.warn("Yield queue streaming dispatch failed", { kind, error: formatError(error) });
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
idleMessages.push(message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (mode === "idle" && idleMessages.length > 0) {
|
|
97
|
+
try {
|
|
98
|
+
await this.#options.injectIdle(idleMessages);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logger.warn("Yield queue idle dispatch failed", { error: formatError(error) });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
clear(): void {
|
|
106
|
+
this.#entries.clear();
|
|
107
|
+
this.#idleFlushPending = false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#scheduleIdleFlush(): void {
|
|
111
|
+
if (this.#idleFlushPending) return;
|
|
112
|
+
this.#idleFlushPending = true;
|
|
113
|
+
try {
|
|
114
|
+
this.#options.scheduleIdleFlush(async () => {
|
|
115
|
+
this.#idleFlushPending = false;
|
|
116
|
+
if (this.#options.isStreaming()) return;
|
|
117
|
+
await this.flush("idle");
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.#idleFlushPending = false;
|
|
121
|
+
logger.warn("Yield queue idle flush scheduling failed", { error: formatError(error) });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#drain(kind: string): unknown[] {
|
|
126
|
+
const entries = this.#entries.get(kind);
|
|
127
|
+
if (!entries || entries.length === 0) return [];
|
|
128
|
+
this.#entries.delete(kind);
|
|
129
|
+
return entries;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#build(kind: string, dispatcher: StoredDispatcher, entries: unknown[]): AgentMessage | null {
|
|
133
|
+
const survivors: unknown[] = [];
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
if (dispatcher.isStale) {
|
|
136
|
+
let stale: boolean;
|
|
137
|
+
try {
|
|
138
|
+
stale = dispatcher.isStale(entry);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
logger.warn("Yield queue stale check failed", { kind, error: formatError(error) });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (stale) continue;
|
|
144
|
+
}
|
|
145
|
+
survivors.push(entry);
|
|
146
|
+
}
|
|
147
|
+
if (survivors.length === 0) return null;
|
|
148
|
+
try {
|
|
149
|
+
return dispatcher.build(survivors);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
logger.warn("Yield queue build failed", { kind, error: formatError(error) });
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -13,7 +13,7 @@ export function formatDuration(ms: number): string {
|
|
|
13
13
|
return `${days}d`;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
type ProgressBarTheme = Pick<Theme, "bold" | "fg">;
|
|
16
|
+
type ProgressBarTheme = Pick<Theme, "bold" | "fg" | "getFgAnsi">;
|
|
17
17
|
|
|
18
18
|
const unstyledProgressBarTheme: ProgressBarTheme = {
|
|
19
19
|
fg(_color, text) {
|
|
@@ -22,6 +22,9 @@ const unstyledProgressBarTheme: ProgressBarTheme = {
|
|
|
22
22
|
bold(text) {
|
|
23
23
|
return text;
|
|
24
24
|
},
|
|
25
|
+
getFgAnsi() {
|
|
26
|
+
return "";
|
|
27
|
+
},
|
|
25
28
|
};
|
|
26
29
|
|
|
27
30
|
function resolveProgressBarTheme(uiTheme: ProgressBarTheme | undefined): ProgressBarTheme {
|
package/src/task/worktree.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import * as natives from "@oh-my-pi/pi-natives";
|
|
6
|
-
import { getWorktreeDir, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { getWorktreeDir, hashPath, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import * as git from "../utils/git";
|
|
8
8
|
|
|
9
9
|
const { IsoBackendKind } = natives;
|
|
@@ -26,10 +26,6 @@ export interface WorktreeBaseline {
|
|
|
26
26
|
nested: Array<{ relativePath: string; baseline: RepoBaseline }>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export function getEncodedProjectName(cwd: string): string {
|
|
30
|
-
return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
29
|
export async function getRepoRoot(cwd: string): Promise<string> {
|
|
34
30
|
const repoRoot = await git.repo.root(cwd);
|
|
35
31
|
if (!repoRoot) {
|
|
@@ -316,8 +312,7 @@ export async function ensureIsolation(
|
|
|
316
312
|
preferred?: IsoBackendKind,
|
|
317
313
|
): Promise<IsolationHandle> {
|
|
318
314
|
const repoRoot = await getRepoRoot(baseCwd);
|
|
319
|
-
const
|
|
320
|
-
const baseDir = getWorktreeDir(encodedProject, id);
|
|
315
|
+
const baseDir = getWorktreeDir(`${id}-${hashPath(repoRoot)}`);
|
|
321
316
|
const mergedDir = path.join(baseDir, "merged");
|
|
322
317
|
|
|
323
318
|
const resolution = natives.isoResolve(preferred ?? null);
|
|
@@ -540,24 +540,24 @@ async function withSoftTimeout<T>(promise: Promise<T>, timeoutMs: number, label:
|
|
|
540
540
|
}
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
];
|
|
543
|
+
const STEALTH_PATCH_SCRIPTS = [
|
|
544
|
+
stealthTamperingScript,
|
|
545
|
+
stealthActivityScript,
|
|
546
|
+
stealthHairlineScript,
|
|
547
|
+
stealthBotdScript,
|
|
548
|
+
stealthIframeScript,
|
|
549
|
+
stealthWebglScript,
|
|
550
|
+
stealthScreenScript,
|
|
551
|
+
stealthFontsScript,
|
|
552
|
+
stealthAudioScript,
|
|
553
|
+
stealthLocaleScript,
|
|
554
|
+
stealthPluginsScript,
|
|
555
|
+
stealthHardwareScript,
|
|
556
|
+
stealthCodecsScript,
|
|
557
|
+
stealthWorkerScript,
|
|
558
|
+
];
|
|
560
559
|
|
|
560
|
+
function buildStealthInjectionScript(scripts: readonly string[] = STEALTH_PATCH_SCRIPTS): string {
|
|
561
561
|
const joint = scripts
|
|
562
562
|
.map(
|
|
563
563
|
script => `
|
|
@@ -568,43 +568,55 @@ async function injectStealthScripts(page: Page): Promise<void> {
|
|
|
568
568
|
)
|
|
569
569
|
.join(";\n");
|
|
570
570
|
|
|
571
|
-
|
|
571
|
+
return `(() => {
|
|
572
572
|
// Native function cache - captured before any tampering
|
|
573
573
|
const iframe = document.createElement("iframe");
|
|
574
574
|
iframe.style.display = "none";
|
|
575
|
-
document.head.
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
575
|
+
const container = document.head ?? document.documentElement;
|
|
576
|
+
if (!container) return;
|
|
577
|
+
container.appendChild(iframe);
|
|
578
|
+
try {
|
|
579
|
+
const nativeWindow = iframe.contentWindow;
|
|
580
|
+
if (!nativeWindow) return;
|
|
581
|
+
|
|
582
|
+
// Cache pristine native functions
|
|
583
|
+
const Function_toString = nativeWindow.Function.prototype.toString;
|
|
584
|
+
const Object_getOwnPropertyDescriptor = nativeWindow.Object.getOwnPropertyDescriptor;
|
|
585
|
+
const Object_getOwnPropertyDescriptors = nativeWindow.Object.getOwnPropertyDescriptors;
|
|
586
|
+
const Object_getPrototypeOf = nativeWindow.Object.getPrototypeOf;
|
|
587
|
+
const Object_defineProperty = nativeWindow.Object.defineProperty;
|
|
588
|
+
const Object_getOwnPropertyDescriptorOriginal = nativeWindow.Object.getOwnPropertyDescriptor;
|
|
589
|
+
const Object_create = nativeWindow.Object.create;
|
|
590
|
+
const Object_keys = nativeWindow.Object.keys;
|
|
591
|
+
const Object_getOwnPropertyNames = nativeWindow.Object.getOwnPropertyNames;
|
|
592
|
+
const Object_entries = nativeWindow.Object.entries;
|
|
593
|
+
const Object_setPrototypeOf = nativeWindow.Object.setPrototypeOf;
|
|
594
|
+
const Object_assign = nativeWindow.Object.assign;
|
|
595
|
+
const Window_setTimeout = nativeWindow.setTimeout;
|
|
596
|
+
const Math_random = nativeWindow.Math.random;
|
|
597
|
+
const Math_floor = nativeWindow.Math.floor;
|
|
598
|
+
const Math_max = nativeWindow.Math.max;
|
|
599
|
+
const Math_min = nativeWindow.Math.min;
|
|
600
|
+
const Window_Event = nativeWindow.Event;
|
|
601
|
+
const Promise_resolve = nativeWindow.Promise.resolve.bind(nativeWindow.Promise);
|
|
602
|
+
const Window_Blob = nativeWindow.Blob;
|
|
603
|
+
const Window_Proxy = nativeWindow.Proxy;
|
|
604
|
+
const Intl_DateTimeFormat = nativeWindow.Intl.DateTimeFormat;
|
|
605
|
+
const Date_constructor = nativeWindow.Date;
|
|
606
|
+
|
|
607
|
+
${joint}
|
|
608
|
+
} finally {
|
|
609
|
+
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
|
|
610
|
+
}})();`;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function injectStealthScripts(page: Page): Promise<void> {
|
|
614
|
+
await page.evaluateOnNewDocument(buildStealthInjectionScript());
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Builds the browser-page stealth bootstrap source for regression tests. */
|
|
618
|
+
export function buildStealthInjectionScriptForTest(scripts: readonly string[] = STEALTH_PATCH_SCRIPTS): string {
|
|
619
|
+
return buildStealthInjectionScript(scripts);
|
|
608
620
|
}
|
|
609
621
|
|
|
610
622
|
/** Apply stealth patches + UA override to a headless page. Idempotent within a tab. */
|