@oh-my-pi/pi-coding-agent 17.2.2 → 17.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 +14 -0
- package/dist/{CHANGELOG-xfpkakrn.md → CHANGELOG-c5hpqt9r.md} +14 -0
- package/dist/cli.js +2970 -2970
- package/dist/types/modes/components/ask-dialog.d.ts +0 -1
- package/dist/types/tools/browser/launch.d.ts +30 -0
- package/dist/types/tools/browser/registry.d.ts +6 -1
- package/dist/types/tools/browser/shared-daemon.d.ts +26 -0
- package/package.json +12 -12
- package/src/launch/broker.ts +59 -41
- package/src/modes/components/ask-dialog.ts +65 -23
- package/src/session/agent-session.ts +1 -1
- package/src/session/session-maintenance.ts +2 -2
- package/src/tools/bash.ts +11 -5
- package/src/tools/browser/launch.ts +54 -9
- package/src/tools/browser/registry.ts +123 -30
- package/src/tools/browser/shared-daemon.ts +190 -0
- package/src/tools/browser.ts +1 -1
- package/src/web/search/providers/codex.ts +31 -6
|
@@ -25,7 +25,6 @@ interface AskDialogOptions {
|
|
|
25
25
|
}
|
|
26
26
|
export declare class AskDialogComponent implements Component {
|
|
27
27
|
#private;
|
|
28
|
-
private readonly questions;
|
|
29
28
|
private readonly callbacks;
|
|
30
29
|
private readonly options;
|
|
31
30
|
constructor(questions: ExtensionAskDialogQuestion[], callbacks: AskDialogCallbacks, options?: AskDialogOptions);
|
|
@@ -37,7 +37,37 @@ export interface LaunchHeadlessResult {
|
|
|
37
37
|
*/
|
|
38
38
|
userDataDir?: string;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Base Chromium argv shared by process-local puppeteer launches and the
|
|
42
|
+
* broker-owned shared browser: sandbox/stealth flags, window size, and
|
|
43
|
+
* PUPPETEER_PROXY* env-derived proxy flags.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildHeadlessLaunchArgs(viewport: {
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
}): string[];
|
|
40
49
|
export declare function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promise<LaunchHeadlessResult>;
|
|
50
|
+
/** Fully resolved executable and argv for a broker-spawned shared Chromium. */
|
|
51
|
+
export interface SharedBrowserLaunchSpec {
|
|
52
|
+
executablePath: string;
|
|
53
|
+
args: string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the executable and complete argv for a shared Chromium the daemon
|
|
57
|
+
* broker spawns directly (no puppeteer inside the broker). Mirrors
|
|
58
|
+
* `launchHeadlessBrowser` flag assembly — puppeteer's default args minus the
|
|
59
|
+
* stealth-suppressed set — plus `--remote-debugging-port=0` so every client
|
|
60
|
+
* attaches over CDP. Returns null when no Chromium executable resolves;
|
|
61
|
+
* callers fall back to a process-local launch.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveSharedBrowserLaunchSpec(opts: {
|
|
64
|
+
headless: boolean;
|
|
65
|
+
userDataDir: string;
|
|
66
|
+
viewport?: {
|
|
67
|
+
width: number;
|
|
68
|
+
height: number;
|
|
69
|
+
};
|
|
70
|
+
}): Promise<SharedBrowserLaunchSpec | null>;
|
|
41
71
|
/**
|
|
42
72
|
* Remove an OMP-owned headless Chromium profile directory, tolerating the brief
|
|
43
73
|
* window on Windows in which Chromium (or an orphaned browser subprocess) still
|
|
@@ -25,8 +25,13 @@ export interface PuppeteerBrowserHandle extends BrowserHandleCommon {
|
|
|
25
25
|
browser: Browser;
|
|
26
26
|
cdpUrl?: string;
|
|
27
27
|
pid?: number;
|
|
28
|
-
/** OMP-owned temp Chromium profile directory removed on dispose (headless launches). */
|
|
28
|
+
/** OMP-owned temp Chromium profile directory removed on dispose (process-local headless launches). */
|
|
29
29
|
userDataDir?: string;
|
|
30
|
+
/** Broker daemon backing this handle; dispose disconnects instead of closing, kill routes to the broker. */
|
|
31
|
+
sharedDaemon?: {
|
|
32
|
+
name: string;
|
|
33
|
+
projectDir: string;
|
|
34
|
+
};
|
|
30
35
|
subprocess?: Subprocess;
|
|
31
36
|
stealth: {
|
|
32
37
|
browserSession: CDPSession | null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Broker-owned browser endpoint one omp process can attach to. */
|
|
2
|
+
export interface SharedBrowserEndpoint {
|
|
3
|
+
wsEndpoint: string;
|
|
4
|
+
daemonName: string;
|
|
5
|
+
/** Canonical project directory owning the broker (used to address later stop requests). */
|
|
6
|
+
projectDir: string;
|
|
7
|
+
}
|
|
8
|
+
/** Stable broker daemon name for the shared automation browser. */
|
|
9
|
+
export declare function sharedBrowserDaemonName(headless: boolean): string;
|
|
10
|
+
/**
|
|
11
|
+
* Ensure the project-shared automation Chromium is running and reachable,
|
|
12
|
+
* launching it under the daemon broker when needed. Idempotent across
|
|
13
|
+
* processes: losers of the start race adopt the winner's endpoint on the next
|
|
14
|
+
* describe round. Returns null when the shared path is unavailable (no
|
|
15
|
+
* resolvable Chromium, broker failure, or a daemon that never becomes
|
|
16
|
+
* reachable); callers fall back to a process-local launch.
|
|
17
|
+
*/
|
|
18
|
+
export declare function ensureSharedBrowser(opts: {
|
|
19
|
+
projectDir: string;
|
|
20
|
+
headless: boolean;
|
|
21
|
+
viewport?: {
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
};
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}): Promise<SharedBrowserEndpoint | null>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "17.2.
|
|
4
|
+
"version": "17.2.3",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
53
53
|
"@babel/parser": "^7.29.7",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@oh-my-pi/hashline": "17.2.
|
|
56
|
-
"@oh-my-pi/omp-stats": "17.2.
|
|
57
|
-
"@oh-my-pi/pi-agent-core": "17.2.
|
|
58
|
-
"@oh-my-pi/pi-ai": "17.2.
|
|
59
|
-
"@oh-my-pi/pi-catalog": "17.2.
|
|
60
|
-
"@oh-my-pi/pi-mnemopi": "17.2.
|
|
61
|
-
"@oh-my-pi/pi-natives": "17.2.
|
|
62
|
-
"@oh-my-pi/pi-tui": "17.2.
|
|
63
|
-
"@oh-my-pi/pi-utils": "17.2.
|
|
64
|
-
"@oh-my-pi/pi-wire": "17.2.
|
|
65
|
-
"@oh-my-pi/snapcompact": "17.2.
|
|
55
|
+
"@oh-my-pi/hashline": "17.2.3",
|
|
56
|
+
"@oh-my-pi/omp-stats": "17.2.3",
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "17.2.3",
|
|
58
|
+
"@oh-my-pi/pi-ai": "17.2.3",
|
|
59
|
+
"@oh-my-pi/pi-catalog": "17.2.3",
|
|
60
|
+
"@oh-my-pi/pi-mnemopi": "17.2.3",
|
|
61
|
+
"@oh-my-pi/pi-natives": "17.2.3",
|
|
62
|
+
"@oh-my-pi/pi-tui": "17.2.3",
|
|
63
|
+
"@oh-my-pi/pi-utils": "17.2.3",
|
|
64
|
+
"@oh-my-pi/pi-wire": "17.2.3",
|
|
65
|
+
"@oh-my-pi/snapcompact": "17.2.3",
|
|
66
66
|
"@opentelemetry/api": "^1.9.1",
|
|
67
67
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
68
68
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
package/src/launch/broker.ts
CHANGED
|
@@ -342,6 +342,15 @@ class DaemonBroker {
|
|
|
342
342
|
readonly #token: string;
|
|
343
343
|
readonly #idleGraceMs: number;
|
|
344
344
|
readonly #records = new Map<string, ManagedDaemon>();
|
|
345
|
+
/**
|
|
346
|
+
* Names reserved by an in-flight `start` before its record lands in
|
|
347
|
+
* `#records`. Requests dispatch concurrently, and `#start` awaits (cwd stat,
|
|
348
|
+
* log open) between the duplicate check and the record insert; without a
|
|
349
|
+
* synchronous reservation two clients can both pass the check and spawn
|
|
350
|
+
* duplicate processes — one exits on a held resource (e.g. a Chromium
|
|
351
|
+
* profile lock) or keeps running untracked.
|
|
352
|
+
*/
|
|
353
|
+
readonly #startingNames = new Set<string>();
|
|
345
354
|
readonly #clients = new Set<net.Socket>();
|
|
346
355
|
readonly #finished = Promise.withResolvers<void>();
|
|
347
356
|
readonly #sockets = new Set<net.Socket>();
|
|
@@ -500,50 +509,59 @@ class DaemonBroker {
|
|
|
500
509
|
) {
|
|
501
510
|
throw new Error('Windows batch files require application "cmd.exe" with the batch path after "/c"');
|
|
502
511
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (existing && !terminalState(existing.snapshot.state)) {
|
|
506
|
-
throw new Error(`Daemon ${spec.name} is already ${existing.snapshot.state}`);
|
|
512
|
+
if (this.#startingNames.has(spec.name)) {
|
|
513
|
+
throw new Error(`Daemon ${spec.name} is already starting`);
|
|
507
514
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
515
|
+
this.#startingNames.add(spec.name);
|
|
516
|
+
let record: ManagedDaemon;
|
|
517
|
+
try {
|
|
518
|
+
const existing = this.#records.get(spec.name);
|
|
519
|
+
if (existing) await this.#refreshDetached(existing);
|
|
520
|
+
if (existing && !terminalState(existing.snapshot.state)) {
|
|
521
|
+
throw new Error(`Daemon ${spec.name} is already ${existing.snapshot.state}`);
|
|
522
|
+
}
|
|
523
|
+
if (spec.ready?.log) {
|
|
524
|
+
try {
|
|
525
|
+
new RegExp(spec.ready.log, "u");
|
|
526
|
+
} catch (error) {
|
|
527
|
+
throw new Error(`Invalid readiness regex: ${error instanceof Error ? error.message : String(error)}`);
|
|
528
|
+
}
|
|
513
529
|
}
|
|
530
|
+
const stat = await fs.stat(spec.cwd);
|
|
531
|
+
if (!stat.isDirectory()) throw new Error(`Daemon cwd is not a directory: ${spec.cwd}`);
|
|
532
|
+
const dir = path.join(this.#runtimeDir, "daemons", spec.name);
|
|
533
|
+
const now = Date.now();
|
|
534
|
+
record = {
|
|
535
|
+
spec,
|
|
536
|
+
snapshot: {
|
|
537
|
+
name: spec.name,
|
|
538
|
+
id: crypto.randomUUID(),
|
|
539
|
+
state: "starting",
|
|
540
|
+
createdAt: now,
|
|
541
|
+
startedAt: now,
|
|
542
|
+
restartCount: 0,
|
|
543
|
+
outputBytes: 0,
|
|
544
|
+
owner,
|
|
545
|
+
persist: spec.persist,
|
|
546
|
+
detached: spec.detached,
|
|
547
|
+
},
|
|
548
|
+
dir,
|
|
549
|
+
log: await DaemonLog.open(dir),
|
|
550
|
+
generation: 0,
|
|
551
|
+
stopRequested: false,
|
|
552
|
+
logReady: !spec.ready?.log,
|
|
553
|
+
portReady: spec.ready?.port === undefined,
|
|
554
|
+
readinessBuffer: "",
|
|
555
|
+
outputOffset: 0,
|
|
556
|
+
readyPattern: spec.ready?.log ? new RegExp(spec.ready.log, "u") : undefined,
|
|
557
|
+
consecutiveFailures: 0,
|
|
558
|
+
persistQueue: Promise.resolve(),
|
|
559
|
+
};
|
|
560
|
+
syncReadyPending(record);
|
|
561
|
+
this.#records.set(spec.name, record);
|
|
562
|
+
} finally {
|
|
563
|
+
this.#startingNames.delete(spec.name);
|
|
514
564
|
}
|
|
515
|
-
const stat = await fs.stat(spec.cwd);
|
|
516
|
-
if (!stat.isDirectory()) throw new Error(`Daemon cwd is not a directory: ${spec.cwd}`);
|
|
517
|
-
const dir = path.join(this.#runtimeDir, "daemons", spec.name);
|
|
518
|
-
const now = Date.now();
|
|
519
|
-
const record: ManagedDaemon = {
|
|
520
|
-
spec,
|
|
521
|
-
snapshot: {
|
|
522
|
-
name: spec.name,
|
|
523
|
-
id: crypto.randomUUID(),
|
|
524
|
-
state: "starting",
|
|
525
|
-
createdAt: now,
|
|
526
|
-
startedAt: now,
|
|
527
|
-
restartCount: 0,
|
|
528
|
-
outputBytes: 0,
|
|
529
|
-
owner,
|
|
530
|
-
persist: spec.persist,
|
|
531
|
-
detached: spec.detached,
|
|
532
|
-
},
|
|
533
|
-
dir,
|
|
534
|
-
log: await DaemonLog.open(dir),
|
|
535
|
-
generation: 0,
|
|
536
|
-
stopRequested: false,
|
|
537
|
-
logReady: !spec.ready?.log,
|
|
538
|
-
portReady: spec.ready?.port === undefined,
|
|
539
|
-
readinessBuffer: "",
|
|
540
|
-
outputOffset: 0,
|
|
541
|
-
readyPattern: spec.ready?.log ? new RegExp(spec.ready.log, "u") : undefined,
|
|
542
|
-
consecutiveFailures: 0,
|
|
543
|
-
persistQueue: Promise.resolve(),
|
|
544
|
-
};
|
|
545
|
-
syncReadyPending(record);
|
|
546
|
-
this.#records.set(spec.name, record);
|
|
547
565
|
await this.#launch(record);
|
|
548
566
|
let readyTimedOut = false;
|
|
549
567
|
if (spec.ready && !terminalState(record.snapshot.state)) {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
wrapTextWithAnsi,
|
|
16
16
|
} from "@oh-my-pi/pi-tui";
|
|
17
17
|
import type {
|
|
18
|
+
ExtensionAskDialogOption,
|
|
18
19
|
ExtensionAskDialogQuestion,
|
|
19
20
|
ExtensionAskDialogResultItem,
|
|
20
21
|
ExtensionAskDialogSubmitResult,
|
|
@@ -337,6 +338,45 @@ function renderRowLabel(
|
|
|
337
338
|
return lines;
|
|
338
339
|
}
|
|
339
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Coerce untrusted dialog questions into a render-safe shape. The live ask
|
|
343
|
+
* dialog is reached from the public `askDialog` extension surface and from
|
|
344
|
+
* streamed tool args, where a question entry can arrive with a missing or
|
|
345
|
+
* non-string `question` field. The render helpers (`replaceTabs`,
|
|
346
|
+
* `renderQuestionTitle`, `questionTabLabel`) assume strings, so a malformed
|
|
347
|
+
* entry throws and takes down the whole TUI render loop. Mirrors
|
|
348
|
+
* `normalizeRenderQuestions` on the transcript path.
|
|
349
|
+
*/
|
|
350
|
+
function normalizeDialogQuestions(questions: ExtensionAskDialogQuestion[]): ExtensionAskDialogQuestion[] {
|
|
351
|
+
if (!Array.isArray(questions)) return [];
|
|
352
|
+
const out: ExtensionAskDialogQuestion[] = [];
|
|
353
|
+
for (const entry of questions) {
|
|
354
|
+
if (!entry || typeof entry !== "object") continue;
|
|
355
|
+
const q = entry as Partial<ExtensionAskDialogQuestion>;
|
|
356
|
+
const options: ExtensionAskDialogOption[] = [];
|
|
357
|
+
if (Array.isArray(q.options)) {
|
|
358
|
+
for (const opt of q.options) {
|
|
359
|
+
if (!opt || typeof opt !== "object") continue;
|
|
360
|
+
const o = opt as Partial<ExtensionAskDialogOption>;
|
|
361
|
+
options.push({
|
|
362
|
+
label: typeof o.label === "string" ? o.label : "",
|
|
363
|
+
...(typeof o.description === "string" ? { description: o.description } : {}),
|
|
364
|
+
...(typeof o.preview === "string" ? { preview: o.preview } : {}),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
out.push({
|
|
369
|
+
id: typeof q.id === "string" ? q.id : "?",
|
|
370
|
+
question: typeof q.question === "string" ? q.question : "",
|
|
371
|
+
...(typeof q.header === "string" ? { header: q.header } : {}),
|
|
372
|
+
options,
|
|
373
|
+
...(typeof q.multi === "boolean" ? { multi: q.multi } : {}),
|
|
374
|
+
...(Number.isInteger(q.recommended) ? { recommended: q.recommended } : {}),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
340
380
|
export class AskDialogComponent implements Component {
|
|
341
381
|
#states: QuestionState[];
|
|
342
382
|
#activeTabIndex = 0;
|
|
@@ -352,13 +392,15 @@ export class AskDialogComponent implements Component {
|
|
|
352
392
|
#stableHeight: { key: string; total: number } | undefined;
|
|
353
393
|
#previewCache: PreviewRenderCache = new Map();
|
|
354
394
|
#overflowLayouts = new WeakMap<ExtensionAskDialogQuestion, Set<string>>();
|
|
395
|
+
readonly #questions: ExtensionAskDialogQuestion[];
|
|
355
396
|
|
|
356
397
|
constructor(
|
|
357
|
-
|
|
398
|
+
questions: ExtensionAskDialogQuestion[],
|
|
358
399
|
private readonly callbacks: AskDialogCallbacks,
|
|
359
400
|
private readonly options: AskDialogOptions = {},
|
|
360
401
|
) {
|
|
361
|
-
this.#
|
|
402
|
+
this.#questions = normalizeDialogQuestions(questions);
|
|
403
|
+
this.#states = this.#questions.map(question => {
|
|
362
404
|
const recommended = Number.isInteger(question.recommended) ? question.recommended : 0;
|
|
363
405
|
const maxIndex = Math.max(0, question.options.length - 1);
|
|
364
406
|
return {
|
|
@@ -474,8 +516,8 @@ export class AskDialogComponent implements Component {
|
|
|
474
516
|
const tabBarRows = this.#hasSubmitTab() ? 1 : 0;
|
|
475
517
|
const mdTheme = getMarkdownTheme();
|
|
476
518
|
let needed = MIN_DIALOG_ROWS;
|
|
477
|
-
for (let index = 0; index < this
|
|
478
|
-
const question = this
|
|
519
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
520
|
+
const question = this.#questions[index];
|
|
479
521
|
const state = this.#states[index];
|
|
480
522
|
if (!question || !state) continue;
|
|
481
523
|
const headerRows = tabBarRows + renderQuestionTitle(question, width).length;
|
|
@@ -493,7 +535,7 @@ export class AskDialogComponent implements Component {
|
|
|
493
535
|
if (this.#hasSubmitTab()) {
|
|
494
536
|
// Warning line + blank, one summary line per question, blank, and
|
|
495
537
|
// the Submit row; note lines added later scroll within the body.
|
|
496
|
-
const body = 2 + this
|
|
538
|
+
const body = 2 + this.#questions.length + 2;
|
|
497
539
|
needed = Math.max(needed, chrome + tabBarRows + 1 + Math.max(MIN_BODY_ROWS, body));
|
|
498
540
|
}
|
|
499
541
|
return Math.min(needed, maxHeight);
|
|
@@ -507,11 +549,11 @@ export class AskDialogComponent implements Component {
|
|
|
507
549
|
// Multi questions confirm on the Submit tab (Enter toggles, never
|
|
508
550
|
// submits), so any multi question forces the tab even when there is
|
|
509
551
|
// only one question.
|
|
510
|
-
return this
|
|
552
|
+
return this.#questions.length > 1 || this.#questions.some(question => question.multi);
|
|
511
553
|
}
|
|
512
554
|
|
|
513
555
|
#submitTabIndex(): number {
|
|
514
|
-
return this
|
|
556
|
+
return this.#questions.length;
|
|
515
557
|
}
|
|
516
558
|
|
|
517
559
|
#isSubmitTab(): boolean {
|
|
@@ -519,7 +561,7 @@ export class AskDialogComponent implements Component {
|
|
|
519
561
|
}
|
|
520
562
|
|
|
521
563
|
#currentQuestionIndex(): number {
|
|
522
|
-
return clamp(this.#activeTabIndex, 0, Math.max(0, this
|
|
564
|
+
return clamp(this.#activeTabIndex, 0, Math.max(0, this.#questions.length - 1));
|
|
523
565
|
}
|
|
524
566
|
|
|
525
567
|
#requestRender(): void {
|
|
@@ -530,7 +572,7 @@ export class AskDialogComponent implements Component {
|
|
|
530
572
|
const lines: string[] = [];
|
|
531
573
|
if (this.#hasSubmitTab()) {
|
|
532
574
|
const tabs: Tab[] = [
|
|
533
|
-
...this
|
|
575
|
+
...this.#questions.map((question, index) => ({
|
|
534
576
|
id: String(index),
|
|
535
577
|
label: questionTabLabel(question, index),
|
|
536
578
|
})),
|
|
@@ -545,7 +587,7 @@ export class AskDialogComponent implements Component {
|
|
|
545
587
|
return lines;
|
|
546
588
|
}
|
|
547
589
|
const questionIndex = this.#currentQuestionIndex();
|
|
548
|
-
const question = this
|
|
590
|
+
const question = this.#questions[questionIndex];
|
|
549
591
|
if (!question) return lines;
|
|
550
592
|
lines.push(...renderQuestionTitle(question, width));
|
|
551
593
|
return lines;
|
|
@@ -559,7 +601,7 @@ export class AskDialogComponent implements Component {
|
|
|
559
601
|
const scroll = indicator ? ` ${indicator} scroll ·` : "";
|
|
560
602
|
return `Enter submit · ↑/↓ scroll ·${scroll} ${cancel}`;
|
|
561
603
|
}
|
|
562
|
-
const question = this
|
|
604
|
+
const question = this.#questions[this.#currentQuestionIndex()];
|
|
563
605
|
const action = question?.multi ? "Space/Enter toggle · n note" : "Enter select · n note";
|
|
564
606
|
const tabs = this.#hasSubmitTab() ? " · Tab/←/→" : "";
|
|
565
607
|
if (this.#questionCanPage && indicator) {
|
|
@@ -585,7 +627,7 @@ export class AskDialogComponent implements Component {
|
|
|
585
627
|
}
|
|
586
628
|
|
|
587
629
|
#activeQuestionState(): { question: ExtensionAskDialogQuestion; state: QuestionState } | undefined {
|
|
588
|
-
const question = this
|
|
630
|
+
const question = this.#questions[this.#currentQuestionIndex()];
|
|
589
631
|
const state = this.#states[this.#currentQuestionIndex()];
|
|
590
632
|
if (!question || !state) return undefined;
|
|
591
633
|
return { question, state };
|
|
@@ -672,18 +714,18 @@ export class AskDialogComponent implements Component {
|
|
|
672
714
|
}
|
|
673
715
|
|
|
674
716
|
#switchTab(direction: 1 | -1): void {
|
|
675
|
-
const tabCount = this
|
|
717
|
+
const tabCount = this.#questions.length + 1;
|
|
676
718
|
this.#activeTabIndex = (this.#activeTabIndex + direction + tabCount) % tabCount;
|
|
677
719
|
this.#submitScrollOffset = 0;
|
|
678
720
|
}
|
|
679
721
|
|
|
680
722
|
#advanceAfterQuestion(): void {
|
|
681
723
|
const current = this.#currentQuestionIndex();
|
|
682
|
-
if (this
|
|
724
|
+
if (this.#questions.length === 1) {
|
|
683
725
|
this.#finishSubmit();
|
|
684
726
|
return;
|
|
685
727
|
}
|
|
686
|
-
this.#activeTabIndex = current + 1 < this
|
|
728
|
+
this.#activeTabIndex = current + 1 < this.#questions.length ? current + 1 : this.#submitTabIndex();
|
|
687
729
|
this.#submitScrollOffset = 0;
|
|
688
730
|
this.#requestRender();
|
|
689
731
|
}
|
|
@@ -829,8 +871,8 @@ export class AskDialogComponent implements Component {
|
|
|
829
871
|
);
|
|
830
872
|
allLines.push("");
|
|
831
873
|
}
|
|
832
|
-
for (let index = 0; index < this
|
|
833
|
-
const question = this
|
|
874
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
875
|
+
const question = this.#questions[index];
|
|
834
876
|
const state = this.#states[index];
|
|
835
877
|
if (!question || !state) continue;
|
|
836
878
|
const label = questionTabLabel(question, index);
|
|
@@ -895,8 +937,8 @@ export class AskDialogComponent implements Component {
|
|
|
895
937
|
|
|
896
938
|
#unansweredCount(): number {
|
|
897
939
|
let count = 0;
|
|
898
|
-
for (let index = 0; index < this
|
|
899
|
-
const question = this
|
|
940
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
941
|
+
const question = this.#questions[index];
|
|
900
942
|
const state = this.#states[index];
|
|
901
943
|
if (!question || !state) continue;
|
|
902
944
|
if (state.selectedOptions.size === 0 && state.customInput === undefined) count += 1;
|
|
@@ -911,8 +953,8 @@ export class AskDialogComponent implements Component {
|
|
|
911
953
|
return;
|
|
912
954
|
}
|
|
913
955
|
this.options.onTimeout?.();
|
|
914
|
-
for (let index = 0; index < this
|
|
915
|
-
const question = this
|
|
956
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
957
|
+
const question = this.#questions[index];
|
|
916
958
|
const state = this.#states[index];
|
|
917
959
|
if (!question || !state) continue;
|
|
918
960
|
if (state.selectedOptions.size === 0 && state.customInput === undefined) {
|
|
@@ -952,8 +994,8 @@ export class AskDialogComponent implements Component {
|
|
|
952
994
|
|
|
953
995
|
#buildResults(): ExtensionAskDialogResultItem[] {
|
|
954
996
|
const results: ExtensionAskDialogResultItem[] = [];
|
|
955
|
-
for (let index = 0; index < this
|
|
956
|
-
const question = this
|
|
997
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
998
|
+
const question = this.#questions[index];
|
|
957
999
|
const state = this.#states[index];
|
|
958
1000
|
if (!question || !state) continue;
|
|
959
1001
|
const selectedOptions = question.options
|
|
@@ -7018,7 +7018,7 @@ export class AgentSession {
|
|
|
7018
7018
|
// shared provider state map is still required so Codex can allocate
|
|
7019
7019
|
// websocket state under that side-channel session id.
|
|
7020
7020
|
sessionId: `${cacheSessionId}:side:${Snowflake.next()}`,
|
|
7021
|
-
promptCacheKey:
|
|
7021
|
+
promptCacheKey: this.agent.promptCacheKey ?? this.agent.sessionId,
|
|
7022
7022
|
preferWebsockets: this.#preferWebsockets,
|
|
7023
7023
|
providerSessionState: this.#providerSessionState,
|
|
7024
7024
|
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
@@ -1539,7 +1539,7 @@ export class SessionMaintenance {
|
|
|
1539
1539
|
thinkingLevel: this.#host.thinkingLevel(),
|
|
1540
1540
|
tools: this.#host.agent.state.tools,
|
|
1541
1541
|
sessionId: this.#host.sessionId(),
|
|
1542
|
-
promptCacheKey: this.#host.sessionId
|
|
1542
|
+
promptCacheKey: this.#host.agent.promptCacheKey ?? this.#host.agent.sessionId,
|
|
1543
1543
|
providerSessionState: this.#host.providerSessionState,
|
|
1544
1544
|
// Route every summarization HTTP request through the
|
|
1545
1545
|
// session's side-stream transport so the provider
|
|
@@ -2585,7 +2585,7 @@ export class SessionMaintenance {
|
|
|
2585
2585
|
thinkingLevel: this.#host.thinkingLevel(),
|
|
2586
2586
|
tools: this.#host.agent.state.tools,
|
|
2587
2587
|
sessionId: this.#host.sessionId(),
|
|
2588
|
-
promptCacheKey: this.#host.sessionId
|
|
2588
|
+
promptCacheKey: this.#host.agent.promptCacheKey ?? this.#host.agent.sessionId,
|
|
2589
2589
|
providerSessionState: this.#host.providerSessionState,
|
|
2590
2590
|
codexCompaction,
|
|
2591
2591
|
},
|
package/src/tools/bash.ts
CHANGED
|
@@ -843,13 +843,18 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
843
843
|
return { kind: "steer" };
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
+
// Cancellable threshold: a bare Bun.sleep(thresholdMs) leaves a live, ref'd
|
|
847
|
+
// timer for the full threshold after the command finishes (or abort/steer)
|
|
848
|
+
// wins the race first — delaying SDK/headless shutdown and accumulating
|
|
849
|
+
// timers under fast command rates. Settle a withResolvers promise from
|
|
850
|
+
// setTimeout so the finally can clear it regardless of which waiter wins.
|
|
851
|
+
const { promise: thresholdPromise, resolve: resolveThreshold } = Promise.withResolvers<{
|
|
852
|
+
kind: "running";
|
|
853
|
+
}>();
|
|
854
|
+
const thresholdTimer = setTimeout(() => resolveThreshold({ kind: "running" }), thresholdMs);
|
|
846
855
|
const waiters: Array<
|
|
847
856
|
Promise<ManagedBashJobCompletion | { kind: "running" } | { kind: "steer" } | { kind: "aborted" }>
|
|
848
|
-
> = [job.completion,
|
|
849
|
-
|
|
850
|
-
if (!signal && !steeringSignal) {
|
|
851
|
-
return await Promise.race(waiters);
|
|
852
|
-
}
|
|
857
|
+
> = [job.completion, thresholdPromise];
|
|
853
858
|
|
|
854
859
|
const { promise: abortedPromise, resolve: resolveAborted } = Promise.withResolvers<{ kind: "aborted" }>();
|
|
855
860
|
const onAbort = () => resolveAborted({ kind: "aborted" });
|
|
@@ -866,6 +871,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
866
871
|
try {
|
|
867
872
|
return await Promise.race(waiters);
|
|
868
873
|
} finally {
|
|
874
|
+
clearTimeout(thresholdTimer);
|
|
869
875
|
signal?.removeEventListener("abort", onAbort);
|
|
870
876
|
steeringSignal?.removeEventListener("abort", onSteer);
|
|
871
877
|
}
|
|
@@ -296,19 +296,17 @@ export interface LaunchHeadlessResult {
|
|
|
296
296
|
userDataDir?: string;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
};
|
|
306
|
-
const puppeteer = await loadPuppeteer();
|
|
299
|
+
/**
|
|
300
|
+
* Base Chromium argv shared by process-local puppeteer launches and the
|
|
301
|
+
* broker-owned shared browser: sandbox/stealth flags, window size, and
|
|
302
|
+
* PUPPETEER_PROXY* env-derived proxy flags.
|
|
303
|
+
*/
|
|
304
|
+
export function buildHeadlessLaunchArgs(viewport: { width: number; height: number }): string[] {
|
|
307
305
|
const launchArgs = [
|
|
308
306
|
"--no-sandbox",
|
|
309
307
|
"--disable-setuid-sandbox",
|
|
310
308
|
"--disable-blink-features=AutomationControlled",
|
|
311
|
-
`--window-size=${
|
|
309
|
+
`--window-size=${viewport.width},${viewport.height}`,
|
|
312
310
|
];
|
|
313
311
|
const proxy = process.env.PUPPETEER_PROXY;
|
|
314
312
|
if (proxy) {
|
|
@@ -324,6 +322,18 @@ export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promis
|
|
|
324
322
|
if (ignoreCert === "true" || ignoreCert === "1" || ignoreCert === "yes" || ignoreCert === "on") {
|
|
325
323
|
launchArgs.push("--ignore-certificate-errors");
|
|
326
324
|
}
|
|
325
|
+
return launchArgs;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promise<LaunchHeadlessResult> {
|
|
329
|
+
const vp = opts.viewport ?? DEFAULT_VIEWPORT;
|
|
330
|
+
const initialViewport = {
|
|
331
|
+
width: vp.width,
|
|
332
|
+
height: vp.height,
|
|
333
|
+
deviceScaleFactor: vp.deviceScaleFactor ?? DEFAULT_VIEWPORT.deviceScaleFactor,
|
|
334
|
+
};
|
|
335
|
+
const puppeteer = await loadPuppeteer();
|
|
336
|
+
const launchArgs = buildHeadlessLaunchArgs(initialViewport);
|
|
327
337
|
for (const arg of opts.args ?? []) {
|
|
328
338
|
if (!launchArgs.includes(arg)) launchArgs.push(arg);
|
|
329
339
|
}
|
|
@@ -357,6 +367,41 @@ export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promis
|
|
|
357
367
|
}
|
|
358
368
|
}
|
|
359
369
|
|
|
370
|
+
/** Fully resolved executable and argv for a broker-spawned shared Chromium. */
|
|
371
|
+
export interface SharedBrowserLaunchSpec {
|
|
372
|
+
executablePath: string;
|
|
373
|
+
args: string[];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Resolve the executable and complete argv for a shared Chromium the daemon
|
|
378
|
+
* broker spawns directly (no puppeteer inside the broker). Mirrors
|
|
379
|
+
* `launchHeadlessBrowser` flag assembly — puppeteer's default args minus the
|
|
380
|
+
* stealth-suppressed set — plus `--remote-debugging-port=0` so every client
|
|
381
|
+
* attaches over CDP. Returns null when no Chromium executable resolves;
|
|
382
|
+
* callers fall back to a process-local launch.
|
|
383
|
+
*/
|
|
384
|
+
export async function resolveSharedBrowserLaunchSpec(opts: {
|
|
385
|
+
headless: boolean;
|
|
386
|
+
userDataDir: string;
|
|
387
|
+
viewport?: { width: number; height: number };
|
|
388
|
+
}): Promise<SharedBrowserLaunchSpec | null> {
|
|
389
|
+
const executablePath = await ensureChromiumExecutable();
|
|
390
|
+
if (!executablePath) return null;
|
|
391
|
+
const puppeteer = await loadPuppeteer();
|
|
392
|
+
const vp = opts.viewport ?? DEFAULT_VIEWPORT;
|
|
393
|
+
const ignored = new Set(stealthIgnoreDefaultArgs(executablePath));
|
|
394
|
+
const defaults = await puppeteer.defaultArgs({
|
|
395
|
+
headless: opts.headless,
|
|
396
|
+
args: buildHeadlessLaunchArgs(vp),
|
|
397
|
+
userDataDir: opts.userDataDir,
|
|
398
|
+
});
|
|
399
|
+
return {
|
|
400
|
+
executablePath,
|
|
401
|
+
args: [...defaults.filter(arg => !ignored.has(arg)), "--remote-debugging-port=0"],
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
360
405
|
/**
|
|
361
406
|
* Remove an OMP-owned headless Chromium profile directory, tolerating the brief
|
|
362
407
|
* window on Windows in which Chromium (or an orphaned browser subprocess) still
|