Simplify Feynman theme and composer behavior
This commit is contained in:
@@ -2,60 +2,56 @@
|
|||||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||||
"name": "feynman",
|
"name": "feynman",
|
||||||
"vars": {
|
"vars": {
|
||||||
"ink": "#d9d3c7",
|
"ink": "#d3c6aa",
|
||||||
"paper": "#181614",
|
"paper": "#2d353b",
|
||||||
"paper2": "#1c1917",
|
"paper2": "#343f44",
|
||||||
"paper3": "#221f1c",
|
"paper3": "#3a464c",
|
||||||
"panel": "#27231f",
|
"panel": "#374247",
|
||||||
"moss": "#24332c",
|
"stone": "#9da9a0",
|
||||||
"moss2": "#202c26",
|
"ash": "#859289",
|
||||||
"stone": "#aaa79d",
|
"darkAsh": "#5c6a72",
|
||||||
"ash": "#909d91",
|
"sage": "#a7c080",
|
||||||
"darkAsh": "#4f4a44",
|
"teal": "#7fbbb3",
|
||||||
"oxide": "#b76e4c",
|
"rose": "#e67e80",
|
||||||
"gold": "#d0a85c",
|
"violet": "#d699b6",
|
||||||
"sage": "#86d8a4",
|
"selection": "#425047",
|
||||||
"teal": "#69d6c4",
|
"successBg": "#2f3b32",
|
||||||
"rose": "#c97b84",
|
"errorBg": "#3b3135"
|
||||||
"violet": "#a98dc6",
|
|
||||||
"selection": "#302b27",
|
|
||||||
"successBg": "#1d2520",
|
|
||||||
"errorBg": "#2b1f21"
|
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"accent": "sage",
|
"accent": "sage",
|
||||||
"border": "stone",
|
"border": "stone",
|
||||||
"borderAccent": "sage",
|
"borderAccent": "teal",
|
||||||
"borderMuted": "darkAsh",
|
"borderMuted": "darkAsh",
|
||||||
"success": "sage",
|
"success": "sage",
|
||||||
"error": "rose",
|
"error": "rose",
|
||||||
"warning": "sage",
|
"warning": "stone",
|
||||||
"muted": "stone",
|
"muted": "stone",
|
||||||
"dim": "ash",
|
"dim": "ash",
|
||||||
"text": "ink",
|
"text": "ink",
|
||||||
"thinkingText": "sage",
|
"thinkingText": "stone",
|
||||||
|
|
||||||
"selectedBg": "selection",
|
"selectedBg": "selection",
|
||||||
"userMessageBg": "moss",
|
"userMessageBg": "panel",
|
||||||
"userMessageText": "",
|
"userMessageText": "",
|
||||||
"customMessageBg": "moss2",
|
"customMessageBg": "paper2",
|
||||||
"customMessageText": "",
|
"customMessageText": "",
|
||||||
"customMessageLabel": "sage",
|
"customMessageLabel": "stone",
|
||||||
"toolPendingBg": "paper2",
|
"toolPendingBg": "paper2",
|
||||||
"toolSuccessBg": "successBg",
|
"toolSuccessBg": "successBg",
|
||||||
"toolErrorBg": "errorBg",
|
"toolErrorBg": "errorBg",
|
||||||
"toolTitle": "sage",
|
"toolTitle": "ink",
|
||||||
"toolOutput": "stone",
|
"toolOutput": "stone",
|
||||||
|
|
||||||
"mdHeading": "sage",
|
"mdHeading": "sage",
|
||||||
"mdLink": "teal",
|
"mdLink": "teal",
|
||||||
"mdLinkUrl": "stone",
|
"mdLinkUrl": "ash",
|
||||||
"mdCode": "teal",
|
"mdCode": "teal",
|
||||||
"mdCodeBlock": "ink",
|
"mdCodeBlock": "ink",
|
||||||
"mdCodeBlockBorder": "ash",
|
"mdCodeBlockBorder": "stone",
|
||||||
"mdQuote": "stone",
|
"mdQuote": "stone",
|
||||||
"mdQuoteBorder": "ash",
|
"mdQuoteBorder": "stone",
|
||||||
"mdHr": "ash",
|
"mdHr": "darkAsh",
|
||||||
"mdListBullet": "sage",
|
"mdListBullet": "sage",
|
||||||
|
|
||||||
"toolDiffAdded": "sage",
|
"toolDiffAdded": "sage",
|
||||||
@@ -82,8 +78,8 @@
|
|||||||
"bashMode": "sage"
|
"bashMode": "sage"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"pageBg": "#141210",
|
"pageBg": "#2d353b",
|
||||||
"cardBg": "#1c1917",
|
"cardBg": "#343f44",
|
||||||
"infoBg": "#27221d"
|
"infoBg": "#374247"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ const piPackageRoot = resolve(appRoot, "node_modules", "@mariozechner", "pi-codi
|
|||||||
const packageJsonPath = resolve(piPackageRoot, "package.json");
|
const packageJsonPath = resolve(piPackageRoot, "package.json");
|
||||||
const cliPath = resolve(piPackageRoot, "dist", "cli.js");
|
const cliPath = resolve(piPackageRoot, "dist", "cli.js");
|
||||||
const interactiveModePath = resolve(piPackageRoot, "dist", "modes", "interactive", "interactive-mode.js");
|
const interactiveModePath = resolve(piPackageRoot, "dist", "modes", "interactive", "interactive-mode.js");
|
||||||
const footerPath = resolve(piPackageRoot, "dist", "modes", "interactive", "components", "footer.js");
|
const interactiveThemePath = resolve(piPackageRoot, "dist", "modes", "interactive", "theme", "theme.js");
|
||||||
|
const piTuiRoot = resolve(appRoot, "node_modules", "@mariozechner", "pi-tui");
|
||||||
|
const editorPath = resolve(piTuiRoot, "dist", "components", "editor.js");
|
||||||
const workspaceRoot = resolve(appRoot, ".pi", "npm", "node_modules");
|
const workspaceRoot = resolve(appRoot, ".pi", "npm", "node_modules");
|
||||||
const webAccessPath = resolve(workspaceRoot, "pi-web-access", "index.ts");
|
const webAccessPath = resolve(workspaceRoot, "pi-web-access", "index.ts");
|
||||||
const sessionSearchIndexerPath = resolve(
|
const sessionSearchIndexerPath = resolve(
|
||||||
@@ -103,40 +105,187 @@ if (existsSync(interactiveModePath)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsSync(footerPath)) {
|
if (existsSync(interactiveThemePath)) {
|
||||||
const footerSource = readFileSync(footerPath, "utf8");
|
let themeSource = readFileSync(interactiveThemePath, "utf8");
|
||||||
const footerOriginal = [
|
const desiredGetEditorTheme = [
|
||||||
' // Add thinking level indicator if model supports reasoning',
|
"export function getEditorTheme() {",
|
||||||
' let rightSideWithoutProvider = modelName;',
|
" return {",
|
||||||
' if (state.model?.reasoning) {',
|
' borderColor: (text) => " ".repeat(text.length),',
|
||||||
' const thinkingLevel = state.thinkingLevel || "off";',
|
' bgColor: (text) => theme.bg("userMessageBg", text),',
|
||||||
' rightSideWithoutProvider =',
|
' placeholderText: "Ask Feynman to research anything",',
|
||||||
' thinkingLevel === "off" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;',
|
' placeholder: (text) => theme.fg("dim", text),',
|
||||||
' }',
|
" selectList: getSelectListTheme(),",
|
||||||
' // Prepend the provider in parentheses if there are multiple providers and there\'s enough room',
|
" };",
|
||||||
' let rightSide = rightSideWithoutProvider;',
|
"}",
|
||||||
' if (this.footerData.getAvailableProviderCount() > 1 && state.model) {',
|
|
||||||
' rightSide = `(${state.model.provider}) ${rightSideWithoutProvider}`;',
|
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const footerReplacement = [
|
themeSource = themeSource.replace(
|
||||||
' // Add thinking level indicator if model supports reasoning',
|
/export function getEditorTheme\(\) \{[\s\S]*?\n\}\nexport function getSettingsListTheme\(\) \{/m,
|
||||||
' const modelLabel = theme.fg("accent", modelName);',
|
`${desiredGetEditorTheme}\nexport function getSettingsListTheme() {`,
|
||||||
' let rightSideWithoutProvider = modelLabel;',
|
);
|
||||||
' if (state.model?.reasoning) {',
|
writeFileSync(interactiveThemePath, themeSource, "utf8");
|
||||||
' const thinkingLevel = state.thinkingLevel || "off";',
|
|
||||||
' const separator = theme.fg("dim", " • ");',
|
|
||||||
' rightSideWithoutProvider = thinkingLevel === "off"',
|
|
||||||
' ? `${modelLabel}${separator}${theme.fg("muted", "thinking off")}`',
|
|
||||||
' : `${modelLabel}${separator}${theme.getThinkingBorderColor(thinkingLevel)(thinkingLevel)}`;',
|
|
||||||
' }',
|
|
||||||
' // Prepend the provider in parentheses if there are multiple providers and there\'s enough room',
|
|
||||||
' let rightSide = rightSideWithoutProvider;',
|
|
||||||
' if (this.footerData.getAvailableProviderCount() > 1 && state.model) {',
|
|
||||||
' rightSide = `${theme.fg("muted", `(${state.model.provider})`)} ${rightSideWithoutProvider}`;',
|
|
||||||
].join("\n");
|
|
||||||
if (footerSource.includes(footerOriginal)) {
|
|
||||||
writeFileSync(footerPath, footerSource.replace(footerOriginal, footerReplacement), "utf8");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existsSync(editorPath)) {
|
||||||
|
let editorSource = readFileSync(editorPath, "utf8");
|
||||||
|
const importOriginal = 'import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils.js";';
|
||||||
|
const importReplacement = 'import { applyBackgroundToLine, getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils.js";';
|
||||||
|
if (!editorSource.includes("applyBackgroundToLine") && editorSource.includes(importOriginal)) {
|
||||||
|
editorSource = editorSource.replace(importOriginal, importReplacement);
|
||||||
|
}
|
||||||
|
const desiredRender = [
|
||||||
|
" render(width) {",
|
||||||
|
" const maxPadding = Math.max(0, Math.floor((width - 1) / 2));",
|
||||||
|
" const paddingX = Math.min(this.paddingX, maxPadding);",
|
||||||
|
" const contentWidth = Math.max(1, width - paddingX * 2);",
|
||||||
|
" // Layout width: with padding the cursor can overflow into it,",
|
||||||
|
" // without padding we reserve 1 column for the cursor.",
|
||||||
|
" const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));",
|
||||||
|
" // Store for cursor navigation (must match wrapping width)",
|
||||||
|
" this.lastWidth = layoutWidth;",
|
||||||
|
' const horizontal = this.borderColor("─");',
|
||||||
|
" const bgColor = this.theme.bgColor;",
|
||||||
|
" // Layout the text",
|
||||||
|
" const layoutLines = this.layoutText(layoutWidth);",
|
||||||
|
" // Calculate max visible lines: 30% of terminal height, minimum 5 lines",
|
||||||
|
" const terminalRows = this.tui.terminal.rows;",
|
||||||
|
" const maxVisibleLines = Math.max(5, Math.floor(terminalRows * 0.3));",
|
||||||
|
" // Find the cursor line index in layoutLines",
|
||||||
|
" let cursorLineIndex = layoutLines.findIndex((line) => line.hasCursor);",
|
||||||
|
" if (cursorLineIndex === -1)",
|
||||||
|
" cursorLineIndex = 0;",
|
||||||
|
" // Adjust scroll offset to keep cursor visible",
|
||||||
|
" if (cursorLineIndex < this.scrollOffset) {",
|
||||||
|
" this.scrollOffset = cursorLineIndex;",
|
||||||
|
" }",
|
||||||
|
" else if (cursorLineIndex >= this.scrollOffset + maxVisibleLines) {",
|
||||||
|
" this.scrollOffset = cursorLineIndex - maxVisibleLines + 1;",
|
||||||
|
" }",
|
||||||
|
" // Clamp scroll offset to valid range",
|
||||||
|
" const maxScrollOffset = Math.max(0, layoutLines.length - maxVisibleLines);",
|
||||||
|
" this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScrollOffset));",
|
||||||
|
" // Get visible lines slice",
|
||||||
|
" const visibleLines = layoutLines.slice(this.scrollOffset, this.scrollOffset + maxVisibleLines);",
|
||||||
|
" const result = [];",
|
||||||
|
' const leftPadding = " ".repeat(paddingX);',
|
||||||
|
" const rightPadding = leftPadding;",
|
||||||
|
" // Render top padding row. When background fill is active, mimic the user-message block",
|
||||||
|
" // instead of the stock editor chrome.",
|
||||||
|
" if (bgColor) {",
|
||||||
|
" if (this.scrollOffset > 0) {",
|
||||||
|
" const indicator = ` ↑ ${this.scrollOffset} more`;",
|
||||||
|
" result.push(applyBackgroundToLine(indicator, width, bgColor));",
|
||||||
|
" }",
|
||||||
|
" else {",
|
||||||
|
' result.push(applyBackgroundToLine("", width, bgColor));',
|
||||||
|
" }",
|
||||||
|
" }",
|
||||||
|
" else if (this.scrollOffset > 0) {",
|
||||||
|
" const indicator = `─── ↑ ${this.scrollOffset} more `;",
|
||||||
|
" const remaining = width - visibleWidth(indicator);",
|
||||||
|
' result.push(this.borderColor(indicator + "─".repeat(Math.max(0, remaining))));',
|
||||||
|
" }",
|
||||||
|
" else {",
|
||||||
|
" result.push(horizontal.repeat(width));",
|
||||||
|
" }",
|
||||||
|
" // Render each visible layout line",
|
||||||
|
" // Emit hardware cursor marker only when focused and not showing autocomplete",
|
||||||
|
" const emitCursorMarker = this.focused && !this.autocompleteState;",
|
||||||
|
" const showPlaceholder = this.state.lines.length === 1 &&",
|
||||||
|
' this.state.lines[0] === "" &&',
|
||||||
|
' typeof this.theme.placeholderText === "string" &&',
|
||||||
|
" this.theme.placeholderText.length > 0;",
|
||||||
|
" for (let visibleIndex = 0; visibleIndex < visibleLines.length; visibleIndex++) {",
|
||||||
|
" const layoutLine = visibleLines[visibleIndex];",
|
||||||
|
" const isFirstLayoutLine = this.scrollOffset + visibleIndex === 0;",
|
||||||
|
" let displayText = layoutLine.text;",
|
||||||
|
" let lineVisibleWidth = visibleWidth(layoutLine.text);",
|
||||||
|
" let cursorInPadding = false;",
|
||||||
|
" const isPlaceholderLine = showPlaceholder && isFirstLayoutLine;",
|
||||||
|
" // Add cursor if this line has it",
|
||||||
|
" if (isPlaceholderLine) {",
|
||||||
|
" const marker = emitCursorMarker ? CURSOR_MARKER : \"\";",
|
||||||
|
" const rawPlaceholder = this.theme.placeholderText;",
|
||||||
|
" const graphemes = [...segmenter.segment(rawPlaceholder)];",
|
||||||
|
' const firstGrapheme = graphemes[0]?.segment ?? " ";',
|
||||||
|
" const restRaw = rawPlaceholder.slice(firstGrapheme.length);",
|
||||||
|
' const restStyled = typeof this.theme.placeholder === "function"',
|
||||||
|
" ? this.theme.placeholder(restRaw)",
|
||||||
|
" : restRaw;",
|
||||||
|
' displayText = marker + `\\x1b[7m${firstGrapheme}\\x1b[27m` + restStyled;',
|
||||||
|
" lineVisibleWidth = visibleWidth(rawPlaceholder);",
|
||||||
|
" }",
|
||||||
|
" else if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {",
|
||||||
|
" const before = displayText.slice(0, layoutLine.cursorPos);",
|
||||||
|
" const after = displayText.slice(layoutLine.cursorPos);",
|
||||||
|
" // Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning)",
|
||||||
|
' const marker = emitCursorMarker ? CURSOR_MARKER : "";',
|
||||||
|
" if (after.length > 0) {",
|
||||||
|
" // Cursor is on a character (grapheme) - replace it with highlighted version",
|
||||||
|
" // Get the first grapheme from 'after'",
|
||||||
|
" const afterGraphemes = [...segmenter.segment(after)];",
|
||||||
|
' const firstGrapheme = afterGraphemes[0]?.segment || "";',
|
||||||
|
" const restAfter = after.slice(firstGrapheme.length);",
|
||||||
|
' const cursor = `\\x1b[7m${firstGrapheme}\\x1b[27m`;',
|
||||||
|
" displayText = before + marker + cursor + restAfter;",
|
||||||
|
" // lineVisibleWidth stays the same - we're replacing, not adding",
|
||||||
|
" }",
|
||||||
|
" else {",
|
||||||
|
" // Cursor is at the end - add highlighted space",
|
||||||
|
' const cursor = "\\x1b[7m \\x1b[27m";',
|
||||||
|
" displayText = before + marker + cursor;",
|
||||||
|
" lineVisibleWidth = lineVisibleWidth + 1;",
|
||||||
|
" // If cursor overflows content width into the padding, flag it",
|
||||||
|
" if (lineVisibleWidth > contentWidth && paddingX > 0) {",
|
||||||
|
" cursorInPadding = true;",
|
||||||
|
" }",
|
||||||
|
" }",
|
||||||
|
" }",
|
||||||
|
" // Calculate padding based on actual visible width",
|
||||||
|
' const padding = " ".repeat(Math.max(0, contentWidth - lineVisibleWidth));',
|
||||||
|
" const lineRightPadding = cursorInPadding ? rightPadding.slice(1) : rightPadding;",
|
||||||
|
" const renderedLine = `${leftPadding}${displayText}${padding}${lineRightPadding}`;",
|
||||||
|
" result.push(bgColor ? applyBackgroundToLine(renderedLine, width, bgColor) : renderedLine);",
|
||||||
|
" }",
|
||||||
|
" // Render bottom padding row. When background fill is active, mimic the user-message block",
|
||||||
|
" // instead of the stock editor chrome.",
|
||||||
|
" const linesBelow = layoutLines.length - (this.scrollOffset + visibleLines.length);",
|
||||||
|
" if (bgColor) {",
|
||||||
|
" if (linesBelow > 0) {",
|
||||||
|
" const indicator = ` ↓ ${linesBelow} more`;",
|
||||||
|
" result.push(applyBackgroundToLine(indicator, width, bgColor));",
|
||||||
|
" }",
|
||||||
|
" else {",
|
||||||
|
' result.push(applyBackgroundToLine("", width, bgColor));',
|
||||||
|
" }",
|
||||||
|
" }",
|
||||||
|
" else if (linesBelow > 0) {",
|
||||||
|
" const indicator = `─── ↓ ${linesBelow} more `;",
|
||||||
|
" const remaining = width - visibleWidth(indicator);",
|
||||||
|
' const bottomLine = this.borderColor(indicator + "─".repeat(Math.max(0, remaining)));',
|
||||||
|
" result.push(bottomLine);",
|
||||||
|
" }",
|
||||||
|
" else {",
|
||||||
|
" const bottomLine = horizontal.repeat(width);",
|
||||||
|
" result.push(bottomLine);",
|
||||||
|
" }",
|
||||||
|
" // Add autocomplete list if active",
|
||||||
|
" if (this.autocompleteState && this.autocompleteList) {",
|
||||||
|
" const autocompleteResult = this.autocompleteList.render(contentWidth);",
|
||||||
|
" for (const line of autocompleteResult) {",
|
||||||
|
" const lineWidth = visibleWidth(line);",
|
||||||
|
' const linePadding = " ".repeat(Math.max(0, contentWidth - lineWidth));',
|
||||||
|
" const autocompleteLine = `${leftPadding}${line}${linePadding}${rightPadding}`;",
|
||||||
|
" result.push(bgColor ? applyBackgroundToLine(autocompleteLine, width, bgColor) : autocompleteLine);",
|
||||||
|
" }",
|
||||||
|
" }",
|
||||||
|
" return result;",
|
||||||
|
" }",
|
||||||
|
].join("\n");
|
||||||
|
editorSource = editorSource.replace(
|
||||||
|
/ render\(width\) \{[\s\S]*?\n handleInput\(data\) \{/m,
|
||||||
|
`${desiredRender}\n handleInput(data) {`,
|
||||||
|
);
|
||||||
|
writeFileSync(editorPath, editorSource, "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsSync(webAccessPath)) {
|
if (existsSync(webAccessPath)) {
|
||||||
|
|||||||
258
src/index.ts
258
src/index.ts
@@ -23,190 +23,6 @@ import {
|
|||||||
import { buildFeynmanSystemPrompt } from "./feynman-prompt.js";
|
import { buildFeynmanSystemPrompt } from "./feynman-prompt.js";
|
||||||
|
|
||||||
type ThinkingLevel = "off" | "low" | "medium" | "high";
|
type ThinkingLevel = "off" | "low" | "medium" | "high";
|
||||||
type Rgb = { r: number; g: number; b: number };
|
|
||||||
type ThemeColorValue = string | number;
|
|
||||||
type ThemeJson = {
|
|
||||||
$schema?: string;
|
|
||||||
name: string;
|
|
||||||
vars?: Record<string, ThemeColorValue>;
|
|
||||||
colors: Record<string, ThemeColorValue>;
|
|
||||||
export?: Record<string, ThemeColorValue>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const OSC11_QUERY = "\u001b]11;?\u0007";
|
|
||||||
const OSC11_RESPONSE_PATTERN =
|
|
||||||
/\u001b]11;(?:rgb:([0-9a-fA-F]{2,4})\/([0-9a-fA-F]{2,4})\/([0-9a-fA-F]{2,4})|#?([0-9a-fA-F]{6}))(?:\u0007|\u001b\\)/;
|
|
||||||
const DEFAULT_SAGE_RGB: Rgb = { r: 0x88, g: 0xa8, b: 0x8a };
|
|
||||||
|
|
||||||
function parseHexComponent(component: string): number {
|
|
||||||
const value = Number.parseInt(component, 16);
|
|
||||||
if (Number.isNaN(value)) {
|
|
||||||
throw new Error(`Invalid OSC 11 component: ${component}`);
|
|
||||||
}
|
|
||||||
if (component.length === 2) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return Math.round(value / ((1 << (component.length * 4)) - 1) * 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseHexColor(color: string): Rgb | undefined {
|
|
||||||
const match = color.trim().match(/^#?([0-9a-fA-F]{6})$/);
|
|
||||||
if (!match) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
r: Number.parseInt(match[1].slice(0, 2), 16),
|
|
||||||
g: Number.parseInt(match[1].slice(2, 4), 16),
|
|
||||||
b: Number.parseInt(match[1].slice(4, 6), 16),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function rgbToHex(rgb: Rgb): string {
|
|
||||||
return `#${[rgb.r, rgb.g, rgb.b]
|
|
||||||
.map((value) => Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0"))
|
|
||||||
.join("")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function blendRgb(base: Rgb, tint: Rgb, alpha: number): Rgb {
|
|
||||||
const mix = (baseChannel: number, tintChannel: number) =>
|
|
||||||
baseChannel + (tintChannel - baseChannel) * alpha;
|
|
||||||
return {
|
|
||||||
r: mix(base.r, tint.r),
|
|
||||||
g: mix(base.g, tint.g),
|
|
||||||
b: mix(base.b, tint.b),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLightRgb(rgb: Rgb): boolean {
|
|
||||||
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
|
|
||||||
return luminance >= 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveThemeColorValue(
|
|
||||||
value: ThemeColorValue | undefined,
|
|
||||||
vars: Record<string, ThemeColorValue> | undefined,
|
|
||||||
visited = new Set<string>(),
|
|
||||||
): ThemeColorValue | undefined {
|
|
||||||
if (value === undefined || typeof value === "number" || value === "" || value.startsWith("#")) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (!vars || !(value in vars) || visited.has(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
visited.add(value);
|
|
||||||
return resolveThemeColorValue(vars[value], vars, visited);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveThemeRgb(
|
|
||||||
value: ThemeColorValue | undefined,
|
|
||||||
vars: Record<string, ThemeColorValue> | undefined,
|
|
||||||
): Rgb | undefined {
|
|
||||||
const resolved = resolveThemeColorValue(value, vars);
|
|
||||||
return typeof resolved === "string" ? parseHexColor(resolved) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deriveMessageBackgrounds(themeJson: ThemeJson, terminalBackgroundHex: string): Pick<ThemeJson["colors"], "userMessageBg" | "customMessageBg"> | undefined {
|
|
||||||
const terminalBackground = parseHexColor(terminalBackgroundHex);
|
|
||||||
if (!terminalBackground) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tint =
|
|
||||||
resolveThemeRgb(themeJson.colors.accent, themeJson.vars) ??
|
|
||||||
resolveThemeRgb(themeJson.vars?.sage, themeJson.vars) ??
|
|
||||||
DEFAULT_SAGE_RGB;
|
|
||||||
const lightBackground = isLightRgb(terminalBackground);
|
|
||||||
const userAlpha = lightBackground ? 0.15 : 0.23;
|
|
||||||
const customAlpha = lightBackground ? 0.11 : 0.17;
|
|
||||||
|
|
||||||
return {
|
|
||||||
userMessageBg: rgbToHex(blendRgb(terminalBackground, tint, userAlpha)),
|
|
||||||
customMessageBg: rgbToHex(blendRgb(terminalBackground, tint, customAlpha)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function probeTerminalBackgroundHex(timeoutMs = 120): Promise<string | undefined> {
|
|
||||||
if (typeof process.env.FEYNMAN_TERMINAL_BG === "string" && process.env.FEYNMAN_TERMINAL_BG.trim()) {
|
|
||||||
return process.env.FEYNMAN_TERMINAL_BG.trim();
|
|
||||||
}
|
|
||||||
if (typeof process.env.PI_TERMINAL_BG === "string" && process.env.PI_TERMINAL_BG.trim()) {
|
|
||||||
return process.env.PI_TERMINAL_BG.trim();
|
|
||||||
}
|
|
||||||
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wasRaw = "isRaw" in input ? Boolean((input as typeof input & { isRaw?: boolean }).isRaw) : false;
|
|
||||||
const wasFlowing = "readableFlowing" in input
|
|
||||||
? (input as typeof input & { readableFlowing?: boolean | null }).readableFlowing
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return await new Promise<string | undefined>((resolve) => {
|
|
||||||
let settled = false;
|
|
||||||
let buffer = "";
|
|
||||||
|
|
||||||
const finish = (value: string | undefined) => {
|
|
||||||
if (settled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
settled = true;
|
|
||||||
clearTimeout(timer);
|
|
||||||
input.off("data", onData);
|
|
||||||
try {
|
|
||||||
if (!wasRaw) {
|
|
||||||
input.setRawMode(false);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore raw mode restore failures and return best-effort detection.
|
|
||||||
}
|
|
||||||
if (wasFlowing !== true) {
|
|
||||||
input.pause();
|
|
||||||
}
|
|
||||||
resolve(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onData = (chunk: string | Buffer) => {
|
|
||||||
buffer += chunk.toString("utf8");
|
|
||||||
const match = buffer.match(OSC11_RESPONSE_PATTERN);
|
|
||||||
if (!match) {
|
|
||||||
if (buffer.length > 512) {
|
|
||||||
finish(undefined);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match[4]) {
|
|
||||||
finish(`#${match[4].toLowerCase()}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
finish(
|
|
||||||
rgbToHex({
|
|
||||||
r: parseHexComponent(match[1]),
|
|
||||||
g: parseHexComponent(match[2]),
|
|
||||||
b: parseHexComponent(match[3]),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
finish(undefined);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const timer = setTimeout(() => finish(undefined), timeoutMs);
|
|
||||||
input.on("data", onData);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!wasRaw) {
|
|
||||||
input.setRawMode(true);
|
|
||||||
}
|
|
||||||
output.write(OSC11_QUERY);
|
|
||||||
} catch {
|
|
||||||
finish(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function printHelp(): void {
|
function printHelp(): void {
|
||||||
console.log(`Feynman commands:
|
console.log(`Feynman commands:
|
||||||
@@ -316,7 +132,6 @@ function patchEmbeddedPiBranding(piPackageRoot: string): void {
|
|||||||
const packageJsonPath = resolve(piPackageRoot, "package.json");
|
const packageJsonPath = resolve(piPackageRoot, "package.json");
|
||||||
const cliPath = resolve(piPackageRoot, "dist", "cli.js");
|
const cliPath = resolve(piPackageRoot, "dist", "cli.js");
|
||||||
const interactiveModePath = resolve(piPackageRoot, "dist", "modes", "interactive", "interactive-mode.js");
|
const interactiveModePath = resolve(piPackageRoot, "dist", "modes", "interactive", "interactive-mode.js");
|
||||||
const footerPath = resolve(piPackageRoot, "dist", "modes", "interactive", "components", "footer.js");
|
|
||||||
|
|
||||||
if (existsSync(packageJsonPath)) {
|
if (existsSync(packageJsonPath)) {
|
||||||
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
||||||
@@ -350,42 +165,6 @@ function patchEmbeddedPiBranding(piPackageRoot: string): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsSync(footerPath)) {
|
|
||||||
const footerSource = readFileSync(footerPath, "utf8");
|
|
||||||
const footerOriginal = [
|
|
||||||
' // Add thinking level indicator if model supports reasoning',
|
|
||||||
' let rightSideWithoutProvider = modelName;',
|
|
||||||
' if (state.model?.reasoning) {',
|
|
||||||
' const thinkingLevel = state.thinkingLevel || "off";',
|
|
||||||
' rightSideWithoutProvider =',
|
|
||||||
' thinkingLevel === "off" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;',
|
|
||||||
' }',
|
|
||||||
' // Prepend the provider in parentheses if there are multiple providers and there\'s enough room',
|
|
||||||
' let rightSide = rightSideWithoutProvider;',
|
|
||||||
' if (this.footerData.getAvailableProviderCount() > 1 && state.model) {',
|
|
||||||
' rightSide = `(${state.model.provider}) ${rightSideWithoutProvider}`;',
|
|
||||||
].join("\n");
|
|
||||||
const footerReplacement = [
|
|
||||||
' // Add thinking level indicator if model supports reasoning',
|
|
||||||
' const modelLabel = theme.fg("accent", modelName);',
|
|
||||||
' let rightSideWithoutProvider = modelLabel;',
|
|
||||||
' if (state.model?.reasoning) {',
|
|
||||||
' const thinkingLevel = state.thinkingLevel || "off";',
|
|
||||||
' const separator = theme.fg("dim", " • ");',
|
|
||||||
' rightSideWithoutProvider = thinkingLevel === "off"',
|
|
||||||
' ? `${modelLabel}${separator}${theme.fg("muted", "thinking off")}`',
|
|
||||||
' : `${modelLabel}${separator}${theme.getThinkingBorderColor(thinkingLevel)(thinkingLevel)}`;',
|
|
||||||
' }',
|
|
||||||
' // Prepend the provider in parentheses if there are multiple providers and there\'s enough room',
|
|
||||||
' let rightSide = rightSideWithoutProvider;',
|
|
||||||
' if (this.footerData.getAvailableProviderCount() > 1 && state.model) {',
|
|
||||||
' rightSide = `${theme.fg("muted", `(${state.model.provider})`)} ${rightSideWithoutProvider}`;',
|
|
||||||
].join("\n");
|
|
||||||
if (footerSource.includes(footerOriginal)) {
|
|
||||||
writeFileSync(footerPath, footerSource.replace(footerOriginal, footerReplacement), "utf8");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchPackageWorkspace(appRoot: string): void {
|
function patchPackageWorkspace(appRoot: string): void {
|
||||||
@@ -495,6 +274,9 @@ function normalizeFeynmanSettings(
|
|||||||
if (!settings.defaultThinkingLevel) {
|
if (!settings.defaultThinkingLevel) {
|
||||||
settings.defaultThinkingLevel = defaultThinkingLevel;
|
settings.defaultThinkingLevel = defaultThinkingLevel;
|
||||||
}
|
}
|
||||||
|
if (settings.editorPaddingX === undefined) {
|
||||||
|
settings.editorPaddingX = 1;
|
||||||
|
}
|
||||||
settings.theme = "feynman";
|
settings.theme = "feynman";
|
||||||
settings.quietStartup = true;
|
settings.quietStartup = true;
|
||||||
settings.collapseChangelog = true;
|
settings.collapseChangelog = true;
|
||||||
@@ -749,7 +531,7 @@ function syncDirectory(sourceDir: string, targetDir: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncFeynmanTheme(appRoot: string, agentDir: string, terminalBackgroundHex?: string): void {
|
function syncFeynmanTheme(appRoot: string, agentDir: string): void {
|
||||||
const sourceThemePath = resolve(appRoot, ".pi", "themes", "feynman.json");
|
const sourceThemePath = resolve(appRoot, ".pi", "themes", "feynman.json");
|
||||||
const targetThemeDir = resolve(agentDir, "themes");
|
const targetThemeDir = resolve(agentDir, "themes");
|
||||||
const targetThemePath = resolve(targetThemeDir, "feynman.json");
|
const targetThemePath = resolve(targetThemeDir, "feynman.json");
|
||||||
@@ -759,32 +541,7 @@ function syncFeynmanTheme(appRoot: string, agentDir: string, terminalBackgroundH
|
|||||||
}
|
}
|
||||||
|
|
||||||
mkdirSync(targetThemeDir, { recursive: true });
|
mkdirSync(targetThemeDir, { recursive: true });
|
||||||
|
writeFileSync(targetThemePath, readFileSync(sourceThemePath, "utf8"), "utf8");
|
||||||
const sourceTheme = readFileSync(sourceThemePath, "utf8");
|
|
||||||
if (!terminalBackgroundHex) {
|
|
||||||
writeFileSync(targetThemePath, sourceTheme, "utf8");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsedTheme = JSON.parse(sourceTheme) as ThemeJson;
|
|
||||||
const derivedBackgrounds = deriveMessageBackgrounds(parsedTheme, terminalBackgroundHex);
|
|
||||||
if (!derivedBackgrounds) {
|
|
||||||
writeFileSync(targetThemePath, sourceTheme, "utf8");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const generatedTheme: ThemeJson = {
|
|
||||||
...parsedTheme,
|
|
||||||
colors: {
|
|
||||||
...parsedTheme.colors,
|
|
||||||
...derivedBackgrounds,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
writeFileSync(targetThemePath, JSON.stringify(generatedTheme, null, 2) + "\n", "utf8");
|
|
||||||
} catch {
|
|
||||||
writeFileSync(targetThemePath, sourceTheme, "utf8");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncFeynmanAgents(appRoot: string, agentDir: string): void {
|
function syncFeynmanAgents(appRoot: string, agentDir: string): void {
|
||||||
@@ -826,10 +583,9 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
const workingDir = resolve(values.cwd ?? process.cwd());
|
const workingDir = resolve(values.cwd ?? process.cwd());
|
||||||
const sessionDir = resolve(values["session-dir"] ?? resolve(homedir(), ".feynman", "sessions"));
|
const sessionDir = resolve(values["session-dir"] ?? resolve(homedir(), ".feynman", "sessions"));
|
||||||
const terminalBackgroundHex = await probeTerminalBackgroundHex();
|
|
||||||
mkdirSync(sessionDir, { recursive: true });
|
mkdirSync(sessionDir, { recursive: true });
|
||||||
mkdirSync(feynmanAgentDir, { recursive: true });
|
mkdirSync(feynmanAgentDir, { recursive: true });
|
||||||
syncFeynmanTheme(appRoot, feynmanAgentDir, terminalBackgroundHex);
|
syncFeynmanTheme(appRoot, feynmanAgentDir);
|
||||||
syncFeynmanAgents(appRoot, feynmanAgentDir);
|
syncFeynmanAgents(appRoot, feynmanAgentDir);
|
||||||
const feynmanSettingsPath = resolve(feynmanAgentDir, "settings.json");
|
const feynmanSettingsPath = resolve(feynmanAgentDir, "settings.json");
|
||||||
const feynmanAuthPath = resolve(feynmanAgentDir, "auth.json");
|
const feynmanAuthPath = resolve(feynmanAgentDir, "auth.json");
|
||||||
@@ -926,8 +682,6 @@ async function main(): Promise<void> {
|
|||||||
...process.env,
|
...process.env,
|
||||||
PI_CODING_AGENT_DIR: feynmanAgentDir,
|
PI_CODING_AGENT_DIR: feynmanAgentDir,
|
||||||
FEYNMAN_CODING_AGENT_DIR: feynmanAgentDir,
|
FEYNMAN_CODING_AGENT_DIR: feynmanAgentDir,
|
||||||
FEYNMAN_TERMINAL_BG: terminalBackgroundHex,
|
|
||||||
PI_TERMINAL_BG: terminalBackgroundHex,
|
|
||||||
FEYNMAN_PI_NPM_ROOT: resolve(appRoot, ".pi", "npm", "node_modules"),
|
FEYNMAN_PI_NPM_ROOT: resolve(appRoot, ".pi", "npm", "node_modules"),
|
||||||
FEYNMAN_SESSION_DIR: sessionDir,
|
FEYNMAN_SESSION_DIR: sessionDir,
|
||||||
PI_SESSION_DIR: sessionDir,
|
PI_SESSION_DIR: sessionDir,
|
||||||
|
|||||||
Reference in New Issue
Block a user