Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pi-slate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ These work when `editor` is selected. No terminal configuration.
| Session dashboard | `/slate session [mcp\|skills\|commands\|tasks\|image]` | Inspect facts, expand/collapse resources, insert commands, or manage the image shelf. |
| Message length | `/slate message-length [default\|all\|<count>]` | How many chat messages stay on screen. `default` is 100. |
| Density | `/slate density [comfortable\|compact]` | Comfortable shows a › prompt in the composer; compact is tighter. |
| Composer metadata | `/slate footer [standard\|minimal]` | Legacy command alias: standard shows model and thinking on the composer; minimal hides them. Not footer-slot ownership. |
| Composer metadata | `/slate footer [standard\|minimal]` | Legacy command alias: standard shows model and thinking on the composer; minimal hides them. When the dashboard is hidden, thinking follows context/MCP on the top edge and the model stays below. Not footer-slot ownership. |
| Header logo | `/logo [original\|minimal]` | Toggle the stock brand mark and the white monochrome mark, or set a style explicitly. |
| Theme | `/theme` (or `/slate theme`) | Opens the Light/Dark picker: Latte and Frappé under Light, Macchiato, Mocha, Tokyo Night, and Black Metal under Dark. 15 palettes per Catppuccin flavor, live preview, no arguments. |
| Bugs | `/slate bug [file\|open]` | Copy a bug report, or open the npm package page. |
Expand Down
13 changes: 7 additions & 6 deletions pi-slate/extensions/pi-slate/composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,11 +462,13 @@ export class ComposerEditor extends CustomEditor {
}
: undefined;
const pidLabel = src.showPid === true ? src.theme.fg("dim", `PID-${AGENT_PID}`) : "";
const resources = src.context
? src.context.resources.split(" · ")
.map((part) => src.theme.fg("dim", part))
.join(metadataSeparator(src.theme, "dim"))
: "";
const resourceParts = src.context ? src.context.resources.split(" · ") : [];
if (src.dashboardVisible === false && src.context && src.footer === "standard" && src.thinking) {
resourceParts.push(src.thinking);
}
const resources = resourceParts
.map((part) => src.theme.fg("dim", part))
.join(metadataSeparator(src.theme, "dim"));
return composerStatusContextEdge(
resources,
width,
Expand Down Expand Up @@ -497,7 +499,6 @@ export class ComposerEditor extends CustomEditor {
const rightParts: string[] = [];
if (src.footer === "standard" && width >= 66) {
rightParts.push(modelStatusLabel(src.model, src.modelDisplay));
if (width >= 80 && src.thinking) rightParts.push(src.thinking);
}
const right = rightParts.length
? ` ${rightParts.map((part) => src.theme.fg("muted", part)).join(metadataSeparator(src.theme))} `
Expand Down
8 changes: 6 additions & 2 deletions pi-slate/extensions/pi-slate/diff-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ export class DiffView implements Component {
}

private fit(text: string, width: number, bg = this.config.colors.contextBg): string {
const clipped = truncateToWidth(text, width, "…");
// Truncation resets SGR before the ellipsis; repaint it rather than exposing
// the host tool/terminal background at the edge of a clipped code cell.
const ellipsis = ansiColor(bg, true) + ansiColor(this.config.colors.fg) + "…";
const clipped = truncateToWidth(text, width, ellipsis);
return ansiColor(bg, true) + clipped + RESET + ansiColor(bg, true)
+ " ".repeat(Math.max(0, width - visibleWidth(clipped))) + RESET;
}
Expand Down Expand Up @@ -204,7 +207,8 @@ export class DiffView implements Component {
if (split) {
const leftWidth = Math.floor((width - 3) / 2);
const rightWidth = width - leftWidth - 3;
const border = ansiColor(this.config.colors.borderFg) + " │ " + RESET;
const border = ansiColor(this.config.colors.contextBg, true)
+ ansiColor(this.config.colors.borderFg) + " │ " + RESET;
output.push(this.fit(ansiColor(this.config.colors.lineNumberFg) + "Before", leftWidth) + border
+ this.fit(ansiColor(this.config.colors.lineNumberFg) + "After", rightWidth));
const pairs = pairRows(this.rows);
Expand Down
35 changes: 31 additions & 4 deletions pi-slate/tests/composer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ test("composer metadata separators match the surrounding metadata tone", () => {
assert.doesNotMatch(labels.right, /\[borderMuted\]/);
});

test("focused composer bottom edge drops ctx and uses readable separators", () => {
test("focused composer puts thinking after MCP on the top edge, leaving the model below", () => {
const editor = new ComposerEditor(
{ terminal: { rows: 24, columns: 100 }, requestRender() {} } as TUI,
{ borderColor: (text) => text } as EditorTheme,
Expand All @@ -187,14 +187,41 @@ test("focused composer bottom edge drops ctx and uses readable separators", () =
}),
);
const lines = editor.render(100);
assert.match(lines[0]!, /\[dim\]ctx 10%\[dim\] · \[dim\]2 mcp/);
assert.match(lines[0]!, /\[dim\]ctx 10%\[dim\] · \[dim\]2 mcp\[dim\] · \[dim\]medium/);
const bottom = lines.at(-1)!;
assert.doesNotMatch(bottom, /ctx 10%/);
assert.match(bottom, /\[muted\] · /);
assert.doesNotMatch(bottom, /ctx 10%|medium| · /);
assert.match(bottom, /sample/);
assert.doesNotMatch(bottom, /borderMuted/);
});

test("top-edge thinking respects minimal metadata, narrow widths, and missing levels", () => {
for (const footer of ["standard", "minimal"] as const) {
for (const thinking of [undefined, "medium"]) {
for (const width of [12, 20, 32, 40, 65, 79, 80, 100]) {
const editor = new ComposerEditor(
{ terminal: { rows: 24, columns: width }, requestRender() {} } as TUI,
{ borderColor: (text) => text } as EditorTheme,
new KeybindingsManager(),
() => ({
project: "slate", branch: null, model: { id: "sample" }, thinking, footer, theme,
dashboardVisible: false,
context: { tokens: "10%/100k tokens", resources: "ctx 10% · 1 mcp" },
}),
);
const lines = editor.render(width).map(stripVTControlCharacters);
assert.ok(lines.every(line => visibleWidth(line) <= width));
if (footer === "standard" && thinking) {
if (width >= 32) assert.match(lines[0]!, /ctx 10% · 1 mcp · medium/);
} else {
assert.doesNotMatch(lines[0]!, /medium/);
}
assert.doesNotMatch(lines[0]!, /undefined/);
assert.doesNotMatch(lines.at(-1)!, /medium/);
}
}
}
});

const labelTheme = {
fg: (name: string, text: string) => `[${name}]${text}`,
italic: (text: string) => `{i}${text}`,
Expand Down
30 changes: 30 additions & 0 deletions pi-slate/tests/diff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,36 @@ test("Shiki colors are composited with diff and character backgrounds", async (t
for (const token of added.tokens ?? []) if (token.color) assert.ok(output.includes(ansiColor(token.color)));
});

test("diff paints every visible cell, including split dividers and clipped ellipses", async (t) => {
const diff = view(t, "unchanged\n\n" + "old ".repeat(80) + "\nremoved\n", "unchanged\n\n" + "new ".repeat(80) + "\n");
const assertPainted = () => {
for (const width of [1, 2, 20, 80, 100, 101, 120]) {
for (const line of diff.render(width)) {
let background: string | undefined;
for (const match of line.matchAll(/\x1b\[([\d;]*)m|([^\x1b]+)/g)) {
if (match[1] !== undefined) {
const codes = match[1].split(";").map(Number);
for (let i = 0; i < codes.length; i++) {
if (codes[i] === 0 || codes[i] === 49) background = undefined;
if (codes[i] === 38 || codes[i] === 48) {
if (codes[i + 1] === 2) {
if (codes[i] === 48) background = codes.slice(i + 2, i + 5).join(",");
i += 4;
}
}
}
} else {
assert.ok(background, `unpainted ${JSON.stringify(match[2])} at width ${width}`);
}
}
}
}
};
assertPainted();
await diff.ready;
assertPainted();
});

test("edit is responsive to actual pane width; write is always unified", async (t) => {
const edit = view(t, "old\n", "new\n");
const write = view(t, "old\n", "new\n", { kind: "write" });
Expand Down
5 changes: 4 additions & 1 deletion pi-slate/tests/quiet-workbench.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,8 @@ test("quiet editor avoids duplicate identity/model and restores compact facts wi
}
}
assert.equal(editor.getText(), "Preserve this draft");
assert.match(editor.render(100).map(stripVTControlCharacters).join("\n"), /sample-model · medium/);
const focused = editor.render(100).map(stripVTControlCharacters);
assert.match(focused[0]!, /ctx 11% · 2 mcp · medium/);
assert.match(focused.at(-1)!, /sample-model/);
assert.doesNotMatch(focused.at(-1)!, /medium/);
});
Loading