@oh-my-pi/pi-coding-agent 13.3.7 → 13.3.9
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 +82 -0
- package/package.json +9 -18
- package/scripts/format-prompts.ts +7 -172
- package/src/config/prompt-templates.ts +2 -54
- package/src/config/settings-schema.ts +24 -0
- package/src/discovery/codex.ts +1 -2
- package/src/discovery/helpers.ts +0 -5
- package/src/lsp/client.ts +8 -0
- package/src/lsp/config.ts +2 -3
- package/src/lsp/index.ts +379 -99
- package/src/lsp/render.ts +21 -31
- package/src/lsp/types.ts +21 -8
- package/src/lsp/utils.ts +193 -1
- package/src/mcp/config-writer.ts +3 -0
- package/src/modes/components/settings-defs.ts +9 -0
- package/src/modes/interactive-mode.ts +8 -1
- package/src/modes/theme/mermaid-cache.ts +4 -4
- package/src/modes/theme/theme.ts +33 -0
- package/src/prompts/system/subagent-user-prompt.md +2 -0
- package/src/prompts/system/system-prompt.md +12 -1
- package/src/prompts/tools/ast-find.md +20 -0
- package/src/prompts/tools/ast-replace.md +21 -0
- package/src/prompts/tools/bash.md +2 -0
- package/src/prompts/tools/hashline.md +26 -8
- package/src/prompts/tools/lsp.md +22 -5
- package/src/sdk.ts +11 -1
- package/src/session/agent-session.ts +261 -82
- package/src/task/executor.ts +8 -5
- package/src/tools/ast-find.ts +316 -0
- package/src/tools/ast-replace.ts +294 -0
- package/src/tools/bash.ts +2 -1
- package/src/tools/browser.ts +2 -8
- package/src/tools/fetch.ts +55 -18
- package/src/tools/index.ts +8 -0
- package/src/tools/path-utils.ts +34 -0
- package/src/tools/python.ts +2 -1
- package/src/tools/renderers.ts +4 -0
- package/src/tools/ssh.ts +2 -1
- package/src/tools/todo-write.ts +34 -0
- package/src/tools/tool-timeouts.ts +29 -0
- package/src/utils/mime.ts +37 -14
- package/src/utils/prompt-format.ts +172 -0
- package/src/web/scrapers/arxiv.ts +12 -12
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/iacr.ts +17 -9
- package/src/web/scrapers/readthedocs.ts +3 -3
- package/src/web/scrapers/twitter.ts +11 -11
- package/src/web/scrapers/wikipedia.ts +4 -5
- package/src/utils/ignore-files.ts +0 -119
package/src/lsp/render.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
formatExpandHint,
|
|
16
16
|
formatMoreItems,
|
|
17
17
|
formatStatusIcon,
|
|
18
|
+
replaceTabs,
|
|
18
19
|
shortenPath,
|
|
19
20
|
TRUNCATE_LENGTHS,
|
|
20
21
|
truncateToWidth,
|
|
@@ -31,9 +32,16 @@ import type { LspParams, LspToolDetails } from "./types";
|
|
|
31
32
|
* Render the LSP tool call in the TUI.
|
|
32
33
|
* Shows: "lsp <operation> <file/filecount>"
|
|
33
34
|
*/
|
|
35
|
+
function sanitizeInlineText(value: string): string {
|
|
36
|
+
return replaceTabs(value).replaceAll(/\r?\n/g, " ");
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
export function renderCall(args: LspParams, _options: RenderResultOptions, theme: Theme): Text {
|
|
35
40
|
const actionLabel = (args.action ?? "request").replace(/_/g, " ");
|
|
36
41
|
const queryPreview = args.query ? truncateToWidth(args.query, TRUNCATE_LENGTHS.SHORT) : undefined;
|
|
42
|
+
const symbolPreview = args.symbol
|
|
43
|
+
? truncateToWidth(sanitizeInlineText(args.symbol), TRUNCATE_LENGTHS.SHORT)
|
|
44
|
+
: undefined;
|
|
37
45
|
|
|
38
46
|
let target: string | undefined;
|
|
39
47
|
let hasFileTarget = false;
|
|
@@ -41,26 +49,17 @@ export function renderCall(args: LspParams, _options: RenderResultOptions, theme
|
|
|
41
49
|
if (args.file) {
|
|
42
50
|
target = shortenPath(args.file);
|
|
43
51
|
hasFileTarget = true;
|
|
44
|
-
} else if (args.files?.length === 1) {
|
|
45
|
-
target = shortenPath(args.files[0]);
|
|
46
|
-
hasFileTarget = true;
|
|
47
|
-
} else if (args.files?.length) {
|
|
48
|
-
target = `${args.files.length} files`;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
if (hasFileTarget && args.line !== undefined) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const endCol = args.end_character !== undefined ? `:${args.end_character}` : "";
|
|
56
|
-
target += `-${args.end_line}${endCol}`;
|
|
55
|
+
target += `:${args.line}`;
|
|
56
|
+
if (symbolPreview) {
|
|
57
|
+
target += ` (${symbolPreview})`;
|
|
57
58
|
}
|
|
58
59
|
} else if (!target && args.line !== undefined) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const endCol = args.end_character !== undefined ? `:${args.end_character}` : "";
|
|
63
|
-
target += `-${args.end_line}${endCol}`;
|
|
60
|
+
target = `line ${args.line}`;
|
|
61
|
+
if (symbolPreview) {
|
|
62
|
+
target += ` (${symbolPreview})`;
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
|
|
@@ -68,9 +67,6 @@ export function renderCall(args: LspParams, _options: RenderResultOptions, theme
|
|
|
68
67
|
if (queryPreview && target) meta.push(`query:${queryPreview}`);
|
|
69
68
|
if (args.new_name) meta.push(`new:${args.new_name}`);
|
|
70
69
|
if (args.apply !== undefined) meta.push(`apply:${args.apply ? "true" : "false"}`);
|
|
71
|
-
if (args.include_declaration !== undefined) {
|
|
72
|
-
meta.push(`include_decl:${args.include_declaration ? "true" : "false"}`);
|
|
73
|
-
}
|
|
74
70
|
|
|
75
71
|
const descriptionParts = [actionLabel];
|
|
76
72
|
if (target) {
|
|
@@ -104,7 +100,7 @@ export function renderResult(
|
|
|
104
100
|
result: { content: Array<{ type: string; text?: string }>; details?: LspToolDetails; isError?: boolean },
|
|
105
101
|
options: RenderResultOptions,
|
|
106
102
|
theme: Theme,
|
|
107
|
-
args?: LspParams
|
|
103
|
+
args?: LspParams,
|
|
108
104
|
): Component {
|
|
109
105
|
const content = result.content?.[0];
|
|
110
106
|
if (!content || content.type !== "text" || !("text" in content) || !content.text) {
|
|
@@ -129,25 +125,19 @@ export function renderResult(
|
|
|
129
125
|
const requestLines: string[] = [];
|
|
130
126
|
if (request?.file) {
|
|
131
127
|
requestLines.push(theme.fg("toolOutput", request.file));
|
|
132
|
-
} else if (request?.files?.length === 1) {
|
|
133
|
-
requestLines.push(theme.fg("toolOutput", request.files[0]));
|
|
134
|
-
} else if (request?.files?.length) {
|
|
135
|
-
requestLines.push(theme.fg("dim", `${request.files.length} file(s)`));
|
|
136
128
|
}
|
|
137
129
|
if (request?.line !== undefined) {
|
|
138
|
-
|
|
139
|
-
requestLines.push(theme.fg("dim", `line ${request.line}${col}`));
|
|
130
|
+
requestLines.push(theme.fg("dim", `line ${request.line}`));
|
|
140
131
|
}
|
|
141
|
-
if (request?.
|
|
142
|
-
|
|
143
|
-
|
|
132
|
+
if (request?.symbol) {
|
|
133
|
+
requestLines.push(theme.fg("dim", `symbol: ${sanitizeInlineText(request.symbol)}`));
|
|
134
|
+
if (request.occurrence !== undefined) {
|
|
135
|
+
requestLines.push(theme.fg("dim", `occurrence: ${request.occurrence}`));
|
|
136
|
+
}
|
|
144
137
|
}
|
|
145
138
|
if (request?.query) requestLines.push(theme.fg("dim", `query: ${request.query}`));
|
|
146
139
|
if (request?.new_name) requestLines.push(theme.fg("dim", `new name: ${request.new_name}`));
|
|
147
140
|
if (request?.apply !== undefined) requestLines.push(theme.fg("dim", `apply: ${request.apply ? "true" : "false"}`));
|
|
148
|
-
if (request?.include_declaration !== undefined) {
|
|
149
|
-
requestLines.push(theme.fg("dim", `include declaration: ${request.include_declaration ? "true" : "false"}`));
|
|
150
|
-
}
|
|
151
141
|
|
|
152
142
|
const outputBlock = new CachedOutputBlock();
|
|
153
143
|
|
package/src/lsp/types.ts
CHANGED
|
@@ -7,19 +7,32 @@ import { type Static, Type } from "@sinclair/typebox";
|
|
|
7
7
|
// =============================================================================
|
|
8
8
|
|
|
9
9
|
export const lspSchema = Type.Object({
|
|
10
|
-
action: StringEnum(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
action: StringEnum(
|
|
11
|
+
[
|
|
12
|
+
"diagnostics",
|
|
13
|
+
"definition",
|
|
14
|
+
"references",
|
|
15
|
+
"hover",
|
|
16
|
+
"symbols",
|
|
17
|
+
"rename",
|
|
18
|
+
"code_actions",
|
|
19
|
+
"type_definition",
|
|
20
|
+
"implementation",
|
|
21
|
+
"status",
|
|
22
|
+
"reload",
|
|
23
|
+
],
|
|
24
|
+
{ description: "LSP operation" },
|
|
25
|
+
),
|
|
14
26
|
file: Type.Optional(Type.String({ description: "File path" })),
|
|
15
27
|
line: Type.Optional(Type.Number({ description: "Line number (1-indexed)" })),
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
symbol: Type.Optional(
|
|
29
|
+
Type.String({ description: "Symbol/substring to locate on the line (used to compute column)" }),
|
|
30
|
+
),
|
|
31
|
+
occurrence: Type.Optional(Type.Number({ description: "Symbol occurrence on line (1-indexed, default: 1)" })),
|
|
19
32
|
query: Type.Optional(Type.String({ description: "Search query or SSR pattern" })),
|
|
20
33
|
new_name: Type.Optional(Type.String({ description: "New name for rename" })),
|
|
21
34
|
apply: Type.Optional(Type.Boolean({ description: "Apply edits (default: true)" })),
|
|
22
|
-
|
|
35
|
+
timeout: Type.Optional(Type.Number({ description: "Request timeout in seconds" })),
|
|
23
36
|
});
|
|
24
37
|
|
|
25
38
|
export type LspParams = Static<typeof lspSchema>;
|
package/src/lsp/utils.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export { truncate } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
4
5
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
5
6
|
import type {
|
|
7
|
+
CodeAction,
|
|
8
|
+
Command,
|
|
6
9
|
Diagnostic,
|
|
7
10
|
DiagnosticSeverity,
|
|
8
11
|
DocumentSymbol,
|
|
@@ -475,7 +478,8 @@ export function formatDocumentSymbol(symbol: DocumentSymbol, indent = 0): string
|
|
|
475
478
|
const prefix = " ".repeat(indent);
|
|
476
479
|
const icon = symbolKindToIcon(symbol.kind);
|
|
477
480
|
const line = symbol.range.start.line + 1;
|
|
478
|
-
const
|
|
481
|
+
const detail = symbol.detail ? ` ${symbol.detail}` : "";
|
|
482
|
+
const results = [`${prefix}${icon} ${symbol.name}${detail} @ line ${line}`];
|
|
479
483
|
|
|
480
484
|
if (symbol.children) {
|
|
481
485
|
for (const child of symbol.children) {
|
|
@@ -496,6 +500,110 @@ export function formatSymbolInformation(symbol: SymbolInformation, cwd: string):
|
|
|
496
500
|
return `${icon} ${symbol.name}${container} @ ${location}`;
|
|
497
501
|
}
|
|
498
502
|
|
|
503
|
+
export function filterWorkspaceSymbols(symbols: SymbolInformation[], query: string): SymbolInformation[] {
|
|
504
|
+
const needle = query.trim().toLowerCase();
|
|
505
|
+
if (!needle) return symbols;
|
|
506
|
+
return symbols.filter(symbol => {
|
|
507
|
+
const fields = [symbol.name, symbol.containerName ?? "", uriToFile(symbol.location.uri)];
|
|
508
|
+
return fields.some(field => field.toLowerCase().includes(needle));
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function dedupeWorkspaceSymbols(symbols: SymbolInformation[]): SymbolInformation[] {
|
|
513
|
+
const seen = new Set<string>();
|
|
514
|
+
const unique: SymbolInformation[] = [];
|
|
515
|
+
for (const symbol of symbols) {
|
|
516
|
+
const key = [
|
|
517
|
+
symbol.name,
|
|
518
|
+
symbol.containerName ?? "",
|
|
519
|
+
symbol.kind,
|
|
520
|
+
symbol.location.uri,
|
|
521
|
+
symbol.location.range.start.line,
|
|
522
|
+
symbol.location.range.start.character,
|
|
523
|
+
].join(":");
|
|
524
|
+
if (seen.has(key)) continue;
|
|
525
|
+
seen.add(key);
|
|
526
|
+
unique.push(symbol);
|
|
527
|
+
}
|
|
528
|
+
return unique;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function formatCodeAction(action: CodeAction | Command, index: number): string {
|
|
532
|
+
const kind = "kind" in action && action.kind ? action.kind : "action";
|
|
533
|
+
const preferred = "isPreferred" in action && action.isPreferred ? " (preferred)" : "";
|
|
534
|
+
const disabled = "disabled" in action && action.disabled ? ` (disabled: ${action.disabled.reason})` : "";
|
|
535
|
+
return `${index}: [${kind}] ${action.title}${preferred}${disabled}`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export interface CodeActionApplyDependencies {
|
|
539
|
+
resolveCodeAction?: (action: CodeAction) => Promise<CodeAction>;
|
|
540
|
+
applyWorkspaceEdit: (edit: WorkspaceEdit) => Promise<string[]>;
|
|
541
|
+
executeCommand: (command: Command) => Promise<void>;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export interface AppliedCodeActionResult {
|
|
545
|
+
title: string;
|
|
546
|
+
edits: string[];
|
|
547
|
+
executedCommands: string[];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function isCommandItem(action: CodeAction | Command): action is Command {
|
|
551
|
+
return typeof action.command === "string";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export async function applyCodeAction(
|
|
555
|
+
action: CodeAction | Command,
|
|
556
|
+
dependencies: CodeActionApplyDependencies,
|
|
557
|
+
): Promise<AppliedCodeActionResult | null> {
|
|
558
|
+
if (isCommandItem(action)) {
|
|
559
|
+
await dependencies.executeCommand(action);
|
|
560
|
+
return { title: action.title, edits: [], executedCommands: [action.command] };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
let resolvedAction = action;
|
|
564
|
+
if (!resolvedAction.edit && dependencies.resolveCodeAction) {
|
|
565
|
+
try {
|
|
566
|
+
resolvedAction = await dependencies.resolveCodeAction(resolvedAction);
|
|
567
|
+
} catch {
|
|
568
|
+
// Resolve is optional; continue with unresolved action.
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const edits = resolvedAction.edit ? await dependencies.applyWorkspaceEdit(resolvedAction.edit) : [];
|
|
573
|
+
const executedCommands: string[] = [];
|
|
574
|
+
if (resolvedAction.command) {
|
|
575
|
+
await dependencies.executeCommand(resolvedAction.command);
|
|
576
|
+
executedCommands.push(resolvedAction.command.command);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (edits.length === 0 && executedCommands.length === 0) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return { title: resolvedAction.title, edits, executedCommands };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const GLOB_PATTERN_CHARS = /[*?[{]/;
|
|
587
|
+
|
|
588
|
+
export function hasGlobPattern(value: string): boolean {
|
|
589
|
+
return GLOB_PATTERN_CHARS.test(value);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export async function collectGlobMatches(
|
|
593
|
+
pattern: string,
|
|
594
|
+
cwd: string,
|
|
595
|
+
maxMatches: number,
|
|
596
|
+
): Promise<{ matches: string[]; truncated: boolean }> {
|
|
597
|
+
const normalizedLimit = Number.isFinite(maxMatches) ? Math.max(1, Math.trunc(maxMatches)) : 1;
|
|
598
|
+
const matches: string[] = [];
|
|
599
|
+
for await (const match of new Bun.Glob(pattern).scan({ cwd })) {
|
|
600
|
+
if (matches.length >= normalizedLimit) {
|
|
601
|
+
return { matches, truncated: true };
|
|
602
|
+
}
|
|
603
|
+
matches.push(match);
|
|
604
|
+
}
|
|
605
|
+
return { matches, truncated: false };
|
|
606
|
+
}
|
|
499
607
|
// =============================================================================
|
|
500
608
|
// Hover Content Extraction
|
|
501
609
|
// =============================================================================
|
|
@@ -525,3 +633,87 @@ export function extractHoverText(
|
|
|
525
633
|
|
|
526
634
|
// =============================================================================
|
|
527
635
|
// General Utilities
|
|
636
|
+
|
|
637
|
+
function firstNonWhitespaceColumn(lineText: string): number {
|
|
638
|
+
const match = lineText.match(/\S/);
|
|
639
|
+
return match ? (match.index ?? 0) : 0;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function findSymbolMatchIndexes(lineText: string, symbol: string, caseInsensitive = false): number[] {
|
|
643
|
+
if (symbol.length === 0) return [];
|
|
644
|
+
const haystack = caseInsensitive ? lineText.toLowerCase() : lineText;
|
|
645
|
+
const needle = caseInsensitive ? symbol.toLowerCase() : symbol;
|
|
646
|
+
const indexes: number[] = [];
|
|
647
|
+
let fromIndex = 0;
|
|
648
|
+
while (fromIndex <= haystack.length - needle.length) {
|
|
649
|
+
const matchIndex = haystack.indexOf(needle, fromIndex);
|
|
650
|
+
if (matchIndex === -1) break;
|
|
651
|
+
indexes.push(matchIndex);
|
|
652
|
+
fromIndex = matchIndex + needle.length;
|
|
653
|
+
}
|
|
654
|
+
return indexes;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function normalizeOccurrence(occurrence?: number): number {
|
|
658
|
+
if (occurrence === undefined || !Number.isFinite(occurrence)) return 1;
|
|
659
|
+
return Math.max(1, Math.trunc(occurrence));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export async function resolveSymbolColumn(
|
|
663
|
+
filePath: string,
|
|
664
|
+
line: number,
|
|
665
|
+
symbol?: string,
|
|
666
|
+
occurrence?: number,
|
|
667
|
+
): Promise<number> {
|
|
668
|
+
const lineNumber = Math.max(1, line);
|
|
669
|
+
const matchOccurrence = normalizeOccurrence(occurrence);
|
|
670
|
+
try {
|
|
671
|
+
const fileText = await Bun.file(filePath).text();
|
|
672
|
+
const lines = fileText.split("\n");
|
|
673
|
+
const targetLine = lines[lineNumber - 1] ?? "";
|
|
674
|
+
if (!symbol) {
|
|
675
|
+
return firstNonWhitespaceColumn(targetLine);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const exactIndexes = findSymbolMatchIndexes(targetLine, symbol);
|
|
679
|
+
const fallbackIndexes = exactIndexes.length > 0 ? exactIndexes : findSymbolMatchIndexes(targetLine, symbol, true);
|
|
680
|
+
if (fallbackIndexes.length === 0) {
|
|
681
|
+
throw new Error(`Symbol "${symbol}" not found on line ${lineNumber}`);
|
|
682
|
+
}
|
|
683
|
+
if (matchOccurrence > fallbackIndexes.length) {
|
|
684
|
+
throw new Error(
|
|
685
|
+
`Symbol "${symbol}" occurrence ${matchOccurrence} is out of bounds on line ${lineNumber} (found ${fallbackIndexes.length})`,
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
return fallbackIndexes[matchOccurrence - 1];
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (isEnoent(error)) {
|
|
691
|
+
throw new Error(`File not found: ${filePath}`);
|
|
692
|
+
}
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export async function readLocationContext(filePath: string, line: number, contextLines = 1): Promise<string[]> {
|
|
698
|
+
const targetLine = Math.max(1, line);
|
|
699
|
+
const surrounding = Math.max(0, contextLines);
|
|
700
|
+
try {
|
|
701
|
+
const fileText = await Bun.file(filePath).text();
|
|
702
|
+
const lines = fileText.split("\n");
|
|
703
|
+
if (lines.length === 0) return [];
|
|
704
|
+
|
|
705
|
+
const startLine = Math.max(1, targetLine - surrounding);
|
|
706
|
+
const endLine = Math.min(lines.length, targetLine + surrounding);
|
|
707
|
+
const context: string[] = [];
|
|
708
|
+
for (let currentLine = startLine; currentLine <= endLine; currentLine++) {
|
|
709
|
+
const content = lines[currentLine - 1] ?? "";
|
|
710
|
+
context.push(`${currentLine}: ${content}`);
|
|
711
|
+
}
|
|
712
|
+
return context;
|
|
713
|
+
} catch (error) {
|
|
714
|
+
if (isEnoent(error)) {
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
throw error;
|
|
718
|
+
}
|
|
719
|
+
}
|
package/src/mcp/config-writer.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import { invalidate as invalidateFsCache } from "../capability/fs";
|
|
9
10
|
|
|
10
11
|
import { validateServerConfig } from "./config";
|
|
11
12
|
import type { MCPConfigFile, MCPServerConfig } from "./types";
|
|
@@ -44,6 +45,8 @@ export async function writeMCPConfigFile(filePath: string, config: MCPConfigFile
|
|
|
44
45
|
|
|
45
46
|
// Rename to final path (atomic on most systems)
|
|
46
47
|
await fs.promises.rename(tmpPath, filePath);
|
|
48
|
+
// Invalidate the capability fs cache so subsequent reads see the new content
|
|
49
|
+
invalidateFsCache(filePath);
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/**
|
|
@@ -149,6 +149,15 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
149
149
|
{ value: "60", label: "60 seconds" },
|
|
150
150
|
{ value: "120", label: "120 seconds" },
|
|
151
151
|
],
|
|
152
|
+
// Global tool timeout ceiling
|
|
153
|
+
"tools.maxTimeout": [
|
|
154
|
+
{ value: "0", label: "No limit" },
|
|
155
|
+
{ value: "30", label: "30 seconds" },
|
|
156
|
+
{ value: "60", label: "60 seconds" },
|
|
157
|
+
{ value: "120", label: "120 seconds" },
|
|
158
|
+
{ value: "300", label: "5 minutes" },
|
|
159
|
+
{ value: "600", label: "10 minutes" },
|
|
160
|
+
],
|
|
152
161
|
// Edit fuzzy threshold
|
|
153
162
|
"edit.fuzzyThreshold": [
|
|
154
163
|
{ value: "0.85", label: "0.85", description: "Lenient" },
|
|
@@ -54,7 +54,7 @@ import { SSHCommandController } from "./controllers/ssh-command-controller";
|
|
|
54
54
|
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
55
55
|
import { setMermaidRenderCallback } from "./theme/mermaid-cache";
|
|
56
56
|
import type { Theme } from "./theme/theme";
|
|
57
|
-
import { getEditorTheme, getMarkdownTheme, onThemeChange, theme } from "./theme/theme";
|
|
57
|
+
import { getEditorTheme, getMarkdownTheme, onTerminalAppearanceChange, onThemeChange, theme } from "./theme/theme";
|
|
58
58
|
import type { CompactionQueuedMessage, InteractiveModeContext, TodoItem, TodoPhase } from "./types";
|
|
59
59
|
import { UiHelpers } from "./utils/ui-helpers";
|
|
60
60
|
|
|
@@ -363,6 +363,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
363
363
|
this.ui.requestRender();
|
|
364
364
|
});
|
|
365
365
|
|
|
366
|
+
// Subscribe to terminal Mode 2031 dark/light appearance change notifications.
|
|
367
|
+
// When the OS or terminal switches between dark and light mode, the terminal
|
|
368
|
+
// sends a DSR and we re-evaluate which theme to use.
|
|
369
|
+
this.ui.terminal.onAppearanceChange(mode => {
|
|
370
|
+
onTerminalAppearanceChange(mode);
|
|
371
|
+
});
|
|
372
|
+
|
|
366
373
|
// Set up git branch watcher
|
|
367
374
|
this.statusLine.watchBranch(() => {
|
|
368
375
|
this.updateEditorTopBorder();
|
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
} from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
8
8
|
|
|
9
|
-
const cache = new Map<
|
|
10
|
-
const pending = new Map<
|
|
11
|
-
const failed = new Set<
|
|
9
|
+
const cache = new Map<bigint, MermaidImage>();
|
|
10
|
+
const pending = new Map<bigint, Promise<MermaidImage | null>>();
|
|
11
|
+
const failed = new Set<bigint>();
|
|
12
12
|
|
|
13
13
|
const defaultOptions: MermaidRenderOptions = {
|
|
14
14
|
theme: "dark",
|
|
@@ -28,7 +28,7 @@ export function setMermaidRenderCallback(callback: (() => void) | null): void {
|
|
|
28
28
|
* Get a pre-rendered mermaid image by hash.
|
|
29
29
|
* Returns null if not cached or rendering failed.
|
|
30
30
|
*/
|
|
31
|
-
export function getMermaidImage(hash:
|
|
31
|
+
export function getMermaidImage(hash: bigint): MermaidImage | null {
|
|
32
32
|
return cache.get(hash) ?? null;
|
|
33
33
|
}
|
|
34
34
|
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -1616,7 +1616,15 @@ export async function getThemeByName(name: string): Promise<Theme | undefined> {
|
|
|
1616
1616
|
}
|
|
1617
1617
|
}
|
|
1618
1618
|
|
|
1619
|
+
/** Appearance reported by Mode 2031 (terminal DSR), or undefined if not (yet) available. */
|
|
1620
|
+
var terminalReportedAppearance: "dark" | "light" | undefined;
|
|
1621
|
+
|
|
1619
1622
|
function detectTerminalBackground(): "dark" | "light" {
|
|
1623
|
+
// Prefer terminal-reported appearance from Mode 2031 (CSI ? 997 ; {1,2} n)
|
|
1624
|
+
if (terminalReportedAppearance) {
|
|
1625
|
+
return terminalReportedAppearance;
|
|
1626
|
+
}
|
|
1627
|
+
// Fallback: COLORFGBG environment variable (static, set once at terminal launch)
|
|
1620
1628
|
const colorfgbg = Bun.env.COLORFGBG || "";
|
|
1621
1629
|
if (colorfgbg) {
|
|
1622
1630
|
const parts = colorfgbg.split(";");
|
|
@@ -1793,6 +1801,31 @@ export function setAutoThemeMapping(mode: "dark" | "light", themeName: string):
|
|
|
1793
1801
|
});
|
|
1794
1802
|
}
|
|
1795
1803
|
|
|
1804
|
+
/**
|
|
1805
|
+
* Called when the terminal reports a dark/light appearance change via Mode 2031.
|
|
1806
|
+
* Updates the cached appearance and triggers auto-theme re-evaluation.
|
|
1807
|
+
* This is the cross-platform mechanism supported by Ghostty, Kitty, Contour,
|
|
1808
|
+
* VTE (GNOME Terminal), and tmux 3.6+.
|
|
1809
|
+
*/
|
|
1810
|
+
export function onTerminalAppearanceChange(mode: "dark" | "light"): void {
|
|
1811
|
+
if (terminalReportedAppearance === mode) return;
|
|
1812
|
+
terminalReportedAppearance = mode;
|
|
1813
|
+
if (!autoDetectedTheme) return;
|
|
1814
|
+
const resolved = getDefaultTheme();
|
|
1815
|
+
if (resolved === currentThemeName) return;
|
|
1816
|
+
currentThemeName = resolved;
|
|
1817
|
+
loadTheme(resolved, getCurrentThemeOptions())
|
|
1818
|
+
.then(loadedTheme => {
|
|
1819
|
+
theme = loadedTheme;
|
|
1820
|
+
if (onThemeChangeCallback) {
|
|
1821
|
+
onThemeChangeCallback();
|
|
1822
|
+
}
|
|
1823
|
+
})
|
|
1824
|
+
.catch(err => {
|
|
1825
|
+
logger.debug("Mode 2031 appearance switch failed", { error: String(err) });
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1796
1829
|
export function setThemeInstance(themeInstance: Theme): void {
|
|
1797
1830
|
autoDetectedTheme = false;
|
|
1798
1831
|
theme = themeInstance;
|
|
@@ -106,7 +106,7 @@ Domain-specific rules from past experience. **MUST** read `rule://<name>` when w
|
|
|
106
106
|
You **MUST** use tools to complete the task.
|
|
107
107
|
|
|
108
108
|
{{#if intentTracing}}
|
|
109
|
-
Every tool call **MUST** include the `{{intentField}}` parameter: one sentence in present participle form (e.g.,
|
|
109
|
+
Every tool call **MUST** include the `{{intentField}}` parameter: one concise sentence in present participle form (e.g., Updating imports), ideally 2-6 words, with no trailing period. This is a contract-level requirement, not optional metadata.
|
|
110
110
|
{{/if}}
|
|
111
111
|
|
|
112
112
|
You **MUST** use the following tools, as effectively as possible, to complete the task:
|
|
@@ -147,10 +147,21 @@ You **MUST NOT** use Python or Bash when a specialized tool exists.
|
|
|
147
147
|
|
|
148
148
|
Semantic questions **MUST** be answered with semantic tools.
|
|
149
149
|
- Where is this thing defined? → `lsp definition`
|
|
150
|
+
- What type does this thing resolve to? → `lsp type_definition`
|
|
151
|
+
- What concrete implementations exist? → `lsp implementation`
|
|
150
152
|
- What uses this thing I'm about to change? → `lsp references`
|
|
151
153
|
- What is this thing? → `lsp hover`
|
|
154
|
+
- Can the server propose fixes/imports/refactors? → `lsp code_actions` (list first, then apply with `apply: true` + `query`)
|
|
152
155
|
{{/has}}
|
|
153
156
|
|
|
157
|
+
{{#ifAny (includes tools "ast_find") (includes tools "ast_replace")}}
|
|
158
|
+
### AST tools for structural code work
|
|
159
|
+
|
|
160
|
+
When AST tools are available, syntax-aware operations take priority over text hacks.
|
|
161
|
+
{{#has tools "ast_find"}}- Use `ast_find` for structural discovery (call shapes, declarations, syntax patterns) before text grep when code structure matters{{/has}}
|
|
162
|
+
{{#has tools "ast_replace"}}- Use `ast_replace` for structural codemods/replacements; do not use bash `sed`/`perl`/`awk` for syntax-level rewrites{{/has}}
|
|
163
|
+
- Use `grep` for plain text/regex lookup only when AST shape is irrelevant
|
|
164
|
+
{{/ifAny}}
|
|
154
165
|
{{#if eagerTasks}}
|
|
155
166
|
<eager-tasks>
|
|
156
167
|
You **SHOULD** delegate work to subagents by default. Working alone is the exception, not the rule.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Performs structural code search using AST matching via native ast-grep.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
- Use this when syntax shape matters more than raw text (calls, declarations, specific language constructs)
|
|
5
|
+
- Prefer a precise `path` scope to keep results targeted and deterministic (`path` accepts files, directories, or glob patterns)
|
|
6
|
+
- `pattern` is required; `lang` is optional (`lang` is inferred per file extension when omitted)
|
|
7
|
+
- Use `selector` only for contextual pattern mode; otherwise provide a direct pattern
|
|
8
|
+
- Enable `include_meta` when metavariable captures are needed in output
|
|
9
|
+
</instruction>
|
|
10
|
+
|
|
11
|
+
<output>
|
|
12
|
+
- Returns grouped matches with file path, byte range, and line/column ranges
|
|
13
|
+
- Includes summary counts (`totalMatches`, `filesWithMatches`, `filesSearched`) and parse issues when present
|
|
14
|
+
</output>
|
|
15
|
+
|
|
16
|
+
<critical>
|
|
17
|
+
- `pattern` is required
|
|
18
|
+
- Set `lang` explicitly to constrain matching when path pattern spans mixed-language trees
|
|
19
|
+
- If exploration is broad/open-ended across subsystems, use Task tool with explore subagent first
|
|
20
|
+
</critical>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Performs structural AST-aware rewrites via native ast-grep.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
- Use for codemods and structural rewrites where plain text replace is unsafe
|
|
5
|
+
- Narrow scope with `path` before replacing (`path` accepts files, directories, or glob patterns)
|
|
6
|
+
- `pattern` + `rewrite` are required; `lang` is optional only when all matched files resolve to a single language
|
|
7
|
+
- Keep `dry_run` enabled unless explicit apply intent is clear
|
|
8
|
+
- Use `max_files` and `max_replacements` as safety caps on broad rewrites
|
|
9
|
+
</instruction>
|
|
10
|
+
|
|
11
|
+
<output>
|
|
12
|
+
- Returns replacement summary, per-file replacement counts, and change previews
|
|
13
|
+
- Reports whether changes were applied or only previewed
|
|
14
|
+
- Includes parse issues when files cannot be processed
|
|
15
|
+
</output>
|
|
16
|
+
|
|
17
|
+
<critical>
|
|
18
|
+
- `pattern` + `rewrite` are required
|
|
19
|
+
- If the path pattern spans multiple languages, set `lang` explicitly for deterministic rewrites
|
|
20
|
+
- For one-off local text edits, prefer the Edit tool instead of AST replace
|
|
21
|
+
</critical>
|
|
@@ -35,6 +35,8 @@ You **MUST** use specialized tools instead of bash for ALL file operations:
|
|
|
35
35
|
|`ls dir/`|`read(path="dir/")`|
|
|
36
36
|
|`cat <<'EOF' > file`|`write(path="file", content="...")`|
|
|
37
37
|
|`sed -i 's/old/new/' file`|`edit(path="file", edits=[...])`|
|
|
38
|
+
- If `ast_find` / `ast_replace` tools are available in the session, you **MUST** use them for structural code search/rewrites instead of bash `grep`/`sed`/`awk`/`perl` pipelines
|
|
39
|
+
- Bash is for command execution, not syntax-aware code transformation; prefer `ast_find` for discovery and `ast_replace` for codemods
|
|
38
40
|
- You **MUST NOT** use Bash for these operations like read, grep, find, edit, write, where specialized tools exist.
|
|
39
41
|
- You **MUST NOT** use `2>&1` | `2>/dev/null` pattern, stdout and stderr are already merged.
|
|
40
42
|
- You **MUST NOT** use `| head -n 50` or `| tail -n 100` pattern, use `head` and `tail` parameters instead.
|
|
@@ -175,26 +175,44 @@ Good — include original `}` in the replaced range when replacement keeps `}`:
|
|
|
175
175
|
Also apply the same rule to `);`, `],`, and `},` closers: if replacement includes the closer token, `end` must include the original closer line.
|
|
176
176
|
</example>
|
|
177
177
|
|
|
178
|
-
<example name="insert between
|
|
178
|
+
<example name="insert between sibling declarations">
|
|
179
179
|
```ts
|
|
180
|
-
{{hlinefull 44 "
|
|
181
|
-
{{hlinefull 45 "
|
|
180
|
+
{{hlinefull 44 "function x() {"}}
|
|
181
|
+
{{hlinefull 45 " runX();"}}
|
|
182
|
+
{{hlinefull 46 "}"}}
|
|
183
|
+
{{hlinefull 47 ""}}
|
|
184
|
+
{{hlinefull 48 "function y() {"}}
|
|
185
|
+
{{hlinefull 49 " runY();"}}
|
|
186
|
+
{{hlinefull 50 "}"}}
|
|
182
187
|
```
|
|
183
188
|
```
|
|
184
189
|
{
|
|
185
190
|
path: "…",
|
|
186
191
|
edits: [{
|
|
187
192
|
op: "prepend",
|
|
188
|
-
pos: "{{hlineref
|
|
189
|
-
lines: [
|
|
193
|
+
pos: "{{hlineref 48 "function y() {"}}",
|
|
194
|
+
lines: [
|
|
195
|
+
"function z() {",
|
|
196
|
+
" runZ();",
|
|
197
|
+
"}",
|
|
198
|
+
""
|
|
199
|
+
]
|
|
190
200
|
}]
|
|
191
201
|
}
|
|
192
202
|
```
|
|
193
203
|
Result:
|
|
194
204
|
```ts
|
|
195
|
-
{{hlinefull 44 "
|
|
196
|
-
{{hlinefull 45 "
|
|
197
|
-
{{hlinefull 46 "
|
|
205
|
+
{{hlinefull 44 "function x() {"}}
|
|
206
|
+
{{hlinefull 45 " runX();"}}
|
|
207
|
+
{{hlinefull 46 "}"}}
|
|
208
|
+
{{hlinefull 47 ""}}
|
|
209
|
+
{{hlinefull 48 "function z() {"}}
|
|
210
|
+
{{hlinefull 49 " runZ();"}}
|
|
211
|
+
{{hlinefull 50 "}"}}
|
|
212
|
+
{{hlinefull 51 ""}}
|
|
213
|
+
{{hlinefull 52 "function y() {"}}
|
|
214
|
+
{{hlinefull 53 " runY();"}}
|
|
215
|
+
{{hlinefull 54 "}"}}
|
|
198
216
|
```
|
|
199
217
|
</example>
|
|
200
218
|
|