Compare commits
9 Commits
5b29f22fb4
...
f908d84a9b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f908d84a9b | ||
|
|
c6df5667f7 | ||
|
|
60095c6f61 | ||
|
|
a641087fae | ||
|
|
248667468c | ||
|
|
fbb00a49ba | ||
|
|
08ac9d02f8 | ||
|
|
9dd1c319e0 | ||
|
|
96bd163442 |
64
nvim/.config/nvim/colors/bearded-arc-light.lua
Normal file
64
nvim/.config/nvim/colors/bearded-arc-light.lua
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
-- Bearded Arc (Light) colorscheme
|
||||||
|
-- Hand-tuned light variant with warm off-white bg and saturated accents
|
||||||
|
|
||||||
|
vim.cmd("hi clear")
|
||||||
|
if vim.fn.exists("syntax_on") then
|
||||||
|
vim.cmd("syntax reset")
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.o.background = "light"
|
||||||
|
vim.g.colors_name = "bearded-arc-light"
|
||||||
|
|
||||||
|
local base_30 = {
|
||||||
|
white = "#2a2a2a",
|
||||||
|
darker_black = "#ede6dc",
|
||||||
|
black = "#faf4ed",
|
||||||
|
black2 = "#f0e8df",
|
||||||
|
one_bg = "#e8e0d4",
|
||||||
|
one_bg2 = "#ddd5c9",
|
||||||
|
one_bg3 = "#d2cabd",
|
||||||
|
grey = "#b0a89c",
|
||||||
|
grey_fg = "#8a8278",
|
||||||
|
grey_fg2 = "#7a7268",
|
||||||
|
light_grey = "#6a6258",
|
||||||
|
red = "#d1344f",
|
||||||
|
baby_pink = "#c44a8a",
|
||||||
|
pink = "#b04a7a",
|
||||||
|
line = "#ddd5c9",
|
||||||
|
green = "#1e9b52",
|
||||||
|
vibrant_green = "#158040",
|
||||||
|
nord_blue = "#2a6a9a",
|
||||||
|
blue = "#1a7db5",
|
||||||
|
yellow = "#b8890f",
|
||||||
|
sun = "#d4a017",
|
||||||
|
purple = "#7b4fc4",
|
||||||
|
dark_purple = "#6a3eb0",
|
||||||
|
teal = "#0e8a82",
|
||||||
|
orange = "#c45a1c",
|
||||||
|
cyan = "#147a8a",
|
||||||
|
statusline_bg = "#f0e8df",
|
||||||
|
lightbg = "#e8e0d4",
|
||||||
|
pmenu_bg = "#1e9b52",
|
||||||
|
folder_bg = "#1a7db5",
|
||||||
|
}
|
||||||
|
|
||||||
|
local base_16 = {
|
||||||
|
base00 = "#faf4ed",
|
||||||
|
base01 = "#f0e8df",
|
||||||
|
base02 = "#e8e0d4",
|
||||||
|
base03 = "#b0a89c",
|
||||||
|
base04 = "#5a5248",
|
||||||
|
base05 = "#3a3228",
|
||||||
|
base06 = "#2a2a2a",
|
||||||
|
base07 = "#0e8a82",
|
||||||
|
base08 = "#d1344f",
|
||||||
|
base09 = "#c45a1c",
|
||||||
|
base0A = "#b8890f",
|
||||||
|
base0B = "#1e9b52",
|
||||||
|
base0C = "#2a6a9a",
|
||||||
|
base0D = "#1a7db5",
|
||||||
|
base0E = "#147a8a",
|
||||||
|
base0F = "#7b4fc4",
|
||||||
|
}
|
||||||
|
|
||||||
|
require("bearded-arc").apply(base_30, base_16)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
-- Bearded Arc colorscheme
|
-- Bearded Arc (Dark) colorscheme
|
||||||
-- Accurate port from NvChad base46 theme with proper highlight group mappings
|
-- Thin wrapper around the highlight engine
|
||||||
|
|
||||||
vim.cmd("hi clear")
|
vim.cmd("hi clear")
|
||||||
if vim.fn.exists("syntax_on") then
|
if vim.fn.exists("syntax_on") then
|
||||||
@@ -9,7 +9,6 @@ end
|
|||||||
vim.o.background = "dark"
|
vim.o.background = "dark"
|
||||||
vim.g.colors_name = "bearded-arc"
|
vim.g.colors_name = "bearded-arc"
|
||||||
|
|
||||||
-- Base 30 color palette (UI colors)
|
|
||||||
local base_30 = {
|
local base_30 = {
|
||||||
white = "#ABB7C1",
|
white = "#ABB7C1",
|
||||||
darker_black = "#19212e",
|
darker_black = "#19212e",
|
||||||
@@ -43,7 +42,6 @@ local base_30 = {
|
|||||||
folder_bg = "#69C3FF",
|
folder_bg = "#69C3FF",
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Base 16 color palette (syntax colors)
|
|
||||||
local base_16 = {
|
local base_16 = {
|
||||||
base00 = "#1c2433",
|
base00 = "#1c2433",
|
||||||
base01 = "#262e3d",
|
base01 = "#262e3d",
|
||||||
@@ -63,277 +61,4 @@ local base_16 = {
|
|||||||
base0F = "#B78AFF",
|
base0F = "#B78AFF",
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Helper function to set highlights
|
require("bearded-arc").apply(base_30, base_16)
|
||||||
local function hi(group, opts)
|
|
||||||
local cmd = "hi " .. group
|
|
||||||
if opts.fg then cmd = cmd .. " guifg=" .. opts.fg end
|
|
||||||
if opts.bg then cmd = cmd .. " guibg=" .. opts.bg end
|
|
||||||
if opts.sp then cmd = cmd .. " guisp=" .. opts.sp end
|
|
||||||
if opts.style then cmd = cmd .. " gui=" .. opts.style end
|
|
||||||
vim.cmd(cmd)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- === UI ELEMENTS (from defaults.lua) ===
|
|
||||||
|
|
||||||
-- Line Numbers & Sidebar (CRITICAL FIX)
|
|
||||||
hi("LineNr", { fg = base_30.grey })
|
|
||||||
hi("CursorLineNr", { fg = base_30.white })
|
|
||||||
hi("SignColumn", { fg = base_16.base03 })
|
|
||||||
hi("FoldColumn", { fg = base_30.grey, bg = base_16.base00 })
|
|
||||||
hi("Folded", { fg = base_30.light_grey, bg = base_16.base01 })
|
|
||||||
|
|
||||||
-- Main Editor
|
|
||||||
hi("Normal", { fg = base_16.base05, bg = base_16.base00 })
|
|
||||||
hi("NormalFloat", { bg = base_30.darker_black })
|
|
||||||
hi("FloatBorder", { fg = base_30.blue })
|
|
||||||
hi("FloatTitle", { fg = base_30.white, bg = base_30.grey })
|
|
||||||
|
|
||||||
-- Cursor and Lines
|
|
||||||
hi("Cursor", { fg = base_16.base00, bg = base_16.base05 })
|
|
||||||
hi("CursorLine", { bg = base_30.black2 })
|
|
||||||
hi("CursorColumn", { bg = base_16.base01 })
|
|
||||||
hi("ColorColumn", { bg = base_16.base01 })
|
|
||||||
|
|
||||||
-- Visual Selection
|
|
||||||
hi("Visual", { bg = base_30.one_bg2 })
|
|
||||||
hi("VisualNOS", { bg = base_30.one_bg2 })
|
|
||||||
|
|
||||||
-- Search
|
|
||||||
hi("Search", { fg = base_16.base00, bg = base_16.base0A })
|
|
||||||
hi("IncSearch", { fg = base_16.base01, bg = base_16.base09 })
|
|
||||||
hi("Substitute", { fg = base_16.base01, bg = base_16.base0A })
|
|
||||||
|
|
||||||
-- Window Separators
|
|
||||||
hi("WinSeparator", { fg = base_30.line })
|
|
||||||
hi("WinBar", { bg = base_30.statusline_bg })
|
|
||||||
hi("WinBarNC", { bg = base_30.statusline_bg })
|
|
||||||
|
|
||||||
-- Completion Menu
|
|
||||||
hi("Pmenu", { bg = base_30.one_bg })
|
|
||||||
hi("PmenuSbar", { bg = base_30.one_bg })
|
|
||||||
hi("PmenuSel", { bg = base_30.pmenu_bg, fg = base_30.black })
|
|
||||||
hi("PmenuThumb", { bg = base_30.grey })
|
|
||||||
|
|
||||||
-- Statusline & Tabline
|
|
||||||
hi("StatusLine", { bg = base_30.statusline_bg })
|
|
||||||
hi("StatusLineNC", { bg = base_30.statusline_bg })
|
|
||||||
hi("TabLine", { fg = base_30.light_grey, bg = base_30.black2 })
|
|
||||||
hi("TabLineSel", { fg = base_30.white, bg = base_30.black })
|
|
||||||
hi("TabLineFill", { bg = base_30.black2 })
|
|
||||||
|
|
||||||
-- Messages & Prompts
|
|
||||||
hi("ErrorMsg", { fg = base_16.base08, bg = base_16.base00 })
|
|
||||||
hi("WarningMsg", { fg = base_16.base0A })
|
|
||||||
hi("MoreMsg", { fg = base_16.base0B })
|
|
||||||
hi("ModeMsg", { fg = base_16.base0B })
|
|
||||||
hi("Question", { fg = base_16.base0D })
|
|
||||||
|
|
||||||
-- Special Elements
|
|
||||||
hi("MatchParen", { bg = base_30.grey, fg = base_30.white })
|
|
||||||
hi("NonText", { fg = base_16.base03 })
|
|
||||||
hi("SpecialKey", { fg = base_16.base03 })
|
|
||||||
hi("Conceal", { fg = base_16.base0D, bg = base_16.base00 })
|
|
||||||
hi("Directory", { fg = base_16.base0D })
|
|
||||||
hi("Title", { fg = base_16.base0D })
|
|
||||||
|
|
||||||
-- === SYNTAX HIGHLIGHTING (from syntax.lua) ===
|
|
||||||
|
|
||||||
hi("Comment", { fg = base_30.grey_fg, style = "italic" })
|
|
||||||
|
|
||||||
-- Constants
|
|
||||||
hi("Constant", { fg = base_16.base09 })
|
|
||||||
hi("String", { fg = base_16.base0B })
|
|
||||||
hi("Character", { fg = base_16.base08 })
|
|
||||||
hi("Number", { fg = base_16.base09 })
|
|
||||||
hi("Boolean", { fg = base_16.base09 })
|
|
||||||
hi("Float", { fg = base_16.base09 })
|
|
||||||
|
|
||||||
-- Identifiers
|
|
||||||
hi("Identifier", { fg = base_16.base08 })
|
|
||||||
hi("Function", { fg = base_16.base0D })
|
|
||||||
|
|
||||||
-- Statements
|
|
||||||
hi("Statement", { fg = base_16.base0E })
|
|
||||||
hi("Conditional", { fg = base_16.base0E })
|
|
||||||
hi("Repeat", { fg = base_16.base0E })
|
|
||||||
hi("Label", { fg = base_16.base0A })
|
|
||||||
hi("Operator", { fg = base_16.base05 })
|
|
||||||
hi("Keyword", { fg = base_16.base0E })
|
|
||||||
hi("Exception", { fg = base_16.base08 })
|
|
||||||
|
|
||||||
-- PreProcessor
|
|
||||||
hi("PreProc", { fg = base_16.base0A })
|
|
||||||
hi("Include", { fg = base_16.base0D })
|
|
||||||
hi("Define", { fg = base_16.base0E })
|
|
||||||
hi("Macro", { fg = base_16.base08 })
|
|
||||||
hi("PreCondit", { fg = base_16.base0A })
|
|
||||||
|
|
||||||
-- Types
|
|
||||||
hi("Type", { fg = base_16.base0A })
|
|
||||||
hi("StorageClass", { fg = base_16.base0A })
|
|
||||||
hi("Structure", { fg = base_16.base0E })
|
|
||||||
hi("Typedef", { fg = base_16.base0A })
|
|
||||||
|
|
||||||
-- Special
|
|
||||||
hi("Special", { fg = base_16.base0C })
|
|
||||||
hi("SpecialChar", { fg = base_16.base0F })
|
|
||||||
hi("Tag", { fg = base_16.base0A })
|
|
||||||
hi("Delimiter", { fg = base_16.base0F })
|
|
||||||
hi("SpecialComment", { fg = base_16.base0C })
|
|
||||||
hi("Debug", { fg = base_16.base08 })
|
|
||||||
|
|
||||||
-- Errors
|
|
||||||
hi("Error", { fg = base_16.base00, bg = base_16.base08 })
|
|
||||||
hi("Todo", { fg = base_16.base0A, bg = base_16.base01 })
|
|
||||||
|
|
||||||
hi("Underlined", { fg = base_16.base0B, style = "underline" })
|
|
||||||
hi("Ignore", { fg = base_16.base0C })
|
|
||||||
|
|
||||||
-- === DIFF ===
|
|
||||||
|
|
||||||
hi("DiffAdd", { fg = base_16.base0B, bg = base_16.base00 })
|
|
||||||
hi("DiffChange", { fg = base_16.base0E, bg = base_16.base00 })
|
|
||||||
hi("DiffDelete", { fg = base_16.base08, bg = base_16.base00 })
|
|
||||||
hi("DiffText", { fg = base_16.base0D, bg = base_16.base01 })
|
|
||||||
|
|
||||||
hi("Added", { fg = base_30.green })
|
|
||||||
hi("Changed", { fg = base_30.yellow })
|
|
||||||
hi("Removed", { fg = base_30.red })
|
|
||||||
|
|
||||||
-- === GIT SIGNS ===
|
|
||||||
|
|
||||||
hi("GitSignsAdd", { fg = base_30.green })
|
|
||||||
hi("GitSignsChange", { fg = base_30.yellow })
|
|
||||||
hi("GitSignsDelete", { fg = base_30.red })
|
|
||||||
|
|
||||||
-- === LSP DIAGNOSTICS (from lsp.lua) ===
|
|
||||||
|
|
||||||
hi("DiagnosticError", { fg = base_30.red })
|
|
||||||
hi("DiagnosticWarn", { fg = base_30.yellow })
|
|
||||||
hi("DiagnosticInfo", { fg = base_30.green })
|
|
||||||
hi("DiagnosticHint", { fg = base_30.purple })
|
|
||||||
|
|
||||||
hi("DiagnosticUnderlineError", { sp = base_30.red, style = "underline" })
|
|
||||||
hi("DiagnosticUnderlineWarn", { sp = base_30.yellow, style = "underline" })
|
|
||||||
hi("DiagnosticUnderlineInfo", { sp = base_30.green, style = "underline" })
|
|
||||||
hi("DiagnosticUnderlineHint", { sp = base_30.purple, style = "underline" })
|
|
||||||
|
|
||||||
hi("LspReferenceText", { bg = base_30.one_bg3 })
|
|
||||||
hi("LspReferenceRead", { bg = base_30.one_bg3 })
|
|
||||||
hi("LspReferenceWrite", { bg = base_30.one_bg3 })
|
|
||||||
|
|
||||||
hi("LspInlayHint", { fg = base_30.light_grey, bg = base_30.black2 })
|
|
||||||
hi("LspSignatureActiveParameter", { fg = base_30.black, bg = base_30.green })
|
|
||||||
|
|
||||||
-- === TELESCOPE (from telescope.lua) ===
|
|
||||||
|
|
||||||
hi("TelescopeBorder", { fg = base_30.grey, bg = base_30.darker_black })
|
|
||||||
hi("TelescopePromptBorder", { fg = base_30.grey, bg = base_30.darker_black })
|
|
||||||
hi("TelescopePromptNormal", { fg = base_16.base05, bg = base_30.darker_black })
|
|
||||||
hi("TelescopeResultsBorder", { fg = base_30.grey, bg = base_30.darker_black })
|
|
||||||
hi("TelescopeResultsNormal", { bg = base_30.darker_black })
|
|
||||||
hi("TelescopePreviewBorder", { fg = base_30.grey, bg = base_30.darker_black })
|
|
||||||
hi("TelescopePreviewNormal", { bg = base_30.darker_black })
|
|
||||||
hi("TelescopeSelection", { bg = base_30.one_bg2, fg = base_30.white })
|
|
||||||
hi("TelescopeSelectionCaret", { fg = base_30.cyan, bg = base_30.one_bg2 })
|
|
||||||
hi("TelescopeMatching", { fg = base_30.blue, style = "bold" })
|
|
||||||
|
|
||||||
-- === TREESITTER (from treesitter.lua) ===
|
|
||||||
|
|
||||||
hi("@variable", { fg = base_16.base05 })
|
|
||||||
hi("@variable.builtin", { fg = base_16.base09 })
|
|
||||||
hi("@variable.parameter", { fg = base_16.base08 })
|
|
||||||
hi("@variable.member", { fg = base_16.base08 })
|
|
||||||
|
|
||||||
hi("@constant", { fg = base_16.base08 })
|
|
||||||
hi("@constant.builtin", { fg = base_16.base09 })
|
|
||||||
hi("@constant.macro", { fg = base_16.base08 })
|
|
||||||
|
|
||||||
hi("@string", { fg = base_16.base0B })
|
|
||||||
hi("@string.regex", { fg = base_16.base0C })
|
|
||||||
hi("@string.escape", { fg = base_16.base0C })
|
|
||||||
hi("@character", { fg = base_16.base08 })
|
|
||||||
hi("@number", { fg = base_16.base09 })
|
|
||||||
hi("@boolean", { fg = base_16.base09 })
|
|
||||||
hi("@float", { fg = base_16.base09 })
|
|
||||||
|
|
||||||
hi("@function", { fg = base_16.base0D })
|
|
||||||
hi("@function.builtin", { fg = base_16.base0D })
|
|
||||||
hi("@function.macro", { fg = base_16.base08 })
|
|
||||||
hi("@function.method", { fg = base_16.base0D })
|
|
||||||
|
|
||||||
hi("@constructor", { fg = base_16.base0C })
|
|
||||||
hi("@parameter", { fg = base_16.base08 })
|
|
||||||
|
|
||||||
hi("@keyword", { fg = base_16.base0E })
|
|
||||||
hi("@keyword.function", { fg = base_16.base0E })
|
|
||||||
hi("@keyword.return", { fg = base_16.base0E })
|
|
||||||
hi("@keyword.operator", { fg = base_16.base0E })
|
|
||||||
|
|
||||||
hi("@conditional", { fg = base_16.base0E })
|
|
||||||
hi("@repeat", { fg = base_16.base0E })
|
|
||||||
hi("@label", { fg = base_16.base0A })
|
|
||||||
hi("@operator", { fg = base_16.base05 })
|
|
||||||
hi("@exception", { fg = base_16.base08 })
|
|
||||||
|
|
||||||
hi("@type", { fg = base_16.base0A })
|
|
||||||
hi("@type.builtin", { fg = base_16.base0A })
|
|
||||||
hi("@type.qualifier", { fg = base_16.base0E })
|
|
||||||
|
|
||||||
hi("@structure", { fg = base_16.base0E })
|
|
||||||
hi("@include", { fg = base_16.base0D })
|
|
||||||
|
|
||||||
hi("@property", { fg = base_16.base08 })
|
|
||||||
hi("@field", { fg = base_16.base08 })
|
|
||||||
|
|
||||||
hi("@punctuation.delimiter", { fg = base_16.base0F })
|
|
||||||
hi("@punctuation.bracket", { fg = base_16.base05 })
|
|
||||||
hi("@punctuation.special", { fg = base_16.base08 })
|
|
||||||
|
|
||||||
hi("@comment", { fg = base_30.grey_fg, style = "italic" })
|
|
||||||
hi("@comment.todo", { fg = base_30.grey, bg = base_30.white })
|
|
||||||
hi("@comment.note", { fg = base_30.white, bg = base_30.blue })
|
|
||||||
hi("@comment.warning", { fg = base_30.black, bg = base_30.yellow })
|
|
||||||
hi("@comment.error", { fg = base_30.white, bg = base_30.red })
|
|
||||||
|
|
||||||
hi("@tag", { fg = base_16.base08 })
|
|
||||||
hi("@tag.attribute", { fg = base_16.base0A })
|
|
||||||
hi("@tag.delimiter", { fg = base_16.base0F })
|
|
||||||
|
|
||||||
hi("@markup.strong", { style = "bold" })
|
|
||||||
hi("@markup.italic", { style = "italic" })
|
|
||||||
hi("@markup.underline", { style = "underline" })
|
|
||||||
hi("@markup.strike", { style = "strikethrough" })
|
|
||||||
hi("@markup.heading", { fg = base_16.base0D, style = "bold" })
|
|
||||||
hi("@markup.link", { fg = base_16.base08, style = "underline" })
|
|
||||||
hi("@markup.link.url", { fg = base_16.base0B, style = "underline" })
|
|
||||||
hi("@markup.list", { fg = base_16.base08 })
|
|
||||||
|
|
||||||
-- === NVIM-TREE ===
|
|
||||||
|
|
||||||
hi("NvimTreeNormal", { bg = base_30.darker_black })
|
|
||||||
hi("NvimTreeNormalNC", { bg = base_30.darker_black })
|
|
||||||
hi("NvimTreeRootFolder", { fg = base_30.red, style = "bold" })
|
|
||||||
hi("NvimTreeGitDirty", { fg = base_30.yellow })
|
|
||||||
hi("NvimTreeGitNew", { fg = base_30.green })
|
|
||||||
hi("NvimTreeGitDeleted", { fg = base_30.red })
|
|
||||||
hi("NvimTreeSpecialFile", { fg = base_30.yellow, style = "underline" })
|
|
||||||
hi("NvimTreeIndentMarker", { fg = base_30.grey })
|
|
||||||
hi("NvimTreeImageFile", { fg = base_30.dark_purple })
|
|
||||||
hi("NvimTreeSymlink", { fg = base_30.cyan })
|
|
||||||
hi("NvimTreeFolderName", { fg = base_30.blue })
|
|
||||||
hi("NvimTreeFolderIcon", { fg = base_30.folder_bg })
|
|
||||||
hi("NvimTreeOpenedFolderName", { fg = base_30.blue })
|
|
||||||
|
|
||||||
-- === WHICH-KEY ===
|
|
||||||
|
|
||||||
hi("WhichKey", { fg = base_30.blue })
|
|
||||||
hi("WhichKeyGroup", { fg = base_30.green })
|
|
||||||
hi("WhichKeyDesc", { fg = base_16.base05 })
|
|
||||||
hi("WhichKeySeparator", { fg = base_30.grey })
|
|
||||||
hi("WhichKeyFloat", { bg = base_30.darker_black })
|
|
||||||
hi("WhichKeyBorder", { fg = base_30.blue, bg = base_30.darker_black })
|
|
||||||
|
|
||||||
-- === HEALTH CHECK ===
|
|
||||||
|
|
||||||
hi("healthSuccess", { fg = base_30.green, bg = base_30.black })
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ require("lazy").setup({
|
|||||||
{ import = "plugins" },
|
{ import = "plugins" },
|
||||||
}, lazy_config)
|
}, lazy_config)
|
||||||
|
|
||||||
-- load theme
|
-- load theme (watches ~/.config/theme-state for dark/light toggle)
|
||||||
vim.cmd.colorscheme("bearded-arc")
|
require("bearded-arc.watcher").setup()
|
||||||
|
|
||||||
-- Load standalone config (migrated from NvChad)
|
-- Load standalone config (migrated from NvChad)
|
||||||
require "options"
|
require "options"
|
||||||
|
|||||||
266
nvim/.config/nvim/lua/bearded-arc/init.lua
Normal file
266
nvim/.config/nvim/lua/bearded-arc/init.lua
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
-- Bearded Arc highlight engine
|
||||||
|
-- Shared logic for dark and light variants
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
-- Helper function to set highlights
|
||||||
|
local function hi(group, opts)
|
||||||
|
local cmd = "hi " .. group
|
||||||
|
if opts.fg then cmd = cmd .. " guifg=" .. opts.fg end
|
||||||
|
if opts.bg then cmd = cmd .. " guibg=" .. opts.bg end
|
||||||
|
if opts.sp then cmd = cmd .. " guisp=" .. opts.sp end
|
||||||
|
if opts.style then cmd = cmd .. " gui=" .. opts.style end
|
||||||
|
vim.cmd(cmd)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.apply(base_30, base_16)
|
||||||
|
-- === UI ELEMENTS ===
|
||||||
|
|
||||||
|
hi("LineNr", { fg = base_30.grey })
|
||||||
|
hi("CursorLineNr", { fg = base_30.white })
|
||||||
|
hi("SignColumn", { fg = base_16.base03 })
|
||||||
|
hi("FoldColumn", { fg = base_30.grey, bg = base_16.base00 })
|
||||||
|
hi("Folded", { fg = base_30.light_grey, bg = base_16.base01 })
|
||||||
|
|
||||||
|
hi("Normal", { fg = base_16.base05, bg = base_16.base00 })
|
||||||
|
hi("NormalFloat", { bg = base_30.darker_black })
|
||||||
|
hi("FloatBorder", { fg = base_30.blue })
|
||||||
|
hi("FloatTitle", { fg = base_30.white, bg = base_30.grey })
|
||||||
|
|
||||||
|
hi("Cursor", { fg = base_16.base00, bg = base_16.base05 })
|
||||||
|
hi("CursorLine", { bg = base_30.black2 })
|
||||||
|
hi("CursorColumn", { bg = base_16.base01 })
|
||||||
|
hi("ColorColumn", { bg = base_16.base01 })
|
||||||
|
|
||||||
|
hi("Visual", { bg = base_30.one_bg2 })
|
||||||
|
hi("VisualNOS", { bg = base_30.one_bg2 })
|
||||||
|
|
||||||
|
hi("Search", { fg = base_16.base00, bg = base_16.base0A })
|
||||||
|
hi("IncSearch", { fg = base_16.base01, bg = base_16.base09 })
|
||||||
|
hi("Substitute", { fg = base_16.base01, bg = base_16.base0A })
|
||||||
|
|
||||||
|
hi("WinSeparator", { fg = base_30.line })
|
||||||
|
hi("WinBar", { bg = base_30.statusline_bg })
|
||||||
|
hi("WinBarNC", { bg = base_30.statusline_bg })
|
||||||
|
|
||||||
|
hi("Pmenu", { bg = base_30.one_bg })
|
||||||
|
hi("PmenuSbar", { bg = base_30.one_bg })
|
||||||
|
hi("PmenuSel", { bg = base_30.pmenu_bg, fg = base_30.black })
|
||||||
|
hi("PmenuThumb", { bg = base_30.grey })
|
||||||
|
|
||||||
|
hi("StatusLine", { bg = base_30.statusline_bg })
|
||||||
|
hi("StatusLineNC", { bg = base_30.statusline_bg })
|
||||||
|
hi("TabLine", { fg = base_30.light_grey, bg = base_30.black2 })
|
||||||
|
hi("TabLineSel", { fg = base_30.white, bg = base_30.black })
|
||||||
|
hi("TabLineFill", { bg = base_30.black2 })
|
||||||
|
|
||||||
|
hi("ErrorMsg", { fg = base_16.base08, bg = base_16.base00 })
|
||||||
|
hi("WarningMsg", { fg = base_16.base0A })
|
||||||
|
hi("MoreMsg", { fg = base_16.base0B })
|
||||||
|
hi("ModeMsg", { fg = base_16.base0B })
|
||||||
|
hi("Question", { fg = base_16.base0D })
|
||||||
|
|
||||||
|
hi("MatchParen", { bg = base_30.grey, fg = base_30.white })
|
||||||
|
hi("NonText", { fg = base_16.base03 })
|
||||||
|
hi("SpecialKey", { fg = base_16.base03 })
|
||||||
|
hi("Conceal", { fg = base_16.base0D, bg = base_16.base00 })
|
||||||
|
hi("Directory", { fg = base_16.base0D })
|
||||||
|
hi("Title", { fg = base_16.base0D })
|
||||||
|
|
||||||
|
-- === SYNTAX HIGHLIGHTING ===
|
||||||
|
|
||||||
|
hi("Comment", { fg = base_30.grey_fg, style = "italic" })
|
||||||
|
|
||||||
|
hi("Constant", { fg = base_16.base09 })
|
||||||
|
hi("String", { fg = base_16.base0B })
|
||||||
|
hi("Character", { fg = base_16.base08 })
|
||||||
|
hi("Number", { fg = base_16.base09 })
|
||||||
|
hi("Boolean", { fg = base_16.base09 })
|
||||||
|
hi("Float", { fg = base_16.base09 })
|
||||||
|
|
||||||
|
hi("Identifier", { fg = base_16.base08 })
|
||||||
|
hi("Function", { fg = base_16.base0D })
|
||||||
|
|
||||||
|
hi("Statement", { fg = base_16.base0E })
|
||||||
|
hi("Conditional", { fg = base_16.base0E })
|
||||||
|
hi("Repeat", { fg = base_16.base0E })
|
||||||
|
hi("Label", { fg = base_16.base0A })
|
||||||
|
hi("Operator", { fg = base_16.base05 })
|
||||||
|
hi("Keyword", { fg = base_16.base0E })
|
||||||
|
hi("Exception", { fg = base_16.base08 })
|
||||||
|
|
||||||
|
hi("PreProc", { fg = base_16.base0A })
|
||||||
|
hi("Include", { fg = base_16.base0D })
|
||||||
|
hi("Define", { fg = base_16.base0E })
|
||||||
|
hi("Macro", { fg = base_16.base08 })
|
||||||
|
hi("PreCondit", { fg = base_16.base0A })
|
||||||
|
|
||||||
|
hi("Type", { fg = base_16.base0A })
|
||||||
|
hi("StorageClass", { fg = base_16.base0A })
|
||||||
|
hi("Structure", { fg = base_16.base0E })
|
||||||
|
hi("Typedef", { fg = base_16.base0A })
|
||||||
|
|
||||||
|
hi("Special", { fg = base_16.base0C })
|
||||||
|
hi("SpecialChar", { fg = base_16.base0F })
|
||||||
|
hi("Tag", { fg = base_16.base0A })
|
||||||
|
hi("Delimiter", { fg = base_16.base0F })
|
||||||
|
hi("SpecialComment", { fg = base_16.base0C })
|
||||||
|
hi("Debug", { fg = base_16.base08 })
|
||||||
|
|
||||||
|
hi("Error", { fg = base_16.base00, bg = base_16.base08 })
|
||||||
|
hi("Todo", { fg = base_16.base0A, bg = base_16.base01 })
|
||||||
|
|
||||||
|
hi("Underlined", { fg = base_16.base0B, style = "underline" })
|
||||||
|
hi("Ignore", { fg = base_16.base0C })
|
||||||
|
|
||||||
|
-- === DIFF ===
|
||||||
|
|
||||||
|
hi("DiffAdd", { fg = base_16.base0B, bg = base_16.base00 })
|
||||||
|
hi("DiffChange", { fg = base_16.base0E, bg = base_16.base00 })
|
||||||
|
hi("DiffDelete", { fg = base_16.base08, bg = base_16.base00 })
|
||||||
|
hi("DiffText", { fg = base_16.base0D, bg = base_16.base01 })
|
||||||
|
|
||||||
|
hi("Added", { fg = base_30.green })
|
||||||
|
hi("Changed", { fg = base_30.yellow })
|
||||||
|
hi("Removed", { fg = base_30.red })
|
||||||
|
|
||||||
|
-- === GIT SIGNS ===
|
||||||
|
|
||||||
|
hi("GitSignsAdd", { fg = base_30.green })
|
||||||
|
hi("GitSignsChange", { fg = base_30.yellow })
|
||||||
|
hi("GitSignsDelete", { fg = base_30.red })
|
||||||
|
|
||||||
|
-- === LSP DIAGNOSTICS ===
|
||||||
|
|
||||||
|
hi("DiagnosticError", { fg = base_30.red })
|
||||||
|
hi("DiagnosticWarn", { fg = base_30.yellow })
|
||||||
|
hi("DiagnosticInfo", { fg = base_30.green })
|
||||||
|
hi("DiagnosticHint", { fg = base_30.purple })
|
||||||
|
|
||||||
|
hi("DiagnosticUnderlineError", { sp = base_30.red, style = "underline" })
|
||||||
|
hi("DiagnosticUnderlineWarn", { sp = base_30.yellow, style = "underline" })
|
||||||
|
hi("DiagnosticUnderlineInfo", { sp = base_30.green, style = "underline" })
|
||||||
|
hi("DiagnosticUnderlineHint", { sp = base_30.purple, style = "underline" })
|
||||||
|
|
||||||
|
hi("LspReferenceText", { bg = base_30.one_bg3 })
|
||||||
|
hi("LspReferenceRead", { bg = base_30.one_bg3 })
|
||||||
|
hi("LspReferenceWrite", { bg = base_30.one_bg3 })
|
||||||
|
|
||||||
|
hi("LspInlayHint", { fg = base_30.light_grey, bg = base_30.black2 })
|
||||||
|
hi("LspSignatureActiveParameter", { fg = base_30.black, bg = base_30.green })
|
||||||
|
|
||||||
|
-- === TELESCOPE ===
|
||||||
|
|
||||||
|
hi("TelescopeBorder", { fg = base_30.grey, bg = base_30.darker_black })
|
||||||
|
hi("TelescopePromptBorder", { fg = base_30.grey, bg = base_30.darker_black })
|
||||||
|
hi("TelescopePromptNormal", { fg = base_16.base05, bg = base_30.darker_black })
|
||||||
|
hi("TelescopeResultsBorder", { fg = base_30.grey, bg = base_30.darker_black })
|
||||||
|
hi("TelescopeResultsNormal", { bg = base_30.darker_black })
|
||||||
|
hi("TelescopePreviewBorder", { fg = base_30.grey, bg = base_30.darker_black })
|
||||||
|
hi("TelescopePreviewNormal", { bg = base_30.darker_black })
|
||||||
|
hi("TelescopeSelection", { bg = base_30.one_bg2, fg = base_30.white })
|
||||||
|
hi("TelescopeSelectionCaret", { fg = base_30.cyan, bg = base_30.one_bg2 })
|
||||||
|
hi("TelescopeMatching", { fg = base_30.blue, style = "bold" })
|
||||||
|
|
||||||
|
-- === TREESITTER ===
|
||||||
|
|
||||||
|
hi("@variable", { fg = base_16.base05 })
|
||||||
|
hi("@variable.builtin", { fg = base_16.base09 })
|
||||||
|
hi("@variable.parameter", { fg = base_16.base08 })
|
||||||
|
hi("@variable.member", { fg = base_16.base08 })
|
||||||
|
|
||||||
|
hi("@constant", { fg = base_16.base08 })
|
||||||
|
hi("@constant.builtin", { fg = base_16.base09 })
|
||||||
|
hi("@constant.macro", { fg = base_16.base08 })
|
||||||
|
|
||||||
|
hi("@string", { fg = base_16.base0B })
|
||||||
|
hi("@string.regex", { fg = base_16.base0C })
|
||||||
|
hi("@string.escape", { fg = base_16.base0C })
|
||||||
|
hi("@character", { fg = base_16.base08 })
|
||||||
|
hi("@number", { fg = base_16.base09 })
|
||||||
|
hi("@boolean", { fg = base_16.base09 })
|
||||||
|
hi("@float", { fg = base_16.base09 })
|
||||||
|
|
||||||
|
hi("@function", { fg = base_16.base0D })
|
||||||
|
hi("@function.builtin", { fg = base_16.base0D })
|
||||||
|
hi("@function.macro", { fg = base_16.base08 })
|
||||||
|
hi("@function.method", { fg = base_16.base0D })
|
||||||
|
|
||||||
|
hi("@constructor", { fg = base_16.base0C })
|
||||||
|
hi("@parameter", { fg = base_16.base08 })
|
||||||
|
|
||||||
|
hi("@keyword", { fg = base_16.base0E })
|
||||||
|
hi("@keyword.function", { fg = base_16.base0E })
|
||||||
|
hi("@keyword.return", { fg = base_16.base0E })
|
||||||
|
hi("@keyword.operator", { fg = base_16.base0E })
|
||||||
|
|
||||||
|
hi("@conditional", { fg = base_16.base0E })
|
||||||
|
hi("@repeat", { fg = base_16.base0E })
|
||||||
|
hi("@label", { fg = base_16.base0A })
|
||||||
|
hi("@operator", { fg = base_16.base05 })
|
||||||
|
hi("@exception", { fg = base_16.base08 })
|
||||||
|
|
||||||
|
hi("@type", { fg = base_16.base0A })
|
||||||
|
hi("@type.builtin", { fg = base_16.base0A })
|
||||||
|
hi("@type.qualifier", { fg = base_16.base0E })
|
||||||
|
|
||||||
|
hi("@structure", { fg = base_16.base0E })
|
||||||
|
hi("@include", { fg = base_16.base0D })
|
||||||
|
|
||||||
|
hi("@property", { fg = base_16.base08 })
|
||||||
|
hi("@field", { fg = base_16.base08 })
|
||||||
|
|
||||||
|
hi("@punctuation.delimiter", { fg = base_16.base0F })
|
||||||
|
hi("@punctuation.bracket", { fg = base_16.base05 })
|
||||||
|
hi("@punctuation.special", { fg = base_16.base08 })
|
||||||
|
|
||||||
|
hi("@comment", { fg = base_30.grey_fg, style = "italic" })
|
||||||
|
hi("@comment.todo", { fg = base_30.grey, bg = base_30.white })
|
||||||
|
hi("@comment.note", { fg = base_30.white, bg = base_30.blue })
|
||||||
|
hi("@comment.warning", { fg = base_30.black, bg = base_30.yellow })
|
||||||
|
hi("@comment.error", { fg = base_30.white, bg = base_30.red })
|
||||||
|
|
||||||
|
hi("@tag", { fg = base_16.base08 })
|
||||||
|
hi("@tag.attribute", { fg = base_16.base0A })
|
||||||
|
hi("@tag.delimiter", { fg = base_16.base0F })
|
||||||
|
|
||||||
|
hi("@markup.strong", { style = "bold" })
|
||||||
|
hi("@markup.italic", { style = "italic" })
|
||||||
|
hi("@markup.underline", { style = "underline" })
|
||||||
|
hi("@markup.strike", { style = "strikethrough" })
|
||||||
|
hi("@markup.heading", { fg = base_16.base0D, style = "bold" })
|
||||||
|
hi("@markup.link", { fg = base_16.base08, style = "underline" })
|
||||||
|
hi("@markup.link.url", { fg = base_16.base0B, style = "underline" })
|
||||||
|
hi("@markup.list", { fg = base_16.base08 })
|
||||||
|
|
||||||
|
-- === NVIM-TREE ===
|
||||||
|
|
||||||
|
hi("NvimTreeNormal", { bg = base_30.darker_black })
|
||||||
|
hi("NvimTreeNormalNC", { bg = base_30.darker_black })
|
||||||
|
hi("NvimTreeRootFolder", { fg = base_30.red, style = "bold" })
|
||||||
|
hi("NvimTreeGitDirty", { fg = base_30.yellow })
|
||||||
|
hi("NvimTreeGitNew", { fg = base_30.green })
|
||||||
|
hi("NvimTreeGitDeleted", { fg = base_30.red })
|
||||||
|
hi("NvimTreeSpecialFile", { fg = base_30.yellow, style = "underline" })
|
||||||
|
hi("NvimTreeIndentMarker", { fg = base_30.grey })
|
||||||
|
hi("NvimTreeImageFile", { fg = base_30.dark_purple })
|
||||||
|
hi("NvimTreeSymlink", { fg = base_30.cyan })
|
||||||
|
hi("NvimTreeFolderName", { fg = base_30.blue })
|
||||||
|
hi("NvimTreeFolderIcon", { fg = base_30.folder_bg })
|
||||||
|
hi("NvimTreeOpenedFolderName", { fg = base_30.blue })
|
||||||
|
|
||||||
|
-- === WHICH-KEY ===
|
||||||
|
|
||||||
|
hi("WhichKey", { fg = base_30.blue })
|
||||||
|
hi("WhichKeyGroup", { fg = base_30.green })
|
||||||
|
hi("WhichKeyDesc", { fg = base_16.base05 })
|
||||||
|
hi("WhichKeySeparator", { fg = base_30.grey })
|
||||||
|
hi("WhichKeyFloat", { bg = base_30.darker_black })
|
||||||
|
hi("WhichKeyBorder", { fg = base_30.blue, bg = base_30.darker_black })
|
||||||
|
|
||||||
|
-- === HEALTH CHECK ===
|
||||||
|
|
||||||
|
hi("healthSuccess", { fg = base_30.green, bg = base_30.black })
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
50
nvim/.config/nvim/lua/bearded-arc/watcher.lua
Normal file
50
nvim/.config/nvim/lua/bearded-arc/watcher.lua
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
-- Theme state file watcher
|
||||||
|
-- Watches ~/.config/theme-state and switches between bearded-arc / bearded-arc-light
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local state_file = vim.fn.expand("~/.config/theme-state")
|
||||||
|
local current_theme = nil
|
||||||
|
|
||||||
|
local function apply_theme(name)
|
||||||
|
if name == current_theme then return end
|
||||||
|
current_theme = name
|
||||||
|
if name == "light" then
|
||||||
|
vim.cmd("colorscheme bearded-arc-light")
|
||||||
|
else
|
||||||
|
vim.cmd("colorscheme bearded-arc")
|
||||||
|
end
|
||||||
|
-- Re-setup lualine with the new palette
|
||||||
|
if package.loaded["configs.lualine"] then
|
||||||
|
require("configs.lualine").setup()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_state()
|
||||||
|
local f = io.open(state_file, "r")
|
||||||
|
if f then
|
||||||
|
local state = f:read("*a"):gsub("%s+", "")
|
||||||
|
f:close()
|
||||||
|
return state == "light" and "light" or "dark"
|
||||||
|
end
|
||||||
|
return "dark"
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.setup()
|
||||||
|
-- Apply initial theme on startup
|
||||||
|
apply_theme(read_state())
|
||||||
|
|
||||||
|
-- Watch for changes
|
||||||
|
local uv = vim.uv or vim.loop
|
||||||
|
local watcher = uv.new_fs_event()
|
||||||
|
if watcher then
|
||||||
|
watcher:start(state_file, {}, function(err, filename, events)
|
||||||
|
if err then return end
|
||||||
|
vim.schedule(function()
|
||||||
|
apply_theme(read_state())
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -1,59 +1,77 @@
|
|||||||
-- Lualine configuration with bearded-arc theme colors
|
-- Lualine configuration with bearded-arc theme colors
|
||||||
local colors = {
|
-- Supports dark and light variants
|
||||||
bg = "#232b3a", -- statusline_bg
|
|
||||||
fg = "#c3cfd9", -- base05
|
|
||||||
white = "#ABB7C1", -- white
|
|
||||||
black = "#1c2433", -- black
|
|
||||||
grey = "#444c5b", -- grey
|
|
||||||
light_grey = "#626a79", -- light_grey
|
|
||||||
|
|
||||||
-- Mode colors
|
local palettes = {
|
||||||
nord_blue = "#6da4cd", -- normal mode
|
dark = {
|
||||||
cyan = "#22ECDB", -- visual mode
|
bg = "#232b3a",
|
||||||
dark_purple = "#B78AFF", -- insert mode
|
fg = "#c3cfd9",
|
||||||
green = "#3CEC85", -- success/terminal
|
white = "#ABB7C1",
|
||||||
red = "#FF738A", -- errors/replace
|
black = "#1c2433",
|
||||||
yellow = "#EACD61", -- warnings
|
grey = "#444c5b",
|
||||||
orange = "#FF955C", -- command
|
light_grey = "#626a79",
|
||||||
blue = "#69C3FF", -- info
|
nord_blue = "#6da4cd",
|
||||||
|
cyan = "#22ECDB",
|
||||||
-- Section backgrounds
|
dark_purple = "#B78AFF",
|
||||||
lightbg = "#303847", -- file section bg
|
green = "#3CEC85",
|
||||||
|
red = "#FF738A",
|
||||||
|
yellow = "#EACD61",
|
||||||
|
orange = "#FF955C",
|
||||||
|
blue = "#69C3FF",
|
||||||
|
lightbg = "#303847",
|
||||||
|
},
|
||||||
|
light = {
|
||||||
|
bg = "#f0e8df",
|
||||||
|
fg = "#3a3228",
|
||||||
|
white = "#2a2a2a",
|
||||||
|
black = "#faf4ed",
|
||||||
|
grey = "#b0a89c",
|
||||||
|
light_grey = "#6a6258",
|
||||||
|
nord_blue = "#2a6a9a",
|
||||||
|
cyan = "#147a8a",
|
||||||
|
dark_purple = "#7b4fc4",
|
||||||
|
green = "#1e9b52",
|
||||||
|
red = "#d1344f",
|
||||||
|
yellow = "#b8890f",
|
||||||
|
orange = "#c45a1c",
|
||||||
|
blue = "#1a7db5",
|
||||||
|
lightbg = "#e8e0d4",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Custom theme based on bearded-arc
|
local function build_theme(colors)
|
||||||
local bearded_arc_theme = {
|
return {
|
||||||
normal = {
|
normal = {
|
||||||
a = { bg = colors.nord_blue, fg = colors.black, gui = "bold" },
|
a = { bg = colors.nord_blue, fg = colors.black, gui = "bold" },
|
||||||
b = { bg = colors.lightbg, fg = colors.white },
|
b = { bg = colors.lightbg, fg = colors.white },
|
||||||
c = { bg = colors.bg, fg = colors.fg },
|
c = { bg = colors.bg, fg = colors.fg },
|
||||||
},
|
},
|
||||||
insert = {
|
insert = {
|
||||||
a = { bg = colors.dark_purple, fg = colors.black, gui = "bold" },
|
a = { bg = colors.dark_purple, fg = colors.black, gui = "bold" },
|
||||||
b = { bg = colors.lightbg, fg = colors.white },
|
b = { bg = colors.lightbg, fg = colors.white },
|
||||||
},
|
},
|
||||||
visual = {
|
visual = {
|
||||||
a = { bg = colors.cyan, fg = colors.black, gui = "bold" },
|
a = { bg = colors.cyan, fg = colors.black, gui = "bold" },
|
||||||
b = { bg = colors.lightbg, fg = colors.white },
|
b = { bg = colors.lightbg, fg = colors.white },
|
||||||
},
|
},
|
||||||
replace = {
|
replace = {
|
||||||
a = { bg = colors.red, fg = colors.black, gui = "bold" },
|
a = { bg = colors.red, fg = colors.black, gui = "bold" },
|
||||||
b = { bg = colors.lightbg, fg = colors.white },
|
b = { bg = colors.lightbg, fg = colors.white },
|
||||||
},
|
},
|
||||||
command = {
|
command = {
|
||||||
a = { bg = colors.orange, fg = colors.black, gui = "bold" },
|
a = { bg = colors.orange, fg = colors.black, gui = "bold" },
|
||||||
b = { bg = colors.lightbg, fg = colors.white },
|
b = { bg = colors.lightbg, fg = colors.white },
|
||||||
},
|
},
|
||||||
terminal = {
|
terminal = {
|
||||||
a = { bg = colors.green, fg = colors.black, gui = "bold" },
|
a = { bg = colors.green, fg = colors.black, gui = "bold" },
|
||||||
b = { bg = colors.lightbg, fg = colors.white },
|
b = { bg = colors.lightbg, fg = colors.white },
|
||||||
},
|
},
|
||||||
inactive = {
|
inactive = {
|
||||||
a = { bg = colors.bg, fg = colors.grey },
|
a = { bg = colors.bg, fg = colors.grey },
|
||||||
b = { bg = colors.bg, fg = colors.grey },
|
b = { bg = colors.bg, fg = colors.grey },
|
||||||
c = { bg = colors.bg, fg = colors.grey },
|
c = { bg = colors.bg, fg = colors.grey },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
end
|
||||||
|
|
||||||
-- Custom component for LSP status
|
-- Custom component for LSP status
|
||||||
local function lsp_status()
|
local function lsp_status()
|
||||||
@@ -115,106 +133,117 @@ local function git_status()
|
|||||||
return table.concat(parts, " ")
|
return table.concat(parts, " ")
|
||||||
end
|
end
|
||||||
|
|
||||||
require("lualine").setup({
|
local M = {}
|
||||||
options = {
|
|
||||||
theme = bearded_arc_theme,
|
function M.setup()
|
||||||
component_separators = { left = "", right = "" },
|
local colors = palettes[vim.o.background] or palettes.dark
|
||||||
section_separators = { left = "", right = "" },
|
local theme = build_theme(colors)
|
||||||
disabled_filetypes = {
|
|
||||||
statusline = { "alpha", "dashboard", "NvimTree", "neo-tree" },
|
require("lualine").setup({
|
||||||
winbar = {},
|
options = {
|
||||||
},
|
theme = theme,
|
||||||
ignore_focus = {},
|
component_separators = { left = "", right = "" },
|
||||||
always_divide_middle = true,
|
section_separators = { left = "", right = "" },
|
||||||
globalstatus = true,
|
disabled_filetypes = {
|
||||||
refresh = {
|
statusline = { "alpha", "dashboard", "NvimTree", "neo-tree" },
|
||||||
statusline = 1000,
|
winbar = {},
|
||||||
tabline = 1000,
|
},
|
||||||
winbar = 1000,
|
ignore_focus = {},
|
||||||
},
|
always_divide_middle = true,
|
||||||
},
|
globalstatus = true,
|
||||||
sections = {
|
refresh = {
|
||||||
lualine_a = {
|
statusline = 1000,
|
||||||
{
|
tabline = 1000,
|
||||||
"mode",
|
winbar = 1000,
|
||||||
fmt = function(str)
|
|
||||||
return str:sub(1, 1) -- Show only first letter (N, I, V, etc.)
|
|
||||||
end,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
lualine_b = {
|
sections = {
|
||||||
{
|
lualine_a = {
|
||||||
"branch",
|
{
|
||||||
icon = "",
|
"mode",
|
||||||
color = { fg = colors.light_grey },
|
fmt = function(str)
|
||||||
},
|
return str:sub(1, 1)
|
||||||
{
|
end,
|
||||||
git_status,
|
|
||||||
color = { bg = colors.lightbg },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
lualine_c = {
|
|
||||||
{
|
|
||||||
"filename",
|
|
||||||
path = 1, -- Relative path
|
|
||||||
symbols = {
|
|
||||||
modified = " ",
|
|
||||||
readonly = " ",
|
|
||||||
unnamed = "[No Name]",
|
|
||||||
newfile = " ",
|
|
||||||
},
|
},
|
||||||
color = { bg = colors.lightbg, fg = colors.white },
|
|
||||||
},
|
},
|
||||||
{
|
lualine_b = {
|
||||||
function()
|
{
|
||||||
return require("nvim-navic").get_location()
|
"branch",
|
||||||
end,
|
icon = "",
|
||||||
cond = function()
|
color = { fg = colors.light_grey },
|
||||||
return package.loaded["nvim-navic"] and require("nvim-navic").is_available()
|
},
|
||||||
end,
|
{
|
||||||
color = { bg = colors.bg, fg = colors.light_grey },
|
git_status,
|
||||||
|
color = { bg = colors.lightbg },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
lualine_c = {
|
||||||
lsp_diagnostics,
|
{
|
||||||
color = { bg = colors.bg },
|
"filename",
|
||||||
|
path = 1,
|
||||||
|
symbols = {
|
||||||
|
modified = " ",
|
||||||
|
readonly = " ",
|
||||||
|
unnamed = "[No Name]",
|
||||||
|
newfile = " ",
|
||||||
|
},
|
||||||
|
color = { bg = colors.lightbg, fg = colors.white },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function()
|
||||||
|
return require("nvim-navic").get_location()
|
||||||
|
end,
|
||||||
|
cond = function()
|
||||||
|
return package.loaded["nvim-navic"] and require("nvim-navic").is_available()
|
||||||
|
end,
|
||||||
|
color = { bg = colors.bg, fg = colors.light_grey },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lsp_diagnostics,
|
||||||
|
color = { bg = colors.bg },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lualine_x = {
|
||||||
|
{
|
||||||
|
lsp_status,
|
||||||
|
color = { fg = colors.nord_blue, bg = colors.bg },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filetype",
|
||||||
|
colored = true,
|
||||||
|
icon_only = false,
|
||||||
|
icon = { align = "right" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lualine_y = {
|
||||||
|
{
|
||||||
|
"progress",
|
||||||
|
color = { bg = colors.lightbg, fg = colors.white },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lualine_z = {
|
||||||
|
{
|
||||||
|
"location",
|
||||||
|
icon = "",
|
||||||
|
color = { bg = colors.green, fg = colors.black },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
lualine_x = {
|
inactive_sections = {
|
||||||
{
|
lualine_a = {},
|
||||||
lsp_status,
|
lualine_b = {},
|
||||||
color = { fg = colors.nord_blue, bg = colors.bg },
|
lualine_c = { "filename" },
|
||||||
},
|
lualine_x = { "location" },
|
||||||
{
|
lualine_y = {},
|
||||||
"filetype",
|
lualine_z = {},
|
||||||
colored = true,
|
|
||||||
icon_only = false,
|
|
||||||
icon = { align = "right" },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
lualine_y = {
|
tabline = {},
|
||||||
{
|
winbar = {},
|
||||||
"progress",
|
inactive_winbar = {},
|
||||||
color = { bg = colors.lightbg, fg = colors.white },
|
extensions = { "lazy", "mason" },
|
||||||
},
|
})
|
||||||
},
|
end
|
||||||
lualine_z = {
|
|
||||||
{
|
M.setup()
|
||||||
"location",
|
|
||||||
icon = "",
|
return M
|
||||||
color = { bg = colors.green, fg = colors.black },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
inactive_sections = {
|
|
||||||
lualine_a = {},
|
|
||||||
lualine_b = {},
|
|
||||||
lualine_c = { "filename" },
|
|
||||||
lualine_x = { "location" },
|
|
||||||
lualine_y = {},
|
|
||||||
lualine_z = {},
|
|
||||||
},
|
|
||||||
tabline = {},
|
|
||||||
winbar = {},
|
|
||||||
inactive_winbar = {},
|
|
||||||
extensions = { "lazy", "mason" },
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
---
|
|
||||||
name: deep-scout
|
|
||||||
description: Thorough architectural exploration. Traces dependencies, maps subsystems, understands the why. Writes structured context for the next agent in the chain.
|
|
||||||
tools: read, write, grep, find, ls, bash
|
|
||||||
model: opencode-go/mimo-v2-pro
|
|
||||||
output: scout.md
|
|
||||||
---
|
|
||||||
|
|
||||||
You are a deep scout. Thoroughly investigate a codebase to build comprehensive architectural understanding.
|
|
||||||
|
|
||||||
Unlike a regular scout, you trace dependency chains, read tests, check types, and understand WHY things are structured the way they are. Your output enables complex refactors and architectural changes.
|
|
||||||
|
|
||||||
**YOUR ONLY JOB IS EXPLORATION AND REPORTING. NEVER implement, write code, edit codebase files, or suggest "now let me implement…". Stop as soon as you have finished your structured report.**
|
|
||||||
|
|
||||||
## Output Protocol
|
|
||||||
|
|
||||||
When your task contains `[Write to: path]`, write your COMPLETE report to that exact path using the `write` tool. After writing, return a brief 1-2 sentence summary (e.g. "Wrote deep architectural context covering 18 files, dependency chains, and 3 risk areas to scout.md").
|
|
||||||
|
|
||||||
Without `[Write to:]`, output your full report as text.
|
|
||||||
|
|
||||||
## Strategy
|
|
||||||
1. Start broad: find/ls to map project structure
|
|
||||||
2. Identify entry points and trace execution flow
|
|
||||||
3. Read types/interfaces that define contracts
|
|
||||||
4. Follow import chains to understand coupling
|
|
||||||
5. Check tests to understand expected behavior
|
|
||||||
6. Read config files that affect behavior
|
|
||||||
7. Look at git history for recent changes if relevant (`git log --oneline -20`)
|
|
||||||
|
|
||||||
## Output format
|
|
||||||
|
|
||||||
# Deep Context
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
Relevant directory tree with annotations.
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
How the system is organized. What the major subsystems are. How they communicate.
|
|
||||||
Include ASCII diagrams if helpful.
|
|
||||||
|
|
||||||
## Type Contracts
|
|
||||||
Key interfaces and types that define boundaries between components.
|
|
||||||
Include actual code:
|
|
||||||
|
|
||||||
**Rule: When an implementation must mirror an existing pattern (e.g. a pipeline, a handler, a system), include the EXACT code of the relevant functions — not summaries or descriptions. The coder needs to copy the pattern, not reconstruct it from prose.**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// From path/to/types.ts:10-40
|
|
||||||
interface ... { }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Execution Flow
|
|
||||||
Trace the relevant code paths step by step. "User calls X → handler Y → service Z → database W."
|
|
||||||
|
|
||||||
## Dependencies & Coupling
|
|
||||||
What depends on what. Which changes will cascade. Where are the stable vs fragile boundaries.
|
|
||||||
|
|
||||||
## Test Coverage
|
|
||||||
What's tested, what isn't. What the tests reveal about expected behavior.
|
|
||||||
|
|
||||||
## Risks & Constraints
|
|
||||||
- Implicit assumptions in the code
|
|
||||||
- Shared mutable state
|
|
||||||
- Performance constraints
|
|
||||||
- Backwards compatibility requirements
|
|
||||||
|
|
||||||
## Files Map
|
|
||||||
Complete list of relevant files with their role:
|
|
||||||
- `path/to/file.ts` - Role/purpose
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**STOP HERE. Do not write any implementation. Do not suggest next steps beyond the files map. Your job is done.**
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: explorer
|
name: explorer
|
||||||
description: Comprehensive codebase and knowledge-base explorer. Maps architecture, traces dependencies, synthesizes cross-cutting context with full code snippets and rationale. Use for deep refactoring, architectural decisions, or understanding complex subsystems. Do NOT use when the user has already provided explicit file paths or when a direct file read would suffice — only invoke for open-ended exploration where the relevant files are unknown.
|
description: Comprehensive codebase and knowledge-base explorer. Maps architecture, traces dependencies, synthesizes cross-cutting context with full code snippets and rationale. Use for deep refactoring, architectural decisions, or understanding complex subsystems. Do NOT use when the user has already provided explicit file paths or when a direct file read would suffice — only invoke for open-ended exploration where the relevant files are unknown.
|
||||||
tools: read, bash, write, mcp:qmd, mcp:opty
|
tools: read, bash, mcp:qmd, mcp:opty
|
||||||
model: opencode-go/mimo-v2-pro
|
model: opencode-go/qwen3.6-plus
|
||||||
defaultProgress: true
|
defaultProgress: true
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,9 +11,9 @@ You are an explorer. Thoroughly investigate a codebase or knowledge base and syn
|
|||||||
**CRITICAL CONSTRAINTS**:
|
**CRITICAL CONSTRAINTS**:
|
||||||
- Do NOT use the subagent tool
|
- Do NOT use the subagent tool
|
||||||
- Do NOT delegate to other agents, especially not to yourself (the explorer agent)
|
- Do NOT delegate to other agents, especially not to yourself (the explorer agent)
|
||||||
- Use ONLY your available tools: read, bash, write, mcp:qmd, mcp:opty
|
- Use ONLY your available tools: read, bash, mcp:qmd, mcp:opty
|
||||||
|
|
||||||
**CRITICAL**: Use the `write` tool to save your complete findings to `/home/jonas/.pi/context.md`. This must be a full document with:
|
**OUTPUT**: Produce your complete findings as the final response. This must be a full analysis with:
|
||||||
- Architecture overview and structure
|
- Architecture overview and structure
|
||||||
- Complete file contents (not summaries)
|
- Complete file contents (not summaries)
|
||||||
- Dependency chains and relationships
|
- Dependency chains and relationships
|
||||||
@@ -21,5 +21,3 @@ You are an explorer. Thoroughly investigate a codebase or knowledge base and syn
|
|||||||
- ASCII diagrams where helpful
|
- ASCII diagrams where helpful
|
||||||
|
|
||||||
Be thorough and comprehensive — include all relevant code snippets and context needed to understand the codebase.
|
Be thorough and comprehensive — include all relevant code snippets and context needed to understand the codebase.
|
||||||
|
|
||||||
After writing to the file, confirm completion with: "Context saved to: /home/jonas/.pi/context.md"
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
name: reviewer-quick
|
|
||||||
description: Fast cross-family review for medium-risk changes. Haiku-speed, Claude-perspective. Outputs review as text.
|
|
||||||
tools: read, bash, grep, find
|
|
||||||
model: opencode/big-pickle
|
|
||||||
---
|
|
||||||
|
|
||||||
You are a code reviewer doing a quick pass. Focus on obvious bugs, security issues, and integration problems. Don't deep-dive into edge cases — flag only what matters.
|
|
||||||
|
|
||||||
## Strategy
|
|
||||||
1. Read the changed files
|
|
||||||
2. Check for obvious bugs, security issues, type errors
|
|
||||||
3. Verify imports and integration with existing code
|
|
||||||
4. Output your full review as text (the framework captures it)
|
|
||||||
|
|
||||||
## Output format
|
|
||||||
|
|
||||||
# Quick Review
|
|
||||||
|
|
||||||
## Verdict: PASS | NEEDS_FIXES
|
|
||||||
|
|
||||||
## Issues (if any)
|
|
||||||
- `file.ts:42` — Issue and fix
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
1-2 sentences.
|
|
||||||
@@ -1,26 +1,18 @@
|
|||||||
---
|
---
|
||||||
name: scout
|
name: scout
|
||||||
description: Fast codebase recon. Finds relevant files, types, and patterns, then writes structured context for the next agent in the chain.
|
description: Fast codebase recon. Finds relevant files, types, and patterns.
|
||||||
tools: read, write, grep, find, ls, bash
|
tools: read, grep, find, ls, bash
|
||||||
skills: opty, qmd
|
skills: opty, qmd
|
||||||
model: opencode-go/mimo-v2-pro
|
model: opencode-go/mimo-v2-pro
|
||||||
output: scout.md
|
output: false
|
||||||
---
|
---
|
||||||
|
|
||||||
You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
|
You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
|
||||||
|
|
||||||
Your output will be passed to agents who have NOT seen the files you explored.
|
|
||||||
|
|
||||||
**YOUR ONLY JOB IS EXPLORATION AND REPORTING. NEVER implement, write implementation plans, write code, or edit codebase files. Stop as soon as you have finished your structured report.**
|
**YOUR ONLY JOB IS EXPLORATION AND REPORTING. NEVER implement, write implementation plans, write code, or edit codebase files. Stop as soon as you have finished your structured report.**
|
||||||
|
|
||||||
**DO ALL WORK YOURSELF using your own tools. NEVER delegate to subagents, NEVER call `subagent(...)`, NEVER invoke `pi` via bash or any other mechanism. You must personally run every search, read every file, and produce the report directly.**
|
**DO ALL WORK YOURSELF using your own tools. NEVER delegate to subagents, NEVER call `subagent(...)`, NEVER invoke `pi` via bash or any other mechanism. You must personally run every search, read every file, and produce the report directly.**
|
||||||
|
|
||||||
## Output Protocol
|
|
||||||
|
|
||||||
When your task contains `[Write to: path]`, write your COMPLETE report to that exact path using the `write` tool. After writing, return a brief 1-2 sentence summary (e.g. "Wrote structured findings covering 12 files, 3 key types, and 2 risks to scout.md").
|
|
||||||
|
|
||||||
Without `[Write to:]`, output your full report as text.
|
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
- **opty** — semantic/HDC code search via CLI: `opty query "description"` to find functions/types/imports by meaning; output includes file + line number
|
- **opty** — semantic/HDC code search via CLI: `opty query "description"` to find functions/types/imports by meaning; output includes file + line number
|
||||||
- **qmd** — knowledge base search via CLI: `qmd query $'lex: X\nvec: Y'` to find docs/notes by keyword or vector; `qmd get <path>` / `qmd multi-get <glob>` to retrieve full documents
|
- **qmd** — knowledge base search via CLI: `qmd query $'lex: X\nvec: Y'` to find docs/notes by keyword or vector; `qmd get <path>` / `qmd multi-get <glob>` to retrieve full documents
|
||||||
|
|||||||
275
pi/.pi/agent/extensions/ask-claude.ts
Normal file
275
pi/.pi/agent/extensions/ask-claude.ts
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
/**
|
||||||
|
* ask-claude — Stream Claude agent reviews into pi.
|
||||||
|
*
|
||||||
|
* For AGENTS to use. Delegates to specialized Claude agents or raw models
|
||||||
|
* for review, analysis, debugging, and second opinions.
|
||||||
|
*
|
||||||
|
* Tool (callable by the LLM):
|
||||||
|
* ask_claude(prompt, agent?, model?, question?, session_id?)
|
||||||
|
* agent — any agent name from ~/.claude/agents/ (e.g. "plan_review", "code_review", "oracle", "debug")
|
||||||
|
* model — model override: "opus", "sonnet", or full model ID
|
||||||
|
* question — specific focus prepended as a review header
|
||||||
|
* session_id — resume a prior conversation (returned in every response)
|
||||||
|
*
|
||||||
|
* Commands:
|
||||||
|
* /review-plan — editor → Claude Opus plan_review → inject result
|
||||||
|
* /review-code — editor → Claude Sonnet code_review → inject result
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineTool, getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
||||||
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { Type } from "@mariozechner/pi-ai";
|
||||||
|
import { Box, Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
||||||
|
import {
|
||||||
|
type ClaudeDetails,
|
||||||
|
type Theme,
|
||||||
|
renderToolCallLine,
|
||||||
|
renderToolResultBox,
|
||||||
|
renderToolBlock,
|
||||||
|
formatUsage,
|
||||||
|
buildEditDiff,
|
||||||
|
formatAnthropicError,
|
||||||
|
runClaude,
|
||||||
|
type RunClaudeResult,
|
||||||
|
} from "../shared/claude-stream.js";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Rendering (ask-claude specific — uses shared tool renderers)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function buildLabel(agent?: string, model?: string): string {
|
||||||
|
if (agent) return `Claude [${agent}]`;
|
||||||
|
if (model) return `Claude [${model}]`;
|
||||||
|
return "Claude Sonnet";
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tool definition
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const AskClaudeParams = Type.Object({
|
||||||
|
prompt: Type.String({
|
||||||
|
description: "Full content to review/analyze. Include all relevant context: CLAUDE.md conventions, files explored, code or plan to review.",
|
||||||
|
}),
|
||||||
|
agent: Type.Optional(Type.String({
|
||||||
|
description: "Agent name from ~/.claude/agents/ (e.g. 'plan_review', 'code_review', 'oracle', 'debug'). Omit to use model= directly.",
|
||||||
|
})),
|
||||||
|
model: Type.Optional(Type.String({
|
||||||
|
description: "Model override: 'opus', 'sonnet', 'haiku', or a full model ID. When agent is set this overrides the agent's default. When agent is omitted this selects the model directly.",
|
||||||
|
})),
|
||||||
|
question: Type.Optional(Type.String({
|
||||||
|
description: "Specific question or focus area prepended to the prompt (e.g. 'Focus on security', 'Are there race conditions?').",
|
||||||
|
})),
|
||||||
|
session_id: Type.Optional(Type.String({
|
||||||
|
description: "Resume a prior conversation. Pass the session_id returned from a previous ask_claude call.",
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
const askClaudeTool = defineTool({
|
||||||
|
name: "ask_claude",
|
||||||
|
label: "Ask Claude",
|
||||||
|
description: [
|
||||||
|
"Ask a Claude agent or model for review, analysis, or a second opinion.",
|
||||||
|
"agent=<name> runs a named agent from ~/.claude/agents/ (e.g. 'plan_review', 'code_review', 'oracle', 'debug').",
|
||||||
|
"Use model= alone for free-form requests without an agent. Use question= to specify a focus.",
|
||||||
|
"Pass session_id from a prior response to continue the same conversation across turns.",
|
||||||
|
"CLAUDE.md and .claude/skills are loaded automatically from the project root.",
|
||||||
|
].join(" "),
|
||||||
|
promptSnippet: "Ask a Claude agent or model for review, analysis, or a second opinion",
|
||||||
|
promptGuidelines: [
|
||||||
|
"Use ask_claude(agent=<name>) to invoke a specialized agent — include all relevant context in the prompt.",
|
||||||
|
"Use ask_claude(model='opus', question='...') for free-form deep analysis.",
|
||||||
|
"Always include the artifact to review (plan, code, problem description) in the prompt.",
|
||||||
|
"Pass session_id back from the previous response to continue the conversation.",
|
||||||
|
],
|
||||||
|
parameters: AskClaudeParams,
|
||||||
|
|
||||||
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
||||||
|
const fullPrompt = params.question
|
||||||
|
? `## Review Focus\n\n${params.question}\n\n## Content\n\n${params.prompt}`
|
||||||
|
: params.prompt;
|
||||||
|
|
||||||
|
const label = buildLabel(params.agent, params.model);
|
||||||
|
const details: ClaudeDetails = { label, done: false, blocks: [], finalText: "", isResume: !!params.session_id };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result: RunClaudeResult = await runClaude(fullPrompt, {
|
||||||
|
agent: params.agent,
|
||||||
|
model: params.model,
|
||||||
|
sessionId: params.session_id,
|
||||||
|
enrichEditDiffs: true, // ask-claude wants diff enrichment
|
||||||
|
cwd: ctx.cwd,
|
||||||
|
signal,
|
||||||
|
onUpdate: (partial) => {
|
||||||
|
Object.assign(details, partial);
|
||||||
|
onUpdate?.({
|
||||||
|
content: [{ type: "text", text: details.finalText || "(thinking…)" }],
|
||||||
|
details: { ...details },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
details.done = true;
|
||||||
|
details.finalText = result.finalText;
|
||||||
|
details.blocks = result.blocks;
|
||||||
|
details.sessionId = result.sessionId;
|
||||||
|
details.costUsd = result.costUsd;
|
||||||
|
details.inputTokens = result.inputTokens;
|
||||||
|
details.outputTokens = result.outputTokens;
|
||||||
|
details.cacheReadTokens = result.cacheReadTokens;
|
||||||
|
details.cacheWriteTokens = result.cacheWriteTokens;
|
||||||
|
|
||||||
|
const sessionNote = result.sessionId
|
||||||
|
? `\n\n---\n*session_id: \`${result.sessionId}\`*`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: (result.finalText || "(no output)") + sessionNote }],
|
||||||
|
details: { ...details },
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const errMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
details.done = true;
|
||||||
|
details.finalText = `Error: ${errMsg}`;
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `**Claude error:** ${errMsg}` }],
|
||||||
|
details: { ...details },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCall(args, theme, _ctx) {
|
||||||
|
const label = buildLabel(args.agent, args.model);
|
||||||
|
let text = theme.fg("toolTitle", theme.bold("ask_claude ")) + theme.fg("accent", `[${label}]`);
|
||||||
|
if (args.question) {
|
||||||
|
text += "\n " + theme.fg("dim", theme.italic(args.question));
|
||||||
|
} else {
|
||||||
|
const lines = args.prompt.split("\n").filter((l) => l.trim()).slice(0, 3);
|
||||||
|
text += "\n " + theme.fg("dim", lines.join("\n "));
|
||||||
|
}
|
||||||
|
return new Text(text, 0, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderResult(result, { isPartial }, theme, _ctx) {
|
||||||
|
const d = result.details as ClaudeDetails | undefined;
|
||||||
|
if (!d) return new Text(theme.fg("muted", "…"), 0, 0);
|
||||||
|
|
||||||
|
const isDone = d.done && !isPartial;
|
||||||
|
const statusIcon = isDone ? theme.fg("success", "✓ ") : theme.fg("warning", "⏳ ");
|
||||||
|
const c = new Container();
|
||||||
|
|
||||||
|
const resume = d.isResume ? theme.fg("dim", " ↩") : "";
|
||||||
|
c.addChild(new Text(statusIcon + theme.fg("toolTitle", theme.bold(d.label)) + resume, 0, 0));
|
||||||
|
|
||||||
|
for (const block of d.blocks ?? []) {
|
||||||
|
if (block.type === "thinking" && block.text.trim()) {
|
||||||
|
c.addChild(new Text(theme.fg("dim", theme.italic(block.text.trimEnd())), 0, 0));
|
||||||
|
} else if (block.type === "tool") {
|
||||||
|
c.addChild(renderToolBlock(block, theme as any));
|
||||||
|
} else if (block.type === "text" && block.text.trim()) {
|
||||||
|
c.addChild(new Spacer(1));
|
||||||
|
if (isDone) {
|
||||||
|
c.addChild(new Markdown(block.text.trim(), 0, 0, getMarkdownTheme()));
|
||||||
|
} else {
|
||||||
|
c.addChild(new Text(theme.fg("dim", block.text.trimEnd()), 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDone) {
|
||||||
|
const usageLine = formatUsage(d);
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (usageLine) parts.push(usageLine);
|
||||||
|
if (d.sessionId) parts.push(`session:${d.sessionId.slice(0, 8)}`);
|
||||||
|
if (parts.length > 0) {
|
||||||
|
c.addChild(new Spacer(1));
|
||||||
|
c.addChild(new Text(theme.fg("dim", parts.join(" ")), 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Extension entry point
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export default function (pi: ExtensionAPI) {
|
||||||
|
pi.registerTool(askClaudeTool);
|
||||||
|
|
||||||
|
// ── /review-plan ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pi.registerCommand("review-plan", {
|
||||||
|
description: "Editor → Claude Opus plan_review → inject review into conversation",
|
||||||
|
handler: async (_args, ctx) => {
|
||||||
|
const input = await ctx.ui.editor(
|
||||||
|
"Plan Review · Claude Opus",
|
||||||
|
"Paste your plan or strategy. Claude will review for correctness, completeness, and risk.",
|
||||||
|
);
|
||||||
|
if (!input?.trim()) { ctx.ui.notify("Cancelled.", "info"); return; }
|
||||||
|
ctx.ui.setStatus("ask-claude", "Asking Claude Opus (plan_review)…");
|
||||||
|
try {
|
||||||
|
const r = await runClaude(input, { agent: "plan_review", cwd: ctx.cwd, onUpdate: () => {} });
|
||||||
|
if (!r.finalText.trim()) { ctx.ui.notify("No output from Claude.", "warning"); return; }
|
||||||
|
pi.sendMessage(
|
||||||
|
{ customType: "ask-claude-review", content: r.finalText.trim(), display: true,
|
||||||
|
details: { label: "Claude Opus · plan_review", output: r.finalText } },
|
||||||
|
{ triggerTurn: true },
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
ctx.ui.notify(`Claude error: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||||
|
} finally { ctx.ui.setStatus("ask-claude", undefined); }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── /review-code ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pi.registerCommand("review-code", {
|
||||||
|
description: "Editor → Claude Sonnet code_review → inject review into conversation",
|
||||||
|
handler: async (_args, ctx) => {
|
||||||
|
const input = await ctx.ui.editor(
|
||||||
|
"Code Review · Claude Sonnet",
|
||||||
|
"Paste code to review. Include the plan it implements and any specific concerns.",
|
||||||
|
);
|
||||||
|
if (!input?.trim()) { ctx.ui.notify("Cancelled.", "info"); return; }
|
||||||
|
ctx.ui.setStatus("ask-claude", "Asking Claude Sonnet (code_review)…");
|
||||||
|
try {
|
||||||
|
const r = await runClaude(input, { agent: "code_review", cwd: ctx.cwd, onUpdate: () => {} });
|
||||||
|
if (!r.finalText.trim()) { ctx.ui.notify("No output from Claude.", "warning"); return; }
|
||||||
|
pi.sendMessage(
|
||||||
|
{ customType: "ask-claude-review", content: r.finalText.trim(), display: true,
|
||||||
|
details: { label: "Claude Sonnet · code_review", output: r.finalText } },
|
||||||
|
{ triggerTurn: true },
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
ctx.ui.notify(`Claude error: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||||
|
} finally { ctx.ui.setStatus("ask-claude", undefined); }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Message renderer for injected reviews ──────────────────────────────
|
||||||
|
|
||||||
|
pi.registerMessageRenderer("ask-claude-review", (message, { expanded }, theme) => {
|
||||||
|
const d = message.details as { label?: string; output?: string } | undefined;
|
||||||
|
const label = d?.label ?? "Claude";
|
||||||
|
const output = (d?.output ?? "").trim();
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
const c = new Container();
|
||||||
|
c.addChild(new Text(theme.fg("accent", "◆ ") + theme.fg("toolTitle", theme.bold(label)), 0, 0));
|
||||||
|
c.addChild(new Spacer(1));
|
||||||
|
c.addChild(new Markdown(output, 0, 0, getMarkdownTheme()));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = theme.fg("accent", "◆ ") + theme.fg("toolTitle", theme.bold(label));
|
||||||
|
const lines = output.split("\n").filter((l) => l.trim());
|
||||||
|
const preview = lines.slice(0, 4).join("\n");
|
||||||
|
if (preview) {
|
||||||
|
text += "\n" + theme.fg("dim", preview);
|
||||||
|
if (lines.length > 4) text += "\n" + theme.fg("muted", `… ${lines.length - 4} more (Ctrl+O)`);
|
||||||
|
}
|
||||||
|
return new Text(text, 0, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
1371
pi/.pi/agent/extensions/chat-claude.ts
Normal file
1371
pi/.pi/agent/extensions/chat-claude.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,419 +0,0 @@
|
|||||||
/**
|
|
||||||
* Claude Account Switch Extension
|
|
||||||
*
|
|
||||||
* Switches between two Claude Pro accounts (personal and work) **without
|
|
||||||
* restarting pi**. Works by swapping auth.json files at the filesystem level,
|
|
||||||
* then reloading the auth storage and forcing an immediate token refresh to
|
|
||||||
* validate the switch.
|
|
||||||
*
|
|
||||||
* Why file-level swaps? Anthropic's OAuth rotates refresh tokens on every
|
|
||||||
* refresh. Calling authStorage.set() can appear to work, but the next
|
|
||||||
* getApiKey() call triggers refreshOAuthTokenWithLock(), which re-reads
|
|
||||||
* auth.json from disk — overwriting in-memory changes if persistence
|
|
||||||
* silently failed. Working at the file level avoids this entirely.
|
|
||||||
*
|
|
||||||
* Setup (one-time per account):
|
|
||||||
* 1. /login → authenticate with personal account
|
|
||||||
* 2. /switch-claude save personal
|
|
||||||
* 3. /login → authenticate with work account
|
|
||||||
* 4. /switch-claude save work
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* /switch-claude — pick account interactively
|
|
||||||
* /switch-claude save <name> — save current pi login as a named profile
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
||||||
import * as fs from "node:fs";
|
|
||||||
import * as os from "node:os";
|
|
||||||
import * as path from "node:path";
|
|
||||||
import { execSync } from "node:child_process";
|
|
||||||
|
|
||||||
const HOME = os.homedir();
|
|
||||||
const AUTH_JSON = path.join(HOME, ".pi/agent/auth.json");
|
|
||||||
const MARKER_FILE = path.join(HOME, ".pi/agent/auth.json.current");
|
|
||||||
const PROFILES_DIR = path.join(HOME, ".pi/agent/profiles");
|
|
||||||
|
|
||||||
type Account = "personal" | "work";
|
|
||||||
|
|
||||||
// ── Session-window helpers ─────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// We store the actual `resets_at` timestamp returned by Claude's usage API
|
|
||||||
// (via the usage:update event) so the switch menu can show a live countdown
|
|
||||||
// to the next session reset rather than a guessed switchedAt + 5h window.
|
|
||||||
|
|
||||||
function sessionStampPath(account: Account): string {
|
|
||||||
return path.join(PROFILES_DIR, `session-${account}.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Persist the actual session-reset timestamp for an account. */
|
|
||||||
function saveSessionResetsAt(account: Account, resetsAt: number): void {
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
sessionStampPath(account),
|
|
||||||
JSON.stringify({ resetsAt }, null, 2),
|
|
||||||
{ mode: 0o600 },
|
|
||||||
);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Load the stored session-reset timestamp (ms epoch), or null. */
|
|
||||||
function loadSessionResetsAt(account: Account): number | null {
|
|
||||||
try {
|
|
||||||
const raw = fs.readFileSync(sessionStampPath(account), "utf-8");
|
|
||||||
const { resetsAt } = JSON.parse(raw) as { resetsAt: number };
|
|
||||||
if (typeof resetsAt === "number") return resetsAt;
|
|
||||||
} catch {}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Format milliseconds as a compact duration string. */
|
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
const totalSec = Math.ceil(ms / 1000);
|
|
||||||
const h = Math.floor(totalSec / 3600);
|
|
||||||
const m = Math.floor((totalSec % 3600) / 60);
|
|
||||||
const s = totalSec % 60;
|
|
||||||
if (h > 0) return `${h}h ${m}m`;
|
|
||||||
if (m > 0) return `${m}m`;
|
|
||||||
return `${s}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One-line session summary appended to each account option in the select menu.
|
|
||||||
*
|
|
||||||
* Window still running: [resets in Xh Ym]
|
|
||||||
* Window already passed: [0 (ready)]
|
|
||||||
* Never recorded: (empty)
|
|
||||||
*/
|
|
||||||
function sessionSummary(account: Account): string {
|
|
||||||
const resetsAt = loadSessionResetsAt(account);
|
|
||||||
if (resetsAt === null) return "";
|
|
||||||
const remaining = resetsAt - Date.now();
|
|
||||||
if (remaining <= 0) return " [0 (ready)]";
|
|
||||||
return ` [resets in ${formatDuration(remaining)}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Profile helpers ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function profilePath(account: Account): string {
|
|
||||||
return path.join(PROFILES_DIR, `auth-${account}.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasProfile(account: Account): boolean {
|
|
||||||
return fs.existsSync(profilePath(account));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save auth.json content directly to a profile file.
|
|
||||||
* This captures the exact on-disk state, including any tokens that were
|
|
||||||
* refreshed behind our back by the auth system.
|
|
||||||
*
|
|
||||||
* We parse + re-serialize the JSON to guard against corrupt auth.json
|
|
||||||
* (e.g. trailing commas left by buggy serializers). If the file can't
|
|
||||||
* be parsed, we skip the save rather than propagate bad data.
|
|
||||||
*/
|
|
||||||
function saveCurrentAuthToProfile(account: Account): boolean {
|
|
||||||
fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
||||||
if (!fs.existsSync(AUTH_JSON)) return false;
|
|
||||||
try {
|
|
||||||
const raw = fs.readFileSync(AUTH_JSON, "utf-8");
|
|
||||||
const parsed = JSON.parse(raw); // validates JSON
|
|
||||||
const clean = JSON.stringify(parsed, null, 2);
|
|
||||||
fs.writeFileSync(profilePath(account), clean, { mode: 0o600 });
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
// auth.json is missing or corrupt — don't propagate bad data
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy a profile file to auth.json. This is an atomic-ish swap that
|
|
||||||
* replaces the entire file rather than merging per-provider.
|
|
||||||
*
|
|
||||||
* Like saveCurrentAuthToProfile, we round-trip through JSON.parse to
|
|
||||||
* ensure we never write corrupt data to auth.json.
|
|
||||||
*/
|
|
||||||
function restoreProfileToAuth(account: Account): void {
|
|
||||||
const raw = fs.readFileSync(profilePath(account), "utf-8");
|
|
||||||
const parsed = JSON.parse(raw); // throws on corrupt profile
|
|
||||||
const clean = JSON.stringify(parsed, null, 2);
|
|
||||||
fs.writeFileSync(AUTH_JSON, clean, { mode: 0o600 });
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMarker(account: Account): void {
|
|
||||||
fs.writeFileSync(MARKER_FILE, account, "utf-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentAccount(): Account | "unknown" {
|
|
||||||
try {
|
|
||||||
const marker = fs.readFileSync(MARKER_FILE, "utf8").trim();
|
|
||||||
if (marker === "personal" || marker === "work") return marker;
|
|
||||||
} catch {}
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Other session detection ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function otherPiSessions(): number[] {
|
|
||||||
try {
|
|
||||||
const myPid = process.pid;
|
|
||||||
// Use a character class [c] trick so pgrep doesn't match its own process
|
|
||||||
const out = execSync("pgrep -f 'pi-[c]oding-agent' 2>/dev/null || true", {
|
|
||||||
encoding: "utf-8",
|
|
||||||
});
|
|
||||||
const pids = out
|
|
||||||
.trim()
|
|
||||||
.split("\n")
|
|
||||||
.map(Number)
|
|
||||||
.filter((p) => p && p !== myPid && !isNaN(p));
|
|
||||||
return pids;
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function killOtherSessions(pids: number[]): number {
|
|
||||||
let killed = 0;
|
|
||||||
for (const pid of pids) {
|
|
||||||
try {
|
|
||||||
process.kill(pid);
|
|
||||||
killed++;
|
|
||||||
} catch {
|
|
||||||
// already dead or permission denied
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return killed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── UI helpers ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function statusLabel(account: Account | "unknown"): string {
|
|
||||||
switch (account) {
|
|
||||||
case "personal":
|
|
||||||
return " personal";
|
|
||||||
case "work":
|
|
||||||
return " work";
|
|
||||||
default:
|
|
||||||
return " claude";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Extension ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
|
||||||
let currentAccount: Account | "unknown" = "unknown";
|
|
||||||
|
|
||||||
// Whenever usage-bars fetches fresh data, save the real resets_at for the
|
|
||||||
// current account so the switch menu shows an accurate live countdown.
|
|
||||||
pi.events.on("usage:update", (event: unknown) => {
|
|
||||||
const e = event as { sessionResetsAt?: number };
|
|
||||||
if (currentAccount !== "unknown" && typeof e.sessionResetsAt === "number") {
|
|
||||||
saveSessionResetsAt(currentAccount, e.sessionResetsAt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
pi.on("session_start", async (_event, ctx) => {
|
|
||||||
// Proper-lockfile creates auth.json.lock as a *directory* (atomic mkdir).
|
|
||||||
// If a regular file exists at that path (e.g. left by an older pi version),
|
|
||||||
// rmdir fails with ENOTDIR → lock acquisition throws → loadError is set →
|
|
||||||
// credentials are never persisted after /login. Delete the stale file and
|
|
||||||
// reload so this session has working auth persistence.
|
|
||||||
const lockPath = AUTH_JSON + ".lock";
|
|
||||||
try {
|
|
||||||
const stat = fs.statSync(lockPath);
|
|
||||||
if (stat.isFile()) {
|
|
||||||
fs.unlinkSync(lockPath);
|
|
||||||
ctx.modelRegistry.authStorage.reload();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// lock doesn't exist or we can't stat it — nothing to fix
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guard against corrupt auth.json (e.g. trailing commas from buggy
|
|
||||||
// serializers). Re-serialize to clean JSON and reload so the auth
|
|
||||||
// system picks up valid credentials.
|
|
||||||
try {
|
|
||||||
const raw = fs.readFileSync(AUTH_JSON, "utf-8");
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
const clean = JSON.stringify(parsed, null, 2);
|
|
||||||
if (clean !== raw) {
|
|
||||||
fs.writeFileSync(AUTH_JSON, clean, { mode: 0o600 });
|
|
||||||
ctx.modelRegistry.authStorage.reload();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// auth.json missing or unparseable — nothing we can fix here
|
|
||||||
}
|
|
||||||
|
|
||||||
currentAccount = getCurrentAccount();
|
|
||||||
ctx.ui.setStatus("claude-account", statusLabel(currentAccount));
|
|
||||||
});
|
|
||||||
|
|
||||||
pi.registerCommand("switch-claude", {
|
|
||||||
description:
|
|
||||||
"Switch between personal () and work () Claude accounts. Use 'save <name>' to save current login as a profile.",
|
|
||||||
handler: async (args, ctx) => {
|
|
||||||
const authStorage = ctx.modelRegistry.authStorage;
|
|
||||||
const trimmed = args?.trim() ?? "";
|
|
||||||
|
|
||||||
// ── Save current auth state as a named profile ──────────────────
|
|
||||||
if (trimmed.startsWith("save ")) {
|
|
||||||
const name = trimmed.slice(5).trim();
|
|
||||||
if (name !== "personal" && name !== "work") {
|
|
||||||
ctx.ui.notify(
|
|
||||||
"Usage: /switch-claude save personal|work",
|
|
||||||
"warning",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!authStorage.has("anthropic")) {
|
|
||||||
ctx.ui.notify(
|
|
||||||
"No Anthropic credentials found. Run /login first.",
|
|
||||||
"warning",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveCurrentAuthToProfile(name as Account);
|
|
||||||
currentAccount = name as Account;
|
|
||||||
setMarker(currentAccount);
|
|
||||||
ctx.ui.setStatus("claude-account", statusLabel(currentAccount));
|
|
||||||
ctx.ui.notify(
|
|
||||||
`Saved current login as ${statusLabel(name as Account)} profile`,
|
|
||||||
"info",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ── Resolve target account (direct arg or interactive) ──────────
|
|
||||||
let newAccount: Account;
|
|
||||||
if (trimmed === "personal" || trimmed === "work") {
|
|
||||||
newAccount = trimmed;
|
|
||||||
} else if (trimmed === "") {
|
|
||||||
const personalLabel = ` personal${currentAccount === "personal" ? " ← current" : ""}${!hasProfile("personal") ? " (no profile saved)" : ""}${sessionSummary("personal")}`;
|
|
||||||
const workLabel = ` work${currentAccount === "work" ? " ← current" : ""}${!hasProfile("work") ? " (no profile saved)" : ""}${sessionSummary("work")}`;
|
|
||||||
|
|
||||||
const accountChoice = await ctx.ui.select(
|
|
||||||
"Switch Claude account:",
|
|
||||||
[personalLabel, workLabel],
|
|
||||||
);
|
|
||||||
if (accountChoice === undefined) return;
|
|
||||||
newAccount = accountChoice.startsWith("") ? "personal" : "work";
|
|
||||||
} else {
|
|
||||||
ctx.ui.notify(
|
|
||||||
"Usage: /switch-claude [personal|work|save <name>]",
|
|
||||||
"warning",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newAccount === currentAccount) {
|
|
||||||
ctx.ui.notify(
|
|
||||||
`Already using ${statusLabel(newAccount)}`,
|
|
||||||
"info",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Warn about other sessions ───────────────────────────────────
|
|
||||||
const otherPids = otherPiSessions();
|
|
||||||
if (otherPids.length > 0) {
|
|
||||||
const sessionChoice = await ctx.ui.select(
|
|
||||||
`⚠️ ${otherPids.length} other pi session(s) detected`,
|
|
||||||
[
|
|
||||||
"Continue anyway",
|
|
||||||
`Kill ${otherPids.length} other instance(s) and continue`,
|
|
||||||
"Cancel",
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (sessionChoice === undefined || sessionChoice.includes("Cancel"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (sessionChoice.includes("Kill")) {
|
|
||||||
const killed = killOtherSessions(otherPids);
|
|
||||||
ctx.ui.notify(`Killed ${killed} pi session(s)`, "info");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasProfile(newAccount)) {
|
|
||||||
ctx.ui.notify(
|
|
||||||
`No profile saved for ${newAccount}. Run /login then /switch-claude save ${newAccount}`,
|
|
||||||
"warning",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Perform the switch ──────────────────────────────────────────
|
|
||||||
try {
|
|
||||||
// 1. Snapshot current auth.json → outgoing profile.
|
|
||||||
// This captures any tokens that were silently refreshed
|
|
||||||
// since the last save (the file is the source of truth,
|
|
||||||
// not the in-memory snapshot from getAll()).
|
|
||||||
if (currentAccount !== "unknown") {
|
|
||||||
saveCurrentAuthToProfile(currentAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Copy incoming profile → auth.json (full file replace).
|
|
||||||
restoreProfileToAuth(newAccount);
|
|
||||||
|
|
||||||
// 3. Tell AuthStorage to re-read the file. This updates
|
|
||||||
// the in-memory credential cache from the new auth.json.
|
|
||||||
authStorage.reload();
|
|
||||||
|
|
||||||
// 4. Force an immediate token refresh to validate the switch.
|
|
||||||
// If the stored refresh token is stale, this will fail now
|
|
||||||
// rather than on the next chat message.
|
|
||||||
const apiKey = await authStorage.getApiKey("anthropic");
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
// Refresh failed → roll back to the previous account.
|
|
||||||
if (currentAccount !== "unknown") {
|
|
||||||
restoreProfileToAuth(currentAccount);
|
|
||||||
authStorage.reload();
|
|
||||||
}
|
|
||||||
ctx.ui.notify(
|
|
||||||
`❌ Switch failed: could not authenticate as ${newAccount}. ` +
|
|
||||||
`The saved refresh token may have expired. ` +
|
|
||||||
`Run /login then /switch-claude save ${newAccount} to re-save.`,
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Success — the refresh worked, auth.json now has fresh
|
|
||||||
// tokens. Save them back to the profile so next switch
|
|
||||||
// has the latest refresh token.
|
|
||||||
saveCurrentAuthToProfile(newAccount);
|
|
||||||
|
|
||||||
currentAccount = newAccount;
|
|
||||||
setMarker(currentAccount);
|
|
||||||
ctx.ui.setStatus("claude-account", statusLabel(currentAccount));
|
|
||||||
pi.events.emit("claude-account:switched", { account: newAccount });
|
|
||||||
ctx.ui.notify(
|
|
||||||
`Switched to ${statusLabel(newAccount)} ✓`,
|
|
||||||
"info",
|
|
||||||
);
|
|
||||||
} catch (e: unknown) {
|
|
||||||
// Something went wrong → try to roll back.
|
|
||||||
try {
|
|
||||||
if (currentAccount !== "unknown" && hasProfile(currentAccount)) {
|
|
||||||
restoreProfileToAuth(currentAccount);
|
|
||||||
authStorage.reload();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// rollback failed too — nothing more we can do
|
|
||||||
}
|
|
||||||
|
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
|
||||||
ctx.ui.notify(
|
|
||||||
`❌ Switch failed: ${msg}. Rolled back to ${statusLabel(currentAccount)}. ` +
|
|
||||||
`You may need to /login and /switch-claude save ${newAccount}.`,
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
1522
pi/.pi/agent/extensions/claude-agent-sdk/package-lock.json
generated
1522
pi/.pi/agent/extensions/claude-agent-sdk/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "claude-agent-sdk",
|
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
|
|
||||||
"change-case": "^5.4.4"
|
|
||||||
},
|
|
||||||
"pi": {
|
|
||||||
"extensions": ["./index.ts"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,10 @@
|
|||||||
* Replaces the built-in pi footer with a single clean line that assembles
|
* Replaces the built-in pi footer with a single clean line that assembles
|
||||||
* status from all other extensions:
|
* status from all other extensions:
|
||||||
*
|
*
|
||||||
* \uEF85 | S ⣿⣶⣀⣀⣀ 34% 2h 55m | W ⣿⣿⣷⣀⣀ 68% ⟳ Fri 09:00 | C ⣿⣀⣀⣀⣀ 20% | Sonnet 4.6 | rust-analyzer | MCP: 1/2
|
* ~dir | S ⣿⣶⣀⣀⣀ 34% 2h 55m | W ⣿⣿⣷⣀⣀ 68% ⟳ Fri 09:00 | C ⣿⣀⣀⣀⣀ 20% | Sonnet 4.6 | rust-analyzer | MCP: 1/2
|
||||||
*
|
*
|
||||||
* Status sources:
|
* Status sources:
|
||||||
* "claude-account" — set by claude-account-switch.ts → just the icon
|
* usage:update event — set by usage-bars extension → S/W bars (Claude usage, always shown)
|
||||||
* "usage-bars" — set by usage-bars extension → S/W bars (pass-through)
|
|
||||||
* ctx.getContextUsage() → C bar (rendered here)
|
* ctx.getContextUsage() → C bar (rendered here)
|
||||||
* ctx.model → model short name
|
* ctx.model → model short name
|
||||||
* "lsp" — set by lsp-pi extension → strip "LSP " prefix
|
* "lsp" — set by lsp-pi extension → strip "LSP " prefix
|
||||||
@@ -77,11 +76,6 @@ function formatDurationMs(ms: number): string {
|
|||||||
return "<1m";
|
return "<1m";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nerd Font codepoints matched to what claude-account-switch.ts emits
|
|
||||||
const ICON_PERSONAL = "\uEF85"; // U+EF85 — home
|
|
||||||
const ICON_WORK = "\uDB80\uDCD6"; // U+F00D6 — briefcase (surrogate pair)
|
|
||||||
const ICON_UNKNOWN = "\uF420"; // U+F420 — claude default
|
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
export default function (pi: ExtensionAPI) {
|
||||||
let ctx: any = null;
|
let ctx: any = null;
|
||||||
let tuiRef: any = null;
|
let tuiRef: any = null;
|
||||||
@@ -112,18 +106,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
: cwd;
|
: cwd;
|
||||||
parts.push(theme.fg("dim", dir));
|
parts.push(theme.fg("dim", dir));
|
||||||
|
|
||||||
// 2. Account icon
|
// 2. S / W usage bars + C bar — joined as one |-separated block
|
||||||
const acctRaw = statuses.get("claude-account");
|
|
||||||
if (acctRaw !== undefined) {
|
|
||||||
const clean = stripAnsi(acctRaw).trim();
|
|
||||||
let icon: string;
|
|
||||||
if (clean.includes("personal")) icon = ICON_PERSONAL;
|
|
||||||
else if (clean.includes("work")) icon = ICON_WORK;
|
|
||||||
else icon = ICON_UNKNOWN;
|
|
||||||
parts.push(theme.fg("dim", icon));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. S / W usage bars + C bar — joined as one |-separated block
|
|
||||||
const usageRaw = statuses.get("usage-bars");
|
const usageRaw = statuses.get("usage-bars");
|
||||||
const contextUsage = ctx?.getContextUsage?.();
|
const contextUsage = ctx?.getContextUsage?.();
|
||||||
{
|
{
|
||||||
@@ -135,8 +118,8 @@ export default function (pi: ExtensionAPI) {
|
|||||||
const session = Math.max(0, Math.min(100, Math.round(usageSession)));
|
const session = Math.max(0, Math.min(100, Math.round(usageSession)));
|
||||||
const weekly = Math.max(0, Math.min(100, Math.round(usageWeekly)));
|
const weekly = Math.max(0, Math.min(100, Math.round(usageWeekly)));
|
||||||
|
|
||||||
let sPart = theme.fg("muted", "S ") + renderBrailleBar(theme, session) + " " + theme.fg("dim", `${session}%`);
|
let sPart = theme.fg("muted", "\uF4F5 S ") + renderBrailleBar(theme, session) + " " + theme.fg("dim", `${session}%`);
|
||||||
let wPart = theme.fg("muted", "W ") + renderBrailleBar(theme, weekly) + " " + theme.fg("dim", `${weekly}%`);
|
let wPart = theme.fg("muted", "\uF4F5 W ") + renderBrailleBar(theme, weekly) + " " + theme.fg("dim", `${weekly}%`);
|
||||||
|
|
||||||
if (sessionResetsAt !== null) {
|
if (sessionResetsAt !== null) {
|
||||||
const msLeft = sessionResetsAt - Date.now();
|
const msLeft = sessionResetsAt - Date.now();
|
||||||
@@ -156,34 +139,51 @@ export default function (pi: ExtensionAPI) {
|
|||||||
|
|
||||||
if (contextUsage && contextUsage.percent !== null) {
|
if (contextUsage && contextUsage.percent !== null) {
|
||||||
const pct = Math.round(contextUsage.percent);
|
const pct = Math.round(contextUsage.percent);
|
||||||
|
const chatStatus = statuses.get("chat-claude");
|
||||||
|
const isChatActive = !!chatStatus && chatStatus.includes("Claude");
|
||||||
|
|
||||||
|
// When chat is active and context is high, show warning indicator
|
||||||
|
let cLabel = "C";
|
||||||
|
let cColor = "muted";
|
||||||
|
if (isChatActive && pct >= 70) {
|
||||||
|
cLabel = pct >= 90 ? "C⚠" : "C⚡";
|
||||||
|
cColor = pct >= 90 ? "error" : "warning";
|
||||||
|
}
|
||||||
|
|
||||||
const cBar =
|
const cBar =
|
||||||
theme.fg("muted", "C ") +
|
theme.fg(cColor, cLabel + " ") +
|
||||||
renderBrailleBar(theme, pct) +
|
renderBrailleBar(theme, pct) +
|
||||||
" " +
|
" " +
|
||||||
theme.fg("dim", `${pct}%`);
|
theme.fg(pct >= 70 && isChatActive ? "warning" : "dim", `${pct}%`);
|
||||||
block = block ? block + pipeSep + cBar : cBar;
|
block = block ? block + pipeSep + cBar : cBar;
|
||||||
}
|
}
|
||||||
if (block) parts.push(block);
|
if (block) parts.push(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Model short name
|
// 3. Model short name
|
||||||
const modelId = ctx?.model?.id;
|
const modelId = ctx?.model?.id;
|
||||||
if (modelId) parts.push(theme.fg("dim", getModelShortName(modelId)));
|
if (modelId) parts.push(theme.fg("dim", getModelShortName(modelId)));
|
||||||
|
|
||||||
// 5. LSP — strip "LSP" prefix and activity dot
|
// 4. LSP — strip "LSP" prefix and activity dot
|
||||||
const lspRaw = statuses.get("lsp");
|
const lspRaw = statuses.get("lsp");
|
||||||
if (lspRaw) {
|
if (lspRaw) {
|
||||||
const clean = stripAnsi(lspRaw).trim().replace(/^LSP\s*[•·]?\s*/i, "").trim();
|
const clean = stripAnsi(lspRaw).trim().replace(/^LSP\s*[•·]?\s*/i, "").trim();
|
||||||
if (clean) parts.push(theme.fg("dim", clean));
|
if (clean) parts.push(theme.fg("dim", clean));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. MCP — strip " servers" suffix
|
// 5. MCP — strip " servers" suffix
|
||||||
const mcpRaw = statuses.get("mcp");
|
const mcpRaw = statuses.get("mcp");
|
||||||
if (mcpRaw) {
|
if (mcpRaw) {
|
||||||
const clean = stripAnsi(mcpRaw).trim().replace(/\s*servers?.*$/, "").trim();
|
const clean = stripAnsi(mcpRaw).trim().replace(/\s*servers?.*$/, "").trim();
|
||||||
if (clean) parts.push(theme.fg("dim", clean));
|
if (clean) parts.push(theme.fg("dim", clean));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6. Active Claude chat session
|
||||||
|
const chatRaw = statuses.get("chat-claude");
|
||||||
|
if (chatRaw) {
|
||||||
|
parts.push(theme.fg("accent", stripAnsi(chatRaw).trim()));
|
||||||
|
}
|
||||||
|
|
||||||
return parts.join(sep);
|
return parts.join(sep);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Event handlers
|
// Event handlers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
pi.on("session_start", async (_event, _ctx) => {
|
pi.on("session_start", (_event, _ctx) => {
|
||||||
ctx = _ctx;
|
ctx = _ctx;
|
||||||
installFooter(_ctx);
|
installFooter(_ctx);
|
||||||
});
|
});
|
||||||
@@ -230,11 +230,6 @@ export default function (pi: ExtensionAPI) {
|
|||||||
if (tuiRef) tuiRef.requestRender();
|
if (tuiRef) tuiRef.requestRender();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-render when account switches (usage:update comes from usage-bars setStatus which
|
|
||||||
// already triggers a render, but account icon needs a nudge too)
|
|
||||||
pi.events.on("claude-account:switched", () => {
|
|
||||||
if (tuiRef) tuiRef.requestRender();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for usage updates — store raw values so we can build bars + dynamic
|
// Listen for usage updates — store raw values so we can build bars + dynamic
|
||||||
// countdown directly rather than parsing the ANSI status string from usage-bars.
|
// countdown directly rather than parsing the ANSI status string from usage-bars.
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
/**
|
|
||||||
* Footer Display Extension
|
|
||||||
*
|
|
||||||
* Replaces the built-in pi footer with a single clean line that assembles
|
|
||||||
* status from all other extensions:
|
|
||||||
*
|
|
||||||
* \uEF85 | S ⣿⣶⣀⣀⣀ 34% 2h 55m | W ⣿⣿⣷⣀⣀ 68% ⟳ Fri 09:00 | C ⣿⣀⣀⣀⣀ 20% | Sonnet 4.6 | rust-analyzer | MCP: 1/2
|
|
||||||
*
|
|
||||||
* Status sources:
|
|
||||||
* "claude-account" — set by claude-account-switch.ts → just the icon
|
|
||||||
* "usage-bars" — set by usage-bars extension → S/W bars (pass-through)
|
|
||||||
* ctx.getContextUsage() → C bar (rendered here)
|
|
||||||
* ctx.model → model short name
|
|
||||||
* "lsp" — set by lsp-pi extension → strip "LSP " prefix
|
|
||||||
* "mcp" — set by pi-mcp-adapter → strip " servers" suffix
|
|
||||||
*/
|
|
||||||
|
|
||||||
import os from "os";
|
|
||||||
import path from "path";
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
||||||
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Braille gradient bar — used here only for the context (C) bar
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
const BRAILLE_GRADIENT = "\u28C0\u28C4\u28E4\u28E6\u28F6\u28F7\u28FF";
|
|
||||||
const BRAILLE_EMPTY = "\u28C0";
|
|
||||||
const BAR_WIDTH = 5;
|
|
||||||
|
|
||||||
function renderBrailleBar(theme: any, value: number): string {
|
|
||||||
const v = Math.max(0, Math.min(100, Math.round(value)));
|
|
||||||
const levels = BRAILLE_GRADIENT.length - 1;
|
|
||||||
const totalSteps = BAR_WIDTH * levels;
|
|
||||||
const filledSteps = Math.round((v / 100) * totalSteps);
|
|
||||||
const full = Math.floor(filledSteps / levels);
|
|
||||||
const partial = filledSteps % levels;
|
|
||||||
const empty = BAR_WIDTH - full - (partial ? 1 : 0);
|
|
||||||
const color = v >= 90 ? "error" : v >= 70 ? "warning" : "success";
|
|
||||||
const filled = BRAILLE_GRADIENT[BRAILLE_GRADIENT.length - 1]!.repeat(Math.max(0, full));
|
|
||||||
const partialChar = partial ? BRAILLE_GRADIENT[partial]! : "";
|
|
||||||
const emptyChars = BRAILLE_EMPTY.repeat(Math.max(0, empty));
|
|
||||||
return theme.fg(color, filled + partialChar) + theme.fg("dim", emptyChars);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function stripAnsi(text: string): string {
|
|
||||||
return text.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModelShortName(modelId: string): string {
|
|
||||||
// claude-haiku-4-5 → "Haiku 4.5", claude-sonnet-4-6 → "Sonnet 4.6"
|
|
||||||
const m = modelId.match(/^claude-([a-z]+)-([\d]+(?:-[\d]+)*)(?:-\d{8})?$/);
|
|
||||||
if (m) {
|
|
||||||
const family = m[1]!.charAt(0).toUpperCase() + m[1]!.slice(1);
|
|
||||||
return `${family} ${m[2]!.replace(/-/g, ".")}`;
|
|
||||||
}
|
|
||||||
// claude-3-5-sonnet, claude-3-opus, etc.
|
|
||||||
const m2 = modelId.match(/^claude-[\d-]+-([a-z]+)/);
|
|
||||||
if (m2) return m2[1]!.charAt(0).toUpperCase() + m2[1]!.slice(1);
|
|
||||||
return modelId.replace(/^claude-/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nerd Font codepoints matched to what claude-account-switch.ts emits
|
|
||||||
const ICON_PERSONAL = "\uEF85"; // U+EF85 — home
|
|
||||||
const ICON_WORK = "\uDB80\uDCD6"; // U+F00D6 — briefcase (surrogate pair)
|
|
||||||
const ICON_UNKNOWN = "\uF420"; // U+F420 — claude default
|
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
|
||||||
let ctx: any = null;
|
|
||||||
let tuiRef: any = null;
|
|
||||||
let footerDataRef: any = null;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Footer line builder — called on every render
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function buildFooterLine(theme: any): string {
|
|
||||||
const sep = theme.fg("dim", " · ");
|
|
||||||
const pipeSep = theme.fg("dim", " | ");
|
|
||||||
const parts: string[] = [];
|
|
||||||
|
|
||||||
const statuses: ReadonlyMap<string, string> =
|
|
||||||
footerDataRef?.getExtensionStatuses?.() ?? new Map();
|
|
||||||
|
|
||||||
// 1. Current working directory
|
|
||||||
const home = os.homedir();
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const dir = cwd.startsWith(home)
|
|
||||||
? "~" + path.sep + path.relative(home, cwd)
|
|
||||||
: cwd;
|
|
||||||
parts.push(theme.fg("dim", dir));
|
|
||||||
|
|
||||||
// 2. Account icon
|
|
||||||
const acctRaw = statuses.get("claude-account");
|
|
||||||
if (acctRaw !== undefined) {
|
|
||||||
const clean = stripAnsi(acctRaw).trim();
|
|
||||||
let icon: string;
|
|
||||||
if (clean.includes("personal")) icon = ICON_PERSONAL;
|
|
||||||
else if (clean.includes("work")) icon = ICON_WORK;
|
|
||||||
else icon = ICON_UNKNOWN;
|
|
||||||
parts.push(theme.fg("dim", icon));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. S / W usage bars + C bar — joined as one |-separated block
|
|
||||||
const usageRaw = statuses.get("usage-bars");
|
|
||||||
const contextUsage = ctx?.getContextUsage?.();
|
|
||||||
{
|
|
||||||
let block = usageRaw ?? "";
|
|
||||||
if (contextUsage && contextUsage.percent !== null) {
|
|
||||||
const pct = Math.round(contextUsage.percent);
|
|
||||||
const cBar =
|
|
||||||
theme.fg("muted", "C ") +
|
|
||||||
renderBrailleBar(theme, pct) +
|
|
||||||
" " +
|
|
||||||
theme.fg("dim", `${pct}%`);
|
|
||||||
block = block ? block + pipeSep + cBar : cBar;
|
|
||||||
}
|
|
||||||
if (block) parts.push(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Model short name
|
|
||||||
const modelId = ctx?.model?.id;
|
|
||||||
if (modelId) parts.push(theme.fg("dim", getModelShortName(modelId)));
|
|
||||||
|
|
||||||
// 5. LSP — strip "LSP" prefix and activity dot
|
|
||||||
const lspRaw = statuses.get("lsp");
|
|
||||||
if (lspRaw) {
|
|
||||||
const clean = stripAnsi(lspRaw).trim().replace(/^LSP\s*[•·]?\s*/i, "").trim();
|
|
||||||
if (clean) parts.push(theme.fg("dim", clean));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. MCP — strip " servers" suffix
|
|
||||||
const mcpRaw = statuses.get("mcp");
|
|
||||||
if (mcpRaw) {
|
|
||||||
const clean = stripAnsi(mcpRaw).trim().replace(/\s*servers?.*$/, "").trim();
|
|
||||||
if (clean) parts.push(theme.fg("dim", clean));
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join(sep);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Footer installation
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function installFooter(_ctx: any) {
|
|
||||||
if (!_ctx?.hasUI) return;
|
|
||||||
_ctx.ui.setFooter((_tui: any, theme: any, footerData: any) => {
|
|
||||||
tuiRef = _tui;
|
|
||||||
footerDataRef = footerData;
|
|
||||||
const unsub = footerData.onBranchChange(() => _tui.requestRender());
|
|
||||||
return {
|
|
||||||
dispose: unsub,
|
|
||||||
invalidate() {},
|
|
||||||
render(width: number): string[] {
|
|
||||||
return [truncateToWidth(buildFooterLine(theme) || "", width)];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Event handlers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
pi.on("session_start", async (_event, _ctx) => {
|
|
||||||
ctx = _ctx;
|
|
||||||
installFooter(_ctx);
|
|
||||||
});
|
|
||||||
|
|
||||||
pi.on("session_shutdown", (_event, _ctx) => {
|
|
||||||
if (_ctx?.hasUI) _ctx.ui.setFooter(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-render after turns so context usage stays current
|
|
||||||
pi.on("turn_end", (_event, _ctx) => {
|
|
||||||
ctx = _ctx;
|
|
||||||
if (tuiRef) tuiRef.requestRender();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-render when model changes (updates model name in footer)
|
|
||||||
pi.on("model_select", (_event, _ctx) => {
|
|
||||||
ctx = _ctx;
|
|
||||||
if (tuiRef) tuiRef.requestRender();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-render when account switches (usage:update comes from usage-bars setStatus which
|
|
||||||
// already triggers a render, but account icon needs a nudge too)
|
|
||||||
pi.events.on("claude-account:switched", () => {
|
|
||||||
if (tuiRef) tuiRef.requestRender();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* Local Explorer Extension
|
|
||||||
*
|
|
||||||
* Previously auto-injected a scout hint into every cloud-model session.
|
|
||||||
* Now a no-op — scout delegation is opt-in via the `local-scout` skill.
|
|
||||||
* Invoke with /skill:local-scout or by mentioning "use scout" in a prompt.
|
|
||||||
*
|
|
||||||
* Placed in: ~/.pi/agent/extensions/local-explorer.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
||||||
|
|
||||||
export default function (_pi: ExtensionAPI) {
|
|
||||||
// No automatic injection — scout is opt-in via the local-scout skill.
|
|
||||||
}
|
|
||||||
56
pi/.pi/agent/extensions/pi-ask-mcp/README.md
Normal file
56
pi/.pi/agent/extensions/pi-ask-mcp/README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# pi-ask-mcp
|
||||||
|
|
||||||
|
A minimal MCP stdio server that gives Claude **one** tool — `ask` — which routes
|
||||||
|
structured questions back to pi's native ask UI instead of using Claude's
|
||||||
|
built-in `AskUserQuestion`.
|
||||||
|
|
||||||
|
This is **not** a regular pi extension. It is a subprocess of `claude`, which is
|
||||||
|
itself a subprocess of the `chat-claude` extension. The pi-side counterpart is
|
||||||
|
[`shared/pi-ask-bridge.ts`](../../shared/pi-ask-bridge.ts), which:
|
||||||
|
|
||||||
|
1. Opens a Unix-domain socket per chat session.
|
||||||
|
2. Generates an `--mcp-config` JSON pointing here, with `PI_ASK_SOCKET=<sock>`.
|
||||||
|
3. Translates `ask` requests off the socket into
|
||||||
|
`askSingleQuestionWithInlineNote` / `askQuestionsWithTabs` calls and writes
|
||||||
|
the result back.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
pi
|
||||||
|
└── chat-claude
|
||||||
|
├── pi-ask-bridge (UDS server, owns ui.custom)
|
||||||
|
└── claude -p ... --mcp-config <generated.json> --disallowed-tools AskUserQuestion
|
||||||
|
└── pi-ask-mcp/server.js (this file)
|
||||||
|
↳ on tools/call ask → connect $PI_ASK_SOCKET → ask → reply
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why a hand-written MCP server
|
||||||
|
|
||||||
|
No `@modelcontextprotocol/sdk` dependency, no transpile step, no
|
||||||
|
`node_modules`. The MCP stdio protocol is small enough (~6 method handlers)
|
||||||
|
that writing it directly keeps the file self-contained and trivially
|
||||||
|
portable. Claude CLI spawns it via `node server.js`.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
Stdio (with Claude): JSON-RPC 2.0 over newline-delimited JSON.
|
||||||
|
|
||||||
|
Socket (with pi-ask-bridge): NDJSON, one request → one response, then close.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// → pi
|
||||||
|
{ "id": "uuid", "type": "ask",
|
||||||
|
"questions": [
|
||||||
|
{ "id": "auth", "question": "Auth method?",
|
||||||
|
"options": [{"label": "OAuth"}, {"label": "API key"}],
|
||||||
|
"multi": false, "recommended": 0 }
|
||||||
|
] }
|
||||||
|
|
||||||
|
// ← pi (success)
|
||||||
|
{ "id": "uuid", "type": "result",
|
||||||
|
"results": [{ "id": "auth", "selectedOptions": ["OAuth"] }] }
|
||||||
|
|
||||||
|
// ← pi (cancel / error)
|
||||||
|
{ "id": "uuid", "type": "error", "message": "cancelled" }
|
||||||
|
```
|
||||||
7
pi/.pi/agent/extensions/pi-ask-mcp/package.json
Normal file
7
pi/.pi/agent/extensions/pi-ask-mcp/package.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "pi-ask-mcp",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "server.js",
|
||||||
|
"description": "Minimal MCP stdio server bridging Claude → pi-ask-bridge."
|
||||||
|
}
|
||||||
195
pi/.pi/agent/extensions/pi-ask-mcp/server.js
Executable file
195
pi/.pi/agent/extensions/pi-ask-mcp/server.js
Executable file
@@ -0,0 +1,195 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// pi-ask-mcp/server.js
|
||||||
|
//
|
||||||
|
// Minimal MCP stdio server that exposes ONE tool: `ask`.
|
||||||
|
// Bridges Claude → pi via a Unix-domain socket: when Claude calls the tool,
|
||||||
|
// this server forwards the question(s) to pi over $PI_ASK_SOCKET, awaits
|
||||||
|
// the user's answer, and returns it as the tool result.
|
||||||
|
//
|
||||||
|
// Wire format with Claude (stdin/stdout): JSON-RPC 2.0 over NDJSON.
|
||||||
|
// Wire format with pi (PI_ASK_SOCKET): NDJSON request/response, see
|
||||||
|
// ../../shared/pi-ask-bridge.ts.
|
||||||
|
//
|
||||||
|
// This file is INTENTIONALLY plain JavaScript (no transpile step, no
|
||||||
|
// node_modules) — Claude CLI spawns it via `node <path>`. Keep it small,
|
||||||
|
// dependency-free, and self-contained.
|
||||||
|
|
||||||
|
import { connect } from "node:net";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { createInterface } from "node:readline";
|
||||||
|
|
||||||
|
// ── Configuration ──────────────────────────────────────────────────────────
|
||||||
|
const SOCKET = process.env.PI_ASK_SOCKET;
|
||||||
|
if (!SOCKET) {
|
||||||
|
process.stderr.write("[pi-ask-mcp] PI_ASK_SOCKET env var is required\n");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERVER_INFO = { name: "pi", version: "0.1.0" };
|
||||||
|
const PROTOCOL_VERSION = "2024-11-05";
|
||||||
|
const SOCKET_TIMEOUT_MS = 15 * 60 * 1000; // matches runClaude's default
|
||||||
|
|
||||||
|
// ── Tool schema (kept in sync with pi-ask-tool/index.ts AskParamsSchema) ──
|
||||||
|
const ASK_INPUT_SCHEMA = {
|
||||||
|
type: "object",
|
||||||
|
required: ["questions"],
|
||||||
|
properties: {
|
||||||
|
questions: {
|
||||||
|
type: "array",
|
||||||
|
minItems: 1,
|
||||||
|
description: "One or more questions to ask the user.",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
required: ["id", "question", "options"],
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "Stable id (e.g. 'auth', 'cache')." },
|
||||||
|
question: { type: "string", description: "Question text shown to the user." },
|
||||||
|
options: {
|
||||||
|
type: "array",
|
||||||
|
minItems: 1,
|
||||||
|
description: "2-5 concise options. Do NOT include 'Other' (UI adds it).",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
required: ["label"],
|
||||||
|
properties: {
|
||||||
|
label: { type: "string", description: "Option display label." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
multi: { type: "boolean", description: "Allow multi-select. Defaults to false." },
|
||||||
|
recommended: { type: "number", description: "0-indexed recommended option (default highlight)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ASK_DESCRIPTION = [
|
||||||
|
"Ask the user one or more structured questions through pi's native TUI.",
|
||||||
|
"Use this whenever a choice materially affects the outcome — instead of",
|
||||||
|
"guessing or the built-in AskUserQuestion. Provide 2-5 concise options.",
|
||||||
|
"Set multi=true when multiple answers are valid. Do NOT include an 'Other'",
|
||||||
|
"option (UI adds it automatically). The result is a JSON array of",
|
||||||
|
"{id, selectedOptions[], customInput?} per question — empty selectedOptions",
|
||||||
|
"means the user cancelled.",
|
||||||
|
].join(" ");
|
||||||
|
|
||||||
|
// ── stdio framing: NDJSON ──────────────────────────────────────────────────
|
||||||
|
const rl = createInterface({ input: process.stdin });
|
||||||
|
const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
|
||||||
|
const log = (msg) => process.stderr.write(`[pi-ask-mcp] ${msg}\n`);
|
||||||
|
|
||||||
|
// ── socket round-trip to pi-ask-bridge ─────────────────────────────────────
|
||||||
|
function askPi(args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const sock = connect(SOCKET);
|
||||||
|
const id = randomUUID();
|
||||||
|
let buf = "";
|
||||||
|
let settled = false;
|
||||||
|
const finish = (fn, val) => { if (settled) return; settled = true; clearTimeout(t); fn(val); try { sock.end(); } catch {} };
|
||||||
|
const t = setTimeout(
|
||||||
|
() => finish(reject, new Error(`pi-ask bridge timeout after ${SOCKET_TIMEOUT_MS / 1000}s`)),
|
||||||
|
SOCKET_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
sock.on("connect", () => sock.write(JSON.stringify({ id, type: "ask", ...args }) + "\n"));
|
||||||
|
sock.on("data", (d) => {
|
||||||
|
buf += d.toString();
|
||||||
|
const nl = buf.indexOf("\n");
|
||||||
|
if (nl < 0) return;
|
||||||
|
try { finish(resolve, JSON.parse(buf.slice(0, nl))); }
|
||||||
|
catch (err) { finish(reject, err); }
|
||||||
|
});
|
||||||
|
sock.on("error", (err) => finish(reject, err));
|
||||||
|
sock.on("close", () => {
|
||||||
|
if (!settled) finish(reject, new Error("pi-ask bridge closed connection without reply"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JSON-RPC method handlers ───────────────────────────────────────────────
|
||||||
|
async function handleRequest(req) {
|
||||||
|
const { id, method, params } = req;
|
||||||
|
try {
|
||||||
|
switch (method) {
|
||||||
|
case "initialize":
|
||||||
|
return ok(id, {
|
||||||
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
|
capabilities: { tools: {} },
|
||||||
|
serverInfo: SERVER_INFO,
|
||||||
|
});
|
||||||
|
case "tools/list":
|
||||||
|
return ok(id, {
|
||||||
|
tools: [{ name: "ask", description: ASK_DESCRIPTION, inputSchema: ASK_INPUT_SCHEMA }],
|
||||||
|
});
|
||||||
|
case "tools/call": {
|
||||||
|
const name = params?.name;
|
||||||
|
const args = params?.arguments ?? {};
|
||||||
|
if (name !== "ask") return err(id, -32602, `unknown tool: ${name}`);
|
||||||
|
const reply = await askPi(args);
|
||||||
|
if (reply.type === "error") {
|
||||||
|
return ok(id, {
|
||||||
|
isError: true,
|
||||||
|
content: [{ type: "text", text: `(user did not answer: ${reply.message})` }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ok(id, {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(reply.results, null, 2) }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case "ping": return ok(id, {});
|
||||||
|
case "resources/list": return ok(id, { resources: [] });
|
||||||
|
case "prompts/list": return ok(id, { prompts: [] });
|
||||||
|
default: return err(id, -32601, `method not found: ${method}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return err(id, -32603, e instanceof Error ? e.message : String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = (id, result) => ({ jsonrpc: "2.0", id, result });
|
||||||
|
const err = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
|
||||||
|
|
||||||
|
// ── main loop ──────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Track in-flight handlers so we don't exit before they finish. Without this,
|
||||||
|
// `node server.js <<<input` (or any case where stdin closes mid-request) would
|
||||||
|
// race the async tools/call handler against rl 'close' → process.exit, and
|
||||||
|
// the reply would silently disappear.
|
||||||
|
let inflight = 0;
|
||||||
|
let stdinClosed = false;
|
||||||
|
function drainAndExit(code = 0) {
|
||||||
|
if (inflight === 0) process.exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOne(msg) {
|
||||||
|
// Notifications carry no id and expect no response.
|
||||||
|
if (msg.id === undefined || msg.id === null) {
|
||||||
|
if (msg.method === "exit") drainAndExit(0);
|
||||||
|
return; // notifications/initialized, notifications/cancelled, etc.
|
||||||
|
}
|
||||||
|
inflight += 1;
|
||||||
|
try {
|
||||||
|
const reply = await handleRequest(msg);
|
||||||
|
if (reply) send(reply);
|
||||||
|
} finally {
|
||||||
|
inflight -= 1;
|
||||||
|
if (stdinClosed) drainAndExit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.on("line", (line) => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
let msg;
|
||||||
|
try { msg = JSON.parse(line); } catch { return; }
|
||||||
|
if (Array.isArray(msg)) {
|
||||||
|
for (const m of msg) void handleOne(m);
|
||||||
|
} else {
|
||||||
|
void handleOne(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.on("close", () => { stdinClosed = true; drainAndExit(0); });
|
||||||
|
process.on("SIGTERM", () => process.exit(0));
|
||||||
|
process.on("SIGINT", () => process.exit(0));
|
||||||
|
log("ready, socket=" + SOCKET);
|
||||||
23
pi/.pi/agent/extensions/pi-ask-tool/README.md
Normal file
23
pi/.pi/agent/extensions/pi-ask-tool/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Pi Ask Tool Extension
|
||||||
|
|
||||||
|
This extension bridges Claude Code's ask functionality into pi's TUI, allowing users to ask questions and receive answers directly in the TUI interface.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Seamless integration with pi's TUI
|
||||||
|
- Support for all Claude agents (plan_review, code_review, debug, oracle)
|
||||||
|
- Multi-turn conversations with session management
|
||||||
|
- Context-aware responses based on codebase exploration
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Run the CLI agent: `pi-ask-tool`
|
||||||
|
2. Type your question in the TUI
|
||||||
|
3. Receive answers directly in the TUI interface
|
||||||
|
4. Continue the conversation or start new ones
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Default agent: `code_review`
|
||||||
|
Default model: `sonnet`
|
||||||
|
Session persistence: Enabled
|
||||||
37
pi/.pi/agent/extensions/pi-ask-tool/cli.ts
Normal file
37
pi/.pi/agent/extensions/pi-ask-tool/cli.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Pi Ask Tool CLI Agent
|
||||||
|
|
||||||
|
import { ask_claude } from "../../@piplugin/ask-claude"
|
||||||
|
import { sessionId } from '../shared';
|
||||||
|
|
||||||
|
export async function start() {
|
||||||
|
console.log('Pi Ask Tool initialized');
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Get user input from TUI (simplified for example)
|
||||||
|
const userInput = await getTUIInput('Ask a question:');
|
||||||
|
|
||||||
|
// Handle multi-turn sessions via sessionId
|
||||||
|
const response = await ask_claude({
|
||||||
|
prompt: userInput,
|
||||||
|
agent: 'code_review', // Default agent
|
||||||
|
session_id: sessionId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Display answer in TUI
|
||||||
|
await showTUIResult(response);
|
||||||
|
|
||||||
|
// Update session context if needed
|
||||||
|
sessionId = response.session_id || sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock TUI handlers - implement actual TUI integration
|
||||||
|
async function getTUIInput(question: string) {
|
||||||
|
// Replace with real TUI input method
|
||||||
|
const input = process.stdin.read().toString();
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showTUIResult(result: any) {
|
||||||
|
console.log('Answer:', result.summary || result);
|
||||||
|
}
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
/**
|
|
||||||
* Qwen Provider Extension
|
|
||||||
*
|
|
||||||
* Registers Qwen 3.5 models via the qwen.ai OAuth flow (chat.qwen.ai).
|
|
||||||
* Based on the upstream custom-provider-qwen-cli example.
|
|
||||||
*
|
|
||||||
* Models:
|
|
||||||
* - qwen3.5-plus (Qwen3.5 best — rivals Qwen3-Max, 1M ctx, cheaper)
|
|
||||||
* - qwen3.5-flash (Qwen3.5 fast & cheap, 1M ctx)
|
|
||||||
* - qwen3-max (Qwen3 flagship, strongest reasoning, 262K ctx)
|
|
||||||
* - qwen-plus (Qwen3 balanced, 1M ctx)
|
|
||||||
* - qwen-flash (Qwen3 fast, 1M ctx)
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* /login qwen-cli (browser OAuth)
|
|
||||||
* or set QWEN_CLI_API_KEY=...
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { OAuthCredentials, OAuthLoginCallbacks } from "@mariozechner/pi-ai";
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Constants
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const QWEN_DEVICE_CODE_ENDPOINT = "https://chat.qwen.ai/api/v1/oauth2/device/code";
|
|
||||||
const QWEN_TOKEN_ENDPOINT = "https://chat.qwen.ai/api/v1/oauth2/token";
|
|
||||||
const QWEN_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56";
|
|
||||||
const QWEN_SCOPE = "openid profile email model.completion";
|
|
||||||
const QWEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
||||||
const QWEN_DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
||||||
const QWEN_POLL_INTERVAL_MS = 2000;
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// PKCE Helpers
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {
|
|
||||||
const array = new Uint8Array(32);
|
|
||||||
crypto.getRandomValues(array);
|
|
||||||
const verifier = btoa(String.fromCharCode(...array))
|
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_")
|
|
||||||
.replace(/=+$/, "");
|
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const data = encoder.encode(verifier);
|
|
||||||
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
||||||
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
|
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_")
|
|
||||||
.replace(/=+$/, "");
|
|
||||||
|
|
||||||
return { verifier, challenge };
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// OAuth Implementation
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface DeviceCodeResponse {
|
|
||||||
device_code: string;
|
|
||||||
user_code: string;
|
|
||||||
verification_uri: string;
|
|
||||||
verification_uri_complete?: string;
|
|
||||||
expires_in: number;
|
|
||||||
interval?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TokenResponse {
|
|
||||||
access_token: string;
|
|
||||||
refresh_token?: string;
|
|
||||||
token_type: string;
|
|
||||||
expires_in: number;
|
|
||||||
resource_url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (signal?.aborted) {
|
|
||||||
reject(new Error("Login cancelled"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const timeout = setTimeout(resolve, ms);
|
|
||||||
signal?.addEventListener(
|
|
||||||
"abort",
|
|
||||||
() => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
reject(new Error("Login cancelled"));
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startDeviceFlow(): Promise<{ deviceCode: DeviceCodeResponse; verifier: string }> {
|
|
||||||
const { verifier, challenge } = await generatePKCE();
|
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
|
||||||
client_id: QWEN_CLIENT_ID,
|
|
||||||
scope: QWEN_SCOPE,
|
|
||||||
code_challenge: challenge,
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
});
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
Accept: "application/json",
|
|
||||||
};
|
|
||||||
const requestId = globalThis.crypto?.randomUUID?.();
|
|
||||||
if (requestId) headers["x-request-id"] = requestId;
|
|
||||||
|
|
||||||
const response = await fetch(QWEN_DEVICE_CODE_ENDPOINT, {
|
|
||||||
method: "POST",
|
|
||||||
headers,
|
|
||||||
body: body.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error(`Device code request failed: ${response.status} ${text}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as DeviceCodeResponse;
|
|
||||||
|
|
||||||
if (!data.device_code || !data.user_code || !data.verification_uri) {
|
|
||||||
throw new Error("Invalid device code response: missing required fields");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { deviceCode: data, verifier };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollForToken(
|
|
||||||
deviceCode: string,
|
|
||||||
verifier: string,
|
|
||||||
intervalSeconds: number | undefined,
|
|
||||||
expiresIn: number,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
): Promise<TokenResponse> {
|
|
||||||
const deadline = Date.now() + expiresIn * 1000;
|
|
||||||
const resolvedIntervalSeconds =
|
|
||||||
typeof intervalSeconds === "number" && Number.isFinite(intervalSeconds) && intervalSeconds > 0
|
|
||||||
? intervalSeconds
|
|
||||||
: QWEN_POLL_INTERVAL_MS / 1000;
|
|
||||||
let intervalMs = Math.max(1000, Math.floor(resolvedIntervalSeconds * 1000));
|
|
||||||
|
|
||||||
const handleTokenError = async (error: string, description?: string): Promise<boolean> => {
|
|
||||||
switch (error) {
|
|
||||||
case "authorization_pending":
|
|
||||||
await abortableSleep(intervalMs, signal);
|
|
||||||
return true;
|
|
||||||
case "slow_down":
|
|
||||||
intervalMs = Math.min(intervalMs + 5000, 10000);
|
|
||||||
await abortableSleep(intervalMs, signal);
|
|
||||||
return true;
|
|
||||||
case "expired_token":
|
|
||||||
throw new Error("Device code expired. Please restart authentication.");
|
|
||||||
case "access_denied":
|
|
||||||
throw new Error("Authorization denied by user.");
|
|
||||||
default:
|
|
||||||
throw new Error(`Token request failed: ${error} - ${description || ""}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
|
||||||
if (signal?.aborted) throw new Error("Login cancelled");
|
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
|
||||||
grant_type: QWEN_GRANT_TYPE,
|
|
||||||
client_id: QWEN_CLIENT_ID,
|
|
||||||
device_code: deviceCode,
|
|
||||||
code_verifier: verifier,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(QWEN_TOKEN_ENDPOINT, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
body: body.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseText = await response.text();
|
|
||||||
let data: (TokenResponse & { error?: string; error_description?: string }) | null = null;
|
|
||||||
if (responseText) {
|
|
||||||
try {
|
|
||||||
data = JSON.parse(responseText) as TokenResponse & { error?: string; error_description?: string };
|
|
||||||
} catch {
|
|
||||||
data = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = data?.error;
|
|
||||||
const errorDescription = data?.error_description;
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (error && (await handleTokenError(error, errorDescription))) continue;
|
|
||||||
throw new Error(`Token request failed: ${response.status} ${response.statusText}. Response: ${responseText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.access_token) return data;
|
|
||||||
|
|
||||||
if (error && (await handleTokenError(error, errorDescription))) continue;
|
|
||||||
|
|
||||||
throw new Error("Token request failed: missing access token in response");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Authentication timed out. Please try again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loginQwen(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
||||||
const { deviceCode, verifier } = await startDeviceFlow();
|
|
||||||
const authUrl = deviceCode.verification_uri_complete || deviceCode.verification_uri;
|
|
||||||
const instructions = deviceCode.verification_uri_complete
|
|
||||||
? undefined
|
|
||||||
: `Enter code: ${deviceCode.user_code}`;
|
|
||||||
callbacks.onAuth({ url: authUrl, instructions });
|
|
||||||
|
|
||||||
const tokenResponse = await pollForToken(
|
|
||||||
deviceCode.device_code,
|
|
||||||
verifier,
|
|
||||||
deviceCode.interval,
|
|
||||||
deviceCode.expires_in,
|
|
||||||
callbacks.signal,
|
|
||||||
);
|
|
||||||
|
|
||||||
const expiresAt = Date.now() + tokenResponse.expires_in * 1000 - 5 * 60 * 1000;
|
|
||||||
|
|
||||||
return {
|
|
||||||
refresh: tokenResponse.refresh_token || "",
|
|
||||||
access: tokenResponse.access_token,
|
|
||||||
expires: expiresAt,
|
|
||||||
enterpriseUrl: tokenResponse.resource_url,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshQwenToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
||||||
const body = new URLSearchParams({
|
|
||||||
grant_type: "refresh_token",
|
|
||||||
refresh_token: credentials.refresh,
|
|
||||||
client_id: QWEN_CLIENT_ID,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(QWEN_TOKEN_ENDPOINT, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
body: body.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error(`Token refresh failed: ${response.status} ${text}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as TokenResponse;
|
|
||||||
if (!data.access_token) throw new Error("Token refresh failed: no access token in response");
|
|
||||||
|
|
||||||
const expiresAt = Date.now() + data.expires_in * 1000 - 5 * 60 * 1000;
|
|
||||||
|
|
||||||
return {
|
|
||||||
refresh: data.refresh_token || credentials.refresh,
|
|
||||||
access: data.access_token,
|
|
||||||
expires: expiresAt,
|
|
||||||
enterpriseUrl: data.resource_url ?? credentials.enterpriseUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQwenBaseUrl(resourceUrl?: string): string {
|
|
||||||
if (!resourceUrl) return QWEN_DEFAULT_BASE_URL;
|
|
||||||
let url = resourceUrl.startsWith("http") ? resourceUrl : `https://${resourceUrl}`;
|
|
||||||
if (!url.endsWith("/v1")) url = `${url}/v1`;
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Extension Entry Point
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
|
||||||
pi.registerProvider("qwen-cli", {
|
|
||||||
baseUrl: QWEN_DEFAULT_BASE_URL,
|
|
||||||
apiKey: "QWEN_CLI_API_KEY",
|
|
||||||
api: "openai-completions",
|
|
||||||
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: "qwen3.5-plus",
|
|
||||||
name: "Qwen 3.5 Plus (Best — rivals Qwen3-Max)",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 1000000,
|
|
||||||
maxTokens: 65536,
|
|
||||||
compat: {
|
|
||||||
supportsDeveloperRole: false,
|
|
||||||
supportsReasoningEffort: false,
|
|
||||||
maxTokensField: "max_tokens",
|
|
||||||
thinkingFormat: "qwen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "qwen3.5-flash",
|
|
||||||
name: "Qwen 3.5 Flash (Fast & Cheap)",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 1000000,
|
|
||||||
maxTokens: 65536,
|
|
||||||
compat: {
|
|
||||||
supportsDeveloperRole: false,
|
|
||||||
supportsReasoningEffort: false,
|
|
||||||
maxTokensField: "max_tokens",
|
|
||||||
thinkingFormat: "qwen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "qwen3-max",
|
|
||||||
name: "Qwen 3 Max (Flagship, strongest reasoning)",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 262144,
|
|
||||||
maxTokens: 32768,
|
|
||||||
compat: {
|
|
||||||
supportsDeveloperRole: false,
|
|
||||||
supportsReasoningEffort: false,
|
|
||||||
maxTokensField: "max_tokens",
|
|
||||||
thinkingFormat: "qwen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "qwen-plus",
|
|
||||||
name: "Qwen 3 Plus (Balanced)",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 1000000,
|
|
||||||
maxTokens: 32768,
|
|
||||||
compat: {
|
|
||||||
supportsDeveloperRole: false,
|
|
||||||
supportsReasoningEffort: false,
|
|
||||||
maxTokensField: "max_tokens",
|
|
||||||
thinkingFormat: "qwen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "qwen-flash",
|
|
||||||
name: "Qwen 3 Flash (Fast)",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 1000000,
|
|
||||||
maxTokens: 32768,
|
|
||||||
compat: {
|
|
||||||
supportsDeveloperRole: false,
|
|
||||||
supportsReasoningEffort: false,
|
|
||||||
maxTokensField: "max_tokens",
|
|
||||||
thinkingFormat: "qwen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
oauth: {
|
|
||||||
name: "Qwen CLI",
|
|
||||||
login: loginQwen,
|
|
||||||
refreshToken: refreshQwenToken,
|
|
||||||
getApiKey: (cred) => cred.access,
|
|
||||||
modifyModels: (models, cred) => {
|
|
||||||
const baseUrl = getQwenBaseUrl(cred.enterpriseUrl as string | undefined);
|
|
||||||
return models.map((m) => (m.provider === "qwen-cli" ? { ...m, baseUrl } : m));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -55,6 +55,8 @@ export interface UsageData {
|
|||||||
/** Unix ms timestamp of when the session window resets (from the raw API response). */
|
/** Unix ms timestamp of when the session window resets (from the raw API response). */
|
||||||
sessionResetsAt?: number;
|
sessionResetsAt?: number;
|
||||||
weeklyResetsIn?: string;
|
weeklyResetsIn?: string;
|
||||||
|
/** Unix ms timestamp of when the weekly window resets. */
|
||||||
|
weeklyResetsAt?: number;
|
||||||
extraSpend?: number;
|
extraSpend?: number;
|
||||||
extraLimit?: number;
|
extraLimit?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -321,13 +323,34 @@ export function formatResetsAt(isoDate: string, nowMs = Date.now()): string {
|
|||||||
return formatDuration(diffSeconds);
|
return formatDuration(diffSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CLAUDE_CREDENTIALS_FILE = path.join(os.homedir(), ".claude", ".credentials.json");
|
||||||
|
|
||||||
export function readAuth(authFile = DEFAULT_AUTH_FILE): AuthData | null {
|
export function readAuth(authFile = DEFAULT_AUTH_FILE): AuthData | null {
|
||||||
|
let result: AuthData | null = null;
|
||||||
|
|
||||||
|
// Read pi auth.json for non-Claude providers
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(fs.readFileSync(authFile, "utf-8"));
|
const parsed = JSON.parse(fs.readFileSync(authFile, "utf-8"));
|
||||||
return asObject(parsed) as AuthData;
|
result = asObject(parsed) as AuthData;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
result = {} as AuthData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read Claude credentials from ~/.claude/.credentials.json
|
||||||
|
try {
|
||||||
|
const claudeRaw = fs.readFileSync(CLAUDE_CREDENTIALS_FILE, "utf-8");
|
||||||
|
const claudeCreds = JSON.parse(claudeRaw);
|
||||||
|
const oauth = claudeCreds?.claudeAiOauth;
|
||||||
|
if (oauth?.accessToken) {
|
||||||
|
result!.anthropic = {
|
||||||
|
access: oauth.accessToken,
|
||||||
|
refresh: oauth.refreshToken,
|
||||||
|
expires: typeof oauth.expiresAt === "number" ? oauth.expiresAt : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function writeAuth(auth: AuthData, authFile = DEFAULT_AUTH_FILE): boolean {
|
export function writeAuth(auth: AuthData, authFile = DEFAULT_AUTH_FILE): boolean {
|
||||||
@@ -647,6 +670,9 @@ export async function fetchClaudeUsage(token: string, config: RequestConfig = {}
|
|||||||
const sessionResetsAt = data?.five_hour?.resets_at
|
const sessionResetsAt = data?.five_hour?.resets_at
|
||||||
? new Date(data.five_hour.resets_at).getTime()
|
? new Date(data.five_hour.resets_at).getTime()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const weeklyResetsAt = data?.seven_day?.resets_at
|
||||||
|
? new Date(data.seven_day.resets_at).getTime()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const usage: UsageData = {
|
const usage: UsageData = {
|
||||||
session: readPercentCandidate(data?.five_hour?.utilization) ?? 0,
|
session: readPercentCandidate(data?.five_hour?.utilization) ?? 0,
|
||||||
@@ -654,6 +680,7 @@ export async function fetchClaudeUsage(token: string, config: RequestConfig = {}
|
|||||||
sessionResetsIn: data?.five_hour?.resets_at ? formatResetsAt(data.five_hour.resets_at) : undefined,
|
sessionResetsIn: data?.five_hour?.resets_at ? formatResetsAt(data.five_hour.resets_at) : undefined,
|
||||||
sessionResetsAt: Number.isFinite(sessionResetsAt) ? sessionResetsAt : undefined,
|
sessionResetsAt: Number.isFinite(sessionResetsAt) ? sessionResetsAt : undefined,
|
||||||
weeklyResetsIn: data?.seven_day?.resets_at ? formatResetsAt(data.seven_day.resets_at) : undefined,
|
weeklyResetsIn: data?.seven_day?.resets_at ? formatResetsAt(data.seven_day.resets_at) : undefined,
|
||||||
|
weeklyResetsAt: Number.isFinite(weeklyResetsAt) ? weeklyResetsAt : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data?.extra_usage?.is_enabled) {
|
if (data?.extra_usage?.is_enabled) {
|
||||||
|
|||||||
@@ -306,14 +306,27 @@ export default function (pi: ExtensionAPI) {
|
|||||||
const active = state.activeProvider;
|
const active = state.activeProvider;
|
||||||
const data = active ? state[active] : null;
|
const data = active ? state[active] : null;
|
||||||
|
|
||||||
// Always emit event for other extensions (e.g. footer-display)
|
// Always emit Claude usage for other extensions (e.g. footer-display)
|
||||||
if (data && !data.error) {
|
// so S/W bars are visible regardless of active model.
|
||||||
|
const claudeData = state.claude;
|
||||||
|
if (claudeData && !claudeData.error) {
|
||||||
|
pi.events.emit("usage:update", {
|
||||||
|
session: claudeData.session,
|
||||||
|
weekly: claudeData.weekly,
|
||||||
|
sessionResetsIn: claudeData.sessionResetsIn,
|
||||||
|
sessionResetsAt: claudeData.sessionResetsAt,
|
||||||
|
weeklyResetsIn: claudeData.weeklyResetsIn,
|
||||||
|
weeklyResetsAt: claudeData.weeklyResetsAt,
|
||||||
|
});
|
||||||
|
} else if (data && !data.error) {
|
||||||
|
// Fallback to active provider data if Claude data unavailable
|
||||||
pi.events.emit("usage:update", {
|
pi.events.emit("usage:update", {
|
||||||
session: data.session,
|
session: data.session,
|
||||||
weekly: data.weekly,
|
weekly: data.weekly,
|
||||||
sessionResetsIn: data.sessionResetsIn,
|
sessionResetsIn: data.sessionResetsIn,
|
||||||
sessionResetsAt: data.sessionResetsAt,
|
sessionResetsAt: data.sessionResetsAt,
|
||||||
weeklyResetsIn: data.weeklyResetsIn,
|
weeklyResetsIn: data.weeklyResetsIn,
|
||||||
|
weeklyResetsAt: data.weeklyResetsAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +385,48 @@ export default function (pi: ExtensionAPI) {
|
|||||||
const auth = readAuth();
|
const auth = readAuth();
|
||||||
const active = state.activeProvider;
|
const active = state.activeProvider;
|
||||||
|
|
||||||
|
// Always try to fetch Claude data so S/W bars show regardless of active provider
|
||||||
|
if (auth && canShowForProvider("claude", auth, endpoints)) {
|
||||||
|
try {
|
||||||
|
const cache = readUsageCache();
|
||||||
|
const now = Date.now();
|
||||||
|
const cacheTtl = options.cacheTtl ?? CACHE_TTL_MS;
|
||||||
|
const claudeBlockedUntil = cache?.rateLimitedUntil?.claude ?? 0;
|
||||||
|
if (now < claudeBlockedUntil) {
|
||||||
|
if (cache?.data?.claude) state.claude = cache.data.claude;
|
||||||
|
} else {
|
||||||
|
const claudeCacheFresh = cache && now - cache.timestamp < cacheTtl && cache.data?.claude;
|
||||||
|
if (claudeCacheFresh && !options.forceFresh) {
|
||||||
|
state.claude = cache.data.claude;
|
||||||
|
} else {
|
||||||
|
const claudeAccess = auth.anthropic?.access;
|
||||||
|
if (claudeAccess) {
|
||||||
|
const claudeResult = await fetchClaudeUsage(claudeAccess);
|
||||||
|
state.claude = claudeResult;
|
||||||
|
if (!claudeResult.error) {
|
||||||
|
const nextCache: import("./core").UsageCache = {
|
||||||
|
timestamp: now,
|
||||||
|
data: { ...(cache?.data ?? {}), claude: claudeResult },
|
||||||
|
rateLimitedUntil: { ...(cache?.rateLimitedUntil ?? {}) },
|
||||||
|
};
|
||||||
|
delete nextCache.rateLimitedUntil!.claude;
|
||||||
|
writeUsageCache(nextCache);
|
||||||
|
} else if (claudeResult.error === "HTTP 429") {
|
||||||
|
// Record backoff even when Claude is not the active provider —
|
||||||
|
// without this the prefetch would hammer the API on every poll.
|
||||||
|
const nextCache: import("./core").UsageCache = {
|
||||||
|
timestamp: cache?.timestamp ?? now,
|
||||||
|
data: { ...(cache?.data ?? {}) },
|
||||||
|
rateLimitedUntil: { ...(cache?.rateLimitedUntil ?? {}), claude: now + RATE_LIMITED_BACKOFF_MS },
|
||||||
|
};
|
||||||
|
writeUsageCache(nextCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
if (!canShowForProvider(active, auth, endpoints) || !auth || !active) {
|
if (!canShowForProvider(active, auth, endpoints) || !auth || !active) {
|
||||||
state.lastPoll = Date.now(); updateStatus(); return;
|
state.lastPoll = Date.now(); updateStatus(); return;
|
||||||
}
|
}
|
||||||
@@ -478,7 +533,9 @@ export default function (pi: ExtensionAPI) {
|
|||||||
await Promise.race([runPollInner(options), timeout]);
|
await Promise.race([runPollInner(options), timeout]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const POLL_TIMEOUT_MS = 30_000;
|
// Must be less than the 25 000 ms timeout inside runPoll so the guard fires
|
||||||
|
// before runPoll's finally-block clears pollInFlight.
|
||||||
|
const POLL_TIMEOUT_MS = 20_000;
|
||||||
|
|
||||||
async function poll(options: PollOptions = {}) {
|
async function poll(options: PollOptions = {}) {
|
||||||
// If a previous poll has been running longer than POLL_TIMEOUT_MS, abandon it
|
// If a previous poll has been running longer than POLL_TIMEOUT_MS, abandon it
|
||||||
@@ -557,15 +614,6 @@ export default function (pi: ExtensionAPI) {
|
|||||||
await poll({ cacheTtl: ACTIVE_CACHE_TTL_MS });
|
await poll({ cacheTtl: ACTIVE_CACHE_TTL_MS });
|
||||||
});
|
});
|
||||||
|
|
||||||
pi.events.on("claude-account:switched", () => {
|
|
||||||
const cache = readUsageCache();
|
|
||||||
if (cache?.data?.claude) {
|
|
||||||
const nextCache: import("./core").UsageCache = { ...cache, data: { ...cache.data } };
|
|
||||||
delete nextCache.data.claude;
|
|
||||||
writeUsageCache(nextCache);
|
|
||||||
}
|
|
||||||
void poll({ forceFresh: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for OpenCode Go spend events from other extensions
|
// Listen for OpenCode Go spend events from other extensions
|
||||||
pi.events.on("opencode-go:spend", async (amount: number) => {
|
pi.events.on("opencode-go:spend", async (amount: number) => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readdirSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
@@ -101,6 +101,7 @@ local mock_wezterm = {
|
|||||||
log_warn = function() end,
|
log_warn = function() end,
|
||||||
log_error = function() end,
|
log_error = function() end,
|
||||||
on = function() end,
|
on = function() end,
|
||||||
|
add_to_config_reload_watch_list = function() end,
|
||||||
action = setmetatable({}, {
|
action = setmetatable({}, {
|
||||||
__index = function(_, k)
|
__index = function(_, k)
|
||||||
return function(...) return { action = k, args = {...} } end
|
return function(...) return { action = k, args = {...} } end
|
||||||
@@ -397,48 +398,64 @@ function cleanupOldThemes(themesDir: string, keepFile: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
function syncTheme(ctx: any) {
|
||||||
pi.on("session_start", async (_event, ctx) => {
|
const configDir = findConfigDir();
|
||||||
const configDir = findConfigDir();
|
if (!configDir) return;
|
||||||
if (!configDir) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lua = findLua();
|
const lua = findLua();
|
||||||
if (!lua) {
|
if (!lua) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const colors = getWeztermColors(configDir, lua);
|
const colors = getWeztermColors(configDir, lua);
|
||||||
if (!colors) {
|
if (!colors) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const themesDir = join(homedir(), ".pi", "agent", "themes");
|
const themesDir = join(homedir(), ".pi", "agent", "themes");
|
||||||
if (!existsSync(themesDir)) {
|
if (!existsSync(themesDir)) {
|
||||||
mkdirSync(themesDir, { recursive: true });
|
mkdirSync(themesDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = computeThemeHash(colors);
|
const hash = computeThemeHash(colors);
|
||||||
const themeName = `wezterm-sync-${hash}`;
|
const themeName = `wezterm-sync-${hash}`;
|
||||||
const themeFile = `${themeName}.json`;
|
const themeFile = `${themeName}.json`;
|
||||||
const themePath = join(themesDir, themeFile);
|
const themePath = join(themesDir, themeFile);
|
||||||
|
|
||||||
// Skip if already on the correct synced theme (avoids repaint)
|
// Skip if already on the correct synced theme (avoids repaint)
|
||||||
if (ctx.ui.theme.name === themeName) {
|
if (ctx.ui.theme.name === themeName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeJson = generatePiTheme(colors, themeName);
|
const themeJson = generatePiTheme(colors, themeName);
|
||||||
writeFileSync(themePath, JSON.stringify(themeJson, null, 2));
|
writeFileSync(themePath, JSON.stringify(themeJson, null, 2));
|
||||||
|
|
||||||
// Remove old generated themes
|
// Remove old generated themes
|
||||||
cleanupOldThemes(themesDir, themeFile);
|
cleanupOldThemes(themesDir, themeFile);
|
||||||
|
|
||||||
// Set by name so pi loads from the file we just wrote
|
// Set by name so pi loads from the file we just wrote
|
||||||
const result = ctx.ui.setTheme(themeName);
|
const result = ctx.ui.setTheme(themeName);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
ctx.ui.notify(`WezTerm theme sync failed: ${result.error}`, "error");
|
ctx.ui.notify(`WezTerm theme sync failed: ${result.error}`, "error");
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export default function (pi: ExtensionAPI) {
|
||||||
|
let currentCtx: any = null;
|
||||||
|
|
||||||
|
pi.on("session_start", async (_event, ctx) => {
|
||||||
|
currentCtx = ctx;
|
||||||
|
syncTheme(ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch theme-state file for dark/light toggle changes
|
||||||
|
const stateFile = join(homedir(), ".config", "theme-state");
|
||||||
|
try {
|
||||||
|
watch(stateFile, { persistent: false }, (_event) => {
|
||||||
|
// Debounce: wait a tick for the file write to complete
|
||||||
|
setTimeout(() => {
|
||||||
|
if (currentCtx) {
|
||||||
|
syncTheme(currentCtx);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// File may not exist yet — non-fatal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
{
|
{
|
||||||
"providers": {
|
"providers": {
|
||||||
|
"llama-serve": {
|
||||||
|
"baseUrl": "http://localhost:8080/v1",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"apiKey": "dummy",
|
||||||
|
"compat": {
|
||||||
|
"supportsDeveloperRole": false,
|
||||||
|
"supportsReasoningEffort": false
|
||||||
|
},
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "Qwen/Qwen3-9B",
|
||||||
|
"name": "Qwen3.5-9B (Unsloth)",
|
||||||
|
"reasoning": true,
|
||||||
|
"input": ["text"],
|
||||||
|
"contextWindow": 131072,
|
||||||
|
"maxTokens": 8192,
|
||||||
|
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"opencode-go": {
|
"opencode-go": {
|
||||||
"baseUrl": "https://opencode.ai/zen/go/v1",
|
"baseUrl": "https://opencode.ai/zen/go/v1",
|
||||||
"api": "openai-completions",
|
"api": "openai-completions",
|
||||||
|
|||||||
755
pi/.pi/agent/pi-crash.log
Normal file
755
pi/.pi/agent/pi-crash.log
Normal file
@@ -0,0 +1,755 @@
|
|||||||
|
Crash at 2026-04-24T12:06:17.955Z
|
||||||
|
Terminal width: 114
|
||||||
|
Line 451 visible width: 121
|
||||||
|
|
||||||
|
=== All rendered lines ===
|
||||||
|
[0] (w=0) [0m]8;;
|
||||||
|
[1] (w=114) [1m[38;2;123;79;196mpi[39m[22m[38;2;156;153;149m v0.67.68[39m [0m]8;;
|
||||||
|
[2] (w=114) [38;2;156;153;149mescape[39m[38;2;115;113;110m interrupt[39m[38;2;115;113;110m · [39m[38;2;156;153;149mctrl+c/ctrl+d[39m[38;2;115;113;110m clear/exit[39m[38;2;115;113;110m · [39m[38;2;156;153;149m/[39m[38;2;115;113;110m commands[39m[38;2;115;113;110m · [39m[38;2;156;153;149m![39m[38;2;115;113;110m bash[39m[38;2;115;113;110m · [39m[38;2;156;153;149mctrl+o[39m[38;2;115;113;110m more[39m [0m]8;;
|
||||||
|
[3] (w=114) [38;2;156;153;149mPress ctrl+o to show full startup help and loaded resources.[39m [0m]8;;
|
||||||
|
[4] (w=114) [0m]8;;
|
||||||
|
[5] (w=114) [38;2;156;153;149mPi can explain its own features and look up its docs. Ask it how to use or extend Pi.[39m [0m]8;;
|
||||||
|
[6] (w=0) [0m]8;;
|
||||||
|
[7] (w=114) [38;2;184;137;15m[Skills][39m [0m]8;;
|
||||||
|
[8] (w=114) [38;2;156;153;149m add-agent, ask-claude, homeassistant-ev, implementor, local-scout, opty, qmd, subagent-implement, subagent-plan,[0m]8;;
|
||||||
|
[9] (w=114) [38;2;156;153;149msubagent-review[39m [0m]8;;
|
||||||
|
[10] (w=0) [0m]8;;
|
||||||
|
[11] (w=114) [38;2;184;137;15m[Prompts][39m [0m]8;;
|
||||||
|
[12] (w=114) [38;2;156;153;149m /implement, /implement-critical, /plan, /review[39m [0m]8;;
|
||||||
|
[13] (w=0) [0m]8;;
|
||||||
|
[14] (w=114) [38;2;184;137;15m[Extensions][39m [0m]8;;
|
||||||
|
[15] (w=114) [38;2;156;153;149m @aliou/pi-guardrails:src, @benvargas/pi-exa-mcp, ask-claude.ts, chat-claude.ts, footer-display.ts, [0m]8;;
|
||||||
|
[16] (w=114) [38;2;156;153;149mgit-checkout-guard.ts, new-with-context.ts, pi, pi-ask-tool/index.ts, pi-claude-bridge, pi-subagents, [0m]8;;
|
||||||
|
[17] (w=114) [38;2;156;153;149mpi-subagents:notify.ts, postpone.ts, usage-bars/index.ts, wezterm-theme-sync/index.ts, worktree.ts[39m [0m]8;;
|
||||||
|
[18] (w=0) [0m]8;;
|
||||||
|
[19] (w=114) [38;2;184;137;15m[Themes][39m [0m]8;;
|
||||||
|
[20] (w=114) [38;2;156;153;149m wezterm-sync-9a35138e[39m [0m]8;;
|
||||||
|
[21] (w=0) [0m]8;;
|
||||||
|
[22] (w=0) [0m]8;;
|
||||||
|
[23] (w=114) [38;2;184;137;15m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────[39m[0m]8;;
|
||||||
|
[24] (w=114) [1m[38;2;184;137;15mUpdate Available[39m[22m [0m]8;;
|
||||||
|
[25] (w=114) [38;2;115;113;110mNew version 0.70.0 is available. [39m[38;2;123;79;196mRun: npm install -g @mariozechner/pi-coding-agent[39m [0m]8;;
|
||||||
|
[26] (w=114) [38;2;115;113;110mChangelog: [39m[38;2;123;79;196mhttps://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md[39m [0m]8;;
|
||||||
|
[27] (w=114) [38;2;184;137;15m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────[39m[0m]8;;
|
||||||
|
[28] (w=0) [0m]8;;
|
||||||
|
[29] (w=114) [38;2;184;137;15m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────[39m[0m]8;;
|
||||||
|
[30] (w=114) [1m[38;2;184;137;15mPackage Updates Available[39m[22m [0m]8;;
|
||||||
|
[31] (w=114) [38;2;115;113;110mPackage updates are available. Run [39m[38;2;123;79;196mpi update[39m [0m]8;;
|
||||||
|
[32] (w=114) [38;2;115;113;110mPackages:[39m [0m]8;;
|
||||||
|
[33] (w=114) - pi-subagents [0m]8;;
|
||||||
|
[34] (w=114) - @aliou/pi-guardrails [0m]8;;
|
||||||
|
[35] (w=114) - pi-claude-bridge [0m]8;;
|
||||||
|
[36] (w=114) [38;2;184;137;15m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────[39m[0m]8;;
|
||||||
|
[37] (w=0) [0m]8;;
|
||||||
|
[38] (w=114) [38;2;156;153;149mEntered chat mode: Claude Opus · resume c9492aa2[39m [0m]8;;
|
||||||
|
[39] (w=0) [0m]8;;
|
||||||
|
[40] (w=114) [38;5;208m╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮[0m[0m]8;;
|
||||||
|
[41] (w=114) [38;5;208m│[0m [38;5;208m[1m▶ you[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[42] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[43] (w=114) [38;5;208m│[0m @agent/extensions/chat-claude.ts truncate all read outputs to max 40 lines. Last line, if truncated, should [38;5;208m│[0m[0m]8;;
|
||||||
|
[44] (w=114) [38;5;208m│[0m be something like '+N lines more lines' or better wording. Last line should be centered [38;5;208m│[0m[0m]8;;
|
||||||
|
[45] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[46] (w=114) [38;5;208m│[0m [38;5;208m◆ [0m[38;5;208m[1mClaude Opus[0m[38;2;156;153;149m session:c9492aa2[39m [38;5;208m│[0m[0m]8;;
|
||||||
|
[47] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[48] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ find /home/jonas/dotfiles/pi/.pi/agent -type d[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[49] (w=114) [38;5;208m│[0m [48;2;224;233;218m <persisted-output> [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[50] (w=114) [38;5;208m│[0m [48;2;224;233;218m Output too large (189KB). Full output saved to: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[51] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/.claude/projects/-home-jonas-dotfiles-pi--pi/c9492aa2-34ca-4d36-a541-83964a2ce1d8/tool-results [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[52] (w=114) [38;5;208m│[0m [48;2;224;233;218m /bh1bg7uzg.txt [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[53] (w=114) [38;5;208m│[0m [48;2;224;233;218m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[54] (w=114) [38;5;208m│[0m [48;2;224;233;218m Preview (first 2KB): [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[55] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[56] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[57] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-- [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[58] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl-- [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[59] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/subagent-artifacts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[60] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[61] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[62] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[63] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197/b7684b6a [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[64] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[65] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197/b7684b6a/run-0 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[66] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[67] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197/95023efc [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[68] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[69] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197/95023efc/run-0 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[70] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[71] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197/5be564de [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[72] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[73] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197/5be564de/run-0 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[74] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T11-01-06-232Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[75] (w=114) [38;5;208m│[0m [48;2;224;233;218m _cae41525-1f66-4dbd-a0da-7c12c770aa76 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[76] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T11-01-06-232Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[77] (w=114) [38;5;208m│[0m [48;2;224;233;218m _cae41525-1f66-4dbd-a0da-7c12c770aa76/294d75f5 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[78] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T11-01-06-232Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[79] (w=114) [38;5;208m│[0m [48;2;224;233;218m _cae41525-1f66-4dbd-a0da-7c12c770aa76/294d75f5/run-0 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[80] (w=114) [38;5;208m│[0m [48;2;224;233;218m ... [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[81] (w=114) [38;5;208m│[0m [48;2;224;233;218m </persisted-output> [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[82] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[83] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ ls /home/jonas/dotfiles/pi/.pi/agent/extensions/ /home/jonas/dotfiles/pi/.pi/agent/shared/ 2>/dev/null[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[84] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/extensions/: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[85] (w=114) [38;5;208m│[0m [48;2;224;233;218m ask-claude.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[86] (w=114) [38;5;208m│[0m [48;2;224;233;218m chat-claude.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[87] (w=114) [38;5;208m│[0m [48;2;224;233;218m exa-mcp.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[88] (w=114) [38;5;208m│[0m [48;2;224;233;218m footer-display.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[89] (w=114) [38;5;208m│[0m [48;2;224;233;218m git-checkout-guard.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[90] (w=114) [38;5;208m│[0m [48;2;224;233;218m new-with-context.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[91] (w=114) [38;5;208m│[0m [48;2;224;233;218m pi-ask-mcp [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[92] (w=114) [38;5;208m│[0m [48;2;224;233;218m pi-ask-tool [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[93] (w=114) [38;5;208m│[0m [48;2;224;233;218m postpone.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[94] (w=114) [38;5;208m│[0m [48;2;224;233;218m usage-bars [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[95] (w=114) [38;5;208m│[0m [48;2;224;233;218m wezterm-theme-sync [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[96] (w=114) [38;5;208m│[0m [48;2;224;233;218m worktree.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[97] (w=114) [38;5;208m│[0m [48;2;224;233;218m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[98] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/shared/: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[99] (w=114) [38;5;208m│[0m [48;2;224;233;218m claude-stream.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[100] (w=114) [38;5;208m│[0m [48;2;224;233;218m pi-ask-bridge.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[101] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[102] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/dotfiles/pi/.pi/agent/shared/claude-stream.ts[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[103] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 1[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[104] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 2[39m * claude-stream — Shared types, rendering, and core spawn/stream logic [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[105] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 3[39m * for ask-claude and chat-claude pi extensions. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[106] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 4[39m * [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[107] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 5[39m * Both extensions spawn `claude -p --output-format stream-json` and parse [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[108] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 6[39m * the same streaming protocol. This module provides: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[109] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 7[39m * - Block types (ThinkingBlock, ToolBlock, TextBlock) [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[110] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 8[39m * - Rendering helpers (tool call lines, result boxes, usage formatting) [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[111] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 9[39m * - runClaude() — the core spawn + stream parser [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[112] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 10[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[113] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 11[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[114] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 12[39m import { spawn } from "node:child_process"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[115] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 13[39m import { readFileSync } from "node:fs"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[116] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 14[39m import { diffLines } from "diff"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[117] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 15[39m import { getMarkdownTheme } from "@mariozechner/pi-coding-agent"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[118] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 16[39m import { Box, Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[119] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 17[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[120] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 18[39m // ============================================================================= [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[121] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 19[39m // Block types [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[122] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 20[39m // ============================================================================= [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[123] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 21[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[124] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 22[39m export interface ThinkingBlock { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[125] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 23[39m type: "thinking"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[126] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 24[39m text: string; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[127] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 25[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[128] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 26[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[129] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 27[39m export interface ToolBlock { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[130] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 28[39m type: "tool"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[131] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 29[39m id: string; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[132] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 30[39m name: string; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[133] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 31[39m inputJson: string; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[134] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 32[39m editContext?: { before: string[]; after: string[]; startLine: number }; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[135] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 33[39m result?: { text: string; isError: boolean }; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[136] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 34[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[137] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 35[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[138] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 36[39m export interface TextBlock { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[139] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 37[39m type: "text"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[140] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 38[39m text: string; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[141] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 39[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[142] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 40[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[143] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m… 726 more lines hidden …[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[144] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[145] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ find /home/jonas/dotfiles/pi/.pi/agent -name "node_modules" -prune -o -name "package.json" -print [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[146] (w=114) [38;5;208m│[0m [48;5;130m [1;97m2>/dev/null | head -5[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[147] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/extensions/pi-ask-mcp/package.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[148] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[149] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ find /home/jonas -path "*/node_modules/@mariozechner/pi-tui*" -name "*.d.ts" 2>/dev/null | head -10[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[150] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/box.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[151] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[152] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[153] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[154] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[155] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[156] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[157] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[158] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[159] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/spacer.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[160] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[161] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ find /home/jonas/node_modules/@mariozechner/pi-tui/dist/components -name "text*"[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[162] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/text.js [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[163] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/text.d.ts.map [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[164] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/text.js.map [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[165] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/text.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[166] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[167] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/node_modules/@mariozechner/pi-tui/dist/components/text.d.ts[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[168] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 1[39m import type { Component } from "../tui.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[169] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 2[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[170] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 3[39m * Text component - displays multi-line text with word wrapping [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[171] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 4[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[172] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 5[39m export declare class Text implements Component { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[173] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 6[39m private text; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[174] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 7[39m private paddingX; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[175] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 8[39m private paddingY; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[176] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 9[39m private customBgFn?; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[177] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m10[39m private cachedText?; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[178] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m11[39m private cachedWidth?; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[179] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m12[39m private cachedLines?; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[180] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m13[39m constructor(text?: string, paddingX?: number, paddingY?: number, customBgFn?: (text: string) => [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[181] (w=114) [38;5;208m│[0m [48;2;224;233;218m string); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[182] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m14[39m setText(text: string): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[183] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m15[39m setCustomBgFn(customBgFn?: (text: string) => string): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[184] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m16[39m invalidate(): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[185] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m17[39m render(width: number): string[]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[186] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m18[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[187] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m19[39m //# sourceMappingURL=text.d.ts.map [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[188] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[189] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/node_modules/@mariozechner/pi-tui/dist/components/text.js[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[190] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 1[39m import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[191] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 2[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[192] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 3[39m * Text component - displays multi-line text with word wrapping [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[193] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 4[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[194] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 5[39m export class Text { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[195] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 6[39m text; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[196] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 7[39m paddingX; // Left/right padding [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[197] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 8[39m paddingY; // Top/bottom padding [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[198] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 9[39m customBgFn; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[199] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m10[39m // Cache for rendered output [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[200] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m11[39m cachedText; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[201] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m12[39m cachedWidth; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[202] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m13[39m cachedLines; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[203] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m14[39m constructor(text = "", paddingX = 1, paddingY = 1, customBgFn) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[204] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m15[39m this.text = text; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[205] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m16[39m this.paddingX = paddingX; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[206] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m17[39m this.paddingY = paddingY; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[207] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m18[39m this.customBgFn = customBgFn; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[208] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m19[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[209] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m20[39m setText(text) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[210] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m21[39m this.text = text; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[211] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m22[39m this.cachedText = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[212] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m23[39m this.cachedWidth = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[213] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m24[39m this.cachedLines = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[214] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m25[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[215] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m26[39m setCustomBgFn(customBgFn) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[216] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m27[39m this.customBgFn = customBgFn; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[217] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m28[39m this.cachedText = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[218] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m29[39m this.cachedWidth = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[219] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m30[39m this.cachedLines = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[220] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m31[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[221] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m32[39m invalidate() { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[222] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m33[39m this.cachedText = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[223] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m34[39m this.cachedWidth = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[224] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m35[39m this.cachedLines = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[225] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m36[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[226] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m37[39m render(width) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[227] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m38[39m // Check cache [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[228] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m39[39m if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[229] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m40[39m return this.cachedLines; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[230] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m… 49 more lines hidden …[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[231] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[232] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/node_modules/@mariozechner/pi-tui/dist/components/box.js[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[233] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 1[39m import { applyBackgroundToLine, visibleWidth } from "../utils.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[234] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 2[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[235] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 3[39m * Box component - a container that applies padding and background to all children [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[236] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 4[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[237] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 5[39m export class Box { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[238] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 6[39m children = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[239] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 7[39m paddingX; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[240] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 8[39m paddingY; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[241] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 9[39m bgFn; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[242] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 10[39m // Cache for rendered output [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[243] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 11[39m cache; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[244] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 12[39m constructor(paddingX = 1, paddingY = 1, bgFn) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[245] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 13[39m this.paddingX = paddingX; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[246] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 14[39m this.paddingY = paddingY; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[247] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 15[39m this.bgFn = bgFn; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[248] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 16[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[249] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 17[39m addChild(component) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[250] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 18[39m this.children.push(component); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[251] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 19[39m this.invalidateCache(); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[252] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 20[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[253] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 21[39m removeChild(component) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[254] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 22[39m const index = this.children.indexOf(component); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[255] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 23[39m if (index !== -1) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[256] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 24[39m this.children.splice(index, 1); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[257] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 25[39m this.invalidateCache(); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[258] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 26[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[259] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 27[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[260] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 28[39m clear() { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[261] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 29[39m this.children = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[262] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 30[39m this.invalidateCache(); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[263] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 31[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[264] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 32[39m setBgFn(bgFn) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[265] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 33[39m this.bgFn = bgFn; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[266] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 34[39m // Don't invalidate here - we'll detect bgFn changes by sampling output [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[267] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 35[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[268] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 36[39m invalidateCache() { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[269] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 37[39m this.cache = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[270] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 38[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[271] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 39[39m matchCache(width, childLines, bgSample) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[272] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 40[39m const cache = this.cache; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[273] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m… 64 more lines hidden …[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[274] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[275] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ cat /home/jonas/node_modules/@mariozechner/pi-tui/dist/index.d.ts 2>/dev/null | head -50[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[276] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { type AutocompleteItem, type AutocompleteProvider, CombinedAutocompleteProvider, type [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[277] (w=114) [38;5;208m│[0m [48;2;224;233;218m SlashCommand, } from "./autocomplete.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[278] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Box } from "./components/box.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[279] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { CancellableLoader } from "./components/cancellable-loader.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[280] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Editor, type EditorOptions, type EditorTheme } from "./components/editor.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[281] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Image, type ImageOptions, type ImageTheme } from "./components/image.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[282] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Input } from "./components/input.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[283] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Loader } from "./components/loader.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[284] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { type DefaultTextStyle, Markdown, type MarkdownTheme } from "./components/markdown.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[285] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { type SelectItem, SelectList, type SelectListTheme } from "./components/select-list.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[286] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { type SettingItem, SettingsList, type SettingsListTheme } from "./components/settings-list.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[287] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Spacer } from "./components/spacer.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[288] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Text } from "./components/text.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[289] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { TruncatedText } from "./components/truncated-text.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[290] (w=114) [38;5;208m│[0m [48;2;224;233;218m export type { EditorComponent } from "./editor-component.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[291] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { type FuzzyMatch, fuzzyFilter, fuzzyMatch } from "./fuzzy.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[292] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { DEFAULT_EDITOR_KEYBINDINGS, type EditorAction, type EditorKeybindingsConfig, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[293] (w=114) [38;5;208m│[0m [48;2;224;233;218m EditorKeybindingsManager, getEditorKeybindings, setEditorKeybindings, } from "./keybindings.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[294] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { isKeyRelease, isKeyRepeat, isKittyProtocolActive, Key, type KeyEventType, type KeyId, matchesKey, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[295] (w=114) [38;5;208m│[0m [48;2;224;233;218m parseKey, setKittyProtocolActive, } from "./keys.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[296] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from "./stdin-buffer.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[297] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { ProcessTerminal, type Terminal } from "./terminal.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[298] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { allocateImageId, type CellDimensions, calculateImageRows, deleteAllKittyImages, deleteKittyImage, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[299] (w=114) [38;5;208m│[0m [48;2;224;233;218m detectCapabilities, encodeITerm2, encodeKitty, getCapabilities, getCellDimensions, getGifDimensions, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[300] (w=114) [38;5;208m│[0m [48;2;224;233;218m getImageDimensions, getJpegDimensions, getPngDimensions, getWebpDimensions, type ImageDimensions, type [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[301] (w=114) [38;5;208m│[0m [48;2;224;233;218m ImageProtocol, type ImageRenderOptions, imageFallback, renderImage, resetCapabilitiesCache, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[302] (w=114) [38;5;208m│[0m [48;2;224;233;218m setCellDimensions, type TerminalCapabilities, } from "./terminal-image.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[303] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { type Component, Container, CURSOR_MARKER, type Focusable, isFocusable, type OverlayAnchor, type [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[304] (w=114) [38;5;208m│[0m [48;2;224;233;218m OverlayHandle, type OverlayMargin, type OverlayOptions, type SizeValue, TUI, } from "./tui.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[305] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[306] (w=114) [38;5;208m│[0m [48;2;224;233;218m //# sourceMappingURL=index.d.ts.map [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[307] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[308] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ cat /home/jonas/node_modules/@mariozechner/pi-tui/dist/tui.d.ts | head -100[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[309] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[310] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Minimal TUI implementation with differential rendering [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[311] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[312] (w=114) [38;5;208m│[0m [48;2;224;233;218m import type { Terminal } from "./terminal.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[313] (w=114) [38;5;208m│[0m [48;2;224;233;218m import { visibleWidth } from "./utils.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[314] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[315] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Component interface - all components must implement this [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[316] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[317] (w=114) [38;5;208m│[0m [48;2;224;233;218m export interface Component { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[318] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[319] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Render the component to lines for the given viewport width [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[320] (w=114) [38;5;208m│[0m [48;2;224;233;218m * @param width - Current viewport width [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[321] (w=114) [38;5;208m│[0m [48;2;224;233;218m * @returns Array of strings, each representing a line [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[322] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[323] (w=114) [38;5;208m│[0m [48;2;224;233;218m render(width: number): string[]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[324] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[325] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Optional handler for keyboard input when component has focus [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[326] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[327] (w=114) [38;5;208m│[0m [48;2;224;233;218m handleInput?(data: string): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[328] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[329] (w=114) [38;5;208m│[0m [48;2;224;233;218m * If true, component receives key release events (Kitty protocol). [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[330] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Default is false - release events are filtered out. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[331] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[332] (w=114) [38;5;208m│[0m [48;2;224;233;218m wantsKeyRelease?: boolean; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[333] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[334] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Invalidate any cached rendering state. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[335] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Called when theme changes or when component needs to re-render from scratch. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[336] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[337] (w=114) [38;5;208m│[0m [48;2;224;233;218m invalidate(): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[338] (w=114) [38;5;208m│[0m [48;2;224;233;218m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[339] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[340] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Interface for components that can receive focus and display a hardware cursor. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[341] (w=114) [38;5;208m│[0m [48;2;224;233;218m * When focused, the component should emit CURSOR_MARKER at the cursor position [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[342] (w=114) [38;5;208m│[0m [48;2;224;233;218m * in its render output. TUI will find this marker and position the hardware [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[343] (w=114) [38;5;208m│[0m [48;2;224;233;218m * cursor there for proper IME candidate window positioning. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[344] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[345] (w=114) [38;5;208m│[0m [48;2;224;233;218m export interface Focusable { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[346] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Set by TUI when focus changes. Component should emit CURSOR_MARKER when true. */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[347] (w=114) [38;5;208m│[0m [48;2;224;233;218m focused: boolean; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[348] (w=114) [38;5;208m│[0m [48;2;224;233;218m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[349] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Type guard to check if a component implements Focusable */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[350] (w=114) [38;5;208m│[0m [48;2;224;233;218m export declare function isFocusable(component: Component | null): component is Component & Focusable; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[351] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[352] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Cursor position marker - APC (Application Program Command) sequence. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[353] (w=114) [38;5;208m│[0m [48;2;224;233;218m * This is a zero-width escape sequence that terminals ignore. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[354] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Components emit this at the cursor position when focused. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[355] (w=114) [38;5;208m│[0m [48;2;224;233;218m * TUI finds and strips this marker, then positions the hardware cursor there. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[356] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[357] (w=114) [38;5;208m│[0m [48;2;224;233;218m export declare const CURSOR_MARKER = "\u001B_pi:c\u0007"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[358] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { visibleWidth }; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[359] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[360] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Anchor position for overlays [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[361] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[362] (w=114) [38;5;208m│[0m [48;2;224;233;218m export type OverlayAnchor = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[363] (w=114) [38;5;208m│[0m [48;2;224;233;218m "top-center" | "bottom-center" | "left-center" | "right-center"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[364] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[365] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Margin configuration for overlays [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[366] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[367] (w=114) [38;5;208m│[0m [48;2;224;233;218m export interface OverlayMargin { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[368] (w=114) [38;5;208m│[0m [48;2;224;233;218m top?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[369] (w=114) [38;5;208m│[0m [48;2;224;233;218m right?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[370] (w=114) [38;5;208m│[0m [48;2;224;233;218m bottom?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[371] (w=114) [38;5;208m│[0m [48;2;224;233;218m left?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[372] (w=114) [38;5;208m│[0m [48;2;224;233;218m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[373] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Value that can be absolute (number) or percentage (string like "50%") */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[374] (w=114) [38;5;208m│[0m [48;2;224;233;218m export type SizeValue = number | `${number}%`; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[375] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[376] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Options for overlay positioning and sizing. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[377] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Values can be absolute numbers or percentage strings (e.g., "50%"). [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[378] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[379] (w=114) [38;5;208m│[0m [48;2;224;233;218m export interface OverlayOptions { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[380] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Width in columns, or percentage of terminal width (e.g., "50%") */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[381] (w=114) [38;5;208m│[0m [48;2;224;233;218m width?: SizeValue; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[382] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Minimum width in columns */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[383] (w=114) [38;5;208m│[0m [48;2;224;233;218m minWidth?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[384] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Maximum height in rows, or percentage of terminal height (e.g., "50%") */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[385] (w=114) [38;5;208m│[0m [48;2;224;233;218m maxHeight?: SizeValue; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[386] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Anchor point for positioning (default: 'center') */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[387] (w=114) [38;5;208m│[0m [48;2;224;233;218m anchor?: OverlayAnchor; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[388] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Horizontal offset from anchor position (positive = right) */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[389] (w=114) [38;5;208m│[0m [48;2;224;233;218m offsetX?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[390] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Vertical offset from anchor position (positive = down) */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[391] (w=114) [38;5;208m│[0m [48;2;224;233;218m offsetY?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[392] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Row position: absolute number, or percentage (e.g., "25%" = 25% from top) */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[393] (w=114) [38;5;208m│[0m [48;2;224;233;218m row?: SizeValue; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[394] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Column position: absolute number, or percentage (e.g., "50%" = centered horizontally) */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[395] (w=114) [38;5;208m│[0m [48;2;224;233;218m col?: SizeValue; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[396] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Margin from terminal edges. Number applies to all sides. */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[397] (w=114) [38;5;208m│[0m [48;2;224;233;218m margin?: OverlayMargin | number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[398] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[399] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Control overlay visibility based on terminal dimensions. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[400] (w=114) [38;5;208m│[0m [48;2;224;233;218m * If provided, overlay is only rendered when this returns true. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[401] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Called each render cycle with current terminal dimensions. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[402] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[403] (w=114) [38;5;208m│[0m [48;2;224;233;218m visible?: (termWidth: number, termHeight: number) => boolean; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[404] (w=114) [38;5;208m│[0m [48;2;224;233;218m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[405] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[406] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Handle returned by showOverlay for controlling the overlay [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[407] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[408] (w=114) [38;5;208m│[0m [48;2;224;233;218m export interface OverlayHandle { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[409] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Permanently remove the overlay (cannot be shown again) */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[410] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[411] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ grep -n "class Container" /home/jonas/node_modules/@mariozechner/pi-tui/dist/tui.js | head -5[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[412] (w=114) [38;5;208m│[0m [48;2;224;233;218m 38:export class Container { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[413] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[414] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/node_modules/@mariozechner/pi-tui/dist/tui.js:38-117[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[415] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 38[39m export class Container { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[416] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 39[39m children = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[417] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 40[39m addChild(component) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[418] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 41[39m this.children.push(component); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[419] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 42[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[420] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 43[39m removeChild(component) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[421] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 44[39m const index = this.children.indexOf(component); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[422] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 45[39m if (index !== -1) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[423] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 46[39m this.children.splice(index, 1); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[424] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 47[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[425] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 48[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[426] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 49[39m clear() { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[427] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 50[39m this.children = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[428] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 51[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[429] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 52[39m invalidate() { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[430] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 53[39m for (const child of this.children) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[431] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 54[39m child.invalidate?.(); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[432] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 55[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[433] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 56[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[434] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 57[39m render(width) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[435] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 58[39m const lines = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[436] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 59[39m for (const child of this.children) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[437] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 60[39m lines.push(...child.render(width)); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[438] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 61[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[439] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 62[39m return lines; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[440] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 63[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[441] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 64[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[442] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 65[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[443] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 66[39m * TUI - Main class for managing terminal UI with differential rendering [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[444] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 67[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[445] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 68[39m export class TUI extends Container { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[446] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 69[39m terminal; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[447] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 70[39m previousLines = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[448] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 71[39m previousWidth = 0; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[449] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 72[39m previousHeight = 0; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[450] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 73[39m focusedComponent = null; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[451] (w=121) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 74[39m /** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */[49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[452] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 75[39m onDebug; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[453] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 76[39m renderRequested = false; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[454] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 77[39m cursorRow = 0; // Logical cursor row (end of rendered content) [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[455] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m… 40 more lines hidden …[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[456] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[457] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ grep -n "Container" /home/jonas/node_modules/@mariozechner/pi-tui/dist/tui.d.ts | head -20[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[458] (w=114) [38;5;208m│[0m [48;2;224;233;218m 108: * Container - a component that contains other components [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[459] (w=114) [38;5;208m│[0m [48;2;224;233;218m 110:export declare class Container implements Component { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[460] (w=114) [38;5;208m│[0m [48;2;224;233;218m 121:export declare class TUI extends Container { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[461] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[462] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/node_modules/@mariozechner/pi-tui/dist/tui.d.ts:108-122[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[463] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m108[39m * Container - a component that contains other components [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[464] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m109[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[465] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m110[39m export declare class Container implements Component { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[466] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m111[39m children: Component[]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[467] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m112[39m addChild(component: Component): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[468] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m113[39m removeChild(component: Component): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[469] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m114[39m clear(): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[470] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m115[39m invalidate(): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[471] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m116[39m render(width: number): string[]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[472] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m117[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[473] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m118[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[474] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m119[39m * TUI - Main class for managing terminal UI with differential rendering [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[475] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m120[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[476] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m121[39m export declare class TUI extends Container { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[477] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m122[39m terminal: Terminal; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[478] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[479] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/node_modules/@mariozechner/pi-tui/dist/components/box.d.ts[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[480] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 1[39m import type { Component } from "../tui.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[481] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 2[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[482] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 3[39m * Box component - a container that applies padding and background to all children [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[483] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 4[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[484] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 5[39m export declare class Box implements Component { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[485] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 6[39m children: Component[]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[486] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 7[39m private paddingX; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[487] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 8[39m private paddingY; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[488] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 9[39m private bgFn?; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[489] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m10[39m private cache?; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[490] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m11[39m constructor(paddingX?: number, paddingY?: number, bgFn?: (text: string) => string); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[491] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m12[39m addChild(component: Component): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[492] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m13[39m removeChild(component: Component): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[493] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m14[39m clear(): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[494] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m15[39m setBgFn(bgFn?: (text: string) => string): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[495] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m16[39m private invalidateCache; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[496] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m17[39m private matchCache; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[497] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m18[39m invalidate(): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[498] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m19[39m render(width: number): string[]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[499] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m20[39m private applyBg; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[500] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m21[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[501] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m22[39m //# sourceMappingURL=box.d.ts.map [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[502] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[503] (w=114) [38;5;208m│[0m Now I have enough understanding. Let me look at the exact current state of the tool-block rendering site and [38;5;208m│[0m[0m]8;;
|
||||||
|
[504] (w=114) [38;5;208m│[0m the imports: [38;5;208m│[0m[0m]8;;
|
||||||
|
[505] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[506] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/dotfiles/pi/.pi/agent/extensions/chat-claude.ts:NaN-NaN[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[507] (w=114) [38;5;208m│[0m [48;2;245;221;218m [38;2;156;153;149m[39m [1;91mInputValidationError: Read failed due to the following issue: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[508] (w=114) [38;5;208m│[0m [48;2;245;221;218m [1;91m[38;2;156;153;149m[39m The parameter `offset` type is expected as `number` but provided as `string`[22;23;24;39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[509] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[510] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/dotfiles/pi/.pi/agent/extensions/chat-claude.ts:30-41[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[511] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m30[39m import { CustomEditor, getMarkdownTheme } from "@mariozechner/pi-coding-agent"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[512] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m31[39m import type { ExtensionAPI, KeybindingsManager } from "@mariozechner/pi-coding-agent"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[513] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m32[39m import { Container, matchesKey, Markdown, Spacer, Text, TUI, visibleWidth, type EditorTheme } from [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[514] (w=114) [38;5;208m│[0m [48;2;224;233;218m "@mariozechner/pi-tui"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[515] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m33[39m import { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[516] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m34[39m formatUsage, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[517] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m35[39m renderToolBlock, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[518] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m36[39m runClaude, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[519] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m37[39m type StreamBlock, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[520] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m38[39m } from "../shared/claude-stream.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[521] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m39[39m import { startAskBridge, type AskBridge } from "../shared/pi-ask-bridge.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[522] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m40[39m import { askSingleQuestionWithInlineNote } from "./pi-ask-tool/ask-inline-ui.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[523] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m41[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[524] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[525] (w=114) [38;5;208m│[0m Now I'll make the changes. First, add [38;2;123;79;196mBox[39m and [38;2;123;79;196mComponent[39m to the imports: [38;5;208m│[0m[0m]8;;
|
||||||
|
[526] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[527] (w=114) [38;5;208m│[0m [48;5;130m [1;97medit ~/dotfiles/pi/.pi/agent/extensions/chat-claude.ts[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[528] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m@@ -1,1 +1,1 @@[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[529] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m-import { Container, matchesKey, Markdown, Spacer, Text, TUI, visibleWidth, type EditorTheme } from [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[530] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m"@mariozechner/pi-tui";[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[531] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+import { Box, Container, matchesKey, Markdown, Spacer, Text, TUI, visibleWidth, type Component, type [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[532] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82mEditorTheme } from "@mariozechner/pi-tui";[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[533] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[534] (w=114) [38;5;208m│[0m Now add the constant and the truncation helper. I'll place it right after the orange-border helper, where [38;5;208m│[0m[0m]8;;
|
||||||
|
[535] (w=114) [38;5;208m│[0m rendering helpers live: [38;5;208m│[0m[0m]8;;
|
||||||
|
[536] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[537] (w=114) [38;5;208m│[0m [48;5;130m [1;97medit ~/dotfiles/pi/.pi/agent/extensions/chat-claude.ts[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[538] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m@@ -1,3 +1,82 @@[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[539] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m-// ---------------------------------------------------------------------------[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[540] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m-// Models / turn types[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[541] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m-// ---------------------------------------------------------------------------[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[542] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// ---------------------------------------------------------------------------[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[543] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// Read-tool result truncation[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[544] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+//[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[545] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// `Read` tool calls inside chat mode often dump entire files into the result[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[546] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// banner — many hundreds of lines, which buries the surrounding conversation.[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[547] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// We cap the rendered file content at MAX_READ_LINES and append a single[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[548] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// centered notice line describing how many lines were hidden. This is a[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[549] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// PRESENTATION-only truncation: `block.result.text` is left untouched, so[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[550] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// resumed sessions / re-renders still see the full content.[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[551] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+//[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[552] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// Centering needs render-time width, so we implement a tiny custom Component[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[553] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// (TruncatedReadResult) and swap it into the Box body produced by the shared[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[554] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// renderToolBlock helper. The same dim line-number formatting used by[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[555] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// renderToolResultBox is preserved so the truncated view looks identical to[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[556] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// the un-truncated one above the notice.[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[557] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// ---------------------------------------------------------------------------[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[558] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+const MAX_READ_LINES = 40;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[559] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[560] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+class TruncatedReadResult implements Component {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[561] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ constructor([39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[562] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ private readonly numbered: { num: string; content: string }[],[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[563] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ private readonly maxNumLen: number,[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[564] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ private readonly dimFn: (s: string) => string,[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[565] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ private readonly noticeFn: (s: string) => string,[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[566] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ ) {}[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[567] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[568] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ invalidate(): void { /* stateless */ }[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[569] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[570] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ render(width: number): string[] {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[571] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const total = this.numbered.length;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[572] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const visible = Math.min(MAX_READ_LINES, total);[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[573] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const lines: string[] = [];[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[574] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ for (let i = 0; i < visible; i++) {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[575] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const l = this.numbered[i];[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[576] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ lines.push(this.dimFn(l.num.padStart(this.maxNumLen)) + " " + l.content);[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[577] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ }[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[578] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ if (total > visible) {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[579] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const hidden = total - visible;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[580] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const notice = `… ${hidden} more line${hidden === 1 ? "" : "s"} hidden …`;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[581] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const visLen = visibleWidth(notice);[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[582] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const left = Math.max(0, Math.floor((width - visLen) / 2));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[583] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ lines.push(" ".repeat(left) + this.noticeFn(notice));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[584] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ }[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[585] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ return lines;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[586] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ }[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[587] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+}[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[588] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[589] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// Wrap shared renderToolBlock: for `Read` tool blocks whose result exceeds[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[590] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// MAX_READ_LINES, replace the Box body's child Text with our truncating[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[591] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// component. All other tool kinds, error results, and short reads pass[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[592] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// through unchanged.[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[593] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+function renderToolBlockTruncated(block: Extract<StreamBlock, { type: "tool" }>, theme: any): Container {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[594] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const c = renderToolBlock(block, theme);[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[595] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ if (block.name.toLowerCase() !== "read") return c;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[596] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ if (!block.result || block.result.isError) return c;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[597] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[598] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const rawLines = block.result.text.split("\n").filter((l) => l.length > 0);[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[599] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ if (rawLines.length <= MAX_READ_LINES) return c;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[600] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[601] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const parsed = rawLines.map((l) => {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[602] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const tab = l.indexOf("\t");[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[603] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ return tab >= 0 ? { num: l.slice(0, tab), content: l.slice(tab + 1) } : { num: "", content: l };[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[604] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ });[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[605] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const maxNumLen = parsed.reduce((m, l) => Math.max(m, l.num.length), 0);[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[606] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[607] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ // renderToolBlock's container is [headerText, bodyBox]. Bail safely if a[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[608] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ // future change to that helper alters the structure.[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[609] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const body = c.children[1];[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[610] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ if (!(body instanceof Box)) return c;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[611] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ body.clear();[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[612] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ body.addChild(new TruncatedReadResult([39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[613] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ parsed,[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[614] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ maxNumLen,[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[615] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ (s) => theme.fg("dim", s),[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[616] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ (s) => theme.fg("dim", s),[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[617] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ ));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[618] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ return c;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[619] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+}[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[620] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[621] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// ---------------------------------------------------------------------------[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[622] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// Models / turn types[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[623] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// ---------------------------------------------------------------------------[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[624] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[625] (w=114) [38;5;208m│[0m Now swap the call site inside [38;2;123;79;196mrenderTurnInto[39m to use the wrapper: [38;5;208m│[0m[0m]8;;
|
||||||
|
[626] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[627] (w=114) [38;5;208m│[0m [48;5;130m [1;97medit ~/dotfiles/pi/.pi/agent/extensions/chat-claude.ts[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[628] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m@@ -1,4 +1,4 @@[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[629] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m- } else if (block.type === "tool") {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[630] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m- if (addedAny) container.addChild(new Spacer(1));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[631] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m- container.addChild(renderToolBlock(block, theme));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[632] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m- addedAny = true;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[633] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ } else if (block.type === "tool") {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[634] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ if (addedAny) container.addChild(new Spacer(1));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[635] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ container.addChild(renderToolBlockTruncated(block, theme));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[636] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ addedAny = true;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[637] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[638] (w=114) [38;5;208m│[0m Now let me verify there's no TypeScript build/check available and the file still parses cleanly: [38;5;208m│[0m[0m]8;;
|
||||||
|
[639] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[640] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ ls /home/jonas/dotfiles/pi/.pi/ /home/jonas/dotfiles/pi/.pi/agent/ 2>/dev/null[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[641] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[642] (w=114) [38;5;208m│[0m [48;2;224;233;218m agent [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[643] (w=114) [38;5;208m│[0m [48;2;224;233;218m web-search.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[644] (w=114) [38;5;208m│[0m [48;2;224;233;218m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[645] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[646] (w=114) [38;5;208m│[0m [48;2;224;233;218m agents [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[647] (w=114) [38;5;208m│[0m [48;2;224;233;218m auth.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[648] (w=114) [38;5;208m│[0m [48;2;224;233;218m auth.json.current [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[649] (w=114) [38;5;208m│[0m [48;2;224;233;218m cache [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[650] (w=114) [38;5;208m│[0m [48;2;224;233;218m extensions [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[651] (w=114) [38;5;208m│[0m [48;2;224;233;218m mcp-cache.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[652] (w=114) [38;5;208m│[0m [48;2;224;233;218m mcp.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[653] (w=114) [38;5;208m│[0m [48;2;224;233;218m models.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[654] (w=114) [38;5;208m│[0m [48;2;224;233;218m profiles [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[655] (w=114) [38;5;208m│[0m [48;2;224;233;218m prompts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[656] (w=114) [38;5;208m│[0m [48;2;224;233;218m run-history.jsonl [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[657] (w=114) [38;5;208m│[0m [48;2;224;233;218m sessions [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[658] (w=114) [38;5;208m│[0m [48;2;224;233;218m settings.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[659] (w=114) [38;5;208m│[0m [48;2;224;233;218m shared [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[660] (w=114) [38;5;208m│[0m [48;2;224;233;218m skills [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[661] (w=114) [38;5;208m│[0m [48;2;224;233;218m themes [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[662] (w=114) [38;5;208m│[0m [48;2;224;233;218m usage-cache.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[663] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[664] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ which tsc 2>/dev/null; ls /home/jonas/node_modules/.bin/tsc 2>/dev/null[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[665] (w=114) [38;5;208m│[0m [48;2;245;221;218m Exit code 2 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[666] (w=114) [38;5;208m│[0m [48;2;245;221;218m /home/jonas/.npm-global/bin/tsc [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[667] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[668] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ cd /home/jonas/dotfiles/pi/.pi/agent && tsc --noEmit --target esnext --module nodenext [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[669] (w=114) [38;5;208m│[0m [48;5;130m [1;97m--moduleResolution nodenext --esModuleInterop --skipLibCheck --strict false extensions/chat-claude.ts 2>&1 [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[670] (w=114) [38;5;208m│[0m [48;5;130m [1;97m| head -40[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[671] (w=114) [38;5;208m│[0m [48;2;224;233;218m shared/pi-ask-bridge.ts(117,37): error TS1470: The 'import.meta' meta-property is not allowed in files [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[672] (w=114) [38;5;208m│[0m [48;2;224;233;218m which will build into CommonJS output. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[673] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[674] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ tsc --noEmit --target esnext --module esnext --moduleResolution bundler --esModuleInterop --skipLibCheck [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[675] (w=114) [38;5;208m│[0m [48;5;130m [1;97m--strict false extensions/chat-claude.ts 2>&1 | head -40[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[676] (w=114) [38;5;208m│[0m [48;2;224;233;218m (Bash completed with no output) [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[677] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[678] (w=114) [38;5;208m│[0m No type errors. Let me show the final relevant sections: [38;5;208m│[0m[0m]8;;
|
||||||
|
[679] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[680] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/dotfiles/pi/.pi/agent/extensions/chat-claude.ts:72-153[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[681] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 72[39m // --------------------------------------------------------------------------- [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[682] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 73[39m // Read-tool result truncation [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[683] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 74[39m // [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[684] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 75[39m // `Read` tool calls inside chat mode often dump entire files into the result [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[685] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 76[39m // banner — many hundreds of lines, which buries the surrounding conversation. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[686] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 77[39m // We cap the rendered file content at MAX_READ_LINES and append a single [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[687] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 78[39m // centered notice line describing how many lines were hidden. This is a [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[688] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 79[39m // PRESENTATION-only truncation: `block.result.text` is left untouched, so [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[689] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 80[39m // resumed sessions / re-renders still see the full content. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[690] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 81[39m // [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[691] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 82[39m // Centering needs render-time width, so we implement a tiny custom Component [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[692] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 83[39m // (TruncatedReadResult) and swap it into the Box body produced by the shared [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[693] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 84[39m // renderToolBlock helper. The same dim line-number formatting used by [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[694] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 85[39m // renderToolResultBox is preserved so the truncated view looks identical to [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[695] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 86[39m // the un-truncated one above the notice. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[696] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 87[39m // --------------------------------------------------------------------------- [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[697] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 88[39m const MAX_READ_LINES = 40; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[698] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 89[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[699] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 90[39m class TruncatedReadResult implements Component { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[700] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 91[39m constructor( [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[701] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 92[39m private readonly numbered: { num: string; content: string }[], [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[702] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 93[39m private readonly maxNumLen: number, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[703] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 94[39m private readonly dimFn: (s: string) => string, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[704] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 95[39m private readonly noticeFn: (s: string) => string, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[705] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 96[39m ) {} [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[706] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 97[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[707] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 98[39m invalidate(): void { /* stateless */ } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[708] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 99[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[709] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m100[39m render(width: number): string[] { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[710] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m101[39m const total = this.numbered.length; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[711] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m102[39m const visible = Math.min(MAX_READ_LINES, total); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[712] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m103[39m const lines: string[] = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[713] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m104[39m for (let i = 0; i < visible; i++) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[714] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m105[39m const l = this.numbered[i]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[715] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m106[39m lines.push(this.dimFn(l.num.padStart(this.maxNumLen)) + " " + l.content); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[716] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m107[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[717] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m108[39m if (total > visible) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[718] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m109[39m const hidden = total - visible; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[719] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m110[39m const notice = `… ${hidden} more line${hidden === 1 ? "" : "s"} hidden …`; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[720] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m111[39m const visLen = visibleWidth(notice); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[721] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m… 42 more lines hidden …[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[722] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[723] (w=114) [38;5;208m│[0m Done. Summary of changes to [38;2;123;79;196m/home/jonas/dotfiles/pi/.pi/agent/extensions/chat-claude.ts[39m: [38;5;208m│[0m[0m]8;;
|
||||||
|
[724] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[725] (w=114) [38;5;208m│[0m [38;2;123;79;196m1. [39m[1mImports[22m — added [38;2;123;79;196mBox[39m and [38;2;123;79;196mtype Component[39m to the [38;2;123;79;196m@mariozechner/pi-tui[39m import. [38;5;208m│[0m[0m]8;;
|
||||||
|
[726] (w=114) [38;5;208m│[0m [38;2;123;79;196m2. [39m[1mNew constant [38;2;123;79;196mMAX_READ_LINES = 40[39m[22m — hard cap on rendered file lines per [38;2;123;79;196mRead[39m tool result. [38;5;208m│[0m[0m]8;;
|
||||||
|
[727] (w=114) [38;5;208m│[0m [38;2;123;79;196m3. [39m[1mNew [38;2;123;79;196mTruncatedReadResult[39m component[22m — a tiny [38;2;123;79;196mComponent[39m that renders the first 40 numbered lines using the [38;5;208m│[0m[0m]8;;
|
||||||
|
[728] (w=114) [38;5;208m│[0m same dim line-number formatting as the shared [38;2;123;79;196mrenderToolResultBox[39m, then appends a single centered notice [38;5;208m│[0m[0m]8;;
|
||||||
|
[729] (w=114) [38;5;208m│[0m line ([38;2;123;79;196m… N more lines hidden …[39m). Centering is computed from the render-time [38;2;123;79;196mwidth[39m, so it tracks terminal [38;5;208m│[0m[0m]8;;
|
||||||
|
[730] (w=114) [38;5;208m│[0m resizes. [38;5;208m│[0m[0m]8;;
|
||||||
|
[731] (w=114) [38;5;208m│[0m [38;2;123;79;196m4. [39m[1mNew [38;2;123;79;196mrenderToolBlockTruncated()[39m wrapper[22m — calls the shared [38;2;123;79;196mrenderToolBlock[39m, and only for [38;2;123;79;196mread[39m tool blocks [38;5;208m│[0m[0m]8;;
|
||||||
|
[732] (w=114) [38;5;208m│[0m whose result exceeds 40 lines, swaps out the Box body's [38;2;123;79;196mText[39m child with [38;2;123;79;196mTruncatedReadResult[39m. Errors, short [38;5;208m│[0m[0m]8;;
|
||||||
|
[733] (w=114) [38;5;208m│[0m reads, and every non-read tool pass through unchanged. The original [38;2;123;79;196mblock.result.text[39m is never mutated, so [38;5;208m│[0m[0m]8;;
|
||||||
|
[734] (w=114) [38;5;208m│[0m resumed sessions and re-renders still see the full content. [38;5;208m│[0m[0m]8;;
|
||||||
|
[735] (w=114) [38;5;208m│[0m [38;2;123;79;196m5. [39m[1mSingle call-site swap[22m in [38;2;123;79;196mrenderTurnInto[39m: [38;2;123;79;196mrenderToolBlock(block, theme)[39m → [38;2;123;79;196mrenderToolBlockTruncated(block, [38;5;208m│[0m[0m]8;;
|
||||||
|
[736] (w=114) [38;5;208m│[0m [38;2;123;79;196mtheme)[39m. [38;5;208m│[0m[0m]8;;
|
||||||
|
[737] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[738] (w=114) [38;5;208m│[0m [38;2;123;79;196mtsc --noEmit[39m on the file produces no errors. [38;5;208m│[0m[0m]8;;
|
||||||
|
[739] (w=114) [38;5;208m╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯[0m[0m]8;;
|
||||||
|
[740] (w=0) [0m]8;;
|
||||||
|
[741] (w=114) [38;2;156;153;149mResuming session c9492aa2 (1h ago): "@agent/extensions/chat-claude.ts truncate all rea…" as Claude Opus. (2 [0m]8;;
|
||||||
|
[742] (w=114) [38;2;156;153;149mhistorical turns loaded)[39m [0m]8;;
|
||||||
|
[743] (w=0) [0m]8;;
|
||||||
|
[744] (w=44) [38;5;208m▌ [0m[38;5;208m[1m◆ CLAUDE CHAT MODE[0m [38;5;208m[1mOPUS[0m [38;5;130msession:c9492aa2[0m[0m]8;;
|
||||||
|
[745] (w=86) [38;5;208m▌ [0m[38;2;156;153;149mType to chat · /claude haiku|sonnet|opus · /claude-new · /claude-end · /claude-abort[39m[0m]8;;
|
||||||
|
[746] (w=114) [38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[0m]8;;
|
||||||
|
[747] (w=114) [7m [0m [0m]8;;
|
||||||
|
[748] (w=114) [38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[0m]8;;
|
||||||
|
[749] (w=114) [38;2;156;153;149m~/dotfiles/pi/.pi[39m[38;2;156;153;149m · [39m[38;2;115;113;110m S [39m[38;2;30;155;82m[39m[38;2;156;153;149m⣀⣀⣀⣀⣀[39m [38;2;156;153;149m0%[39m[38;2;156;153;149m | [39m[38;2;115;113;110m W [39m[38;2;30;155;82m⣄[39m[38;2;156;153;149m⣀⣀⣀⣀[39m [38;2;156;153;149m3%[39m [38;2;156;153;149m⟳ 4d 7h[39m[38;2;156;153;149m | [39m[38;2;115;113;110mC [39m[38;2;30;155;82m[39m[38;2;156;153;149m⣀⣀⣀⣀⣀[39m [38;2;156;153;149m0%[39m[38;2;156;153;149m · [39m[38;2;156;153;149mminimax/minimax-m2.5:free[39m[38;2;156;153;149m · [39m[38;2;123;79;196m◆ Claud[0m...[0m[0m]8;;
|
||||||
779
pi/.pi/agent/shared/claude-stream.ts
Normal file
779
pi/.pi/agent/shared/claude-stream.ts
Normal file
@@ -0,0 +1,779 @@
|
|||||||
|
/**
|
||||||
|
* claude-stream — Shared types, rendering, and core spawn/stream logic
|
||||||
|
* for ask-claude and chat-claude pi extensions.
|
||||||
|
*
|
||||||
|
* Both extensions spawn `claude -p --output-format stream-json` and parse
|
||||||
|
* the same streaming protocol. This module provides:
|
||||||
|
* - Block types (ThinkingBlock, ToolBlock, TextBlock)
|
||||||
|
* - Rendering helpers (tool call lines, result boxes, usage formatting)
|
||||||
|
* - runClaude() — the core spawn + stream parser
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { diffLines } from "diff";
|
||||||
|
import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { Box, Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Block types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface ThinkingBlock {
|
||||||
|
type: "thinking";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolBlock {
|
||||||
|
type: "tool";
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
inputJson: string;
|
||||||
|
editContext?: { before: string[]; after: string[]; startLine: number };
|
||||||
|
result?: { text: string; isError: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextBlock {
|
||||||
|
type: "text";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StreamBlock = ThinkingBlock | ToolBlock | TextBlock;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Details interface (stored in tool result, drives rendering)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface ClaudeDetails {
|
||||||
|
label: string;
|
||||||
|
done: boolean;
|
||||||
|
blocks: StreamBlock[];
|
||||||
|
finalText: string;
|
||||||
|
sessionId?: string;
|
||||||
|
isResume?: boolean;
|
||||||
|
costUsd?: number;
|
||||||
|
inputTokens?: number;
|
||||||
|
outputTokens?: number;
|
||||||
|
cacheReadTokens?: number;
|
||||||
|
cacheWriteTokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Rendering helpers
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function shortenPath(p: string): string {
|
||||||
|
const home = process.env.HOME ?? "";
|
||||||
|
return home && p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUsage(d: ClaudeDetails): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (d.inputTokens) parts.push(`↑${d.inputTokens}`);
|
||||||
|
if (d.outputTokens) parts.push(`↓${d.outputTokens}`);
|
||||||
|
if (d.cacheReadTokens) parts.push(`R${d.cacheReadTokens}`);
|
||||||
|
if (d.cacheWriteTokens) parts.push(`W${d.cacheWriteTokens}`);
|
||||||
|
if (d.costUsd) parts.push(d.costUsd < 0.001 ? `$${(d.costUsd * 1000).toFixed(2)}m` : `$${d.costUsd.toFixed(4)}`);
|
||||||
|
return parts.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Theme = {
|
||||||
|
fg: (c: any, t: string) => string;
|
||||||
|
bg: (c: any, t: string) => string;
|
||||||
|
bold: (t: string) => string;
|
||||||
|
italic: (t: string) => string;
|
||||||
|
dim?: (t: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function renderToolCallLine(block: ToolBlock, theme: Theme): string {
|
||||||
|
let input: Record<string, unknown> = {};
|
||||||
|
try { input = JSON.parse(block.inputJson); } catch { /* ok */ }
|
||||||
|
|
||||||
|
switch (block.name.toLowerCase()) {
|
||||||
|
case "read": {
|
||||||
|
const path = shortenPath(String(input.file_path ?? input.path ?? ""));
|
||||||
|
const offset = Number(input.offset ?? 1);
|
||||||
|
const limit = input.limit != null ? Number(input.limit) : undefined;
|
||||||
|
const range = limit != null ? `:${offset}-${offset + limit - 1}` : "";
|
||||||
|
return theme.fg("muted", "read ") + theme.fg("accent", path) + theme.fg("warning", range);
|
||||||
|
}
|
||||||
|
case "bash": {
|
||||||
|
const cmd = String(input.command ?? "").replace(/\n/g, " ↵ ");
|
||||||
|
return theme.fg("muted", "$ ") + theme.fg("toolOutput", cmd);
|
||||||
|
}
|
||||||
|
case "edit": {
|
||||||
|
const path = shortenPath(String(input.file_path ?? input.path ?? ""));
|
||||||
|
return theme.fg("muted", "edit ") + theme.fg("accent", path);
|
||||||
|
}
|
||||||
|
case "write": {
|
||||||
|
const path = shortenPath(String(input.file_path ?? input.path ?? ""));
|
||||||
|
const lines = String(input.content ?? "").split("\n").length;
|
||||||
|
return theme.fg("muted", "write ") + theme.fg("accent", path) + theme.fg("dim", ` (${lines} lines)`);
|
||||||
|
}
|
||||||
|
case "glob": {
|
||||||
|
const pat = String(input.pattern ?? "");
|
||||||
|
const path = input.path ? shortenPath(String(input.path)) : ".";
|
||||||
|
return theme.fg("muted", "glob ") + theme.fg("accent", pat) + theme.fg("dim", ` in ${path}`);
|
||||||
|
}
|
||||||
|
case "grep": {
|
||||||
|
const pat = String(input.pattern ?? "");
|
||||||
|
const path = input.path ? shortenPath(String(input.path)) : ".";
|
||||||
|
return theme.fg("muted", "grep ") + theme.fg("accent", `"${pat}"`) + theme.fg("dim", ` in ${path}`);
|
||||||
|
}
|
||||||
|
case "mcp__pi__ask": {
|
||||||
|
// Surfaces from the pi-ask-bridge MCP server. input is
|
||||||
|
// { questions: [{ id, question, options[], multi?, recommended? }, ...] }
|
||||||
|
// Show the first question text inline; if there are more, append a count.
|
||||||
|
const qs = Array.isArray(input.questions) ? (input.questions as any[]) : [];
|
||||||
|
const first = qs[0];
|
||||||
|
const head = first?.question ? String(first.question) : "(empty)";
|
||||||
|
const more = qs.length > 1 ? ` (+${qs.length - 1} more)` : "";
|
||||||
|
const tag = first?.id ? ` [${first.id}]` : "";
|
||||||
|
return theme.fg("muted", "ask ") + theme.fg("accent", head) + theme.fg("dim", tag + more);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const desc = typeof input.description === "string" ? input.description
|
||||||
|
: typeof input.prompt === "string" ? input.prompt.split("\n")[0]
|
||||||
|
: block.inputJson;
|
||||||
|
return theme.fg("toolTitle", block.name) + theme.fg("dim", " " + desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderToolResultBox(block: ToolBlock, theme: Theme): Text {
|
||||||
|
if (!block.result) return new Text("", 0, 0);
|
||||||
|
|
||||||
|
let input: Record<string, unknown> = {};
|
||||||
|
try { input = JSON.parse(block.inputJson); } catch { /* ok */ }
|
||||||
|
|
||||||
|
// Strip / restyle synthetic envelopes like <tool_use_error> and
|
||||||
|
// <system_reminder> before any tool-specific parsing.
|
||||||
|
const text = transformSpecialTags(block.result.text);
|
||||||
|
const { isError } = block.result;
|
||||||
|
|
||||||
|
switch (block.name.toLowerCase()) {
|
||||||
|
case "read": {
|
||||||
|
const rawLines = text.split("\n").filter((l) => l.length > 0);
|
||||||
|
const parsed = rawLines.map((l) => {
|
||||||
|
const tab = l.indexOf("\t");
|
||||||
|
return tab >= 0 ? { num: l.slice(0, tab), content: l.slice(tab + 1) } : { num: "", content: l };
|
||||||
|
});
|
||||||
|
const maxNumLen = parsed.reduce((m, l) => Math.max(m, l.num.length), 0);
|
||||||
|
return new Text(
|
||||||
|
parsed.map((l) => theme.fg("dim", l.num.padStart(maxNumLen)) + " " + l.content).join("\n"),
|
||||||
|
0, 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "edit": {
|
||||||
|
if (isError) return new Text(text, 0, 0);
|
||||||
|
const oldStr = String(input.old_string ?? input.oldText ?? "");
|
||||||
|
const newStr = String(input.new_string ?? input.newText ?? "");
|
||||||
|
if (!oldStr && !newStr) return new Text(text.slice(0, 200), 0, 0);
|
||||||
|
const oldLines = oldStr === "" ? [] : oldStr.split("\n");
|
||||||
|
const newLines = newStr === "" ? [] : newStr.split("\n");
|
||||||
|
const ctx = block.editContext;
|
||||||
|
const startLine = ctx?.startLine ?? 1;
|
||||||
|
const header = theme.fg("dim", `@@ -${startLine},${oldLines.length} +${startLine},${newLines.length} @@`);
|
||||||
|
const diff: string[] = [header];
|
||||||
|
for (const l of ctx?.before ?? []) diff.push(theme.fg("dim", " " + l));
|
||||||
|
for (const l of oldLines) diff.push(theme.fg("toolDiffRemoved", "-" + l));
|
||||||
|
for (const l of newLines) diff.push(theme.fg("toolDiffAdded", "+" + l));
|
||||||
|
for (const l of ctx?.after ?? []) diff.push(theme.fg("dim", " " + l));
|
||||||
|
return new Text(diff.join("\n"), 0, 0);
|
||||||
|
}
|
||||||
|
case "write": {
|
||||||
|
if (isError) return new Text(text, 0, 0);
|
||||||
|
const lines = String(input.content ?? "").split("\n");
|
||||||
|
const numWidth = String(lines.length).length;
|
||||||
|
return new Text(
|
||||||
|
lines.map((l, i) => theme.fg("dim", String(i + 1).padStart(numWidth)) + " " + l).join("\n"),
|
||||||
|
0, 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "bash":
|
||||||
|
return new Text(text.trimEnd(), 0, 0);
|
||||||
|
case "mcp__pi__ask": {
|
||||||
|
// The pi-ask-mcp server returns a JSON array of QuestionResults.
|
||||||
|
// Pretty-print as one "id → answer" line per question instead of
|
||||||
|
// dumping raw JSON into the result banner.
|
||||||
|
if (isError) return new Text(text.trim(), 0, 0);
|
||||||
|
let parsed: any;
|
||||||
|
try { parsed = JSON.parse(text); } catch { return new Text(text.trim(), 0, 0); }
|
||||||
|
if (!Array.isArray(parsed)) return new Text(text.trim(), 0, 0);
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const r of parsed) {
|
||||||
|
const id = theme.fg("accent", String(r?.id ?? "?"));
|
||||||
|
const opts = Array.isArray(r?.selectedOptions) ? r.selectedOptions : [];
|
||||||
|
const custom = r?.customInput ? String(r.customInput) : "";
|
||||||
|
const arrow = theme.fg("dim", " → ");
|
||||||
|
let answer: string;
|
||||||
|
if (opts.length === 0 && !custom) {
|
||||||
|
answer = theme.fg("warning", "(cancelled)");
|
||||||
|
} else if (opts.length > 0 && custom) {
|
||||||
|
answer = theme.fg("toolOutput", `[${opts.join(", ")}] + Other: "${custom}"`);
|
||||||
|
} else if (custom) {
|
||||||
|
answer = theme.fg("toolOutput", `Other: "${custom}"`);
|
||||||
|
} else {
|
||||||
|
answer = theme.fg("toolOutput", opts.length === 1 ? opts[0] : `[${opts.join(", ")}]`);
|
||||||
|
}
|
||||||
|
lines.push(id + arrow + answer);
|
||||||
|
}
|
||||||
|
return new Text(lines.join("\n"), 0, 0);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return new Text(text.trim(), 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strip ANSI SGR sequences so we can re-style plain text. */
|
||||||
|
function stripAnsi(s: string): string {
|
||||||
|
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Special-tag transform — used for tool-result text that embeds synthetic
|
||||||
|
// envelopes like <tool_use_error> or <system_reminder>.
|
||||||
|
//
|
||||||
|
// IMPORTANT: all wrappers use PARTIAL resets (`\x1b[22;23;24;39m` — reset
|
||||||
|
// bold/dim/italic/underline and foreground only). If we emitted \x1b[0m in
|
||||||
|
// the middle of a line, it would also wipe any background that Box.bgFn
|
||||||
|
// painted around us, producing a staircase-shaped hole where the padding
|
||||||
|
// loses its colour. Partial resets leave the background intact.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const SGR_FG_RESET = "\x1b[22;23;24;39m";
|
||||||
|
const SGR_BOLD_WHITE = "\x1b[1;97m";
|
||||||
|
const SGR_BOLD_RED = "\x1b[1;91m";
|
||||||
|
const SGR_DIM_GREY = "\x1b[2;90m";
|
||||||
|
|
||||||
|
function transformSpecialTags(text: string): string {
|
||||||
|
// <tool_use_error>message</tool_use_error>
|
||||||
|
// → strip tags, display message in bright bold red
|
||||||
|
text = text.replace(
|
||||||
|
/<tool_use_error>\s*([\s\S]*?)\s*<\/tool_use_error>/g,
|
||||||
|
(_, inner: string) => SGR_BOLD_RED + inner + SGR_FG_RESET,
|
||||||
|
);
|
||||||
|
// <system_reminder> or <system-reminder>
|
||||||
|
// → strip tags, collapse whitespace to one line, truncate at ~100 chars
|
||||||
|
// with a trailing "...", render in dim grey
|
||||||
|
text = text.replace(
|
||||||
|
/<system[-_]reminder>\s*([\s\S]*?)\s*<\/system[-_]reminder>/g,
|
||||||
|
(_, inner: string) => {
|
||||||
|
const oneLine = inner.replace(/\s+/g, " ").trim();
|
||||||
|
const MAX = 100;
|
||||||
|
const snippet = oneLine.length > MAX ? oneLine.slice(0, MAX).trimEnd() : oneLine;
|
||||||
|
return SGR_DIM_GREY + snippet + "..." + SGR_FG_RESET;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a tool block as two stacked banners:
|
||||||
|
* 1) header — bold-white title on an ORANGE background (matches the
|
||||||
|
* chat-claude outer border colour).
|
||||||
|
* 2) result body (if any) — on the theme's pending / success / error bg.
|
||||||
|
*
|
||||||
|
* The header deliberately uses a different background from the result, so
|
||||||
|
* the "staircase" where the banners have different widths is an intentional
|
||||||
|
* stylistic cue, not a rendering glitch.
|
||||||
|
*/
|
||||||
|
const ORANGE_BG_FN = (s: string) => "\x1b[48;5;130m" + s + "\x1b[0m";
|
||||||
|
|
||||||
|
export function renderToolBlock(block: ToolBlock, theme: Theme): Container {
|
||||||
|
const c = new Container();
|
||||||
|
|
||||||
|
// ---- Header banner (orange) ----
|
||||||
|
// Strip per-segment colours from renderToolCallLine so the title renders
|
||||||
|
// uniformly bold-white over the orange background. Partial-reset at the
|
||||||
|
// end keeps the orange bg alive for the trailing padding.
|
||||||
|
const headerText = SGR_BOLD_WHITE + stripAnsi(renderToolCallLine(block, theme)) + SGR_FG_RESET;
|
||||||
|
c.addChild(new Text(headerText, 2, 0, ORANGE_BG_FN));
|
||||||
|
|
||||||
|
// ---- Result body ----
|
||||||
|
if (block.result !== undefined) {
|
||||||
|
const bgFn = block.result.isError
|
||||||
|
? (s: string) => theme.bg("toolErrorBg", s)
|
||||||
|
: (s: string) => theme.bg("toolSuccessBg", s);
|
||||||
|
const box = new Box(2, 0, bgFn);
|
||||||
|
box.addChild(renderToolResultBox(block, theme));
|
||||||
|
c.addChild(box);
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a unified diff string for an edit tool call. */
|
||||||
|
export function buildEditDiff(oldStr: string, newStr: string, contextLines = 3): string {
|
||||||
|
const changes = diffLines(oldStr, newStr);
|
||||||
|
if (changes.length === 1 && (changes[0].added || changes[0].removed)) {
|
||||||
|
const oldCount = (oldStr ? oldStr.split("\n").length : 0);
|
||||||
|
const newCount = (newStr ? newStr.split("\n").length : 0);
|
||||||
|
const lines: string[] = [
|
||||||
|
`@@ -1,${oldCount} +1,${newCount} @@`,
|
||||||
|
...oldStr.split("\n").map((l) => "-" + l),
|
||||||
|
...newStr.split("\n").map((l) => "+" + l),
|
||||||
|
];
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctxLines: string[] = oldStr.split("\n");
|
||||||
|
const hunks: { start: number; lines: string[] }[] = [];
|
||||||
|
let currentHunk: { start: number; lines: string[] } | null = null;
|
||||||
|
let lineIdx = 0;
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
const lines = change.value.endsWith("\n") ? change.value.slice(0, -1).split("\n") : change.value.split("\n");
|
||||||
|
if (change.added) {
|
||||||
|
if (!currentHunk) currentHunk = { start: lineIdx, lines: [] };
|
||||||
|
for (const l of lines) currentHunk.lines.push("+" + l);
|
||||||
|
} else if (change.removed) {
|
||||||
|
if (!currentHunk) currentHunk = { start: lineIdx, lines: [] };
|
||||||
|
for (const l of lines) currentHunk.lines.push("-" + l);
|
||||||
|
lineIdx += lines.length;
|
||||||
|
} else {
|
||||||
|
for (const l of lines) {
|
||||||
|
if (!currentHunk) { lineIdx++; continue; }
|
||||||
|
currentHunk.lines.push(" " + l);
|
||||||
|
if (currentHunk.lines.length >= contextLines * 2 + 1) {
|
||||||
|
hunks.push(currentHunk);
|
||||||
|
currentHunk = null;
|
||||||
|
}
|
||||||
|
lineIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentHunk) hunks.push(currentHunk);
|
||||||
|
|
||||||
|
if (hunks.length === 0) return "(no changes)";
|
||||||
|
|
||||||
|
const merged: { start: number; lines: string[] }[] = [];
|
||||||
|
for (const h of hunks) {
|
||||||
|
if (merged.length === 0 || h.start <= merged[merged.length - 1].start + merged[merged.length - 1].lines.length) {
|
||||||
|
if (merged.length > 0) {
|
||||||
|
const prev = merged[merged.length - 1];
|
||||||
|
prev.lines.push(...h.lines.slice(Math.max(0, prev.lines.length - (h.start - prev.start))));
|
||||||
|
} else {
|
||||||
|
merged.push({ ...h, lines: [...h.lines] });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
merged.push({ ...h, lines: [...h.lines] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultLines: string[] = [];
|
||||||
|
for (const h of merged) {
|
||||||
|
const removed = h.lines.filter((l) => l.startsWith("-")).length;
|
||||||
|
const added = h.lines.filter((l) => l.startsWith("+")).length;
|
||||||
|
const total = h.lines.length;
|
||||||
|
resultLines.push(`@@ -${h.start},${removed + (total - removed - added)} +${h.start},${added + (total - removed - added)} @@`);
|
||||||
|
resultLines.push(...h.lines);
|
||||||
|
}
|
||||||
|
return resultLines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Error helpers
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const ANTHROPIC_ERROR_MAP: [RegExp, string][] = [
|
||||||
|
[/rate_limit/i, "Rate limited by Anthropic — please try again in a few seconds."],
|
||||||
|
[/invalid_api_key/i, "Invalid Anthropic API key — check your ANTHROPIC_API_KEY."],
|
||||||
|
[/permission_error/i, "Permission denied — check your Anthropic account and API key."],
|
||||||
|
[/not_found.*model/i, "Model not found — the specified model is not available."],
|
||||||
|
[/context_length/i, "Context window exceeded — the input is too large for this model."],
|
||||||
|
[/overloaded/i, "Anthropic is overloaded — please retry shortly."],
|
||||||
|
[/billing/i, "Billing issue — check your Anthropic account status."],
|
||||||
|
[/invalid_request/i, "Invalid request — the prompt or parameters may be malformed."],
|
||||||
|
];
|
||||||
|
|
||||||
|
export function formatAnthropicError(raw: string, exitCode?: number): string {
|
||||||
|
for (const [re, msg] of ANTHROPIC_ERROR_MAP) {
|
||||||
|
if (re.test(raw)) return msg;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw.trim());
|
||||||
|
if (parsed.type === "error" && parsed.error?.message) {
|
||||||
|
const em = parsed.error.message;
|
||||||
|
for (const [re, msg] of ANTHROPIC_ERROR_MAP) {
|
||||||
|
if (re.test(em)) return msg;
|
||||||
|
}
|
||||||
|
return em;
|
||||||
|
}
|
||||||
|
} catch { /* not JSON */ }
|
||||||
|
for (const line of raw.split("\n")) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed.startsWith("{")) continue;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (parsed.type === "error" && parsed.error?.message) {
|
||||||
|
const em = parsed.error.message;
|
||||||
|
for (const [re, msg] of ANTHROPIC_ERROR_MAP) {
|
||||||
|
if (re.test(em)) return msg;
|
||||||
|
}
|
||||||
|
return em;
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
}
|
||||||
|
if (raw.trim()) return raw.trim().slice(0, 500);
|
||||||
|
return `claude process exited with code ${exitCode ?? "unknown"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Core: spawn claude CLI and stream all output
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface RunClaudeOptions {
|
||||||
|
model?: string; // default: "sonnet"
|
||||||
|
agent?: string; // --agent <name> (from ~/.claude/agents/)
|
||||||
|
sessionId?: string; // --resume <id> (multi-turn)
|
||||||
|
noSessionPersistence?: boolean; // --no-session-persistence (ask-claude default)
|
||||||
|
enrichEditDiffs?: boolean; // enrich edit tool results with unified diffs
|
||||||
|
mcpConfigPath?: string; // --mcp-config <path> (e.g. pi-ask-bridge)
|
||||||
|
disallowedTools?: string[]; // --disallowed-tools (e.g. ["AskUserQuestion"])
|
||||||
|
// Extended-thinking effort level ("low"|"medium"|"high"|"xhigh"|"max").
|
||||||
|
// REQUIRED for thinking blocks to appear: in `-p` / stream-json mode the
|
||||||
|
// CLI does NOT honour the user's interactive `defaultThinkingLevel`
|
||||||
|
// setting — thinking_delta events are only emitted when `--effort` is
|
||||||
|
// passed explicitly. Pass "off" to leave the flag off entirely.
|
||||||
|
effort?: string;
|
||||||
|
extraEnv?: NodeJS.ProcessEnv; // merged on top of process.env for the child
|
||||||
|
cwd: string;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
timeoutMs?: number; // default: 15 min
|
||||||
|
onUpdate: (partial: { blocks: StreamBlock[]; finalText: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunClaudeResult {
|
||||||
|
finalText: string;
|
||||||
|
blocks: StreamBlock[];
|
||||||
|
sessionId: string;
|
||||||
|
costUsd: number;
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
cacheReadTokens: number;
|
||||||
|
cacheWriteTokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runClaude(prompt: string, opts: RunClaudeOptions): Promise<RunClaudeResult> {
|
||||||
|
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
||||||
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const args = ["-p", "--output-format", "stream-json", "--include-partial-messages"];
|
||||||
|
|
||||||
|
// Session mode: resume takes priority, then agent, then plain model
|
||||||
|
if (opts.sessionId) {
|
||||||
|
args.push("--resume", opts.sessionId);
|
||||||
|
} else if (opts.agent) {
|
||||||
|
args.push("--agent", opts.agent);
|
||||||
|
if (opts.model) args.push("--model", opts.model);
|
||||||
|
if (opts.noSessionPersistence) args.push("--no-session-persistence");
|
||||||
|
} else {
|
||||||
|
args.push("--model", opts.model ?? "sonnet");
|
||||||
|
if (opts.noSessionPersistence) args.push("--no-session-persistence");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extended-thinking effort — must be passed explicitly in -p mode;
|
||||||
|
// the interactive `defaultThinkingLevel` setting does NOT apply here.
|
||||||
|
// Callers pass "off" to suppress the flag (e.g. ask-claude where
|
||||||
|
// raw speed matters more than thought traces).
|
||||||
|
if (opts.effort && opts.effort !== "off") {
|
||||||
|
args.push("--effort", opts.effort);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP config (e.g. pi-ask-bridge for routing AskUserQuestion-style
|
||||||
|
// requests through pi's native UI). Additive — does NOT pass
|
||||||
|
// --strict-mcp-config, so the user's other configured MCP servers
|
||||||
|
// (exa, sentry, …) remain available to Claude.
|
||||||
|
if (opts.mcpConfigPath) {
|
||||||
|
args.push("--mcp-config", opts.mcpConfigPath);
|
||||||
|
}
|
||||||
|
// Tool denylist — typically ["AskUserQuestion"] when the MCP
|
||||||
|
// server is providing a replacement.
|
||||||
|
if (opts.disallowedTools?.length) {
|
||||||
|
args.push("--disallowed-tools", opts.disallowedTools.join(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
const proc = spawn("claude", args, {
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
cwd: opts.cwd,
|
||||||
|
env: opts.extraEnv ? { ...process.env, ...opts.extraEnv } : process.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
proc.stdin.write(prompt, "utf8");
|
||||||
|
proc.stdin.end();
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as NodeJS.ErrnoException).code !== "EPIPE") reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = "";
|
||||||
|
const blocks: StreamBlock[] = [];
|
||||||
|
const pendingTools = new Map<number, { name: string; inputJson: string; id: string }>();
|
||||||
|
|
||||||
|
let sessionId = "";
|
||||||
|
let costUsd = 0, inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheWriteTokens = 0;
|
||||||
|
|
||||||
|
const getOrCreateTextBlock = (): TextBlock => {
|
||||||
|
const last = blocks[blocks.length - 1];
|
||||||
|
if (last?.type === "text") return last;
|
||||||
|
const b: TextBlock = { type: "text", text: "" };
|
||||||
|
blocks.push(b);
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrCreateThinkingBlock = (): ThinkingBlock => {
|
||||||
|
const last = blocks[blocks.length - 1];
|
||||||
|
if (last?.type === "thinking") return last;
|
||||||
|
const b: ThinkingBlock = { type: "thinking", text: "" };
|
||||||
|
blocks.push(b);
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = () => {
|
||||||
|
const finalText = blocks.filter((b): b is TextBlock => b.type === "text").map((b) => b.text).join("");
|
||||||
|
opts.onUpdate({ blocks: [...blocks], finalText });
|
||||||
|
};
|
||||||
|
|
||||||
|
const processLine = (line: string) => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
let event: any;
|
||||||
|
try { event = JSON.parse(line); } catch { return; }
|
||||||
|
|
||||||
|
if (event.type === "stream_event") {
|
||||||
|
const e = event.event as any;
|
||||||
|
if (!e) return;
|
||||||
|
|
||||||
|
if (e.type === "content_block_start") {
|
||||||
|
const cb = e.content_block;
|
||||||
|
if (cb?.type === "tool_use") {
|
||||||
|
pendingTools.set(e.index as number, { name: cb.name, inputJson: "", id: cb.id });
|
||||||
|
}
|
||||||
|
} else if (e.type === "content_block_delta") {
|
||||||
|
const d = e.delta as any;
|
||||||
|
if (d?.type === "text_delta") {
|
||||||
|
getOrCreateTextBlock().text += d.text as string;
|
||||||
|
emit();
|
||||||
|
} else if (d?.type === "thinking_delta") {
|
||||||
|
getOrCreateThinkingBlock().text += d.thinking as string;
|
||||||
|
emit();
|
||||||
|
} else if (d?.type === "input_json_delta") {
|
||||||
|
const tool = pendingTools.get(e.index as number);
|
||||||
|
if (tool) tool.inputJson += d.partial_json as string ?? "";
|
||||||
|
}
|
||||||
|
} else if (e.type === "content_block_stop") {
|
||||||
|
const tool = pendingTools.get(e.index as number);
|
||||||
|
if (tool) {
|
||||||
|
// Claude CLI's --include-partial-messages can emit an `assistant`
|
||||||
|
// event with the completed tool_use BEFORE content_block_stop
|
||||||
|
// arrives. That path already pushed a block with the same id;
|
||||||
|
// update it in place instead of pushing a duplicate.
|
||||||
|
const existing = blocks.find(
|
||||||
|
(b): b is ToolBlock => b.type === "tool" && b.id === tool.id,
|
||||||
|
);
|
||||||
|
const target: ToolBlock = existing ?? {
|
||||||
|
type: "tool", id: tool.id, name: tool.name, inputJson: tool.inputJson,
|
||||||
|
};
|
||||||
|
if (existing) {
|
||||||
|
// Prefer the streamed inputJson (it's been accumulating and
|
||||||
|
// matches what claude-code actually executed).
|
||||||
|
existing.inputJson = tool.inputJson;
|
||||||
|
}
|
||||||
|
// For edit tools, read file context before the edit executes
|
||||||
|
if (tool.name.toLowerCase() === "edit" && !target.editContext) {
|
||||||
|
try {
|
||||||
|
const inp = JSON.parse(tool.inputJson) as Record<string, unknown>;
|
||||||
|
const fp = String(inp.file_path ?? inp.path ?? "");
|
||||||
|
const oldStr = String(inp.old_string ?? inp.oldText ?? "");
|
||||||
|
if (fp && oldStr) {
|
||||||
|
const fileLines = readFileSync(fp, "utf8").split("\n");
|
||||||
|
const oldLines = oldStr.split("\n");
|
||||||
|
let startIdx = -1;
|
||||||
|
outer: for (let i = 0; i <= fileLines.length - oldLines.length; i++) {
|
||||||
|
for (let j = 0; j < oldLines.length; j++) {
|
||||||
|
if (fileLines[i + j] !== oldLines[j]) continue outer;
|
||||||
|
}
|
||||||
|
startIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (startIdx >= 0) {
|
||||||
|
target.editContext = {
|
||||||
|
before: fileLines.slice(Math.max(0, startIdx - 3), startIdx),
|
||||||
|
after: fileLines.slice(startIdx + oldLines.length, startIdx + oldLines.length + 3),
|
||||||
|
startLine: startIdx + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
if (!existing) blocks.push(target);
|
||||||
|
pendingTools.delete(e.index as number);
|
||||||
|
emit();
|
||||||
|
}
|
||||||
|
} else if (e.type === "message_delta") {
|
||||||
|
const u = e.usage as any;
|
||||||
|
if (u) {
|
||||||
|
inputTokens = u.input_tokens ?? inputTokens;
|
||||||
|
outputTokens = u.output_tokens ?? outputTokens;
|
||||||
|
cacheReadTokens = u.cache_read_input_tokens ?? cacheReadTokens;
|
||||||
|
cacheWriteTokens = u.cache_creation_input_tokens ?? cacheWriteTokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event.type === "user") {
|
||||||
|
for (const c of (event.message?.content ?? []) as any[]) {
|
||||||
|
if (c.type !== "tool_result") continue;
|
||||||
|
// tool_result.content may be a plain string (typical for bash/read
|
||||||
|
// output) or an array of {type,text}/{type,image} blocks. Handle both.
|
||||||
|
let text: string;
|
||||||
|
if (typeof c.content === "string") {
|
||||||
|
text = c.content;
|
||||||
|
} else if (Array.isArray(c.content)) {
|
||||||
|
text = (c.content as any[])
|
||||||
|
.filter((x) => x.type === "text")
|
||||||
|
.map((x) => x.text as string)
|
||||||
|
.join("\n");
|
||||||
|
} else {
|
||||||
|
text = "";
|
||||||
|
}
|
||||||
|
const toolId = c.tool_use_id as string;
|
||||||
|
const toolBlock = blocks.findLast((b): b is ToolBlock => b.type === "tool" && b.id === toolId);
|
||||||
|
if (toolBlock && toolBlock.name.toLowerCase() === "edit" && !c.is_error && opts.enrichEditDiffs) {
|
||||||
|
try {
|
||||||
|
const inp = JSON.parse(toolBlock.inputJson) as Record<string, unknown>;
|
||||||
|
const oldStr = String(inp.old_string ?? inp.oldText ?? "");
|
||||||
|
const newStr = String(inp.new_string ?? inp.newText ?? "");
|
||||||
|
if (oldStr || newStr) {
|
||||||
|
const diff = buildEditDiff(oldStr, newStr);
|
||||||
|
text = `${text}\n\n--- edit diff ---\n${diff}\n--- end diff ---`;
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
if (toolBlock) toolBlock.result = { text, isError: c.is_error === true };
|
||||||
|
emit();
|
||||||
|
}
|
||||||
|
} else if (event.type === "assistant") {
|
||||||
|
const content: any[] = (event as any).message?.content ?? [];
|
||||||
|
for (const cb of content) {
|
||||||
|
if (cb.type !== "tool_use") continue;
|
||||||
|
const exists = blocks.some((b): b is ToolBlock => b.type === "tool" && b.id === cb.id);
|
||||||
|
if (!exists) blocks.push({ type: "tool", id: cb.id, name: cb.name, inputJson: JSON.stringify(cb.input ?? {}) });
|
||||||
|
}
|
||||||
|
emit();
|
||||||
|
} else if (event.type === "result") {
|
||||||
|
sessionId = event.session_id ?? "";
|
||||||
|
costUsd = event.total_cost_usd ?? 0;
|
||||||
|
const u = event.usage as any;
|
||||||
|
if (u) {
|
||||||
|
inputTokens = u.input_tokens ?? inputTokens;
|
||||||
|
outputTokens = u.output_tokens ?? outputTokens;
|
||||||
|
cacheReadTokens = u.cache_read_input_tokens ?? cacheReadTokens;
|
||||||
|
cacheWriteTokens = u.cache_creation_input_tokens ?? cacheWriteTokens;
|
||||||
|
}
|
||||||
|
} else if (event.type === "error") {
|
||||||
|
const errMsg = event.error?.message ?? event.message ?? JSON.stringify(event);
|
||||||
|
reject(new Error(formatAnthropicError(errMsg)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
proc.stdout.on("data", (data: Buffer) => {
|
||||||
|
buffer += data.toString();
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() ?? "";
|
||||||
|
for (const line of lines) processLine(line);
|
||||||
|
});
|
||||||
|
|
||||||
|
let stderrOutput = "";
|
||||||
|
proc.stderr.on("data", (data: Buffer) => { stderrOutput += data.toString(); });
|
||||||
|
|
||||||
|
proc.on("close", (code) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (buffer.trim()) processLine(buffer);
|
||||||
|
const finalText = blocks.filter((b): b is TextBlock => b.type === "text").map((b) => b.text).join("");
|
||||||
|
if (code !== 0) {
|
||||||
|
if (timeoutFired) {
|
||||||
|
reject(new Error(`Claude timed out after ${timeoutMs / 1000}s — the process was killed. Consider using a simpler prompt or a faster model.`));
|
||||||
|
} else {
|
||||||
|
const errMsg = formatAnthropicError(stderrOutput.trim(), code);
|
||||||
|
const detail = finalText ? ` (partial output: ${finalText.slice(0, 100)})` : "";
|
||||||
|
reject(new Error(errMsg + detail));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve({ finalText, blocks, sessionId, costUsd, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens });
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("error", (err) => {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
||||||
|
reject(new Error("Claude CLI not found — install it with `npm install -g @anthropic-ai/claude-code`"));
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.signal) {
|
||||||
|
const kill = () => {
|
||||||
|
try { if (!proc.killed) proc.kill("SIGTERM"); } catch { /* ok */ }
|
||||||
|
setTimeout(() => { try { if (!proc.killed) proc.kill("SIGKILL"); } catch { /* ok */ } }, 3000);
|
||||||
|
};
|
||||||
|
if (opts.signal.aborted) kill();
|
||||||
|
else opts.signal.addEventListener("abort", kill, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeoutFired = false;
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
timeoutFired = true;
|
||||||
|
try { proc.kill("SIGTERM"); } catch { /* ok */ }
|
||||||
|
setTimeout(() => { try { if (!proc.killed) proc.kill("SIGKILL"); } catch { /* ok */ } }, 3000);
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
proc.prependListener("close", () => { clearTimeout(timeoutId); });
|
||||||
|
proc.prependListener("error", () => { clearTimeout(timeoutId); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// RenderResult factory — produces the common result rendering for both extensions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function renderClaudeResult(
|
||||||
|
result: { details?: ClaudeDetails; isPartial?: boolean },
|
||||||
|
theme: Theme,
|
||||||
|
opts?: { showSession?: boolean }
|
||||||
|
): Container | Text {
|
||||||
|
const d = result.details as ClaudeDetails | undefined;
|
||||||
|
if (!d) return new Text(theme.fg("muted", "…"), 0, 0);
|
||||||
|
|
||||||
|
const isDone = d.done && !result.isPartial;
|
||||||
|
const icon = isDone ? theme.fg("success", "✓ ") : theme.fg("warning", "⏳ ");
|
||||||
|
const resume = d.isResume ? theme.fg("dim", " ↩") : "";
|
||||||
|
const c = new Container();
|
||||||
|
|
||||||
|
c.addChild(new Text(icon + theme.fg("toolTitle", theme.bold(d.label)) + resume, 0, 0));
|
||||||
|
|
||||||
|
for (const block of d.blocks ?? []) {
|
||||||
|
if (block.type === "thinking" && block.text.trim()) {
|
||||||
|
c.addChild(new Text(theme.fg("dim", theme.italic(block.text.trimEnd())), 0, 0));
|
||||||
|
} else if (block.type === "tool") {
|
||||||
|
c.addChild(renderToolBlock(block, theme));
|
||||||
|
} else if (block.type === "text" && block.text.trim()) {
|
||||||
|
c.addChild(new Spacer(1));
|
||||||
|
if (isDone) {
|
||||||
|
c.addChild(new Markdown(block.text.trim(), 0, 0, getMarkdownTheme()));
|
||||||
|
} else {
|
||||||
|
c.addChild(new Text(theme.fg("dim", block.text.trimEnd()), 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDone) {
|
||||||
|
const usageLine = formatUsage(d);
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (usageLine) parts.push(usageLine);
|
||||||
|
if (opts?.showSession && d.sessionId) parts.push(`session:${d.sessionId.slice(0, 8)}`);
|
||||||
|
if (parts.length > 0) {
|
||||||
|
c.addChild(new Spacer(1));
|
||||||
|
c.addChild(new Text(theme.fg("dim", parts.join(" ")), 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
201
pi/.pi/agent/shared/pi-ask-bridge.ts
Normal file
201
pi/.pi/agent/shared/pi-ask-bridge.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* pi-ask-bridge — Unix-domain socket server that pipes ask requests from
|
||||||
|
* the pi-ask-mcp subprocess (running inside Claude CLI inside chat-claude)
|
||||||
|
* into pi's native ask UI (askSingleQuestionWithInlineNote / askQuestionsWithTabs).
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
*
|
||||||
|
* pi process
|
||||||
|
* └── chat-claude extension
|
||||||
|
* ├── AskBridge (here) — listens on $PI_ASK_SOCKET
|
||||||
|
* └── claude -p ... --mcp-config <generated>
|
||||||
|
* └── pi-ask-mcp/server.js
|
||||||
|
* ↳ on tools/call ask → connects to $PI_ASK_SOCKET,
|
||||||
|
* sends question, awaits answer
|
||||||
|
*
|
||||||
|
* Lifecycle: start one bridge per chat-claude session; close on exit.
|
||||||
|
* Concurrency: pi's ui.custom overlay is modal, so asks are serialised
|
||||||
|
* across all open connections via an internal promise chain.
|
||||||
|
*
|
||||||
|
* Wire format (NDJSON, one message per line):
|
||||||
|
*
|
||||||
|
* request → { id, type: "ask", questions: [{id, question, options[], multi?, recommended?}, ...] }
|
||||||
|
* response ← { id, type: "result", results: [{id, selectedOptions[], customInput?}, ...] }
|
||||||
|
* error ← { id, type: "error", message: "…" }
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createServer, type Server as NetServer, type Socket } from "node:net";
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import type { ExtensionUIContext } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { askSingleQuestionWithInlineNote } from "../extensions/pi-ask-tool/ask-inline-ui.js";
|
||||||
|
import { askQuestionsWithTabs } from "../extensions/pi-ask-tool/ask-tabs-ui.js";
|
||||||
|
import type { AskQuestion, AskSelection } from "../extensions/pi-ask-tool/ask-logic.js";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Public API
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface AskBridge {
|
||||||
|
/** Path to the generated --mcp-config JSON, suitable for `claude --mcp-config`. */
|
||||||
|
mcpConfigPath: string;
|
||||||
|
/** Absolute path to the underlying Unix socket (informational). */
|
||||||
|
socketPath: string;
|
||||||
|
/** How many ask requests this bridge has served so far. */
|
||||||
|
count(): number;
|
||||||
|
/** Stop accepting connections, remove socket + temp dir. Idempotent. */
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartAskBridgeOptions {
|
||||||
|
/** pi UI context (must come from an interactive session). */
|
||||||
|
ui: ExtensionUIContext;
|
||||||
|
/**
|
||||||
|
* Absolute path to extensions/pi-ask-mcp/server.js. Auto-derived from
|
||||||
|
* import.meta.url when omitted (assumes the conventional layout).
|
||||||
|
*/
|
||||||
|
mcpServerEntry?: string;
|
||||||
|
/**
|
||||||
|
* MCP server name surfaced in the tool prefix. Defaults to "pi", which
|
||||||
|
* makes the tool name `mcp__pi__ask` in Claude's tool stream.
|
||||||
|
*/
|
||||||
|
serverName?: string;
|
||||||
|
/** Optional notification fired whenever a new ask is served. */
|
||||||
|
onAsk?: (totalSoFar: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startAskBridge(opts: StartAskBridgeOptions): AskBridge {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "pi-ask-")); // 0700 perms
|
||||||
|
const sock = join(dir, "ask.sock");
|
||||||
|
let askCount = 0;
|
||||||
|
let closed = false;
|
||||||
|
|
||||||
|
const server: NetServer = createServer((conn) =>
|
||||||
|
handleConnection(conn, opts, () => {
|
||||||
|
askCount += 1;
|
||||||
|
opts.onAsk?.(askCount);
|
||||||
|
return askCount;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
server.on("error", () => { /* socket disappeared, etc. — bridge is single-tenant, ignore */ });
|
||||||
|
server.listen(sock);
|
||||||
|
|
||||||
|
const mcpEntry = opts.mcpServerEntry ?? defaultMcpEntry();
|
||||||
|
const serverName = opts.serverName ?? "pi";
|
||||||
|
const cfgPath = join(dir, "mcp.json");
|
||||||
|
writeFileSync(cfgPath, JSON.stringify({
|
||||||
|
mcpServers: {
|
||||||
|
[serverName]: {
|
||||||
|
command: "node",
|
||||||
|
args: [mcpEntry],
|
||||||
|
env: { PI_ASK_SOCKET: sock },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
socketPath: sock,
|
||||||
|
mcpConfigPath: cfgPath,
|
||||||
|
count: () => askCount,
|
||||||
|
close: () => {
|
||||||
|
if (closed) return;
|
||||||
|
closed = true;
|
||||||
|
try { server.close(); } catch { /* noop */ }
|
||||||
|
try { rmSync(dir, { recursive: true, force: true }); } catch { /* noop */ }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Internals
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function defaultMcpEntry(): string {
|
||||||
|
// shared/pi-ask-bridge.ts → ../extensions/pi-ask-mcp/server.js
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
return join(here, "..", "extensions", "pi-ask-mcp", "server.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
// pi.ui.custom is modal — only one overlay can be on screen at a time.
|
||||||
|
// Serialise asks across ALL connections via this single promise chain.
|
||||||
|
let askChain: Promise<unknown> = Promise.resolve();
|
||||||
|
|
||||||
|
function handleConnection(
|
||||||
|
conn: Socket,
|
||||||
|
opts: StartAskBridgeOptions,
|
||||||
|
bumpCount: () => number,
|
||||||
|
) {
|
||||||
|
let buf = "";
|
||||||
|
conn.on("data", (data) => {
|
||||||
|
buf += data.toString();
|
||||||
|
let nl = buf.indexOf("\n");
|
||||||
|
while (nl >= 0) {
|
||||||
|
const line = buf.slice(0, nl);
|
||||||
|
buf = buf.slice(nl + 1);
|
||||||
|
handleLine(line, conn, opts, bumpCount);
|
||||||
|
nl = buf.indexOf("\n");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
conn.on("error", () => { /* peer might disappear if Claude is killed mid-flight */ });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLine(
|
||||||
|
line: string,
|
||||||
|
conn: Socket,
|
||||||
|
opts: StartAskBridgeOptions,
|
||||||
|
bumpCount: () => number,
|
||||||
|
) {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
let msg: any;
|
||||||
|
try { msg = JSON.parse(line); } catch { return; }
|
||||||
|
if (msg.type !== "ask") return;
|
||||||
|
const id = String(msg.id ?? "");
|
||||||
|
const questions = Array.isArray(msg.questions) ? (msg.questions as AskQuestion[]) : [];
|
||||||
|
|
||||||
|
askChain = askChain.then(async () => {
|
||||||
|
bumpCount();
|
||||||
|
try {
|
||||||
|
const results = await askViaPiUI(opts.ui, questions);
|
||||||
|
writeReply(conn, { id, type: "result", results });
|
||||||
|
} catch (err) {
|
||||||
|
writeReply(conn, {
|
||||||
|
id,
|
||||||
|
type: "error",
|
||||||
|
message: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeReply(conn: Socket, msg: unknown) {
|
||||||
|
try { conn.write(JSON.stringify(msg) + "\n"); conn.end(); } catch { /* gone */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuestionResult {
|
||||||
|
id: string;
|
||||||
|
selectedOptions: string[];
|
||||||
|
customInput?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function askViaPiUI(
|
||||||
|
ui: ExtensionUIContext,
|
||||||
|
questions: AskQuestion[],
|
||||||
|
): Promise<QuestionResult[]> {
|
||||||
|
if (questions.length === 0) return [];
|
||||||
|
|
||||||
|
if (questions.length === 1 && !questions[0].multi) {
|
||||||
|
const sel: AskSelection = await askSingleQuestionWithInlineNote(ui, questions[0]);
|
||||||
|
return [toResult(questions[0], sel)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab = await askQuestionsWithTabs(ui, questions);
|
||||||
|
return questions.map((q, i) => toResult(q, tab.selections[i] ?? { selectedOptions: [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toResult(q: AskQuestion, sel: AskSelection): QuestionResult {
|
||||||
|
const out: QuestionResult = { id: q.id, selectedOptions: [...sel.selectedOptions] };
|
||||||
|
if (sel.customInput) out.customInput = sel.customInput;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
110
pi/.pi/agent/skills/add-agent/SKILL.md
Normal file
110
pi/.pi/agent/skills/add-agent/SKILL.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
name: add-agent
|
||||||
|
description: "Add a new Claude agent definition to ~/.claude/agents/ and an accompanying skill to ~/.pi/agent/skills/. Use when creating new specialized agents."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Add Agent
|
||||||
|
|
||||||
|
When the user wants to create a new specialized Claude agent:
|
||||||
|
|
||||||
|
## Agent File Convention
|
||||||
|
|
||||||
|
Create a `.md` file in `~/.claude/agents/` with this structure:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: agent_name
|
||||||
|
description: One-line description of what the agent does
|
||||||
|
tools: Read, Bash[, Edit, Write] # include only what's needed
|
||||||
|
model: sonnet | opus
|
||||||
|
---
|
||||||
|
|
||||||
|
You are an [role]. [1-2 sentence description of purpose].
|
||||||
|
|
||||||
|
Available tools:
|
||||||
|
- read: Read file contents
|
||||||
|
- bash: Execute bash commands
|
||||||
|
- edit: Make surgical edits to files # if applicable
|
||||||
|
- write: Create or overwrite files # if applicable
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
- Use bash for file operations: prefer `rg` over grep, `fd` over find, glob patterns for batch file matching
|
||||||
|
- Use read to examine files [before editing] # adapt phrasing
|
||||||
|
- [Agent-specific guidelines]
|
||||||
|
- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did
|
||||||
|
- Be concise in your responses
|
||||||
|
- Show file paths clearly when working with files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Rules
|
||||||
|
|
||||||
|
1. **`name`** in frontmatter must match what skills reference as `agent: "name"`
|
||||||
|
2. **`tools`** — only include tools the agent actually needs. Read-only agents use `Read, Bash`
|
||||||
|
3. **`model`** — `sonnet` for execution/review, `opus` for strategic/advisory work
|
||||||
|
4. **Always include** the "output plain text directly" guideline — agents without it tend to use `cat`/`echo` instead of responding directly
|
||||||
|
5. **Bash guideline** should read: `prefer \`rg\` over grep, \`fd\` over find, glob patterns for batch file matching`
|
||||||
|
6. **Bash tool description** should be: `Execute bash commands`
|
||||||
|
|
||||||
|
## Skill File Convention
|
||||||
|
|
||||||
|
Create `~/.pi/agent/skills/<skill-name>/SKILL.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: skill-name
|
||||||
|
description: "What triggers this skill. Use when [condition]."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skill Title
|
||||||
|
|
||||||
|
When [trigger condition]:
|
||||||
|
|
||||||
|
## What to include in the prompt
|
||||||
|
|
||||||
|
1. **[Section 1]** — description
|
||||||
|
2. **[Section 2]** — description
|
||||||
|
...
|
||||||
|
|
||||||
|
The `agent_name` agent has `Tool1` and `Tool2` tools only — [what it can/can't do].
|
||||||
|
|
||||||
|
## How to call
|
||||||
|
|
||||||
|
\```
|
||||||
|
ask_claude({
|
||||||
|
agent: "agent_name",
|
||||||
|
question: "Specific instruction for the agent.",
|
||||||
|
prompt: `
|
||||||
|
## Section 1
|
||||||
|
[Template]
|
||||||
|
|
||||||
|
## Section 2
|
||||||
|
[Template]
|
||||||
|
`
|
||||||
|
})
|
||||||
|
\```
|
||||||
|
|
||||||
|
## After the review
|
||||||
|
|
||||||
|
- [What to do with the agent's output]
|
||||||
|
- [How to summarize for the user]
|
||||||
|
- [When to loop back or escalate]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Skill Key Rules
|
||||||
|
|
||||||
|
1. **Skill `name`** should be kebab-case (e.g., `claude-debug`, not `claude_debug`)
|
||||||
|
2. **Description** must be in quotes if it contains special characters
|
||||||
|
3. **`agent:` in `ask_claude()`** must match the agent's `name:` exactly
|
||||||
|
4. **Include a "What to include" section** — gives the calling agent a template
|
||||||
|
5. **Include a "How to call" section** — with a concrete `ask_claude()` example
|
||||||
|
6. **Include an "After the review" section** — what to do with the output
|
||||||
|
|
||||||
|
## Existing Agents (for reference)
|
||||||
|
|
||||||
|
| Agent | Model | Tools | Purpose |
|
||||||
|
|-------|-------|-------|---------|
|
||||||
|
| `minimal` | sonnet | Read, Bash, Edit, Write | General coding |
|
||||||
|
| `code_review` | sonnet | Read, Bash, Edit, Write | Review & fix code |
|
||||||
|
| `plan_review` | opus | Read, Bash | Review plans |
|
||||||
|
| `debug` | sonnet | Read, Bash | Trace bugs |
|
||||||
|
| `oracle` | opus | Read, Bash | Strategic guidance |
|
||||||
153
pi/.pi/agent/skills/ask-claude/SKILL.md
Normal file
153
pi/.pi/agent/skills/ask-claude/SKILL.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
name: ask-claude
|
||||||
|
description: "Invoke Claude (any agent or model) for an opinion, review, or analysis. Use when the user asks you to ask a specific Claude about something specific — e.g. trade-offs of an approach, a focused review, or a second opinion."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ask Claude
|
||||||
|
|
||||||
|
Use `ask_claude` to consult Claude with any combination of agent, model, question, and prompt.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
| Parameter | Required | Description |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| `prompt` | yes | The content for Claude to reason about (context, code, plan, bug report, etc.) |
|
||||||
|
| `question` | no | What specifically to ask — prepended as a focused review directive |
|
||||||
|
| `agent` | no | Agent name from `~/.claude/agents/`. See **Available agents** below. |
|
||||||
|
| `model` | no | Override the model: `"opus"`, `"sonnet"`, `"haiku"` |
|
||||||
|
| `session_id` | no | Resume a prior conversation (returned in every response) |
|
||||||
|
|
||||||
|
If neither `agent` nor `model` is set, defaults to Claude Sonnet.
|
||||||
|
|
||||||
|
> **Multi-turn:** Every response includes a `session_id`. Pass it back in a subsequent `ask_claude()` call to continue the same conversation with the same agent/model.
|
||||||
|
|
||||||
|
## Available agents
|
||||||
|
|
||||||
|
See `~/.claude/agents/` for the full list. Common agents:
|
||||||
|
|
||||||
|
| Agent | Model | Tools | Use when |
|
||||||
|
|-------|-------|-------|----------|
|
||||||
|
| `plan_review` | Opus | Read, Bash | Reviewing plans for correctness, completeness, feasibility, and risk |
|
||||||
|
| `code_review` | Sonnet | Read, Bash, Edit, Write | Reviewing implementations; can apply fixes directly |
|
||||||
|
| `debug` | Sonnet | Read, Bash | Tracing bugs and root causes; will NOT apply fixes |
|
||||||
|
| `oracle` | Opus | Read, Bash | Hard problems, architectural decisions, when you're stuck |
|
||||||
|
|
||||||
|
Pick the agent that matches the task. If unsure, ask the user which agent to use, or use a raw `model=` instead of an agent.
|
||||||
|
|
||||||
|
## Common patterns
|
||||||
|
|
||||||
|
### Plan review
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
agent: "plan_review",
|
||||||
|
question: "Review for correctness, completeness, feasibility, and risk. Highlight missing steps or unclear requirements.",
|
||||||
|
prompt: `
|
||||||
|
## Project Context
|
||||||
|
[Key facts from CLAUDE.md]
|
||||||
|
|
||||||
|
## Codebase Exploration
|
||||||
|
[Modules/files you read]
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
[Your full plan, step by step]
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
[Anything uncertain]
|
||||||
|
`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code review (with fix capability)
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
agent: "code_review",
|
||||||
|
question: "Review for bugs, architectural issues, style, and correctness. Apply fixes for any issues you find.",
|
||||||
|
prompt: `
|
||||||
|
## Project Conventions
|
||||||
|
[Relevant context not in CLAUDE.md]
|
||||||
|
|
||||||
|
## What Was Implemented
|
||||||
|
[Brief description]
|
||||||
|
|
||||||
|
## Plan That Was Followed
|
||||||
|
[The implementation plan]
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
- path/to/file.ts — [what changed]
|
||||||
|
|
||||||
|
## Code to Review
|
||||||
|
[Paste key sections OR instruct the agent to read the files above]
|
||||||
|
`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
agent: "debug",
|
||||||
|
question: "Trace the root cause. Provide specific file paths, line numbers, and the exact code responsible. Suggest a fix but do not apply it.",
|
||||||
|
prompt: `
|
||||||
|
## The Issue
|
||||||
|
[What's happening vs. what should happen]
|
||||||
|
|
||||||
|
## Error Output
|
||||||
|
[Any logs, stack traces, or error messages]
|
||||||
|
|
||||||
|
## Relevant Files
|
||||||
|
- path/to/file.ts — [why it's relevant]
|
||||||
|
|
||||||
|
## What I've Already Tried
|
||||||
|
[Hypotheses tested, things ruled out]
|
||||||
|
`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Oracle (hard problems)
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
agent: "oracle",
|
||||||
|
question: "Analyze this problem and provide guidance. Explain your reasoning and recommend the best path forward.",
|
||||||
|
prompt: `
|
||||||
|
## The Problem
|
||||||
|
[What you're trying to solve and why it's hard]
|
||||||
|
|
||||||
|
## Codebase Context
|
||||||
|
[Relevant files, modules, and patterns explored]
|
||||||
|
|
||||||
|
## Options Considered
|
||||||
|
[Approaches evaluated and their trade-offs]
|
||||||
|
|
||||||
|
## What I Need Clarified
|
||||||
|
[Specific question or decision point]
|
||||||
|
`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Free-form model question (no agent)
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
model: "opus",
|
||||||
|
question: "What are the trade-offs of this approach vs using a message queue?",
|
||||||
|
prompt: `We're considering polling a database table every 5 seconds for new jobs instead of a queue...`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Continuing a conversation
|
||||||
|
When Claude returns a `session_id` in its response, pass it back to continue the same conversation:
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
agent: "code_review",
|
||||||
|
question: "Apply the fixes you identified.",
|
||||||
|
session_id: "<session_id from prior response>",
|
||||||
|
prompt: "Please proceed with the fixes."
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## After the response
|
||||||
|
|
||||||
|
- Summarize Claude's key points for the user.
|
||||||
|
- If Claude raises blockers or important concerns, address them before proceeding.
|
||||||
|
- For plan reviews: if blockers or missing steps are flagged, update the plan and re-invoke if changes are significant.
|
||||||
|
- For code reviews: Claude (via `code_review`) may apply fixes directly — summarize what was changed.
|
||||||
|
- For debugging: apply the fix yourself or delegate to a coding agent.
|
||||||
|
- For oracle: proceed with the recommended approach, or loop back if more clarification is needed.
|
||||||
50
pi/.pi/agent/skills/implementor/SKILL.md
Normal file
50
pi/.pi/agent/skills/implementor/SKILL.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: implementor
|
||||||
|
description: "Delegate implementation of a fix or feature to the implementor agent. Use when you have a clear plan, bug fix, or feature to implement and want a focused agent to handle the full coding + build-verify cycle."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Implementor
|
||||||
|
|
||||||
|
When you need to implement a fix, feature, or refactoring with full build verification:
|
||||||
|
|
||||||
|
## What to include in the prompt
|
||||||
|
|
||||||
|
1. **Goal** — what needs to be built or fixed, in 1-2 sentences
|
||||||
|
2. **Context** — relevant files, functions, or modules (include the actual code or file paths)
|
||||||
|
3. **Plan** — step-by-step description of the changes to make (if you have one)
|
||||||
|
4. **Constraints** — any project conventions, patterns to follow, or things to avoid
|
||||||
|
5. **Verification** — how to confirm the implementation is correct (build command, test names, etc.)
|
||||||
|
|
||||||
|
The `implementor` agent has `Read`, `Bash`, `Edit`, and `Write` tools only — it can read code, run commands, and make changes. It cannot use semantic search or web search.
|
||||||
|
|
||||||
|
## How to call
|
||||||
|
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
agent: "implementor",
|
||||||
|
question: "Implement [brief description of the fix or feature].",
|
||||||
|
prompt: `
|
||||||
|
## Goal
|
||||||
|
[What needs to be done]
|
||||||
|
|
||||||
|
## Context
|
||||||
|
[Relevant files and code snippets]
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
[Step-by-step changes, or "investigate and determine the best approach"]
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
[Project conventions, patterns to follow, things to avoid]
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
[Build command, tests to run, how to confirm correctness]
|
||||||
|
`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## After the review
|
||||||
|
|
||||||
|
- Check whether the agent reported successful build/verification
|
||||||
|
- If it failed, either re-invoke with the error details or fix the remaining issues directly
|
||||||
|
- Summarize for the user: what files changed, what was verified, and any remaining concerns
|
||||||
|
- For complex implementations, consider running `diagnostics` sub-agent or relevant tests afterward as a second pass
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
{
|
{
|
||||||
"$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": "wezterm-sync-ba8a76f5",
|
"name": "wezterm-sync-9a35138e",
|
||||||
"vars": {
|
"vars": {
|
||||||
"bg": "#1c2433",
|
"bg": "#faf4ed",
|
||||||
"fg": "#afbbd2",
|
"fg": "#2a2a2a",
|
||||||
"accent": "#b78aff",
|
"accent": "#7b4fc4",
|
||||||
"accentAlt": "#ff955c",
|
"accentAlt": "#c45a1c",
|
||||||
"link": "#69c3ff",
|
"link": "#1a7db5",
|
||||||
"error": "#ff738a",
|
"error": "#d1344f",
|
||||||
"success": "#3cec85",
|
"success": "#1e9b52",
|
||||||
"warning": "#eacd61",
|
"warning": "#b8890f",
|
||||||
"muted": "#7c869a",
|
"muted": "#73716e",
|
||||||
"dim": "#5e687b",
|
"dim": "#9c9995",
|
||||||
"borderMuted": "#414a5b",
|
"borderMuted": "#c6c2bc",
|
||||||
"selectedBg": "#28303f",
|
"selectedBg": "#eee8e1",
|
||||||
"userMsgBg": "#242c3b",
|
"userMsgBg": "#f2ece5",
|
||||||
"toolPendingBg": "#212938",
|
"toolPendingBg": "#f5efe8",
|
||||||
"toolSuccessBg": "#203c3d",
|
"toolSuccessBg": "#e0e9da",
|
||||||
"toolErrorBg": "#372d3d",
|
"toolErrorBg": "#f5ddda",
|
||||||
"customMsgBg": "#282c43"
|
"customMsgBg": "#f0e7ea"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"accent": "accent",
|
"accent": "accent",
|
||||||
@@ -74,8 +74,8 @@
|
|||||||
"bashMode": "success"
|
"bashMode": "success"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"pageBg": "#141c2b",
|
"pageBg": "#fffcf5",
|
||||||
"cardBg": "#1c2433",
|
"cardBg": "#faf4ed",
|
||||||
"infoBg": "#353839"
|
"infoBg": "#f2e7d2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"hide_thinking_block": true
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,8 @@ exec_always ~/.config/sway/scripts/workspace-colors.py
|
|||||||
exec_always eww daemon && (eww open bar0 || true) && (eww open bar1 || true)
|
exec_always eww daemon && (eww open bar0 || true) && (eww open bar1 || true)
|
||||||
exec_always mako
|
exec_always mako
|
||||||
|
|
||||||
|
exec /usr/libexec/polkit-mate-authentication-agent-1
|
||||||
|
|
||||||
exec swayidle -w \
|
exec swayidle -w \
|
||||||
timeout 900 '~/.local/bin/smart-dim.sh' \
|
timeout 900 '~/.local/bin/smart-dim.sh' \
|
||||||
resume 'brightnessctl -r'
|
resume 'brightnessctl -r'
|
||||||
@@ -143,6 +145,9 @@ mode "resize" {
|
|||||||
}
|
}
|
||||||
bindsym $mod+r mode "resize"
|
bindsym $mod+r mode "resize"
|
||||||
#
|
#
|
||||||
|
# Toggle dark/light theme
|
||||||
|
bindsym $mod+y exec ~/.config/sway/scripts/toggle-theme.sh
|
||||||
|
|
||||||
# Utilities:
|
# Utilities:
|
||||||
#
|
#
|
||||||
# Special keys to adjust volume via PulseAudio
|
# Special keys to adjust volume via PulseAudio
|
||||||
|
|||||||
23
sway/.config/sway/scripts/toggle-theme.sh
Executable file
23
sway/.config/sway/scripts/toggle-theme.sh
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Toggle between dark and light themes for wezterm and nvim
|
||||||
|
# Source of truth: ~/.config/theme-state
|
||||||
|
|
||||||
|
STATE_FILE="$HOME/.config/theme-state"
|
||||||
|
|
||||||
|
# Read current state
|
||||||
|
current="dark"
|
||||||
|
if [ -f "$STATE_FILE" ]; then
|
||||||
|
current=$(cat "$STATE_FILE" | tr -d '[:space:]')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Toggle
|
||||||
|
if [ "$current" = "light" ]; then
|
||||||
|
next="dark"
|
||||||
|
else
|
||||||
|
next="light"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write new state
|
||||||
|
# - wezterm watches this file via add_to_config_reload_watch_list and auto-reloads
|
||||||
|
# - nvim watches this file via vim.uv.fs_event and auto-switches colorscheme
|
||||||
|
echo "$next" > "$STATE_FILE"
|
||||||
81
wezterm/.config/wezterm/theme/bearded-arc-light.lua
Normal file
81
wezterm/.config/wezterm/theme/bearded-arc-light.lua
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
-- Bearded Arc (Light)
|
||||||
|
-- Hand-tuned light variant with warm off-white bg and saturated accents
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
M.palette = {
|
||||||
|
base = '#faf4ed',
|
||||||
|
overlay = '#f0e8df',
|
||||||
|
muted = '#7a8598',
|
||||||
|
text = '#2a2a2a',
|
||||||
|
red = '#d1344f',
|
||||||
|
yellow = '#b8890f',
|
||||||
|
orange = '#c45a1c',
|
||||||
|
green = '#1e9b52',
|
||||||
|
cyan = '#1a7db5',
|
||||||
|
purple = '#7b4fc4',
|
||||||
|
highlight = '#5a6a7a',
|
||||||
|
}
|
||||||
|
|
||||||
|
local active_tab = {
|
||||||
|
bg_color = M.palette.overlay,
|
||||||
|
fg_color = M.palette.text,
|
||||||
|
}
|
||||||
|
|
||||||
|
local inactive_tab = {
|
||||||
|
bg_color = M.palette.base,
|
||||||
|
fg_color = M.palette.muted,
|
||||||
|
}
|
||||||
|
|
||||||
|
function M.colors()
|
||||||
|
return {
|
||||||
|
foreground = M.palette.text,
|
||||||
|
background = M.palette.base,
|
||||||
|
cursor_bg = M.palette.yellow,
|
||||||
|
cursor_border = M.palette.yellow,
|
||||||
|
cursor_fg = M.palette.base,
|
||||||
|
selection_bg = '#d8d0c6',
|
||||||
|
selection_fg = M.palette.text,
|
||||||
|
|
||||||
|
ansi = {
|
||||||
|
M.palette.overlay,
|
||||||
|
M.palette.red,
|
||||||
|
M.palette.green,
|
||||||
|
M.palette.yellow,
|
||||||
|
M.palette.cyan,
|
||||||
|
M.palette.purple,
|
||||||
|
M.palette.orange,
|
||||||
|
M.palette.text,
|
||||||
|
},
|
||||||
|
|
||||||
|
brights = {
|
||||||
|
M.palette.muted,
|
||||||
|
M.palette.red,
|
||||||
|
M.palette.green,
|
||||||
|
M.palette.yellow,
|
||||||
|
M.palette.cyan,
|
||||||
|
M.palette.purple,
|
||||||
|
M.palette.orange,
|
||||||
|
M.palette.text,
|
||||||
|
},
|
||||||
|
|
||||||
|
tab_bar = {
|
||||||
|
background = M.palette.base,
|
||||||
|
active_tab = active_tab,
|
||||||
|
inactive_tab = inactive_tab,
|
||||||
|
inactive_tab_hover = active_tab,
|
||||||
|
new_tab = inactive_tab,
|
||||||
|
new_tab_hover = active_tab,
|
||||||
|
inactive_tab_edge = M.palette.muted,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.window_frame()
|
||||||
|
return {
|
||||||
|
active_titlebar_bg = M.palette.base,
|
||||||
|
inactive_titlebar_bg = M.palette.base,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
81
wezterm/.config/wezterm/theme/bearded-arc.lua
Normal file
81
wezterm/.config/wezterm/theme/bearded-arc.lua
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
-- Bearded Arc (Dark)
|
||||||
|
-- Based on: https://github.com/BeardedBear/bearded-theme
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
M.palette = {
|
||||||
|
base = '#1c2433',
|
||||||
|
overlay = '#212a3b',
|
||||||
|
muted = '#4a5e84',
|
||||||
|
text = '#afbbd2',
|
||||||
|
red = '#FF738A',
|
||||||
|
yellow = '#EACD61',
|
||||||
|
orange = '#FF955C',
|
||||||
|
green = '#3CEC85',
|
||||||
|
cyan = '#69C3FF',
|
||||||
|
purple = '#B78AFF',
|
||||||
|
highlight = '#8196b5',
|
||||||
|
}
|
||||||
|
|
||||||
|
local active_tab = {
|
||||||
|
bg_color = M.palette.overlay,
|
||||||
|
fg_color = M.palette.text,
|
||||||
|
}
|
||||||
|
|
||||||
|
local inactive_tab = {
|
||||||
|
bg_color = M.palette.base,
|
||||||
|
fg_color = M.palette.muted,
|
||||||
|
}
|
||||||
|
|
||||||
|
function M.colors()
|
||||||
|
return {
|
||||||
|
foreground = M.palette.text,
|
||||||
|
background = M.palette.base,
|
||||||
|
cursor_bg = M.palette.yellow,
|
||||||
|
cursor_border = M.palette.yellow,
|
||||||
|
cursor_fg = M.palette.text,
|
||||||
|
selection_bg = '#405275',
|
||||||
|
selection_fg = M.palette.text,
|
||||||
|
|
||||||
|
ansi = {
|
||||||
|
M.palette.overlay,
|
||||||
|
M.palette.red,
|
||||||
|
M.palette.green,
|
||||||
|
M.palette.yellow,
|
||||||
|
M.palette.cyan,
|
||||||
|
M.palette.purple,
|
||||||
|
M.palette.orange,
|
||||||
|
M.palette.text,
|
||||||
|
},
|
||||||
|
|
||||||
|
brights = {
|
||||||
|
M.palette.muted,
|
||||||
|
M.palette.red,
|
||||||
|
M.palette.green,
|
||||||
|
M.palette.yellow,
|
||||||
|
M.palette.cyan,
|
||||||
|
M.palette.purple,
|
||||||
|
M.palette.orange,
|
||||||
|
M.palette.text,
|
||||||
|
},
|
||||||
|
|
||||||
|
tab_bar = {
|
||||||
|
background = M.palette.base,
|
||||||
|
active_tab = active_tab,
|
||||||
|
inactive_tab = inactive_tab,
|
||||||
|
inactive_tab_hover = active_tab,
|
||||||
|
new_tab = inactive_tab,
|
||||||
|
new_tab_hover = active_tab,
|
||||||
|
inactive_tab_edge = M.palette.muted,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.window_frame()
|
||||||
|
return {
|
||||||
|
active_titlebar_bg = M.palette.base,
|
||||||
|
inactive_titlebar_bg = M.palette.base,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -1,84 +1,11 @@
|
|||||||
-- Bearded Arc Theme
|
-- Theme registry
|
||||||
-- Based on: https://github.com/BeardedBear/bearded-theme
|
-- Standalone themes: bearded-arc (dark), bearded-arc-light
|
||||||
-- Adapted for WezTerm
|
-- Legacy variants: moon, dawn
|
||||||
|
|
||||||
local main = {}
|
local main = require('theme/bearded-arc')
|
||||||
|
local light = require('theme/bearded-arc-light')
|
||||||
local main_palette = {
|
|
||||||
base = '#1c2433',
|
|
||||||
overlay = '#212a3b',
|
|
||||||
muted = '#4a5e84',
|
|
||||||
text = '#afbbd2',
|
|
||||||
red = '#FF738A',
|
|
||||||
yellow = '#EACD61',
|
|
||||||
orange = '#FF955C',
|
|
||||||
green = '#3CEC85',
|
|
||||||
cyan = '#69C3FF',
|
|
||||||
purple = '#B78AFF',
|
|
||||||
highlight = '#8196b5',
|
|
||||||
}
|
|
||||||
|
|
||||||
local main_active_tab = {
|
|
||||||
bg_color = main_palette.overlay,
|
|
||||||
fg_color = main_palette.text,
|
|
||||||
}
|
|
||||||
|
|
||||||
local main_inactive_tab = {
|
|
||||||
bg_color = main_palette.base,
|
|
||||||
fg_color = main_palette.muted,
|
|
||||||
}
|
|
||||||
|
|
||||||
function main.colors()
|
|
||||||
return {
|
|
||||||
foreground = main_palette.text,
|
|
||||||
background = main_palette.base,
|
|
||||||
cursor_bg = main_palette.yellow,
|
|
||||||
cursor_border = main_palette.yellow,
|
|
||||||
cursor_fg = main_palette.text,
|
|
||||||
selection_bg = '#405275',
|
|
||||||
selection_fg = main_palette.text,
|
|
||||||
|
|
||||||
ansi = {
|
|
||||||
main_palette.overlay,
|
|
||||||
main_palette.red,
|
|
||||||
main_palette.green,
|
|
||||||
main_palette.yellow,
|
|
||||||
main_palette.cyan,
|
|
||||||
main_palette.purple,
|
|
||||||
main_palette.orange,
|
|
||||||
main_palette.text,
|
|
||||||
},
|
|
||||||
|
|
||||||
brights = {
|
|
||||||
main_palette.muted,
|
|
||||||
main_palette.red,
|
|
||||||
main_palette.green,
|
|
||||||
main_palette.yellow,
|
|
||||||
main_palette.cyan,
|
|
||||||
main_palette.purple,
|
|
||||||
main_palette.orange,
|
|
||||||
main_palette.text,
|
|
||||||
},
|
|
||||||
|
|
||||||
tab_bar = {
|
|
||||||
background = main_palette.base,
|
|
||||||
active_tab = main_active_tab,
|
|
||||||
inactive_tab = main_inactive_tab,
|
|
||||||
inactive_tab_hover = main_active_tab,
|
|
||||||
new_tab = main_inactive_tab,
|
|
||||||
new_tab_hover = main_active_tab,
|
|
||||||
inactive_tab_edge = main_palette.muted, -- (Fancy tab bar only)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function main.window_frame() -- (Fancy tab bar only)
|
|
||||||
return {
|
|
||||||
active_titlebar_bg = main_palette.base,
|
|
||||||
inactive_titlebar_bg = main_palette.base,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
|
-- Moon variant (kept here for backward compat)
|
||||||
local moon = {}
|
local moon = {}
|
||||||
|
|
||||||
local moon_palette = {
|
local moon_palette = {
|
||||||
@@ -144,18 +71,19 @@ function moon.colors()
|
|||||||
inactive_tab_hover = moon_active_tab,
|
inactive_tab_hover = moon_active_tab,
|
||||||
new_tab = moon_inactive_tab,
|
new_tab = moon_inactive_tab,
|
||||||
new_tab_hover = moon_active_tab,
|
new_tab_hover = moon_active_tab,
|
||||||
inactive_tab_edge = moon_palette.muted, -- (Fancy tab bar only)
|
inactive_tab_edge = moon_palette.muted,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
function moon.window_frame() -- (Fancy tab bar only)
|
function moon.window_frame()
|
||||||
return {
|
return {
|
||||||
active_titlebar_bg = moon_palette.base,
|
active_titlebar_bg = moon_palette.base,
|
||||||
inactive_titlebar_bg = moon_palette.base,
|
inactive_titlebar_bg = moon_palette.base,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Dawn variant (kept here for backward compat)
|
||||||
local dawn = {}
|
local dawn = {}
|
||||||
|
|
||||||
local dawn_palette = {
|
local dawn_palette = {
|
||||||
@@ -221,12 +149,12 @@ function dawn.colors()
|
|||||||
inactive_tab_hover = dawn_active_tab,
|
inactive_tab_hover = dawn_active_tab,
|
||||||
new_tab = dawn_inactive_tab,
|
new_tab = dawn_inactive_tab,
|
||||||
new_tab_hover = dawn_active_tab,
|
new_tab_hover = dawn_active_tab,
|
||||||
inactive_tab_edge = dawn_palette.muted, -- (Fancy tab bar only)
|
inactive_tab_edge = dawn_palette.muted,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
function dawn.window_frame() -- (Fancy tab bar only)
|
function dawn.window_frame()
|
||||||
return {
|
return {
|
||||||
active_titlebar_bg = dawn_palette.base,
|
active_titlebar_bg = dawn_palette.base,
|
||||||
inactive_titlebar_bg = dawn_palette.base,
|
inactive_titlebar_bg = dawn_palette.base,
|
||||||
@@ -235,6 +163,7 @@ end
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
main = main,
|
main = main,
|
||||||
|
light = light,
|
||||||
moon = moon,
|
moon = moon,
|
||||||
dawn = dawn,
|
dawn = dawn,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,31 @@
|
|||||||
local wezterm = require 'wezterm'
|
local wezterm = require 'wezterm'
|
||||||
local config = {}
|
local config = {}
|
||||||
local theme = require('theme/init').main
|
local themes = require('theme/init')
|
||||||
|
|
||||||
|
-- Watch theme-state file so wezterm auto-reloads when toggle script writes to it
|
||||||
|
wezterm.add_to_config_reload_watch_list(
|
||||||
|
os.getenv("HOME") .. "/.config/theme-state"
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Determine current theme from state file
|
||||||
|
local function get_theme()
|
||||||
|
local f = io.open(os.getenv("HOME") .. "/.config/theme-state", "r")
|
||||||
|
if f then
|
||||||
|
local state = f:read("*a"):gsub("%s+", "")
|
||||||
|
f:close()
|
||||||
|
if state == "light" then return themes.light end
|
||||||
|
end
|
||||||
|
return themes.main
|
||||||
|
end
|
||||||
|
|
||||||
|
local theme = get_theme()
|
||||||
|
|
||||||
-- Font configuration
|
-- Font configuration
|
||||||
config.font = wezterm.font 'JetBrainsMono Nerd Font'
|
config.font = wezterm.font 'JetBrainsMono Nerd Font'
|
||||||
config.font_size = 11.0
|
config.font_size = 11.0
|
||||||
|
|
||||||
config.colors = theme.colors()
|
config.colors = theme.colors()
|
||||||
|
config.window_frame = theme.window_frame()
|
||||||
|
|
||||||
-- Window
|
-- Window
|
||||||
config.enable_tab_bar = true
|
config.enable_tab_bar = true
|
||||||
@@ -33,6 +52,13 @@ config.enable_kitty_keyboard = true
|
|||||||
config.keys = {
|
config.keys = {
|
||||||
{ key = 'LeftArrow', mods = 'CTRL|SHIFT', action = wezterm.action.ActivateTabRelative(-1) },
|
{ key = 'LeftArrow', mods = 'CTRL|SHIFT', action = wezterm.action.ActivateTabRelative(-1) },
|
||||||
{ key = 'RightArrow', mods = 'CTRL|SHIFT', action = wezterm.action.ActivateTabRelative(1) },
|
{ key = 'RightArrow', mods = 'CTRL|SHIFT', action = wezterm.action.ActivateTabRelative(1) },
|
||||||
|
{
|
||||||
|
key = 'n',
|
||||||
|
mods = 'CTRL|SHIFT',
|
||||||
|
action = wezterm.action_callback(function(win, pane)
|
||||||
|
local tab, window = pane:move_to_new_window()
|
||||||
|
end),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Machine-local overrides (not tracked in dotfiles)
|
-- Machine-local overrides (not tracked in dotfiles)
|
||||||
|
|||||||
11
zshrc/.zshrc
11
zshrc/.zshrc
@@ -1,5 +1,6 @@
|
|||||||
export PATH="$HOME/.local/share/nvim/mason/bin:$PATH"
|
export PATH="$HOME/.local/share/nvim/mason/bin:$PATH"
|
||||||
export PATH="$PATH:/home/jonas/.local/bin"
|
export PATH="$PATH:/home/jonas/.local/bin"
|
||||||
|
export PATH="$HOME/go/bin:$PATH"
|
||||||
|
|
||||||
# History configuration
|
# History configuration
|
||||||
HISTFILE=~/.zsh_history
|
HISTFILE=~/.zsh_history
|
||||||
@@ -47,11 +48,10 @@ export USE_BUILTIN_RIPGREP=0
|
|||||||
export REAL_DEBRID_API_KEY="HQVUOC3ALUHIIQCFQK4UOV2GVGVLQKKHEWFWKN77G6URFBTQMUTQ"
|
export REAL_DEBRID_API_KEY="HQVUOC3ALUHIIQCFQK4UOV2GVGVLQKKHEWFWKN77G6URFBTQMUTQ"
|
||||||
export HF_TOKEN=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
|
export HF_TOKEN=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
|
||||||
export OPENCODE_API_KEY=sk-oU07900FhkKHUSpwEI8SiWjoZmV2kg8TUhVwfRWcPOjaHHq8b0dCPrb1PQNuLnh0
|
export OPENCODE_API_KEY=sk-oU07900FhkKHUSpwEI8SiWjoZmV2kg8TUhVwfRWcPOjaHHq8b0dCPrb1PQNuLnh0
|
||||||
|
export LLAMA_CACHE="/home/jonas/gguf_models"
|
||||||
|
|
||||||
export PATH=~/.npm-global/bin:$PATH
|
export PATH=~/.npm-global/bin:$PATH
|
||||||
|
|
||||||
export LD_LIBRARY_PATH=/home/jonas/.local/lib64:$LD_LIBRARY_PATH
|
|
||||||
|
|
||||||
function y() {
|
function y() {
|
||||||
local tmp="$(mktemp -t "yazi-cwd.XXXXXX")" cwd
|
local tmp="$(mktemp -t "yazi-cwd.XXXXXX")" cwd
|
||||||
yazi "$@" --cwd-file="$tmp"
|
yazi "$@" --cwd-file="$tmp"
|
||||||
@@ -72,3 +72,10 @@ if command -v tmux &>/dev/null && [ -z "$TMUX" ]; then
|
|||||||
tmux new-session -As moshi
|
tmux new-session -As moshi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Android build environment
|
||||||
|
export ANDROID_NDK_HOME="$HOME/Android/Sdk/ndk/27.2.12479018"
|
||||||
|
export ANDROID_KEYSTORE_PATH="$HOME/projects/brain/upload.jks"
|
||||||
|
export ANDROID_KEYSTORE_PASSWORD="ArtHouse5"
|
||||||
|
export ANDROID_KEY_ALIAS="upload"
|
||||||
|
export ANDROID_KEY_PASSWORD="ArtHouse5"
|
||||||
|
|||||||
Reference in New Issue
Block a user