@oh-my-pi/pi-coding-agent 13.3.6 → 13.3.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 +115 -0
- package/package.json +9 -18
- package/scripts/format-prompts.ts +7 -172
- package/src/capability/mcp.ts +5 -0
- package/src/cli/args.ts +1 -0
- package/src/config/prompt-templates.ts +9 -55
- package/src/config/settings-schema.ts +24 -0
- package/src/discovery/builtin.ts +1 -0
- package/src/discovery/codex.ts +1 -2
- package/src/discovery/helpers.ts +0 -5
- package/src/discovery/mcp-json.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +1 -1
- 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/mcp/config.ts +1 -0
- package/src/mcp/oauth-flow.ts +3 -1
- package/src/mcp/types.ts +5 -0
- package/src/modes/components/settings-defs.ts +9 -0
- package/src/modes/components/status-line.ts +1 -1
- package/src/modes/controllers/mcp-command-controller.ts +6 -2
- 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/custom-system-prompt.md +0 -10
- package/src/prompts/system/subagent-user-prompt.md +2 -0
- package/src/prompts/system/system-prompt.md +12 -9
- 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/prompts/tools/task.md +0 -1
- package/src/sdk.ts +11 -5
- package/src/session/agent-session.ts +293 -83
- package/src/system-prompt.ts +3 -34
- package/src/task/executor.ts +8 -7
- package/src/task/index.ts +8 -55
- package/src/task/template.ts +2 -4
- package/src/task/types.ts +0 -5
- package/src/task/worktree.ts +6 -2
- 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/jtd-to-json-schema.ts +29 -13
- 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/submit-result.ts +143 -44
- 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
|
/**
|
package/src/mcp/config.ts
CHANGED
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface MCPOAuthConfig {
|
|
|
22
22
|
clientSecret?: string;
|
|
23
23
|
/** OAuth scopes (space-separated) */
|
|
24
24
|
scopes?: string;
|
|
25
|
+
/** Custom callback port (default: 3000) */
|
|
26
|
+
callbackPort?: number;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -37,7 +39,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
37
39
|
private config: MCPOAuthConfig,
|
|
38
40
|
ctrl: OAuthController,
|
|
39
41
|
) {
|
|
40
|
-
super(ctrl, DEFAULT_PORT, CALLBACK_PATH);
|
|
42
|
+
super(ctrl, config.callbackPort ?? DEFAULT_PORT, CALLBACK_PATH);
|
|
41
43
|
this.#resolvedClientId = this.#resolveClientId(config);
|
|
42
44
|
}
|
|
43
45
|
|
package/src/mcp/types.ts
CHANGED
|
@@ -59,6 +59,11 @@ interface MCPServerConfigBase {
|
|
|
59
59
|
timeout?: number;
|
|
60
60
|
/** Authentication configuration (optional) */
|
|
61
61
|
auth?: MCPAuthConfig;
|
|
62
|
+
/** OAuth configuration for servers requiring explicit client credentials */
|
|
63
|
+
oauth?: {
|
|
64
|
+
clientId?: string;
|
|
65
|
+
callbackPort?: number;
|
|
66
|
+
};
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
/** Stdio server configuration */
|
|
@@ -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" },
|
|
@@ -163,7 +163,7 @@ export class StatusLineComponent implements Component {
|
|
|
163
163
|
// Fire async fetch, return cached value
|
|
164
164
|
(async () => {
|
|
165
165
|
try {
|
|
166
|
-
const result = await $`git status --porcelain`.quiet().nothrow();
|
|
166
|
+
const result = await $`git --no-optional-locks status --porcelain`.quiet().nothrow();
|
|
167
167
|
|
|
168
168
|
if (result.exitCode !== 0) {
|
|
169
169
|
this.#cachedGitStatus = null;
|
|
@@ -282,9 +282,10 @@ export class MCPCommandController {
|
|
|
282
282
|
const credentialId = await this.#handleOAuthFlow(
|
|
283
283
|
oauth.authorizationUrl,
|
|
284
284
|
oauth.tokenUrl,
|
|
285
|
-
oauth.clientId ?? "",
|
|
285
|
+
oauth.clientId ?? finalConfig.oauth?.clientId ?? "",
|
|
286
286
|
"",
|
|
287
287
|
oauth.scopes ?? "",
|
|
288
|
+
finalConfig.oauth?.callbackPort,
|
|
288
289
|
);
|
|
289
290
|
finalConfig = {
|
|
290
291
|
...finalConfig,
|
|
@@ -352,6 +353,7 @@ export class MCPCommandController {
|
|
|
352
353
|
clientId: string,
|
|
353
354
|
clientSecret: string,
|
|
354
355
|
scopes: string,
|
|
356
|
+
callbackPort?: number,
|
|
355
357
|
): Promise<string> {
|
|
356
358
|
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
357
359
|
let parsedAuthUrl: URL;
|
|
@@ -377,6 +379,7 @@ export class MCPCommandController {
|
|
|
377
379
|
clientId: resolvedClientId,
|
|
378
380
|
clientSecret: clientSecret || undefined,
|
|
379
381
|
scopes: scopes || undefined,
|
|
382
|
+
callbackPort,
|
|
380
383
|
},
|
|
381
384
|
{
|
|
382
385
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
@@ -1198,9 +1201,10 @@ export class MCPCommandController {
|
|
|
1198
1201
|
const credentialId = await this.#handleOAuthFlow(
|
|
1199
1202
|
oauth.authorizationUrl,
|
|
1200
1203
|
oauth.tokenUrl,
|
|
1201
|
-
oauth.clientId ?? "",
|
|
1204
|
+
oauth.clientId ?? found.config.oauth?.clientId ?? "",
|
|
1202
1205
|
"",
|
|
1203
1206
|
oauth.scopes ?? "",
|
|
1207
|
+
found.config.oauth?.callbackPort,
|
|
1204
1208
|
);
|
|
1205
1209
|
|
|
1206
1210
|
const updated: MCPServerConfig = {
|
|
@@ -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;
|
|
@@ -40,16 +40,6 @@ If a skill covers your output, you **MUST** read `skill://<name>` before proceed
|
|
|
40
40
|
{{/list}}
|
|
41
41
|
</skills>
|
|
42
42
|
{{/if}}
|
|
43
|
-
{{#if preloadedSkills.length}}
|
|
44
|
-
Following skills are preloaded in full; you **MUST** apply instructions directly.
|
|
45
|
-
<preloaded-skills>
|
|
46
|
-
{{#list preloadedSkills join="\n"}}
|
|
47
|
-
<skill name="{{name}}">
|
|
48
|
-
{{content}}
|
|
49
|
-
</skill>
|
|
50
|
-
{{/list}}
|
|
51
|
-
</preloaded-skills>
|
|
52
|
-
{{/if}}
|
|
53
43
|
{{#if rules.length}}
|
|
54
44
|
Rules are local constraints.
|
|
55
45
|
You **MUST** read `rule://<name>` when working in that domain.
|
|
@@ -93,14 +93,6 @@ You **MUST** use the following skills, to save you time, when working in their d
|
|
|
93
93
|
{{/each}}
|
|
94
94
|
{{/if}}
|
|
95
95
|
|
|
96
|
-
{{#if preloadedSkills.length}}
|
|
97
|
-
Preloaded skills:
|
|
98
|
-
{{#each preloadedSkills}}
|
|
99
|
-
## {{name}}
|
|
100
|
-
{{content}}
|
|
101
|
-
{{/each}}
|
|
102
|
-
{{/if}}
|
|
103
|
-
|
|
104
96
|
{{#if rules.length}}
|
|
105
97
|
# Rules
|
|
106
98
|
Domain-specific rules from past experience. **MUST** read `rule://<name>` when working in their territory.
|
|
@@ -114,7 +106,7 @@ Domain-specific rules from past experience. **MUST** read `rule://<name>` when w
|
|
|
114
106
|
You **MUST** use tools to complete the task.
|
|
115
107
|
|
|
116
108
|
{{#if intentTracing}}
|
|
117
|
-
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.
|
|
118
110
|
{{/if}}
|
|
119
111
|
|
|
120
112
|
You **MUST** use the following tools, as effectively as possible, to complete the task:
|
|
@@ -155,10 +147,21 @@ You **MUST NOT** use Python or Bash when a specialized tool exists.
|
|
|
155
147
|
|
|
156
148
|
Semantic questions **MUST** be answered with semantic tools.
|
|
157
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`
|
|
158
152
|
- What uses this thing I'm about to change? → `lsp references`
|
|
159
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`)
|
|
160
155
|
{{/has}}
|
|
161
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}}
|
|
162
165
|
{{#if eagerTasks}}
|
|
163
166
|
<eager-tasks>
|
|
164
167
|
You **SHOULD** delegate work to subagents by default. Working alone is the exception, not the rule.
|