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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ jobs:
continue-on-error: true
- name: Build and smoke-test npm artifact
run: npm run build --prefix "${{ matrix.package }}"
- if: matrix.package == 'pi-slate'
name: Real interactive compatibility with latest Pi and packed Slate
run: npm run test:compat:latest --prefix pi-slate
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: npm-${{ matrix.package }}
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/slate-compat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Slate upstream compatibility

on:
schedule:
- cron: '19 6 * * *'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: slate-upstream-compatibility
cancel-in-progress: false

jobs:
compatibility:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '24'
cache: npm
cache-dependency-path: pi-slate/package-lock.json
- run: npm ci --prefix pi-slate --ignore-scripts --no-audit --no-fund
- name: Real interactive baseline
run: npm run test:compat --prefix pi-slate
- name: Build and smoke-test artifact
run: npm run build --prefix pi-slate
- name: Real interactive latest Pi with packed Slate
run: npm run test:compat:latest --prefix pi-slate
6 changes: 3 additions & 3 deletions pi-ask/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pi-ask/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"typebox": "*"
},
"overrides": {
"undici": "8.12.2"
"undici": "8.12.4"
},
"devDependencies": {
"@earendil-works/pi-ai": "^1.4.0",
Expand Down
6 changes: 3 additions & 3 deletions pi-automode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pi-automode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"typebox": "*"
},
"overrides": {
"undici": "8.12.2"
"undici": "8.12.4"
},
"devDependencies": {
"@earendil-works/pi-coding-agent": "1.3.0",
Expand Down
17 changes: 17 additions & 0 deletions pi-slate/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

## Unreleased

### Changed

- MCP is now configuration-only: removed private Pi imports, version gates, and duplicated server validation. Pi's `/mcp` owns schema validity, authentication, and connection health. Slate still reports malformed/unreadable files and displays unknown enabled state neutrally.
- Optional UI capability loss, styling ownership conflicts, and adapter faults use quiet native fallbacks with `/slate diagnostics` details instead of startup warnings. Faulted adapters are isolated and retried on session replacement or reload; real settings and user-action failures remain visible.
- One chat-surface owner projects the visible messages, then paints grouped or native rows. Tool grouping no longer patches the host or negotiates with a second wrapper; faults retain the window in the same frame. Canonical messages and successor render/mouse owners remain intact.
- Removed rendered-text filtering of native new-session notices so genuine conversation content cannot be mistaken for a banner and discarded.

### Fixed

- Sidebar layout now uses root-identity leases without intercepting `setLayoutRoot`, including Pi's forwarding TUI proxy and successor layouts.
- Editor adapters unwind in reverse order and retained disposed image/selection wrappers delegate to native behavior. Paste reveals require undo support, handle blank-ending bodies, and cannot collapse stale ranges over edited prompt content.

### Development

- Added real regular/fullscreen startup, mount, render, session replacement, and shutdown checks for default/no/full surfaces against the pinned baseline and a freshly resolved latest Pi running the packed Slate artifact. Cases isolate HOME/settings and prohibit execution-time networking.
- Added a daily upstream compatibility canary, latest-host PR gating, architecture guards against private/computed host imports, and production-wiring tests for quiet fallbacks, fault isolation, and secret-free diagnostics.

## 0.2.0

Four Catppuccin flavors behind a single /theme picker, and edits and writes that keep their diffs visible in collapsed tool cards.
Expand Down
29 changes: 29 additions & 0 deletions pi-slate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ The sidebar keeps session facts visible without sending them to a model: a live

![Slate session dashboard](https://raw.githubusercontent.com/GaganSD/coding-agent-toolkit/1e84be304270c8d859e04e7005dc3cf674fbd574/pi-slate/assets/slate-session.png)

MCP indicators show **requested configuration**, not schema validity or connection health. Slate reads enabled flags without importing Pi's MCP internals, validating transports/authentication, running credential commands, or connecting to servers. Unreadable/malformed files remain visible; unknown enabled state is a neutral `—`. Use `/mcp` for authoritative validation and live status.

### Background Tasks & Subagents

Other extensions can publish detached terminals or jobs to the sidebar's task shelf through the `pi:background-tasks` event on Pi's event bus; snapshots are display-only and cleared with their session. With `editor` and a fullscreen `sidebar` selected, Slate can also lend the sidebar's chat slot to a pi-subagents worker conversation — a UI-only handshake with no new model tools — and restores the same surfaces on return. Without a compatible slot, pi-subagents uses its own fallback.
Expand Down Expand Up @@ -123,6 +125,7 @@ These work when `editor` is selected. No terminal configuration.
| Working indicator | `/slate progress [waves\|dna-helix\|snake\|cylon-scanner]` | Animate the composer's working-status slot. `waves` is the default. |
| Sidebar width | `/slate width [default\|narrow\|medium\|wide\|<percent>]` | Choose the sidebar width and return to standard mode. `default` is 20%. |
| Session dashboard | `/slate session [mcp\|skills\|commands\|tasks\|image]` | Inspect facts, expand/collapse resources, insert commands, or manage the image shelf. |
| Diagnostics | `/slate diagnostics` | Inspect optional feature availability, native fallbacks, and next steps without changing preferences or adding model context. |
| 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. When the dashboard is hidden, thinking follows context/MCP on the top edge and the model stays below. Not footer-slot ownership. |
Expand All @@ -145,6 +148,32 @@ Changes are saved and require `/reload`; appearance settings stay live. A fresh
| `tool-cards` | Highlighted edit/write cards and grouped reads |
| `transcript` | Visible-message window |

## Compatibility and diagnostics

Slate treats an unavailable optional presentation feature as a **fallback**, not a server or application failure. Regular terminal mode, a missing renderer hook, or a later extension taking a layout slot no longer generates a startup warning. Real settings/file problems and failed copy/save/open actions remain visible.

Run `/slate diagnostics` for feature-level `active` / `fallback` details. Adapter exceptions are isolated and quarantined for that session; `/reload` retries them. Reports retain fixed, actionable reasons—not exception text, credentials, raw configuration, or conversation contents. Fallbacks do not enable fullscreen, rewrite preferences, or take back another extension's slots.

Compatibility boundaries:

- Production imports only public Pi package entrypoints; an architecture test rejects private/computed host imports.
- MCP is a read-only configuration projection. Pi owns server validation, authentication, and connections.
- Sidebar layout uses root-identity leases, not intercepted setters or function-identity assumptions.
- One chat-surface owner copies the visible message list and paints grouped or native rows without removing host components. Active work stays visible; grouping faults retain the window in the same frame. Unknown render/mouse owners are preserved, and cleanup is not dependent on wrapper order.
- Editor adapters unwind in reverse installation order. Retained disposed wrappers become native delegates. Paste reveals require undo support and never collapse a buffer edited since the reveal.
- Native notices and user/assistant messages are never discarded by matching their rendered text.

Development checks retain the locked baseline host and separately exercise the **packed artifact with freshly resolved latest stable Pi**:

```sh
npm run test:unit
npm run test:integration # includes real baseline-host startup/rendering
npm run build
npm run test:compat:latest # temporary latest host + packed Slate consumer
```

The real-host probe covers regular/fullscreen mode, default/no/full surfaces, initialization, rendering, session replacement, and shutdown. Each case runs in a fresh process with isolated settings/HOME, no provider credentials, and execution-time networking blocked. The latest-host canary runs in Slate's PR validation and a dedicated daily workflow. Optional UI tree/editor adapters still require contract coverage; these checks detect upstream drift rather than promising compatibility with every future host.

## License

[MIT](LICENSE) © [Gagan Devagiri](https://github.com/GaganSD)
108 changes: 108 additions & 0 deletions pi-slate/extensions/pi-slate/chat-surface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Container, ScrollView, isViewportTUI, type Component, type TUI } from "@earendil-works/pi-tui";
import type { ToolGroups } from "./tool-groups.ts";

export function childComponents(component: Component): Component[] {
const stack = component as { entries?: Array<{ component?: Component }> };
if (Array.isArray(stack.entries) && stack.entries.some((entry) => entry?.component)) {
return stack.entries.flatMap((entry) => (entry.component ? [entry.component] : []));
}
const container = component as Container;
return Array.isArray(container.children) ? container.children : [];
}

export function findPrimaryScrollView(root: Component): ScrollView | undefined {
const seen = new Set<Component>();
const queue = [root];
while (queue.length > 0) {
const node = queue.shift();
if (!node || seen.has(node)) continue;
seen.add(node);
if (node instanceof ScrollView && node.primary) return node;
queue.push(...childComponents(node));
}
return undefined;
}

export function findChatContainer(root: Component | undefined): Container | undefined {
if (!root) return undefined;
const scroll = findPrimaryScrollView(root);
if (!scroll) return undefined;
const content = childComponents(scroll)[0];
if (!content || typeof (content as Container).addChild !== "function") return undefined;
const nested = childComponents(content).filter((child) => (
child instanceof Container && !(child instanceof ScrollView)
));
const chat = nested.at(-1) ?? content;
return typeof (chat as Container).addChild === "function" ? chat as Container : undefined;
}

export function layoutRootOf(tui: TUI | undefined): Component | undefined {
if (!tui || !isViewportTUI(tui)) return undefined;
return (tui as TUI & { layoutRoot?: Component }).layoutRoot;
}

export type ChatSurfaceOptions = {
limit?: number;
groups?: ToolGroups;
active?: () => boolean;
onGroupingFault?: () => void;
};

/** One owner for the display pipeline: project first, then paint grouped or native rows. */
export class ChatSurface {
readonly container: Container;
private options: ChatSurfaceOptions;
private readonly originalRender: Container["render"];
private readonly originalMouse: Container["handleMouse"];
private readonly installedRender: Container["render"];
private readonly installedMouse: Container["handleMouse"];
private readonly display = new Container();
private paintedGroups: ToolGroups | undefined;
private disposed = false;

constructor(container: Container, options: ChatSurfaceOptions) {
this.container = container; this.options = { ...options };
if (container.render !== Container.prototype.render || container.handleMouse !== Container.prototype.handleMouse) {
throw new Error("Unrecognized chat owner; native transcript retained");
}
this.originalRender = container.render;
this.originalMouse = container.handleMouse;
this.installedRender = width => {
if (this.disposed) return this.originalRender.call(container, width);
const limit = this.options.limit ?? Number.POSITIVE_INFINITY;
const start = !this.options.active?.() && Number.isFinite(limit) ? -Math.max(1, Math.floor(limit)) : 0;
this.display.children = container.children.slice(start);
this.paintedGroups = undefined;
if (this.options.groups) {
try {
const rows = this.options.groups.render(this.display.children, width);
this.paintedGroups = this.options.groups;
return rows;
} catch {
this.options.groups = undefined;
this.options.onGroupingFault?.();
}
}
return this.display.render(width);
Comment thread
GaganSD marked this conversation as resolved.
Resolved
};
this.installedMouse = event => {
if (this.disposed) return this.originalMouse.call(container, event);
return this.paintedGroups ? this.paintedGroups.handleMouse(event) : this.display.handleMouse(event);
};
try {
container.render = this.installedRender;
container.handleMouse = this.installedMouse;
} catch (error) { this.dispose(); throw error; }
}

update(options: ChatSurfaceOptions): void { if (!this.disposed) this.options = { ...options }; }
dispose(): void {
if (this.disposed) return;
this.disposed = true;
if (this.container.render === this.installedRender) this.container.render = this.originalRender;
if (this.container.handleMouse === this.installedMouse) this.container.handleMouse = this.originalMouse;
this.display.children = [];
this.paintedGroups = undefined;
this.options = {};
}
}
Loading
Loading