@oh-my-pi/pi-coding-agent 16.4.6 → 16.4.8
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 +25 -0
- package/dist/cli.js +2754 -2754
- package/dist/types/modes/components/model-browser.d.ts +11 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/package.json +12 -12
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/modes/components/model-browser.ts +42 -29
- package/src/modes/components/model-hub.ts +32 -26
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/interactive-mode.ts +13 -0
- package/src/web/search/providers/perplexity.ts +18 -2
|
@@ -78,6 +78,8 @@ export declare class ModelBrowser implements Component {
|
|
|
78
78
|
setPerfStats(perf: ReadonlyMap<string, ModelPerfStats>): void;
|
|
79
79
|
setMaxVisible(rows: number): void;
|
|
80
80
|
setShowProvider(show: boolean): void;
|
|
81
|
+
/** Focused: accent cursor + selected-row background band. Unfocused: dim cursor, no band. */
|
|
82
|
+
setFocused(focused: boolean): void;
|
|
81
83
|
/** Total rendered height for the current `maxVisible` (host layout budgeting). */
|
|
82
84
|
get renderedRows(): number;
|
|
83
85
|
get query(): string;
|
|
@@ -86,7 +88,13 @@ export declare class ModelBrowser implements Component {
|
|
|
86
88
|
get visibleCount(): number;
|
|
87
89
|
/** Move selection to `selector`; false when it is not in the current view. */
|
|
88
90
|
selectSelector(selector: string): boolean;
|
|
89
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Move the selection by `delta` rows, skipping disabled rows. Single steps
|
|
93
|
+
* wrap at the ends; `wrap: false` (page/home/end jumps) clamps instead.
|
|
94
|
+
*/
|
|
95
|
+
moveSelection(delta: number, options?: {
|
|
96
|
+
wrap?: boolean;
|
|
97
|
+
}): void;
|
|
90
98
|
handleInput(data: string): void;
|
|
91
99
|
/** Cancel-key ladder: clear a non-empty query first, then bubble to the host. */
|
|
92
100
|
handleCancel(): void;
|
|
@@ -95,6 +103,8 @@ export declare class ModelBrowser implements Component {
|
|
|
95
103
|
* row (the search row).
|
|
96
104
|
*/
|
|
97
105
|
routeMouse(event: SgrMouseEvent, line: number): void;
|
|
106
|
+
/** Drop the hover band. Hosts call this when the pointer leaves the browser pane. */
|
|
107
|
+
clearHover(): void;
|
|
98
108
|
render(width: number): string[];
|
|
99
109
|
invalidate(): void;
|
|
100
110
|
}
|
|
@@ -23,6 +23,8 @@ export interface PlanReviewOverlayCallbacks {
|
|
|
23
23
|
onPick: (label: string) => void;
|
|
24
24
|
/** Invoked on Esc / cancel. */
|
|
25
25
|
onCancel: () => void;
|
|
26
|
+
/** Invoked with the current full plan text when the copy hotkey is pressed. */
|
|
27
|
+
onCopyPlan?: (content: string) => void | Promise<void>;
|
|
26
28
|
/** Invoked when the external-editor key is pressed (overlay stays open). */
|
|
27
29
|
onExternalEditor?: () => void;
|
|
28
30
|
/** Invoked when the external-editor key edits the active annotation draft. */
|
|
@@ -9,6 +9,10 @@ export declare const WELCOME_SESSION_SLOTS = 4;
|
|
|
9
9
|
* the box height is constant regardless of how many servers a project has.
|
|
10
10
|
*/
|
|
11
11
|
export declare const WELCOME_LSP_SLOTS = 4;
|
|
12
|
+
/** Pick a tip from `tips`, biased toward "[NEW]" tips by {@link NEW_TIP_WEIGHT};
|
|
13
|
+
* `r` is a uniform sample in [0, 1). Returns "" when `tips` is empty.
|
|
14
|
+
* Exported for tests. */
|
|
15
|
+
export declare function pickWeightedTip(tips: readonly string[], r: number): string;
|
|
12
16
|
export declare function renderWelcomeTip(tip: string, boxWidth: number, phase?: number): string[];
|
|
13
17
|
export interface RecentSession {
|
|
14
18
|
name: string;
|
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": "16.4.
|
|
4
|
+
"version": "16.4.8",
|
|
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": "0.25.0",
|
|
53
53
|
"@babel/parser": "^7.29.7",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@oh-my-pi/hashline": "16.4.
|
|
56
|
-
"@oh-my-pi/omp-stats": "16.4.
|
|
57
|
-
"@oh-my-pi/pi-agent-core": "16.4.
|
|
58
|
-
"@oh-my-pi/pi-ai": "16.4.
|
|
59
|
-
"@oh-my-pi/pi-catalog": "16.4.
|
|
60
|
-
"@oh-my-pi/pi-mnemopi": "16.4.
|
|
61
|
-
"@oh-my-pi/pi-natives": "16.4.
|
|
62
|
-
"@oh-my-pi/pi-tui": "16.4.
|
|
63
|
-
"@oh-my-pi/pi-utils": "16.4.
|
|
64
|
-
"@oh-my-pi/pi-wire": "16.4.
|
|
65
|
-
"@oh-my-pi/snapcompact": "16.4.
|
|
55
|
+
"@oh-my-pi/hashline": "16.4.8",
|
|
56
|
+
"@oh-my-pi/omp-stats": "16.4.8",
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "16.4.8",
|
|
58
|
+
"@oh-my-pi/pi-ai": "16.4.8",
|
|
59
|
+
"@oh-my-pi/pi-catalog": "16.4.8",
|
|
60
|
+
"@oh-my-pi/pi-mnemopi": "16.4.8",
|
|
61
|
+
"@oh-my-pi/pi-natives": "16.4.8",
|
|
62
|
+
"@oh-my-pi/pi-tui": "16.4.8",
|
|
63
|
+
"@oh-my-pi/pi-utils": "16.4.8",
|
|
64
|
+
"@oh-my-pi/pi-wire": "16.4.8",
|
|
65
|
+
"@oh-my-pi/snapcompact": "16.4.8",
|
|
66
66
|
"@opentelemetry/api": "^1.9.1",
|
|
67
67
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
68
68
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -48,6 +48,15 @@ type BabelClassDeclaration = {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
type BabelLexicalDecl = BabelVariableDeclaration | BabelClassDeclaration;
|
|
51
|
+
type BabelFunctionDeclaration = {
|
|
52
|
+
type: "FunctionDeclaration";
|
|
53
|
+
start: number;
|
|
54
|
+
end: number;
|
|
55
|
+
id: { start: number; end: number; name: string } | null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Top-level declarations whose bindings must survive the cell (demoted and/or published). */
|
|
59
|
+
type BabelPublishableDecl = BabelLexicalDecl | BabelFunctionDeclaration;
|
|
51
60
|
|
|
52
61
|
type BabelExpressionStatement = {
|
|
53
62
|
type: "ExpressionStatement";
|
|
@@ -322,7 +331,7 @@ function collectBindingNames(pattern: unknown, names: string[]): void {
|
|
|
322
331
|
}
|
|
323
332
|
}
|
|
324
333
|
|
|
325
|
-
function getLexicalBindingNames(node:
|
|
334
|
+
function getLexicalBindingNames(node: BabelPublishableDecl): string[] {
|
|
326
335
|
const names: string[] = [];
|
|
327
336
|
if (node.type === "VariableDeclaration") {
|
|
328
337
|
for (const declaration of node.declarations ?? []) collectBindingNames(declaration.id, names);
|
|
@@ -348,40 +357,49 @@ function appendGlobalBindingPublish(source: string, names: readonly string[]): s
|
|
|
348
357
|
* let { a, b } = obj; -> var { a, b } = obj;
|
|
349
358
|
* class Foo extends Bar {} -> var Foo = class extends Bar {};
|
|
350
359
|
*
|
|
351
|
-
* When the source must run inside the async wrapper, demoted `var`s
|
|
352
|
-
*
|
|
353
|
-
*
|
|
360
|
+
* When the source must run inside the async wrapper (top-level `await`), demoted `var`s —
|
|
361
|
+
* and the user's own top-level `var` and `function` declarations — would be scoped to the
|
|
362
|
+
* wrapper function and die with the cell. In that mode we publish every top-level binding
|
|
363
|
+
* back to the wrapper's lexical `this`, which is the worker global object.
|
|
354
364
|
*
|
|
355
|
-
* Nested declarations (inside functions, blocks, classes) are left alone
|
|
365
|
+
* Nested declarations (inside functions, blocks, classes) are left alone — they're
|
|
356
366
|
* scoped to their enclosing function/block regardless of `var` vs `let`/`const`.
|
|
357
367
|
*/
|
|
358
368
|
async function demoteTopLevelLexicals(code: string, options: { publishGlobals?: boolean } = {}): Promise<string> {
|
|
359
|
-
|
|
369
|
+
const publishGlobals = options.publishGlobals === true;
|
|
370
|
+
const fastPath = publishGlobals ? /\b(?:const|let|class|var|function)\b/ : /\b(?:const|let|class)\b/;
|
|
371
|
+
if (!fastPath.test(code)) return code;
|
|
360
372
|
|
|
361
373
|
const ast = await parseProgram(code);
|
|
362
374
|
if (!ast) {
|
|
363
375
|
return code;
|
|
364
376
|
}
|
|
365
377
|
|
|
366
|
-
const targets:
|
|
378
|
+
const targets: Array<{ node: BabelPublishableDecl; demote: boolean }> = [];
|
|
367
379
|
for (const node of ast.program.body) {
|
|
368
380
|
if (node.type === "VariableDeclaration") {
|
|
369
381
|
const decl = node as unknown as BabelVariableDeclaration;
|
|
370
|
-
if (decl.kind === "const" || decl.kind === "let") targets.push(decl);
|
|
382
|
+
if (decl.kind === "const" || decl.kind === "let") targets.push({ node: decl, demote: true });
|
|
383
|
+
else if (publishGlobals) targets.push({ node: decl, demote: false });
|
|
371
384
|
} else if (node.type === "ClassDeclaration") {
|
|
372
385
|
const decl = node as unknown as BabelClassDeclaration;
|
|
373
|
-
if (decl.id) targets.push(decl);
|
|
386
|
+
if (decl.id) targets.push({ node: decl, demote: true });
|
|
387
|
+
} else if (publishGlobals && node.type === "FunctionDeclaration") {
|
|
388
|
+
const decl = node as unknown as BabelFunctionDeclaration;
|
|
389
|
+
if (decl.id) targets.push({ node: decl, demote: false });
|
|
374
390
|
}
|
|
375
391
|
}
|
|
376
392
|
if (targets.length === 0) return code;
|
|
377
393
|
|
|
378
|
-
targets.sort((a, b) => b.start - a.start);
|
|
394
|
+
targets.sort((a, b) => b.node.start - a.node.start);
|
|
379
395
|
let result = code;
|
|
380
|
-
for (const node of targets) {
|
|
396
|
+
for (const { node, demote } of targets) {
|
|
381
397
|
const segment = result.slice(node.start, node.end);
|
|
382
|
-
const bindingNames =
|
|
398
|
+
const bindingNames = publishGlobals ? getLexicalBindingNames(node) : [];
|
|
383
399
|
let replacement: string;
|
|
384
|
-
if (
|
|
400
|
+
if (!demote) {
|
|
401
|
+
replacement = segment;
|
|
402
|
+
} else if (node.type === "VariableDeclaration") {
|
|
385
403
|
replacement = `var${segment.slice(node.kind.length)}`;
|
|
386
404
|
} else {
|
|
387
405
|
const id = node.id;
|
|
@@ -293,6 +293,8 @@ export class ModelBrowser implements Component {
|
|
|
293
293
|
/** First visible list row; panned by the wheel, snapped to the selection on keyboard navigation. */
|
|
294
294
|
#windowStart = 0;
|
|
295
295
|
#windowCount = 0;
|
|
296
|
+
/** Whether the host pane owns arrow keys; drives cursor strength and the selected-row band. */
|
|
297
|
+
#focused = true;
|
|
296
298
|
|
|
297
299
|
/** Enter or click-on-selected. */
|
|
298
300
|
onActivate?: (item: ModelBrowserItem) => void;
|
|
@@ -345,6 +347,10 @@ export class ModelBrowser implements Component {
|
|
|
345
347
|
setShowProvider(show: boolean): void {
|
|
346
348
|
this.#showProvider = show;
|
|
347
349
|
}
|
|
350
|
+
/** Focused: accent cursor + selected-row background band. Unfocused: dim cursor, no band. */
|
|
351
|
+
setFocused(focused: boolean): void {
|
|
352
|
+
this.#focused = focused;
|
|
353
|
+
}
|
|
348
354
|
|
|
349
355
|
/** Total rendered height for the current `maxVisible` (host layout budgeting). */
|
|
350
356
|
get renderedRows(): number {
|
|
@@ -416,18 +422,27 @@ export class ModelBrowser implements Component {
|
|
|
416
422
|
this.#windowStart = this.#clampWindowStart(this.#windowStart);
|
|
417
423
|
}
|
|
418
424
|
|
|
419
|
-
|
|
425
|
+
/**
|
|
426
|
+
* Move the selection by `delta` rows, skipping disabled rows. Single steps
|
|
427
|
+
* wrap at the ends; `wrap: false` (page/home/end jumps) clamps instead.
|
|
428
|
+
*/
|
|
429
|
+
moveSelection(delta: number, options: { wrap?: boolean } = {}): void {
|
|
420
430
|
const count = this.#visibleItems.length;
|
|
421
431
|
if (count === 0) return;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
this.#
|
|
428
|
-
|
|
432
|
+
if (options.wrap ?? true) {
|
|
433
|
+
let index = this.#selectedIndex;
|
|
434
|
+
for (let step = 0; step < count; step++) {
|
|
435
|
+
index = (index + delta + count) % count;
|
|
436
|
+
const item = this.#visibleItems[index];
|
|
437
|
+
if (item && !this.#isDisabled(item)) {
|
|
438
|
+
this.#setSelectedIndex(index);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
429
441
|
}
|
|
442
|
+
return;
|
|
430
443
|
}
|
|
444
|
+
const target = Math.max(0, Math.min(this.#selectedIndex + delta, count - 1));
|
|
445
|
+
this.#setSelectedIndex(this.#coerceSelectedIndex(target));
|
|
431
446
|
}
|
|
432
447
|
|
|
433
448
|
#setSelectedIndex(index: number): void {
|
|
@@ -514,11 +529,19 @@ export class ModelBrowser implements Component {
|
|
|
514
529
|
return;
|
|
515
530
|
}
|
|
516
531
|
if (matchesSelectPageUp(data)) {
|
|
517
|
-
this.moveSelection(-this.#maxVisible);
|
|
532
|
+
this.moveSelection(-this.#maxVisible, { wrap: false });
|
|
518
533
|
return;
|
|
519
534
|
}
|
|
520
535
|
if (matchesSelectPageDown(data)) {
|
|
521
|
-
this.moveSelection(this.#maxVisible);
|
|
536
|
+
this.moveSelection(this.#maxVisible, { wrap: false });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (matchesKey(data, "home")) {
|
|
540
|
+
this.moveSelection(-this.#visibleItems.length, { wrap: false });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (matchesKey(data, "end")) {
|
|
544
|
+
this.moveSelection(this.#visibleItems.length, { wrap: false });
|
|
522
545
|
return;
|
|
523
546
|
}
|
|
524
547
|
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
@@ -574,6 +597,10 @@ export class ModelBrowser implements Component {
|
|
|
574
597
|
this.#setSelectedIndex(index);
|
|
575
598
|
}
|
|
576
599
|
}
|
|
600
|
+
/** Drop the hover band. Hosts call this when the pointer leaves the browser pane. */
|
|
601
|
+
clearHover(): void {
|
|
602
|
+
this.#hoveredIndex = null;
|
|
603
|
+
}
|
|
577
604
|
|
|
578
605
|
/** List index under a frame-local row, or null when off-list or on a disabled row. */
|
|
579
606
|
#hoverIndexAt(line: number): number | null {
|
|
@@ -585,22 +612,6 @@ export class ModelBrowser implements Component {
|
|
|
585
612
|
return index;
|
|
586
613
|
}
|
|
587
614
|
|
|
588
|
-
#chipsFor(model: Model): string {
|
|
589
|
-
const parts: string[] = [];
|
|
590
|
-
const seen = new Set<string>();
|
|
591
|
-
const pushChip = (role: string) => {
|
|
592
|
-
if (seen.has(role)) return;
|
|
593
|
-
seen.add(role);
|
|
594
|
-
const assignment = this.#roles[role];
|
|
595
|
-
if (!assignment || !modelsAreEqual(assignment.model, model)) return;
|
|
596
|
-
if (getRoleInfo(role, this.#settings).hidden) return;
|
|
597
|
-
parts.push(formatRoleChip(role, assignment, this.#settings));
|
|
598
|
-
};
|
|
599
|
-
for (const role of MODEL_ROLE_IDS) pushChip(role);
|
|
600
|
-
for (const role in this.#roles) pushChip(role);
|
|
601
|
-
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
602
|
-
}
|
|
603
|
-
|
|
604
615
|
/** `0.9s 118t/s` measured-perf cell for the row's meta block; empty when unmeasured or the column is off. */
|
|
605
616
|
#perfCell(item: ModelBrowserItem, mode: PerfMode): string {
|
|
606
617
|
if (mode === "off") return "";
|
|
@@ -627,13 +638,13 @@ export class ModelBrowser implements Component {
|
|
|
627
638
|
return ` ${line} `;
|
|
628
639
|
}
|
|
629
640
|
const disabled = this.#isDisabled(item);
|
|
630
|
-
const prefix = selected ? `${theme.fg("accent", theme.nav.cursor)} ` : " ";
|
|
641
|
+
const prefix = selected && this.#focused ? `${theme.fg("accent", theme.nav.cursor)} ` : " ";
|
|
631
642
|
const providerPrefix = this.#showProvider ? theme.fg("dim", `${item.provider}/`) : "";
|
|
632
643
|
const name = selected ? theme.fg("accent", item.id) : item.id;
|
|
633
644
|
const overLimit = disabled
|
|
634
645
|
? ` ${theme.status.disabled} context>${formatNumber(item.model.contextWindow ?? 0).toLowerCase()}`
|
|
635
646
|
: "";
|
|
636
|
-
let left = `${prefix}${providerPrefix}${name}${
|
|
647
|
+
let left = `${prefix}${providerPrefix}${name}${overLimit}`;
|
|
637
648
|
|
|
638
649
|
// Perf column collapses entirely when no visible row has measurements.
|
|
639
650
|
const perfCol =
|
|
@@ -648,7 +659,9 @@ export class ModelBrowser implements Component {
|
|
|
648
659
|
if (disabled) {
|
|
649
660
|
line = theme.fg("dim", Bun.stripANSI(line));
|
|
650
661
|
}
|
|
651
|
-
|
|
662
|
+
// The bg band is reserved for the mouse: it marks hover, nothing else.
|
|
663
|
+
// Keyboard selection is the cursor glyph + accent name.
|
|
664
|
+
if (hovered && !disabled) {
|
|
652
665
|
line = theme.bg("selectedBg", line);
|
|
653
666
|
}
|
|
654
667
|
return line;
|
|
@@ -461,7 +461,7 @@ export class ModelHubComponent implements Component {
|
|
|
461
461
|
label: providerId,
|
|
462
462
|
providerId,
|
|
463
463
|
locked: isLocked,
|
|
464
|
-
annotation: isLocked ?
|
|
464
|
+
annotation: isLocked ? undefined : String(availableCounts.get(providerId) ?? 0),
|
|
465
465
|
oauth: oauthIds.has(providerId),
|
|
466
466
|
catalogCount: catalogCounts.get(providerId) ?? 0,
|
|
467
467
|
});
|
|
@@ -1468,6 +1468,10 @@ export class ModelHubComponent implements Component {
|
|
|
1468
1468
|
this.#roleHover = null;
|
|
1469
1469
|
if (overBody && this.#isBrowserView(entry)) {
|
|
1470
1470
|
this.#browser.routeMouse(event, bodyLine);
|
|
1471
|
+
} else {
|
|
1472
|
+
// Pointer left the browser pane: without this, the last
|
|
1473
|
+
// hovered row keeps its band while the sidebar hovers too.
|
|
1474
|
+
this.#browser.clearHover();
|
|
1471
1475
|
}
|
|
1472
1476
|
}
|
|
1473
1477
|
return true;
|
|
@@ -1578,11 +1582,10 @@ export class ModelHubComponent implements Component {
|
|
|
1578
1582
|
// While searching, entries the hop skips gray out: locked and
|
|
1579
1583
|
// zero-match providers, an empty Recent, and the Roles view.
|
|
1580
1584
|
const muted = entry.locked || matchCount === 0 || (searching && entry.kind === "roles");
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
: " ";
|
|
1585
|
+
// The sidebar's active entry is state, not a cursor: accent label
|
|
1586
|
+
// plus a cursor glyph while the sidebar owns the arrows. The band
|
|
1587
|
+
// stays in the body pane so the two never look alike.
|
|
1588
|
+
const cursor = active && this.#focus === "scope" ? theme.fg("accent", theme.nav.cursor) : " ";
|
|
1586
1589
|
|
|
1587
1590
|
let icon: string;
|
|
1588
1591
|
if (entry.kind === "recent") {
|
|
@@ -1597,7 +1600,7 @@ export class ModelHubComponent implements Component {
|
|
|
1597
1600
|
const labelStyled = muted
|
|
1598
1601
|
? theme.fg("dim", entry.label)
|
|
1599
1602
|
: active
|
|
1600
|
-
? theme.fg("accent", entry.label)
|
|
1603
|
+
? theme.bold(theme.fg("accent", entry.label))
|
|
1601
1604
|
: entry.label;
|
|
1602
1605
|
|
|
1603
1606
|
const refreshing = entry.providerId ? this.#refreshingProviders.has(entry.providerId) : false;
|
|
@@ -1614,8 +1617,10 @@ export class ModelHubComponent implements Component {
|
|
|
1614
1617
|
line = `${left}${" ".repeat(width - leftWidth - annWidth)}${annotationStyled}`;
|
|
1615
1618
|
} else {
|
|
1616
1619
|
line = truncateToWidth(left, width);
|
|
1620
|
+
const lineWidth = visibleWidth(line);
|
|
1621
|
+
if (lineWidth < width) line += " ".repeat(width - lineWidth);
|
|
1617
1622
|
}
|
|
1618
|
-
if (hovered
|
|
1623
|
+
if (hovered) {
|
|
1619
1624
|
line = theme.bg("selectedBg", line);
|
|
1620
1625
|
}
|
|
1621
1626
|
lines.push(line);
|
|
@@ -1675,6 +1680,17 @@ export class ModelHubComponent implements Component {
|
|
|
1675
1680
|
return truncateToWidth(theme.fg("muted", ` ${text}`), width);
|
|
1676
1681
|
}
|
|
1677
1682
|
|
|
1683
|
+
/** Clamp a roles row to `width`; the bg band is reserved for mouse hover. */
|
|
1684
|
+
#finishRolesRow(line: string, width: number, hovered: boolean): string {
|
|
1685
|
+
let out = truncateToWidth(line, width);
|
|
1686
|
+
if (hovered) {
|
|
1687
|
+
const w = visibleWidth(out);
|
|
1688
|
+
if (w < width) out += " ".repeat(width - w);
|
|
1689
|
+
return theme.bg("selectedBg", out);
|
|
1690
|
+
}
|
|
1691
|
+
return out;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1678
1694
|
#renderRolesView(width: number, rows: number): string[] {
|
|
1679
1695
|
const lines: string[] = [];
|
|
1680
1696
|
lines.push("");
|
|
@@ -1691,12 +1707,14 @@ export class ModelHubComponent implements Component {
|
|
|
1691
1707
|
}
|
|
1692
1708
|
|
|
1693
1709
|
const cycleOrder = this.#cycleOrder();
|
|
1710
|
+
const listFocused = this.#focus === "list";
|
|
1694
1711
|
for (let i = 0; i < this.#rolesRows.length && lines.length < rows - 2; i++) {
|
|
1695
1712
|
const rowDef = this.#rolesRows[i];
|
|
1696
1713
|
if (!rowDef) continue;
|
|
1697
1714
|
const selected = i === this.#roleIndex;
|
|
1698
1715
|
const hovered = i === this.#roleHover;
|
|
1699
|
-
|
|
1716
|
+
// The unfocused pane draws no cursor; accent text still marks the row.
|
|
1717
|
+
const cursor = selected && listFocused ? theme.fg("accent", theme.nav.cursor) : " ";
|
|
1700
1718
|
|
|
1701
1719
|
if (rowDef.kind === "separator") {
|
|
1702
1720
|
lines.push(` ${theme.fg("border", "─".repeat(Math.max(1, width - 6)))}`);
|
|
@@ -1706,10 +1724,7 @@ export class ModelHubComponent implements Component {
|
|
|
1706
1724
|
if (rowDef.kind === "newRole" || rowDef.kind === "newFallback") {
|
|
1707
1725
|
const label = rowDef.kind === "newRole" ? "+ New role…" : "+ New fallback…";
|
|
1708
1726
|
let line = ` ${cursor} ${theme.fg(selected ? "accent" : "dim", label)}`;
|
|
1709
|
-
line =
|
|
1710
|
-
if (hovered && !selected) {
|
|
1711
|
-
line = theme.bg("selectedBg", line);
|
|
1712
|
-
}
|
|
1727
|
+
line = this.#finishRolesRow(line, width, hovered);
|
|
1713
1728
|
lines.push(line);
|
|
1714
1729
|
continue;
|
|
1715
1730
|
}
|
|
@@ -1720,10 +1735,7 @@ export class ModelHubComponent implements Component {
|
|
|
1720
1735
|
const tail = key.slice(slash + 1);
|
|
1721
1736
|
const keyStyled = theme.fg("dim", key.slice(0, slash + 1)) + (selected ? theme.fg("accent", tail) : tail);
|
|
1722
1737
|
let line = ` ${cursor} ${theme.fg("dim", theme.status.shadowed)} ${keyStyled}`;
|
|
1723
|
-
line =
|
|
1724
|
-
if (hovered && !selected) {
|
|
1725
|
-
line = theme.bg("selectedBg", line);
|
|
1726
|
-
}
|
|
1738
|
+
line = this.#finishRolesRow(line, width, hovered);
|
|
1727
1739
|
lines.push(line);
|
|
1728
1740
|
continue;
|
|
1729
1741
|
}
|
|
@@ -1732,10 +1744,7 @@ export class ModelHubComponent implements Component {
|
|
|
1732
1744
|
const branch = theme.fg("dim", `${"".padEnd(tagWidth + 3)}↳`);
|
|
1733
1745
|
const selector = selected ? theme.fg("accent", rowDef.selector) : theme.fg("muted", rowDef.selector);
|
|
1734
1746
|
let line = ` ${cursor} ${branch} ${selector}`;
|
|
1735
|
-
line =
|
|
1736
|
-
if (hovered && !selected) {
|
|
1737
|
-
line = theme.bg("selectedBg", line);
|
|
1738
|
-
}
|
|
1747
|
+
line = this.#finishRolesRow(line, width, hovered);
|
|
1739
1748
|
lines.push(line);
|
|
1740
1749
|
continue;
|
|
1741
1750
|
}
|
|
@@ -1778,12 +1787,8 @@ export class ModelHubComponent implements Component {
|
|
|
1778
1787
|
const lineWidth = visibleWidth(line);
|
|
1779
1788
|
if (rightWidth > 0 && lineWidth + rightWidth + 2 <= width) {
|
|
1780
1789
|
line = `${line}${" ".repeat(width - lineWidth - rightWidth - 1)}${right}`;
|
|
1781
|
-
} else {
|
|
1782
|
-
line = truncateToWidth(line, width);
|
|
1783
|
-
}
|
|
1784
|
-
if (hovered && !selected) {
|
|
1785
|
-
line = theme.bg("selectedBg", line);
|
|
1786
1790
|
}
|
|
1791
|
+
line = this.#finishRolesRow(line, width, hovered);
|
|
1787
1792
|
lines.push(line);
|
|
1788
1793
|
}
|
|
1789
1794
|
|
|
@@ -1981,6 +1986,7 @@ export class ModelHubComponent implements Component {
|
|
|
1981
1986
|
bodyLines.push(...this.#renderLockedView(entry, bodyWidth, contentRows - 1));
|
|
1982
1987
|
} else {
|
|
1983
1988
|
this.#browser.setMaxVisible(contentRows - 1 - 5);
|
|
1989
|
+
this.#browser.setFocused(this.#focus === "list");
|
|
1984
1990
|
bodyLines.push(...this.#browser.render(bodyWidth));
|
|
1985
1991
|
}
|
|
1986
1992
|
|
|
@@ -82,6 +82,8 @@ export interface PlanReviewOverlayCallbacks {
|
|
|
82
82
|
onPick: (label: string) => void;
|
|
83
83
|
/** Invoked on Esc / cancel. */
|
|
84
84
|
onCancel: () => void;
|
|
85
|
+
/** Invoked with the current full plan text when the copy hotkey is pressed. */
|
|
86
|
+
onCopyPlan?: (content: string) => void | Promise<void>;
|
|
85
87
|
/** Invoked when the external-editor key is pressed (overlay stays open). */
|
|
86
88
|
onExternalEditor?: () => void;
|
|
87
89
|
/** Invoked when the external-editor key edits the active annotation draft. */
|
|
@@ -302,6 +304,10 @@ export class PlanReviewOverlay implements Component {
|
|
|
302
304
|
this.callbacks.onExternalEditor();
|
|
303
305
|
return;
|
|
304
306
|
}
|
|
307
|
+
if (this.callbacks.onCopyPlan && keyData === "c") {
|
|
308
|
+
void this.callbacks.onCopyPlan(joinPlanSections(this.#sections));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
305
311
|
if (matchesKey(keyData, "tab") || keyData === "\t") {
|
|
306
312
|
this.#cycleRegion(1);
|
|
307
313
|
return;
|
|
@@ -677,6 +683,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
677
683
|
parts.push("↑↓ scroll", "⇧ faster", "pgup/pgdn", "g/G ends");
|
|
678
684
|
break;
|
|
679
685
|
}
|
|
686
|
+
if (this.callbacks.onCopyPlan) parts.push("c copy");
|
|
680
687
|
parts.push("tab regions");
|
|
681
688
|
if (this.#externalEditorLabel && this.#focus !== "toc") parts.push(`${this.#externalEditorLabel} editor`);
|
|
682
689
|
parts.push(this.#helpSuffix);
|
|
@@ -21,4 +21,5 @@ Pair up live: `/collab` shares your session through an end-to-end encrypted rela
|
|
|
21
21
|
Press ← ← to drill into a running or finished agent and inspect its tool calls and transcript
|
|
22
22
|
Hit a Codex rate limit? `/usage reset` spends a saved reset credit to immediately restore your quota
|
|
23
23
|
No native tool_calling? Inference provider botches parsing them? `PI_DIALECT=glm|kimi|anthropic…` rolls it locally for them!
|
|
24
|
-
Turn on `/advisor` to attach a second model that reviews every turn and quietly injects advice
|
|
24
|
+
Turn on `/advisor` to attach a second model that reviews every turn and quietly injects advice
|
|
25
|
+
Try starting your prompt with a ->, and writing a list (1. Do X, 2. Do Y)
|
|
@@ -44,20 +44,19 @@ const NEW_GLOW_PERIOD_MS = 1500;
|
|
|
44
44
|
* affordance surfaces this many times as often. */
|
|
45
45
|
const NEW_TIP_WEIGHT = 4;
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (r < 0) return TIPS[i] ?? "";
|
|
47
|
+
/** Pick a tip from `tips`, biased toward "[NEW]" tips by {@link NEW_TIP_WEIGHT};
|
|
48
|
+
* `r` is a uniform sample in [0, 1). Returns "" when `tips` is empty.
|
|
49
|
+
* Exported for tests. */
|
|
50
|
+
export function pickWeightedTip(tips: readonly string[], r: number): string {
|
|
51
|
+
if (tips.length === 0) return "";
|
|
52
|
+
const weights = tips.map(tip => (NEW_TIP_MARKER.test(tip) ? NEW_TIP_WEIGHT : 1));
|
|
53
|
+
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
|
54
|
+
let acc = r * total;
|
|
55
|
+
for (let i = 0; i < tips.length; i++) {
|
|
56
|
+
acc -= weights[i] ?? 1;
|
|
57
|
+
if (acc < 0) return tips[i] ?? "";
|
|
59
58
|
}
|
|
60
|
-
return
|
|
59
|
+
return tips[tips.length - 1] ?? "";
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
type ColorEncoding = "ansi-16m" | "ansi-256";
|
|
@@ -161,7 +160,7 @@ export class WelcomeComponent implements Component {
|
|
|
161
160
|
if (theme.getSymbolPreset() === "unicode" && Math.random() < 0.1) {
|
|
162
161
|
this.#selectedTip = "Please use nerdfont 😭.";
|
|
163
162
|
} else {
|
|
164
|
-
this.#selectedTip = pickWeightedTip();
|
|
163
|
+
this.#selectedTip = pickWeightedTip(TIPS, Math.random());
|
|
165
164
|
}
|
|
166
165
|
}
|
|
167
166
|
return this.#selectedTip || undefined;
|
|
@@ -120,6 +120,7 @@ import { formatPhaseDisplayName, todoMatchesAnyDescription } from "../tools/todo
|
|
|
120
120
|
import { ToolError } from "../tools/tool-errors";
|
|
121
121
|
import { vocalizer } from "../tts/vocalizer";
|
|
122
122
|
import { renderTreeList } from "../tui/tree-list";
|
|
123
|
+
import { copyToClipboard } from "../utils/clipboard";
|
|
123
124
|
import type { EventBus } from "../utils/event-bus";
|
|
124
125
|
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
|
125
126
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
|
|
@@ -2517,6 +2518,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2517
2518
|
{
|
|
2518
2519
|
onPick: choice => finish(choice),
|
|
2519
2520
|
onCancel: () => finish(undefined),
|
|
2521
|
+
onCopyPlan: content => void this.#copyPlanToClipboard(content),
|
|
2520
2522
|
onExternalEditor: dialogOptions?.onExternalEditor,
|
|
2521
2523
|
onAnnotationExternalEditor: (draft, commit) => void this.#openPlanAnnotationInExternalEditor(draft, commit),
|
|
2522
2524
|
onPlanEdited: dialogOptions?.onPlanEdited,
|
|
@@ -2583,6 +2585,17 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2583
2585
|
return contextUsage !== undefined && contextUsage.percent > PLAN_KEEP_CONTEXT_DISABLE_THRESHOLD_PERCENT;
|
|
2584
2586
|
}
|
|
2585
2587
|
|
|
2588
|
+
async #copyPlanToClipboard(content: string): Promise<void> {
|
|
2589
|
+
try {
|
|
2590
|
+
await copyToClipboard(content);
|
|
2591
|
+
this.showStatus("Copied plan to clipboard");
|
|
2592
|
+
} catch (error) {
|
|
2593
|
+
this.showWarning(
|
|
2594
|
+
`Failed to copy plan to clipboard: ${error instanceof Error ? error.message : String(error)}`,
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2586
2599
|
async #openPlanInExternalEditor(planFilePath: string): Promise<void> {
|
|
2587
2600
|
const editorCmd = getEditorCommand();
|
|
2588
2601
|
if (!editorCmd) {
|
|
@@ -581,11 +581,27 @@ async function callPerplexityAsk(
|
|
|
581
581
|
search_recency_filter: params.search_recency_filter ?? null,
|
|
582
582
|
is_incognito: true,
|
|
583
583
|
use_schematized_api: true,
|
|
584
|
-
|
|
584
|
+
// `true` (the native app's default) lets the backend classifier skip
|
|
585
|
+
// retrieval for queries it deems answerable from memory — the model then
|
|
586
|
+
// runs ungrounded and refuses with "I don't currently have live access".
|
|
587
|
+
// We are a search tool; always retrieve.
|
|
588
|
+
skip_search_enabled: false,
|
|
589
|
+
// Belt and braces with `skip_search_enabled: false`: the web client sets
|
|
590
|
+
// this to force retrieval even when the skip classifier fires.
|
|
591
|
+
always_search_override: true,
|
|
592
|
+
prompt_source: "user",
|
|
593
|
+
source: "default",
|
|
594
|
+
local_search_enabled: false,
|
|
595
|
+
// Declare no tool-approval UI and no local (Comet) browser agent, so the
|
|
596
|
+
// stream never stalls waiting for a confirmation we cannot render.
|
|
597
|
+
should_ask_for_mcp_tool_confirmation: false,
|
|
598
|
+
supports_tool_approval_modal: false,
|
|
599
|
+
force_enable_browser_agent: false,
|
|
600
|
+
is_local_browser_available: false,
|
|
601
|
+
is_local_browser_allowed: false,
|
|
585
602
|
};
|
|
586
603
|
if (auth.type === "anonymous") {
|
|
587
604
|
requestParams.send_back_text_in_streaming_api = true;
|
|
588
|
-
requestParams.source = "default";
|
|
589
605
|
}
|
|
590
606
|
|
|
591
607
|
const response = await (params.fetch ?? fetch)(PERPLEXITY_OAUTH_ASK_URL, {
|