summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-08-26 08:01:36 -0600
committert <t@tjp.lol>2026-08-26 08:37:15 -0600
commiteeb3499f58fa909df7074e689c0f029c59ea767f (patch)
treee8812fb196fabe41ec2488d1c6567c7656f2af88
parentab829fbcbbac3c50ece8e67953b024710d4b1bc6 (diff)
Let the host fit extension header rows
Return inventory entries as styled text chunks and remove the extension's byte-counting wrapper. The host now owns display-width measurement, wrapping and padding for these rows.
-rw-r--r--init.lua20
-rw-r--r--spec/test_init.lua59
2 files changed, 58 insertions, 21 deletions
diff --git a/init.lua b/init.lua
index e4e5240..6058c9d 100644
--- a/init.lua
+++ b/init.lua
@@ -53,22 +53,6 @@ local function subscribe(ext, name, handler)
end
end
-local function wrap_plain(text, width)
- width = math.max(1, width or 1)
- local lines, from = {}, 1
- while #text - from + 1 > width do
- local window = text:sub(from, from + width - 1)
- local cut = window:match("^.*() ") or width
- lines[#lines + 1] = text:sub(from, from + cut - 1):gsub("%s+$", "")
- from = from + cut
- while from <= #text and text:sub(from, from) == " " do
- from = from + 1
- end
- end
- lines[#lines + 1] = text:sub(from)
- return lines
-end
-
local function install_header(ext, profiles, workflows)
local profile_names, workflow_names = {}, {}
for _, profile in ipairs(profiles.list) do
@@ -91,9 +75,7 @@ local function install_header(ext, profiles, workflows)
{ "workflows", workflow_names },
}) do
local value = #inventory[2] == 0 and "(none)" or table.concat(inventory[2], ", ")
- for _, line in ipairs(wrap_plain(" " .. inventory[1] .. ": " .. value, width)) do
- extras[#extras + 1] = DIM .. line .. RESET
- end
+ extras[#extras + 1] = DIM .. " " .. inventory[1] .. ": " .. value .. RESET
end
local at = (#lines > 0 and lines[#lines] == "") and #lines or (#lines + 1)
for index = #extras, 1, -1 do
diff --git a/spec/test_init.lua b/spec/test_init.lua
index e3b4d55..a9956c4 100644
--- a/spec/test_init.lua
+++ b/spec/test_init.lua
@@ -138,8 +138,13 @@ return {
assert(not plain:find("unusable", 1, true), "invalid workflows stay out of the usable inventory")
assert(handle.commands_by_name["workflow:unusable"] == nil,
"the header inventory matches command registration")
- assert(rendered[#rendered] == "", "annotations stay before the trailing blank")
- assert(#rendered > 4, "inventories wrap at the component width")
+ assert(rendered[1] == "Panto", "inner component output stays first")
+ assert(rendered[2] == "\27[2m subagents: reviewer\27[0m",
+ "the profile inventory stays one styled text chunk")
+ assert(rendered[3] == "\27[2m workflows: review-chain\27[0m",
+ "the workflow inventory follows the profile inventory")
+ assert(rendered[4] == "", "annotations stay before the trailing blank")
+ assert(#rendered == 4, "header injection does not hand-roll wrapped rows")
local primary_messages = handle.ext.agent:conversation():messages()
local guidance = primary_messages[#primary_messages].blocks[1].text
@@ -150,6 +155,56 @@ return {
has(guidance, "Do not actively wait or poll with `sleep`")
end },
+ { "the header injects both inventories verbatim, in order, style-isolated", function()
+ -- Phase 4 of the row-normalization plan deletes the extension's own
+ -- wrapping. What the extension still owns is the ORDER of the two
+ -- inventories, the insertion point, style isolation (each chunk resets
+ -- itself so nothing leaks into the host's rows), and handing names over
+ -- byte-intact so Engine.fit measures the real display width.
+ local header
+ activate_bare(function(handle, ok, err)
+ assert(ok, tostring(err))
+ -- No trailing blank line from the inner component: this exercises
+ -- the append-at-the-end branch of the insertion point, which the
+ -- other header case (with a trailing blank) does not.
+ handle.emit("session_start", {
+ get_component = function()
+ return { render = function() return { "Panto" } end }
+ end,
+ set_component = function(_, component)
+ header = component
+ end,
+ })
+ end)
+ assert(header, "session_start must wrap the header component")
+
+ -- The extension performs no string processing on the names beyond a
+ -- `table.concat`, and reacts to `width` only by forwarding it to the
+ -- inner component. Rendering at an absurdly narrow width and at a wide
+ -- one must therefore be byte-identical: that is what guarantees long
+ -- and non-ASCII profile/workflow names reach Engine.fit intact.
+ local narrow = header:render(1)
+ local wide = header:render(500)
+ assert(#narrow == 3, "expected 3 chunks (inner + two inventories), saw " .. #narrow)
+ for index = 1, #narrow do
+ assert(narrow[index] == wide[index],
+ "the header must not depend on width; chunk " .. index .. " differed")
+ end
+
+ assert(narrow[1] == "Panto", "the inner component's output stays first")
+ local subagents = narrow[2]:match("^\27%[2m subagents: (.*)\27%[0m$")
+ local workflows = narrow[3]:match("^\27%[2m workflows: (.*)\27%[0m$")
+ assert(subagents, "chunk 2 is the profile inventory: " .. narrow[2])
+ assert(workflows, "chunk 3 is the workflow inventory: " .. narrow[3])
+
+ for _, chunk in ipairs({ narrow[2], narrow[3] }) do
+ assert(chunk:sub(1, 4) == "\27[2m", "each inventory chunk opens its own style")
+ assert(chunk:sub(-4) == "\27[0m", "each inventory chunk resets, so styles cannot leak")
+ assert(not chunk:find("\n", 1, true), "the extension emits no rows of its own")
+ assert(not chunk:find("\226\128\166"), "the extension truncates nothing")
+ end
+ end },
+
{ "layered config sets max_concurrent with later layers winning", function()
local tmp = os.tmpname()
os.remove(tmp)