diff --git a/packages/cli/src/patches.test.ts b/packages/cli/src/patches.test.ts index 10a10f254..acca0468e 100644 --- a/packages/cli/src/patches.test.ts +++ b/packages/cli/src/patches.test.ts @@ -8,7 +8,7 @@ * See patches/README.md for details. */ import { describe, test, expect } from "bun:test"; -import { mkdtempSync, rmSync } from "fs"; +import { mkdtempSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { resolve, dirname } from "path"; import { fileURLToPath } from "url"; @@ -341,6 +341,45 @@ describe("pi-coding-agent patched runtime behavior", () => { expect(api.replaceQueuedMessages).toBeUndefined(); }); + test("_expandSkillCommand expands every /skill: token in the message", async () => { + const { AgentSession } = await import( + piCodingAgentPath("dist/core/agent-session.js") + ); + const skillDir = mkdtempSync(resolve(tmpdir(), "pizzapi-skills-")); + try { + const makeSkill = (name: string) => { + const filePath = resolve(skillDir, `${name}.md`); + writeFileSync(filePath, `# ${name}\n\nBody of ${name}\n`); + return { name, filePath, baseDir: skillDir }; + }; + const alpha = makeSkill("alpha"); + const beta = makeSkill("beta"); + const ctx = { + resourceLoader: { getSkills: () => ({ skills: [alpha, beta] }) }, + _extensionRunner: { emitError() {} }, + }; + const expand = (text: string) => + (AgentSession as any).prototype._expandSkillCommand.call(ctx, text); + + // Leading skill keeps upstream semantics: trailing text becomes args. + const leading = expand("/skill:alpha do the thing @file.txt"); + expect(leading).toContain('(null); + const { atMentionOpen, setAtMentionOpen, @@ -469,8 +472,10 @@ export function SessionViewer({ }, [pendingQuestion, sessionId]); // ── Highlighted command value (for cmdk data-selected) ─────────────────── + const skillOnlyMode = commandOpen && skillTriggerOffset !== null; const commandHighlightedValue = React.useMemo(() => { if (!commandOpen) return ""; + if (skillOnlyMode) return skillSuggestions[commandHighlightedIndex]?.name ?? ""; if (isResumeMode) return resumeCandidates[commandHighlightedIndex]?.path ?? ""; if (isRewindMode) return rewindCandidates[commandHighlightedIndex]?.entryId ?? ""; if (isAgentMode) return agentCandidates[commandHighlightedIndex]?.name ?? ""; @@ -485,6 +490,7 @@ export function SessionViewer({ return combined[commandHighlightedIndex]?.name ?? ""; }, [ commandOpen, + skillOnlyMode, isResumeMode, isRewindMode, isAgentMode, @@ -501,6 +507,7 @@ export function SessionViewer({ const commandOptionCount = React.useMemo(() => { if (!commandOpen) return 0; + if (skillOnlyMode) return skillSuggestions.length; if (isResumeMode) return resumeCandidates.length; if (isRewindMode) return rewindCandidates.length; if (isAgentMode) return agentCandidates.length; @@ -508,6 +515,7 @@ export function SessionViewer({ return commandSuggestions.length + extensionSuggestions.length + promptSuggestions.length + skillSuggestions.length; }, [ commandOpen, + skillOnlyMode, isResumeMode, resumeCandidates.length, isRewindMode, @@ -549,6 +557,34 @@ export function SessionViewer({ }, [sessionId, viewerStatus]); // ── handleSubmit ────────────────────────────────────────────────────────── + /** Accept a skill suggestion, inserting it at the trigger (mid-message) or replacing the draft (leading "/"). */ + const pickSkillSuggestion = React.useCallback( + (skillName: string) => { + if (skillTriggerOffset !== null) { + const textarea = promptRef.current; + const cursorPos = textarea?.selectionStart ?? inputRef.current.length; + const value = inputRef.current; + const newValue = + value.slice(0, skillTriggerOffset) + "/" + skillName + " " + value.slice(cursorPos); + setInput(newValue); + const newCursorPosition = skillTriggerOffset + 1 + skillName.length + 1; + requestAnimationFrame(() => { + if (!textarea) return; + textarea.setSelectionRange(newCursorPosition, newCursorPosition); + textarea.focus(); + }); + } else { + setInput(`/${skillName} `); + } + setCommandQuery(""); + setCommandOpen(false); + setSkillTriggerOffset(null); + setCommandHighlightedIndex(0); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [skillTriggerOffset, setInput, inputRef, promptRef], + ); + const handleSubmit = React.useCallback( async (message: PromptInputMessage): Promise => { if (!composerReady) { @@ -1183,7 +1219,10 @@ export function SessionViewer({ className="w-full" value={commandHighlightedValue} onValueChange={(v) => { - if (isResumeMode) { + if (skillOnlyMode) { + const idx = skillSuggestions.findIndex((c) => c.name.toLowerCase() === v.toLowerCase()); + if (idx !== -1) setCommandHighlightedIndex(idx); + } else if (isResumeMode) { const idx = resumeCandidates.findIndex((s) => s.path.toLowerCase() === v.toLowerCase()); if (idx !== -1) setCommandHighlightedIndex(idx); } else if (isRewindMode) { @@ -1204,9 +1243,9 @@ export function SessionViewer({ >
- {isResumeMode ? "Resume session" : isRewindMode ? "Rewind conversation" : isAgentMode ? "Start as agent" : subCommandMode.active ? `/${subCommandMode.parentCommand}` : "Commands"} + {isResumeMode ? "Resume session" : isRewindMode ? "Rewind conversation" : isAgentMode ? "Start as agent" : skillOnlyMode ? "Skills" : subCommandMode.active ? `/${subCommandMode.parentCommand}` : "Commands"} -
@@ -1254,6 +1293,25 @@ export function SessionViewer({ ))} + ) : skillOnlyMode ? ( + <> + No matching skills + + {skillSuggestions.map((skill) => ( + { + pickSkillSuggestion(skill.name); + }}> +
+
+ + /{skill.name} +
+ {skill.description && {skill.description}} +
+
+ ))} +
+ ) : isAgentMode ? ( <> {agentsLoading ? "Loading agents…" : "No agents found"} @@ -1370,10 +1428,7 @@ export function SessionViewer({ {skillSuggestions.map((skill) => ( { - setInput(`/${skill.name} `); - setCommandQuery(""); - setCommandOpen(false); - requestAnimationFrame(() => promptRef.current?.focus()); + pickSkillSuggestion(skill.name); }}>
@@ -1481,22 +1536,51 @@ export function SessionViewer({ setComposerError(null); setInput(next); - const trimmed = next.trimStart(); - if (trimmed.startsWith("/")) { - const { open, query } = resolveCommandPopoverState(trimmed.slice(1), knownCommandNames, keepPopoverOpenNames); - setCommandOpen(open); - setCommandQuery(query); - if (atMentionOpen) { - setAtMentionOpen(false); - setAtMentionQuery(""); - setAtMentionPath(""); - setAtMentionTriggerOffset(0); + // Detect an active @-mention query at the cursor first: + // it takes precedence over the slash-command popover so a + // message can combine skills (/skill:x) with @mentions. + const cursorPosForAt = event.currentTarget.selectionStart ?? next.length; + const scan = scanAtMentionTrigger(next, cursorPosForAt); + + if (scan.triggerOffset === null) { + // Mid-message "/skill:…" token → skill-only suggestions, + // so a second (or third) skill can be picked after the first. + const slash = scanSlashCommandToken(next, cursorPosForAt); + if (slash && slash.offset > 0) { + const t = slash.token.toLowerCase(); + if (t === "" || "skill:".startsWith(t) || t.startsWith("skill:")) { + setCommandOpen(true); + setCommandQuery(slash.token); + setSkillTriggerOffset(slash.offset); + if (atMentionOpen) { + setAtMentionOpen(false); + setAtMentionQuery(""); + setAtMentionPath(""); + setAtMentionTriggerOffset(0); + } + return; + } + } + + const trimmed = next.trimStart(); + if (trimmed.startsWith("/")) { + const { open, query } = resolveCommandPopoverState(trimmed.slice(1), knownCommandNames, keepPopoverOpenNames); + setCommandOpen(open); + setCommandQuery(query); + setSkillTriggerOffset(null); + if (atMentionOpen) { + setAtMentionOpen(false); + setAtMentionQuery(""); + setAtMentionPath(""); + setAtMentionTriggerOffset(0); + } + return; } - return; } setCommandOpen(false); setCommandQuery(""); + setSkillTriggerOffset(null); if (!runnerId) { if (atMentionOpen) { @@ -1508,30 +1592,7 @@ export function SessionViewer({ return; } - const cursorPos = event.currentTarget.selectionStart ?? next.length; - let lastAtIndex = -1; - for (let i = cursorPos - 1; i >= 0; i--) { - if (next[i] === "@") { - if (i === 0 || next[i - 1] === " " || next[i - 1] === "\n" || next[i - 1] === "\t") { - lastAtIndex = i; - break; - } - } - } - - if (lastAtIndex === -1) { - if (atMentionOpen) { - setAtMentionOpen(false); - setAtMentionQuery(""); - setAtMentionPath(""); - setAtMentionTriggerOffset(0); - } - return; - } - - const query = next.slice(lastAtIndex + 1, cursorPos); - const spaceInQuery = query.search(/\s/); - if (spaceInQuery !== -1) { + if (scan.triggerOffset === null) { if (atMentionOpen) { setAtMentionOpen(false); setAtMentionQuery(""); @@ -1542,11 +1603,11 @@ export function SessionViewer({ } setAtMentionOpen(true); - setAtMentionTriggerOffset(lastAtIndex); - setAtMentionQuery(query); - const lastSlash = query.lastIndexOf("/"); + setAtMentionTriggerOffset(scan.triggerOffset); + setAtMentionQuery(scan.query); + const lastSlash = scan.query.lastIndexOf("/"); if (lastSlash !== -1) { - setAtMentionPath(query.slice(0, lastSlash + 1)); + setAtMentionPath(scan.query.slice(0, lastSlash + 1)); } else { setAtMentionPath(""); } @@ -1623,10 +1684,33 @@ export function SessionViewer({ event.stopPropagation(); setCommandOpen(false); setCommandQuery(""); + setSkillTriggerOffset(null); setCommandHighlightedIndex(0); return; } + // Mid-message skill picker: navigate + insert at the token. + if (skillOnlyMode && !isTouchDevice) { + const skills = skillSuggestions; + if (event.key === "ArrowDown" || event.key === "ArrowUp") { + event.preventDefault(); + if (skills.length === 0) return; + setCommandHighlightedIndex((prev) => { + if (event.key === "ArrowDown") return prev < skills.length - 1 ? prev + 1 : 0; + return prev > 0 ? prev - 1 : skills.length - 1; + }); + return; + } + if ((event.key === "Enter" || event.key === "Tab") && !event.shiftKey) { + const highlighted = skills[commandHighlightedIndex] ?? (event.key === "Tab" ? skills[0] : undefined); + if (highlighted) { + event.preventDefault(); + pickSkillSuggestion(highlighted.name); + } + return; + } + } + if (!isTouchDevice && (event.key === "ArrowDown" || event.key === "ArrowUp")) { event.preventDefault(); const totalItems = isResumeMode diff --git a/packages/ui/src/components/session-viewer/SessionViewer.composer.test.tsx b/packages/ui/src/components/session-viewer/SessionViewer.composer.test.tsx index 580fef04a..29bef657b 100644 --- a/packages/ui/src/components/session-viewer/SessionViewer.composer.test.tsx +++ b/packages/ui/src/components/session-viewer/SessionViewer.composer.test.tsx @@ -42,11 +42,12 @@ const { act, cleanup, fireEvent, render, waitFor } = await import( ); const React = (await import("react")).default; const { TooltipProvider } = await import("@/components/ui/tooltip"); +const atMention = await import("../session-viewer/at-mention-handlers"); const { SessionViewer } = await import("../SessionViewer"); afterEach(cleanup); -function setup(options: { onSendInput?: any }) { +function setup(options: { onSendInput?: any; runnerId?: string; runnerInfo?: any }) { const view = render( React.createElement( TooltipProvider, @@ -56,6 +57,8 @@ function setup(options: { onSendInput?: any }) { messages: [], viewerStatus: "Connected", onSendInput: options.onSendInput, + runnerId: options.runnerId, + runnerInfo: options.runnerInfo, } as any), ), ); @@ -136,3 +139,31 @@ describe("SessionViewer composer clear-on-send", () => { await waitFor(() => expect(textarea.value).toBe("world")); }); }); + +describe("scanAtMentionTrigger (composer @-mention alongside slash commands)", () => { + const scan = (text: string) => atMention.scanAtMentionTrigger(text, text.length); + + test("detects @ after a skill command", () => { + expect(scan("/skill:demo @")).toEqual({ triggerOffset: 12, query: "" }); + }); + + test("detects @ query mid-message after a skill command", () => { + expect(scan("/skill:demo @src")).toEqual({ triggerOffset: 12, query: "src" }); + }); + + test("inactive when mention already completed with a space", () => { + expect(scan("/skill:demo @src/ foo")).toEqual({ triggerOffset: null, query: "" }); + }); + + test("plain slash command without @ is inactive", () => { + expect(scan("/skill:demo some args")).toEqual({ triggerOffset: null, query: "" }); + }); + + test("@ mid-word does not trigger", () => { + expect(scan("email me@test.com")).toEqual({ triggerOffset: null, query: "" }); + }); + + test("@ at start of message triggers", () => { + expect(scan("@")).toEqual({ triggerOffset: 0, query: "" }); + }); +}); diff --git a/packages/ui/src/components/session-viewer/at-mention-handlers.ts b/packages/ui/src/components/session-viewer/at-mention-handlers.ts index 59ddd9320..c306871fc 100644 --- a/packages/ui/src/components/session-viewer/at-mention-handlers.ts +++ b/packages/ui/src/components/session-viewer/at-mention-handlers.ts @@ -33,6 +33,31 @@ export interface AtMentionHandlers { export interface AtMentionResult extends AtMentionState, AtMentionHandlers {} +export interface AtMentionScan { + /** Offset of the triggering "@" character, or null when no active mention query sits at the cursor. */ + triggerOffset: number | null; + /** Text between the "@" and the cursor ("" immediately after typing "@"). */ + query: string; +} + +/** + * Scan composer text backwards from the cursor for an active @-mention trigger: + * an "@" at the start of the text or preceded by whitespace, with no whitespace + * between it and the cursor. Runs regardless of whether the text starts with + * "/" so skills and @-mentions can be combined in one message. + */ +export function scanAtMentionTrigger(text: string, cursorPos: number): AtMentionScan { + const end = Math.min(cursorPos, text.length); + for (let i = end - 1; i >= 0; i--) { + if (text[i] !== "@") continue; + if (i !== 0 && text[i - 1] !== " " && text[i - 1] !== "\n" && text[i - 1] !== "\t") continue; + const query = text.slice(i + 1, end); + if (/\s/.test(query)) return { triggerOffset: null, query: "" }; + return { triggerOffset: i, query }; + } + return { triggerOffset: null, query: "" }; +} + /** * Owns all @-mention popover state and the action handlers for file/agent selection, * directory drill-in, back navigation, and popover close. diff --git a/packages/ui/src/components/session-viewer/utils.test.ts b/packages/ui/src/components/session-viewer/utils.test.ts index 744e7c57c..195d339b1 100644 --- a/packages/ui/src/components/session-viewer/utils.test.ts +++ b/packages/ui/src/components/session-viewer/utils.test.ts @@ -12,6 +12,7 @@ import { parseToolInputArgs, extToMime, resolveCommandPopoverState, + scanSlashCommandToken, } from "./utils"; import type { RelayMessage } from "./types"; @@ -465,6 +466,37 @@ describe("extToMime", () => { // ── resolveCommandPopoverState ────────────────────────────────────────────── +describe("scanSlashCommandToken", () => { + const scan = (text: string) => scanSlashCommandToken(text, text.length); + + test("finds a mid-message /skill token after the first skill", () => { + const text = "/skill:alpha do it /skill:be"; + expect(scan(text)).toEqual({ offset: text.lastIndexOf("/"), token: "skill:be" }); + }); + + test("finds bare slash after whitespace", () => { + const text = "/skill:a stuff /"; + expect(scan(text)).toEqual({ offset: text.length - 1, token: "" }); + }); + + test("finds leading slash", () => { + expect(scan("/sk")).toEqual({ offset: 0, token: "sk" }); + }); + + test("finds partial leading token at cursor", () => { + expect(scanSlashCommandToken("/skill:a rest", 5)).toEqual({ offset: 0, token: "skil" }); + }); + + test("no trigger without a slash token at the cursor", () => { + expect(scan("plain text")).toBeNull(); + expect(scanSlashCommandToken("no slash here", 13)).toBeNull(); + }); + + test("no trigger mid-word", () => { + expect(scan("see foo/bar")).toBeNull(); + }); +}); + describe("resolveCommandPopoverState", () => { const known = new Set(["compact", "new", "resume", "skill:beads-ccpm", "skill:double-check"]); const keepOpen = new Set(["resume"]); diff --git a/packages/ui/src/components/session-viewer/utils.ts b/packages/ui/src/components/session-viewer/utils.ts index e337e8046..9a64120fa 100644 --- a/packages/ui/src/components/session-viewer/utils.ts +++ b/packages/ui/src/components/session-viewer/utils.ts @@ -223,6 +223,27 @@ export function extToMime(path: string): string { * "/resume my-session" → open, query="resume my-session" * "/unknown-thing args" → open, query="unknown-thing args" */ +export interface SlashTokenScan { + /** Offset of the triggering "/" character. */ + offset: number; + /** Token text after the "/" up to the cursor (no whitespace). */ + token: string; +} + +/** + * Find a "/"-prefixed token ending at the cursor (start of text or preceded by + * whitespace, no whitespace inside). Returns null when no such token sits at + * the cursor. Used to surface mid-message skill suggestions, mirroring how + * @-mentions work anywhere in the message. + */ +export function scanSlashCommandToken(text: string, cursorPos: number): SlashTokenScan | null { + const end = Math.min(cursorPos, text.length); + let start = end; + while (start > 0 && !/\s/.test(text[start - 1])) start--; + if (start === end || text[start] !== "/") return null; + return { offset: start, token: text.slice(start + 1, end) }; +} + export function resolveCommandPopoverState( afterSlash: string, knownNames: Set, diff --git a/patches/@earendil-works%2Fpi-coding-agent@0.84.2.patch b/patches/@earendil-works%2Fpi-coding-agent@0.84.2.patch index e8ac884fe..51f5b599d 100644 --- a/patches/@earendil-works%2Fpi-coding-agent@0.84.2.patch +++ b/patches/@earendil-works%2Fpi-coding-agent@0.84.2.patch @@ -1,6 +1,11 @@ +diff --git a/node_modules/.bun/@earendil-works+pi-coding-agent@0.84.2+36935d20367572cb/node_modules/@earendil-works/pi-coding-agent/.bun-tag-7c795784f70518c1 b/.bun-tag-7c795784f70518c1 +new file mode 100644 +index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/dist/config.js b/dist/config.js -index 0000000..0000000 100644 -@@ -358,6 +358,10 @@ +index 4600b23493be21d2316303d056ef135313dcef43..85bd9b0d26c0de32a7aabfd0e254fed05659f958 100644 +--- a/dist/config.js ++++ b/dist/config.js +@@ -358,6 +358,10 @@ export function getExamplesPath() { } /** Get path to CHANGELOG.md */ export function getChangelogPath() { @@ -11,7 +16,7 @@ index 0000000..0000000 100644 return resolve(join(getPackageDir(), "CHANGELOG.md")); } /** -@@ -391,7 +395,7 @@ +@@ -391,7 +395,7 @@ const piConfigName = pkg.piConfig?.name; export const PACKAGE_NAME = pkg.name || "@earendil-works/pi-coding-agent"; export const APP_NAME = piConfigName || "pi"; export const APP_TITLE = piConfigName ? APP_NAME : "π"; @@ -20,7 +25,7 @@ index 0000000..0000000 100644 export const VERSION = pkg.version || "0.0.0"; // e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`; -@@ -414,7 +418,8 @@ +@@ -414,7 +418,8 @@ export function getAgentDir() { if (envDir) { return expandTildePath(envDir); } @@ -30,9 +35,79 @@ index 0000000..0000000 100644 } /** Get path to user's custom themes directory */ export function getCustomThemesDir() { +diff --git a/dist/core/agent-session.js b/dist/core/agent-session.js +index 4bcf0d4b823d1cc847d1fa26a86a850761758194..ad31a0c8388e072ef932ed51b89d1bca0b1af609 100644 +--- a/dist/core/agent-session.js ++++ b/dist/core/agent-session.js +@@ -951,29 +951,40 @@ export class AgentSession { + * Emits errors via extension runner if file read fails. + */ + _expandSkillCommand(text) { +- if (!text.startsWith("/skill:")) +- return text; +- const spaceIndex = text.indexOf(" "); +- const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex); +- const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim(); +- const skill = this.resourceLoader.getSkills().skills.find((s) => s.name === skillName); +- if (!skill) +- return text; // Unknown skill, pass through +- try { +- const content = readFileSync(skill.filePath, "utf-8"); +- const body = stripFrontmatter(content).trim(); +- const skillBlock = `\nReferences are relative to ${skill.baseDir}.\n\n${body}\n`; +- return args ? `${skillBlock}\n\n${args}` : skillBlock; +- } +- catch (err) { +- // Emit error like extension commands do +- this._extensionRunner.emitError({ +- extensionPath: skill.filePath, +- event: "skill_expansion", +- error: err instanceof Error ? err.message : String(err), +- }); +- return text; // Return original on error +- } ++ // PizzaPi patch: expand every /skill: token in the message, not ++ // just a leading one, so multiple skills and @-mentions can coexist. ++ const expandOne = (skillName, args, original) => { ++ const skill = this.resourceLoader.getSkills().skills.find((s) => s.name === skillName); ++ if (!skill) ++ return original; // Unknown skill, pass through ++ try { ++ const content = readFileSync(skill.filePath, "utf-8"); ++ const body = stripFrontmatter(content).trim(); ++ const skillBlock = `\nReferences are relative to ${skill.baseDir}.\n\n${body}\n`; ++ return args ? `${skillBlock}\n\n${args}` : skillBlock; ++ } ++ catch (err) { ++ // Emit error like extension commands do ++ this._extensionRunner.emitError({ ++ extensionPath: skill.filePath, ++ event: "skill_expansion", ++ error: err instanceof Error ? err.message : String(err), ++ }); ++ return original; // Return original on error ++ } ++ }; ++ // Leading /skill: keeps upstream semantics: the rest of the text ++ // becomes that skill's arguments. ++ if (text.startsWith("/skill:")) { ++ const spaceIndex = text.indexOf(" "); ++ const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex); ++ const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim(); ++ return expandOne(skillName, args, text); ++ } ++ // Inline /skill: tokens (mid-message) expand in place, no args. ++ return text.replace(/(^|\s)(\/skill:[^\s]+)/g, (_match, sep, token) => { ++ return sep + expandOne(token.slice(7), "", token); ++ }); + } + /** + * Queue a steering message while the agent is running. diff --git a/dist/core/model-resolver.js b/dist/core/model-resolver.js -index 0000000..0000000 100644 -@@ -21,6 +21,7 @@ +index 6b2143e141ad2865616e42ae60b2f382fe443544..2c370fc2129a8a6e53e809070ff4f16f7eb40cf4 100644 +--- a/dist/core/model-resolver.js ++++ b/dist/core/model-resolver.js +@@ -21,6 +21,7 @@ export const defaultModelPerProvider = { "google-vertex": "gemini-3.1-pro-preview", "github-copilot": "gpt-5.4", openrouter: "moonshotai/kimi-k2.6", @@ -41,12 +116,13 @@ index 0000000..0000000 100644 xai: "grok-4.5", groq: "openai/gpt-oss-120b", diff --git a/dist/core/model-runtime.js b/dist/core/model-runtime.js -index 0000000..0000000 100644 -@@ -21,6 +21,32 @@ - this.operation = operation; +index 7d93ba62bdb5d47e129751e8075cbb34620322ad..fcd96e85d1b7f290f48e25df14d39fcd8b61afeb 100644 +--- a/dist/core/model-runtime.js ++++ b/dist/core/model-runtime.js +@@ -22,6 +22,32 @@ export class CredentialSynchronizationError extends Error { this.credential = credential; } -+} + } +// PATCH(pizzapi): Report OpenAI API's published context capacity instead of +// the lower short-context pricing threshold used by Pi's model catalog. +const OPENAI_CONTEXT_WINDOWS = { @@ -72,10 +148,11 @@ index 0000000..0000000 100644 + : { ...model, contextWindow }; + }), + }; - } ++} function mergeHeaders(base, override) { if (!base && !override) -@@ -131,7 +157,7 @@ + return undefined; +@@ -131,7 +157,7 @@ export class ModelRuntime { ]); } recomposeProvider(providerId) { @@ -85,8 +162,10 @@ index 0000000..0000000 100644 if (!base && !this.config.getProvider(providerId) && !extension) { this.models.deleteProvider(providerId); diff --git a/dist/index.d.ts b/dist/index.d.ts -index 0000000..0000000 100644 -@@ -13,6 +13,7 @@ +index f43bc9620f36d198d75abeeb5c84e171de0d5ffe..0d2aab1a54452a0105eb291325ea97fa99e541b7 100644 +--- a/dist/index.d.ts ++++ b/dist/index.d.ts +@@ -13,6 +13,7 @@ export { type ModelScopeDiagnostic, type ResolveCliModelResult, type ResolveMode export { type CreateModelRuntimeOptions, CredentialSynchronizationError, type CredentialSynchronizationOperation, ModelRuntime, type ModelRuntimeAuthOverrides, } from "./core/model-runtime.ts"; export type { PackageManager, PathMetadata, ProgressCallback, ProgressEvent, ResolvedPaths, ResolvedResource, } from "./core/package-manager.ts"; export { DefaultPackageManager } from "./core/package-manager.ts"; @@ -95,8 +174,10 @@ index 0000000..0000000 100644 export { DefaultResourceLoader, loadProjectContextFiles } from "./core/resource-loader.ts"; export { AgentSessionRuntime, type AgentSessionRuntimeDiagnostic, type AgentSessionServices, type CreateAgentSessionFromServicesOptions, type CreateAgentSessionOptions, type CreateAgentSessionResult, type CreateAgentSessionRuntimeFactory, type CreateAgentSessionRuntimeResult, type CreateAgentSessionServicesOptions, createAgentSession, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createBashTool, createCodingTools, createEditTool, createFindTool, createGrepTool, createLsTool, createReadOnlyTools, createReadTool, createWriteTool, type PromptTemplate, } from "./core/sdk.ts"; diff --git a/dist/index.js b/dist/index.js -index 0000000..0000000 100644 -@@ -13,6 +13,7 @@ +index 76d728eb6fa4cac493dfe1ba623ef8170235a310..9ef49b59878f45b35ccb210f8393061431b8fcc4 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -13,6 +13,7 @@ export { ModelRegistry } from "./core/model-registry.js"; export { resolveCliModel, resolveModelScopeWithDiagnostics, } from "./core/model-resolver.js"; export { CredentialSynchronizationError, ModelRuntime, } from "./core/model-runtime.js"; export { DefaultPackageManager } from "./core/package-manager.js"; @@ -105,8 +186,10 @@ index 0000000..0000000 100644 // SDK for programmatic usage export { AgentSessionRuntime, diff --git a/dist/modes/interactive/interactive-mode.js b/dist/modes/interactive/interactive-mode.js -index 0000000..0000000 100644 -@@ -36,7 +36,6 @@ +index 6a88679e03161d2c048e2abab0041e96d214e67b..823413825e1b4e272123359e1add807a1cbe28a0 100644 +--- a/dist/modes/interactive/interactive-mode.js ++++ b/dist/modes/interactive/interactive-mode.js +@@ -36,7 +36,6 @@ import { getCwdRelativePath } from "../../utils/paths.js"; import { getPiUserAgent } from "../../utils/pi-user-agent.js"; import { killTrackedDetachedChildren } from "../../utils/shell.js"; import { ensureTool } from "../../utils/tools-manager.js"; @@ -114,7 +197,7 @@ index 0000000..0000000 100644 import { ArminComponent } from "./components/armin.js"; import { AssistantMessageComponent } from "./components/assistant-message.js"; import { BashExecutionComponent } from "./components/bash-execution.js"; -@@ -777,12 +776,6 @@ +@@ -777,12 +776,6 @@ export class InteractiveMode { .catch(() => { }) .finally(() => clearTimeout(timeout)); } @@ -127,12 +210,10 @@ index 0000000..0000000 100644 // Start package update check asynchronously this.checkForPackageUpdates() .then((updates) => { -@@ -3415,29 +3408,6 @@ - showWarning(warningMessage) { - this.chatContainer.addChild(new Spacer(1)); +@@ -3417,29 +3410,6 @@ export class InteractiveMode { this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0)); -- this.ui.requestRender(); -- } + this.ui.requestRender(); + } - showNewVersionNotification(release) { - const action = theme.fg("accent", `${APP_NAME} update`); - const updateInstruction = theme.fg("muted", `New version ${release.version} is available. Run `) + action; @@ -154,6 +235,8 @@ index 0000000..0000000 100644 - } - this.chatContainer.addChild(new Text(changelogLine, 1, 0)); - this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text))); - this.ui.requestRender(); - } +- this.ui.requestRender(); +- } showPackageUpdateNotification(packages) { + const action = theme.fg("accent", `${APP_NAME} update --extensions`); + const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action; diff --git a/patches/README.md b/patches/README.md index 76a7efac3..51b229b3d 100644 --- a/patches/README.md +++ b/patches/README.md @@ -105,6 +105,7 @@ Source: https://developers.openai.com/api/docs/models/compare | File | Change | |------|--------| | `dist/config.js` | Same `.pizzapi` config-dir / flat-directory / `PIZZAPI_CHANGELOG_PATH` overrides as 0.80.6 | +| `dist/core/agent-session.js` | `_expandSkillCommand` expands **every** `/skill:` token in a message, not just a leading one (leading token keeps trailing-text-as-args semantics; inline tokens expand in place) so multiple skills and `@`-mentions can coexist in one web/TUI message | | `dist/core/model-runtime.js` | Wraps the built-in OpenAI API provider so GPT-5.4+ defaults match OpenAI's published context capacities | | `dist/core/model-resolver.js` | Same `ollama-cloud` default model (`glm-5.1`) as 0.80.6 | | `dist/modes/interactive/interactive-mode.js` | Same version-notification-UI removal as 0.80.6 (upstream shifted a few lines; hunk re-applied manually) |