From eeb3499f58fa909df7074e689c0f029c59ea767f Mon Sep 17 00:00:00 2001 From: t Date: Wed, 26 Aug 2026 08:01:36 -0600 Subject: 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. --- spec/test_init.lua | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) (limited to 'spec/test_init.lua') 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) -- cgit v1.3