Merge remote-tracking branch 'upstream/main' into feat/secret-friendly-names
# Conflicts: # packages/coding-agent/test/sdk-session-isolation.test.ts
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
|
||||
import chalk from "chalk";
|
||||
import { CLI_THINKING_LEVELS, type ConfiguredThinkingLevel, parseCliThinkingLevel } from "../thinking";
|
||||
import { BUILTIN_TOOL_NAMES } from "../tools/builtin-names";
|
||||
import { BUILTIN_TOOL_NAMES, normalizeToolNames } from "../tools/builtin-names";
|
||||
import {
|
||||
OPTIONAL_FLAGS,
|
||||
OPTIONAL_VALUE_FLAGS,
|
||||
@@ -90,6 +90,7 @@ const PARSE_DEPS: ParseDeps = {
|
||||
logger,
|
||||
parseThinking: parseCliThinkingLevel,
|
||||
builtinToolNames: BUILTIN_TOOL_NAMES,
|
||||
normalizeToolNames,
|
||||
thinkingEfforts: CLI_THINKING_LEVELS,
|
||||
};
|
||||
|
||||
@@ -310,6 +311,8 @@ export function getExtraHelpText(): string {
|
||||
PERPLEXITY_API_KEY - Perplexity web search API key (optional; anonymous fallback)
|
||||
PERPLEXITY_COOKIES - Perplexity web search (session cookie)
|
||||
TAVILY_API_KEY - Tavily web search
|
||||
TINYFISH_API_KEY - TinyFish web search
|
||||
FIRECRAWL_API_KEY - Firecrawl web search
|
||||
ANTHROPIC_SEARCH_API_KEY - Anthropic web search (override; isolates search from main ANTHROPIC_API_KEY)
|
||||
ANTHROPIC_SEARCH_BASE_URL - Anthropic web search base URL (override; pairs with ANTHROPIC_SEARCH_API_KEY)
|
||||
|
||||
@@ -330,7 +333,7 @@ ${chalk.bold("Available Tools (default-enabled unless noted):")}
|
||||
edit - Edit files with find/replace
|
||||
write - Write files (creates/overwrites)
|
||||
grep - Search file contents
|
||||
find - Find files by glob pattern
|
||||
glob - Find files by glob pattern
|
||||
lsp - Language server protocol (code intelligence)
|
||||
python - Execute Python code (requires: ${APP_NAME} setup python)
|
||||
notebook - Edit Jupyter notebooks
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
type OAuthCredential,
|
||||
type OAuthProvider,
|
||||
type OAuthProviderInfo,
|
||||
PASTE_CODE_LOGIN_PROVIDERS,
|
||||
PROVIDER_REGISTRY,
|
||||
SqliteAuthCredentialStore,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
@@ -211,6 +212,15 @@ async function runLocalLogin(provider: OAuthProvider): Promise<void> {
|
||||
const storage = new AuthStorage(store);
|
||||
await storage.reload();
|
||||
try {
|
||||
// Only paste-code providers (fixed non-loopback redirect, e.g. GitLab Duo
|
||||
// Agent's vscode:// URI) get the manual paste fallback. An explicit
|
||||
// `onManualCodeInput` is honored for ANY provider (the storage escape hatch),
|
||||
// so for loopback providers we must not pass it: it would make
|
||||
// `OAuthCallbackFlow` race a readline prompt against the HTTP callback and, if
|
||||
// the callback wins, leave that prompt outstanding (dirty/blocked terminal).
|
||||
// `AuthStorage.login` independently refuses to synthesize the default prompt
|
||||
// for non-paste-code providers, so this is defense-in-depth on the same gate.
|
||||
const usesManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(provider);
|
||||
await storage.login(provider, {
|
||||
onAuth({ url, instructions }) {
|
||||
process.stdout.write(`\nOpen this URL in your browser:\n${url}\n`);
|
||||
@@ -223,6 +233,13 @@ async function runLocalLogin(provider: OAuthProvider): Promise<void> {
|
||||
onPrompt(p) {
|
||||
return ask(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
|
||||
},
|
||||
...(usesManualInput
|
||||
? {
|
||||
onManualCodeInput() {
|
||||
return ask("Paste the authorization code (or full redirect URL):");
|
||||
},
|
||||
}
|
||||
: undefined),
|
||||
});
|
||||
process.stdout.write(`\nCredentials saved to ${getAgentDbPath()}\n`);
|
||||
} finally {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Settings,
|
||||
type SettingValue,
|
||||
settings,
|
||||
validateProviderMaxInFlightRequests,
|
||||
} from "../config/settings";
|
||||
import { SETTINGS_SCHEMA } from "../config/settings-schema";
|
||||
import { theme } from "../modes/theme/theme";
|
||||
@@ -218,6 +219,9 @@ function parseAndSetValue(path: SettingPath, rawValue: string): void {
|
||||
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
throw new Error(`Invalid record JSON: ${rawValue}`);
|
||||
}
|
||||
if (path === "providers.maxInFlightRequests") {
|
||||
parsed = validateProviderMaxInFlightRequests(parsed);
|
||||
}
|
||||
parsedValue = parsed;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface ParseDeps {
|
||||
logger: { warn: (message: string, meta?: Record<string, unknown>) => void };
|
||||
parseThinking: (value: string | null | undefined) => ConfiguredThinkingLevel | undefined;
|
||||
builtinToolNames: readonly string[];
|
||||
normalizeToolNames: (values: Iterable<string>) => string[];
|
||||
thinkingEfforts: readonly string[];
|
||||
}
|
||||
|
||||
@@ -147,10 +148,12 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
|
||||
result.models = value.split(",").map(s => s.trim());
|
||||
},
|
||||
"--tools": (result, value, deps) => {
|
||||
const names = value
|
||||
.split(",")
|
||||
.map(s => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const names = deps.normalizeToolNames(
|
||||
value
|
||||
.split(",")
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const valid: string[] = [];
|
||||
for (const name of names) {
|
||||
if (deps.builtinToolNames.includes(name)) {
|
||||
|
||||
@@ -138,14 +138,26 @@ export async function renderGalleryState(
|
||||
return await fixture.renderState(state, width, expanded);
|
||||
}
|
||||
|
||||
const tool = fakeToolFor(name, fixture);
|
||||
// A non-customRendered fixture may borrow another tool's built-in renderer
|
||||
// (e.g. `edit_delete` → `edit`): drive the component under that real tool
|
||||
// name so the sample exercises the exact production branch, not the
|
||||
// custom-tool one (which tints/pads non-framed result rows).
|
||||
const componentName = fixture.customRendered ? name : (fixture.renderer ?? name);
|
||||
const tool = fakeToolFor(componentName, fixture);
|
||||
const streamingArgs = state === "streaming" ? (fixture.streamingArgs ?? fixture.args) : fixture.args;
|
||||
// The component only calls `requestRender`/`requestComponentRender` (via
|
||||
// its loader) during a static render; `imageBudget` is consulted solely
|
||||
// when images render, which the gallery disables. A cast avoids
|
||||
// constructing a real terminal.
|
||||
const ui = { requestRender() {}, requestComponentRender() {} } as unknown as TUI;
|
||||
const component = new ToolExecutionComponent(name, streamingArgs, { showImages: false }, tool, ui, getProjectDir());
|
||||
const component = new ToolExecutionComponent(
|
||||
componentName,
|
||||
streamingArgs,
|
||||
{ showImages: false },
|
||||
tool,
|
||||
ui,
|
||||
getProjectDir(),
|
||||
);
|
||||
component.setExpanded(expanded);
|
||||
|
||||
if (state !== "streaming") {
|
||||
|
||||
@@ -62,6 +62,66 @@ export const editFixtures: Record<string, GalleryFixture> = {
|
||||
},
|
||||
},
|
||||
|
||||
edit_delete: {
|
||||
label: "Delete",
|
||||
// The registry has no `edit_delete` key, so `renderer: "edit"` routes this
|
||||
// fixture through the real built-in edit renderer (see the harness in
|
||||
// `gallery-cli`), keeping the sample identical to a production delete.
|
||||
renderer: "edit",
|
||||
streamingArgs: { file_path: "scripts/prune-changelogs.ts", op: "delete" },
|
||||
args: { file_path: "scripts/prune-changelogs.ts", op: "delete" },
|
||||
result: {
|
||||
content: [{ type: "text", text: "Deleted scripts/prune-changelogs.ts" }],
|
||||
details: {
|
||||
op: "delete",
|
||||
path: "scripts/prune-changelogs.ts",
|
||||
diff: "",
|
||||
oldText: "#!/usr/bin/env bun\n// obsolete changelog pruning helper\n",
|
||||
},
|
||||
},
|
||||
errorResult: {
|
||||
content: [{ type: "text", text: "Edit failed: scripts/prune-changelogs.ts not found" }],
|
||||
isError: true,
|
||||
details: {
|
||||
op: "delete",
|
||||
path: "scripts/prune-changelogs.ts",
|
||||
diff: "",
|
||||
errorText: "Cannot delete scripts/prune-changelogs.ts: the file does not exist.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
edit_move: {
|
||||
label: "Move",
|
||||
renderer: "edit",
|
||||
streamingArgs: { file_path: "scripts/prune-changelogs.ts", rename: "scripts/archived/prune-changelogs.ts" },
|
||||
args: { file_path: "scripts/prune-changelogs.ts", rename: "scripts/archived/prune-changelogs.ts" },
|
||||
result: {
|
||||
content: [{ type: "text", text: "Moved scripts/prune-changelogs.ts to scripts/archived/prune-changelogs.ts" }],
|
||||
details: {
|
||||
op: "update",
|
||||
path: "scripts/archived/prune-changelogs.ts",
|
||||
move: "scripts/archived/prune-changelogs.ts",
|
||||
sourcePath: "scripts/prune-changelogs.ts",
|
||||
diff: "",
|
||||
},
|
||||
},
|
||||
errorResult: {
|
||||
content: [
|
||||
{ type: "text", text: "Edit failed: destination scripts/archived/prune-changelogs.ts already exists" },
|
||||
],
|
||||
isError: true,
|
||||
details: {
|
||||
op: "update",
|
||||
path: "scripts/archived/prune-changelogs.ts",
|
||||
move: "scripts/archived/prune-changelogs.ts",
|
||||
sourcePath: "scripts/prune-changelogs.ts",
|
||||
diff: "",
|
||||
errorText: "MV destination scripts/archived/prune-changelogs.ts already exists.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
apply_patch: {
|
||||
label: "Apply Patch",
|
||||
editMode: "apply_patch",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: sample source-code strings (read fixtures) intentionally contain literal ${...}.
|
||||
// Gallery fixtures for the filesystem tools (read, write, find).
|
||||
// Gallery fixtures for the filesystem tools (read, write, glob).
|
||||
import { ReadToolGroupComponent } from "../../modes/components/read-tool-group";
|
||||
import type { GalleryFixture, GalleryFixtureState, GalleryResult } from "./types";
|
||||
|
||||
const readSnippet = [
|
||||
"export const findToolRenderer = {",
|
||||
"export const globToolRenderer = {",
|
||||
"\tinline: true,",
|
||||
"\trenderCall(args: FindRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {",
|
||||
"\trenderCall(args: GlobRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {",
|
||||
"\t\tconst meta: string[] = [];",
|
||||
"\t\tif (args.limit !== undefined) meta.push(`limit:${args.limit}`);",
|
||||
"",
|
||||
"\t\tconst text = renderStatusLine(",
|
||||
'\t\t\t{ icon: "pending", title: "Find", description: formatFindRenderPaths(args.paths) || "*", meta },',
|
||||
'\t\t\t{ icon: "pending", title: "Glob", description: formatGlobRenderPaths(args.paths) || "*", meta },',
|
||||
"\t\t\tuiTheme,",
|
||||
"\t\t);",
|
||||
"\t\treturn new Text(text, 0, 0);",
|
||||
@@ -99,22 +99,22 @@ export const fsFixtures: Record<string, GalleryFixture> = {
|
||||
read: {
|
||||
label: "Read",
|
||||
// Streaming: path still being typed, selector not yet appended.
|
||||
streamingArgs: { path: "packages/coding-agent/src/tools/find" },
|
||||
args: { path: "packages/coding-agent/src/tools/find.ts:437-448" },
|
||||
streamingArgs: { path: "packages/coding-agent/src/tools/glob" },
|
||||
args: { path: "packages/coding-agent/src/tools/glob.ts:437-448" },
|
||||
result: {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
"[packages/coding-agent/src/tools/find.ts#E48E]",
|
||||
"437:export const findToolRenderer = {",
|
||||
"[packages/coding-agent/src/tools/glob.ts#E48E]",
|
||||
"437:export const globToolRenderer = {",
|
||||
"438:\tinline: true,",
|
||||
"439:\trenderCall(args: FindRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {",
|
||||
"439:\trenderCall(args: GlobRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {",
|
||||
"440:\t\tconst meta: string[] = [];",
|
||||
"441:\t\tif (args.limit !== undefined) meta.push(`limit:${args.limit}`);",
|
||||
"442:",
|
||||
"443:\t\tconst text = renderStatusLine(",
|
||||
'444:\t\t\t{ icon: "pending", title: "Find", description: formatFindRenderPaths(args.paths) || "*", meta },',
|
||||
'444:\t\t\t{ icon: "pending", title: "Glob", description: formatGlobRenderPaths(args.paths) || "*", meta },',
|
||||
"445:\t\t\tuiTheme,",
|
||||
"446:\t\t);",
|
||||
"447:\t\treturn new Text(text, 0, 0);",
|
||||
@@ -124,7 +124,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
|
||||
],
|
||||
details: {
|
||||
kind: "file",
|
||||
resolvedPath: "/Users/dev/Projects/pi/packages/coding-agent/src/tools/find.ts",
|
||||
resolvedPath: "/Users/dev/Projects/pi/packages/coding-agent/src/tools/glob.ts",
|
||||
contentType: "text/typescript",
|
||||
displayContent: { text: readSnippet, startLine: 437 },
|
||||
},
|
||||
@@ -134,7 +134,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: ENOENT: no such file or directory, open 'packages/coding-agent/src/tools/find.ts'",
|
||||
text: "Error: ENOENT: no such file or directory, open 'packages/coding-agent/src/tools/glob.ts'",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -179,8 +179,8 @@ export const fsFixtures: Record<string, GalleryFixture> = {
|
||||
},
|
||||
},
|
||||
|
||||
find: {
|
||||
label: "Find",
|
||||
glob: {
|
||||
label: "Glob",
|
||||
// Streaming: glob half-typed, no limit yet.
|
||||
streamingArgs: { paths: ["packages/coding-agent/src/tools/*-render"] },
|
||||
args: { paths: ["packages/coding-agent/src/**/*.test.ts"], limit: 50 },
|
||||
@@ -191,7 +191,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
|
||||
text: [
|
||||
"packages/coding-agent/src/tools/read.test.ts",
|
||||
"packages/coding-agent/src/tools/write.test.ts",
|
||||
"packages/coding-agent/src/tools/find.test.ts",
|
||||
"packages/coding-agent/src/tools/glob.test.ts",
|
||||
"packages/coding-agent/src/cli/gallery-cli.test.ts",
|
||||
"packages/coding-agent/src/edit/edit.test.ts",
|
||||
].join("\n"),
|
||||
@@ -205,7 +205,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
|
||||
files: [
|
||||
"packages/coding-agent/src/cli/gallery-cli.test.ts",
|
||||
"packages/coding-agent/src/edit/edit.test.ts",
|
||||
"packages/coding-agent/src/tools/find.test.ts",
|
||||
"packages/coding-agent/src/tools/glob.test.ts",
|
||||
"packages/coding-agent/src/tools/read.test.ts",
|
||||
"packages/coding-agent/src/tools/write.test.ts",
|
||||
],
|
||||
@@ -213,7 +213,7 @@ export const fsFixtures: Record<string, GalleryFixture> = {
|
||||
},
|
||||
errorResult: {
|
||||
isError: true,
|
||||
content: [{ type: "text", text: "Find failed: invalid glob pattern '[unclosed'." }],
|
||||
content: [{ type: "text", text: "Glob failed: invalid glob pattern '[unclosed'." }],
|
||||
details: { error: "invalid glob pattern '[unclosed'" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/** Gallery fixtures for the search tools (search, search_tool_bm25, ast_grep). */
|
||||
/** Gallery fixtures for the search tools (grep, search_tool_bm25, ast_grep). */
|
||||
import type { GalleryFixture } from "./types";
|
||||
|
||||
export const searchFixtures: Record<string, GalleryFixture> = {
|
||||
search: {
|
||||
label: "Search",
|
||||
grep: {
|
||||
label: "Grep",
|
||||
streamingArgs: {
|
||||
pattern: "useState",
|
||||
},
|
||||
@@ -101,7 +101,7 @@ export const searchFixtures: Record<string, GalleryFixture> = {
|
||||
limit: 5,
|
||||
total_tools: 142,
|
||||
activated_tools: ["docling_extract_tables", "docling_convert", "pdf_read_text"],
|
||||
active_selected_tools: ["read", "search", "edit", "bash"],
|
||||
active_selected_tools: ["read", "grep", "edit", "bash"],
|
||||
tools: [
|
||||
{
|
||||
name: "docling_extract_tables",
|
||||
|
||||
@@ -326,7 +326,26 @@ function formatAccountHeader(
|
||||
const planType = report.metadata?.planType;
|
||||
if (typeof planType === "string" && planType) header += chalk.dim(` · plan: ${planType}`);
|
||||
const savedResets = report.resetCredits?.availableCount ?? 0;
|
||||
if (savedResets > 0) header += chalk.cyan(` · ✦ ${savedResets} saved reset${savedResets === 1 ? "" : "s"}`);
|
||||
if (savedResets > 0) {
|
||||
header += chalk.cyan(` · ✦ ${savedResets} saved reset${savedResets === 1 ? "" : "s"}`);
|
||||
const credits = report.resetCredits?.credits;
|
||||
if (credits) {
|
||||
const expiries = credits
|
||||
.filter(c => c.expiresAt)
|
||||
.map(c => ({ date: c.expiresAt!, ms: Date.parse(c.expiresAt!) }))
|
||||
.filter(c => !Number.isNaN(c.ms))
|
||||
.sort((a, b) => a.ms - b.ms);
|
||||
const upcoming = expiries.find(c => c.ms > nowMs);
|
||||
if (upcoming) {
|
||||
header += chalk.dim(
|
||||
` · soonest expires in ${formatDuration(upcoming.ms - nowMs)} (${upcoming.date.slice(0, 10)})`,
|
||||
);
|
||||
} else {
|
||||
const lastExpired = expiries.at(-1);
|
||||
if (lastExpired) header += chalk.dim(` · expired (${lastExpired.date.slice(0, 10)})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (report.fetchedAt && nowMs - report.fetchedAt > 90_000) {
|
||||
header += chalk.dim(` · fetched ${formatDuration(nowMs - report.fetchedAt)} ago`);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ ${chalk.bold("Arguments:")}
|
||||
|
||||
${chalk.bold("Options:")}
|
||||
--provider <name> Provider: ${PROVIDERS.join(", ")}
|
||||
--recency <value> Recency filter (Brave/Perplexity): ${RECENCY_OPTIONS.join(", ")}
|
||||
--recency <value> Recency filter (when supported): ${RECENCY_OPTIONS.join(", ")}
|
||||
-l, --limit <n> Max results to return
|
||||
--compact Render condensed output
|
||||
-h, --help Show this help
|
||||
|
||||
Reference in New Issue
Block a user