pi-stereo-view 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First release.
6
+
7
+ - Separate Conversation and Activity while keeping Pi's native message and tool renderers.
8
+ - Arrange panels in columns or rows, swap them, resize them, and zoom either panel.
9
+ - Search each panel with literal, Boolean and regular-expression queries.
10
+ - Show optional timestamps and save layout preferences between sessions.
11
+ - Keep the input dock full width and fall back to combined history in small terminals.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexander Shpuntenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,142 @@
1
- # Temporary Holding Version
1
+ <h1 align="center">
2
+ <img src="https://raw.githubusercontent.com/alexshpunt/pi-stereo-view/main/assets/logo-icon.png" alt="pi-stereo-view" width="480"><br>
3
+ pi-stereo-view
4
+ </h1>
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ <p align="center">Keep Pi conversation separate from tool output and agent activity.</p>
7
+
8
+ <p align="center">
9
+ <a href="https://www.npmjs.com/package/pi-stereo-view"><img src="https://img.shields.io/npm/v/pi-stereo-view" alt="npm version"></a>
10
+ <a href="https://www.npmjs.com/package/pi-stereo-view"><img src="https://img.shields.io/npm/dm/pi-stereo-view" alt="npm downloads"></a>
11
+ <a href="https://github.com/alexshpunt/pi-stereo-view/actions/workflows/ci.yml"><img src="https://github.com/alexshpunt/pi-stereo-view/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI status"></a>
12
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
13
+ </p>
14
+
15
+ You come back to a chat and can't remember what you were talking about?
16
+
17
+ **Split tool output from the agent's replies with pi-stereo-view.**
18
+
19
+ The chat becomes two zones: your conversation with the agent, and the agent's work where it calls tools.
20
+
21
+ | Zone | What's in it |
22
+ | --- | --- |
23
+ | **Conversation** | Your messages, the LLM's replies, compaction summaries and compaction notices. |
24
+ | **Activity** | Tool calls and results, thinking, and other messages generated by Pi. |
25
+
26
+ Messages and tools keep their familiar appearance. The input stays full width below both panels.
27
+
28
+ ## See it in action
29
+
30
+ ![Switching layouts, swapping panels and resizing history](https://raw.githubusercontent.com/alexshpunt/pi-stereo-view/main/assets/demo.gif)
31
+
32
+ [Watch the full demo](https://github.com/alexshpunt/pi-stereo-view/releases/download/v0.1.0/demo.mp4).
33
+
34
+ ### Columns
35
+
36
+ Conversation beside Activity, with a full-width input below both panels.
37
+
38
+ ![Columns layout](https://raw.githubusercontent.com/alexshpunt/pi-stereo-view/main/assets/columns.png)
39
+
40
+ ### Rows
41
+
42
+ Conversation above Activity. Swap the panels whenever you need a different order.
43
+
44
+ ![Rows layout](https://raw.githubusercontent.com/alexshpunt/pi-stereo-view/main/assets/rows.png)
45
+
46
+ ## Install
47
+
48
+ ```sh
49
+ pi install npm:pi-stereo-view
50
+ ```
51
+
52
+ Split history needs Pi's **fullscreen** mode. Choose it in `/settings`, or start Pi with:
53
+
54
+ ```sh
55
+ pi --tui-mode fullscreen
56
+ ```
57
+
58
+ ## Use it
59
+
60
+ Click the toolbar and the buttons in each panel's header:
61
+
62
+ | Button | Action |
63
+ | --- | --- |
64
+ | `[ ▣ ]` / `[ ◫ ]` | Turn split history off / on. |
65
+ | `[ ▤ ]` / `[ ▥ ]` | Switch between rows and columns. |
66
+ | `[ ↕ ]` / `[ ↔ ]` | Swap the panels. |
67
+ | `[ ↗ ]` / `[ ↙ ]` | Zoom a panel / restore the split. |
68
+ | `[ ≡ ]` | Open settings to show or hide timestamps. |
69
+ | `[ ⌕ ]` | Search this panel. |
70
+
71
+ **Resize:** drag the double line between the panels — `═` in rows, `║` in columns.
72
+
73
+ New installs start in Columns: Conversation gets 20% of the width on the left, and Activity gets 80% on the right. Drag the divider or use the size shortcuts to change this.
74
+
75
+ In Rows, Conversation grows with its messages up to half the available history area, then scrolls. Dragging the divider or setting a height changes that limit. Choose **Conversation height: auto** in `≡`, or run `/stereo-view height auto`, to restore automatic sizing.
76
+
77
+ Panels fit short content and scroll when history fills up. Each panel has its own scrolling, text selection and copy.
78
+
79
+ Swap the panels whenever you want. Zoom a panel to give it the whole history area; the input stays visible. Restore it to return to your split. Turning split off returns to one combined history; the toolbar stays available to turn it back on.
80
+
81
+ ## Search history
82
+
83
+ Press **Ctrl+F** to search the last clicked or scrolled panel. A dot in the header marks the active panel; Conversation is active initially. The `⌕` button always searches its own panel.
84
+
85
+ Type a word or phrase to find it without case sensitivity. Combine terms with `AND`, `OR`, `NOT` and parentheses. Put phrases in quotes to keep them together, or start with `regex:` for a regular expression.
86
+
87
+ | Example | Finds |
88
+ | --- | --- |
89
+ | `worker ready` | The literal phrase “worker ready”. |
90
+ | `worker AND ready NOT failed` | Lines containing both “worker” and “ready”, but not “failed”. |
91
+ | `(worker OR server) AND "ready now"` | Lines containing either term and the phrase “ready now”. |
92
+ | `regex:worker_[0-9]+` | Names such as `worker_12`. |
93
+
94
+ Press **Enter** or `[↓]` for the next match, and **Shift+Enter** for the previous one. The counter shows your position and the total matches. Ctrl+F returns to the search field without clearing your query.
95
+
96
+ Search covers the whole current conversation branch, including off-screen messages, hidden thinking and collapsed tool output. It does not search other branches, sessions, attachments or files. Russian and English queries work.
97
+
98
+ A match is revealed in its own panel without moving the other one. If matching text cannot be shown inside a tool result, a temporary **Search match:** fragment shows it. Copying keeps the original text, without highlight colours.
99
+
100
+ **Regex search requires ripgrep (`rg`) to be installed.** Invalid queries and search errors appear in the search field instead of showing incomplete results.
101
+
102
+ Press **Escape** or `[×]` to close search and restore your previous scroll position. Your message draft stays unchanged. Close other dialogs before searching. Search is available in split fullscreen history; combined history and regular mode keep Pi's usual keyboard behaviour.
103
+
104
+ ## Timestamps
105
+
106
+ Conversation and Activity can show a dim local `HH:mm:ss` clock beside messages and tool calls. New tool calls show their start time.
107
+
108
+ Older tool calls may show `~HH:mm:ss` for an approximate time. `--:--:--` means the time is unknown.
109
+
110
+ Use `≡` or `/stereo-view settings` to hide or show clocks. This setting survives restart. Clocks appear in split and zoomed panels, not in combined history. They are display-only and are not added to the conversation sent to the model.
111
+
112
+ ## Commands and shortcuts
113
+
114
+ The same controls are available without the mouse:
115
+
116
+ | Action | Command |
117
+ | --- | --- |
118
+ | Split off / on | `/stereo-view` |
119
+ | Swap panels | `/stereo-view swap` |
120
+ | Rows / columns | `/stereo-view layout rows` / `/stereo-view layout columns` |
121
+ | Conversation height / width | `/stereo-view height 12` / `/stereo-view width 40` |
122
+ | Automatic Conversation height | `/stereo-view height auto` |
123
+ | Scroll a panel | `/stereo-view scroll chat -10` / `/stereo-view scroll activity -10` |
124
+ | Zoom / restore Conversation | `/stereo-view zoom chat` |
125
+ | Zoom / restore Activity | `/stereo-view zoom activity` |
126
+ | Timestamp settings | `/stereo-view settings` |
127
+ | Button legend | `/stereo-view help` |
128
+
129
+ `Ctrl+Alt+T` toggles split, `Ctrl+Alt+S` swaps panels, and `Ctrl+Alt+Left/Right` resizes Conversation along the current split direction.
130
+
131
+ ## Settings and limits
132
+
133
+ Your layout, panel order, sizes, zoom, split on/off and timestamp settings are saved automatically and restored next time.
134
+
135
+ - Tested with Pi 1.1.0. Requires Node.js 22.19 or newer.
136
+ - Regular mode shows one combined history.
137
+ - If the terminal is too small, history temporarily combines without forgetting your split.
138
+ - If your Pi version cannot display the split, normal history stays visible and a warning explains the problem.
139
+
140
+ ## License
141
+
142
+ [MIT](LICENSE) · [Developer guide](https://github.com/alexshpunt/pi-stereo-view/blob/main/development.md)
package/package.json CHANGED
@@ -1,6 +1,66 @@
1
1
  {
2
2
  "name": "pi-stereo-view",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Keep Pi conversation separate from tool output and agent activity.",
6
+ "license": "MIT",
7
+ "author": "Alexander Shpuntenko",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/alexshpunt/pi-stereo-view.git"
11
+ },
12
+ "homepage": "https://github.com/alexshpunt/pi-stereo-view#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/alexshpunt/pi-stereo-view/issues"
15
+ },
16
+ "keywords": [
17
+ "pi-package",
18
+ "pi",
19
+ "coding-agent",
20
+ "transcript",
21
+ "tui",
22
+ "split-pane",
23
+ "pi-extension",
24
+ "pi-coding-agent",
25
+ "terminal-ui"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22.19.0"
29
+ },
30
+ "files": [
31
+ "src",
32
+ "README.md",
33
+ "CHANGELOG.md",
34
+ "LICENSE"
35
+ ],
36
+ "pi": {
37
+ "extensions": [
38
+ "./src/index.ts"
39
+ ],
40
+ "image": "https://raw.githubusercontent.com/alexshpunt/pi-stereo-view/main/assets/logo-wordmark.png",
41
+ "video": "https://github.com/alexshpunt/pi-stereo-view/releases/download/v0.1.0/demo.mp4"
42
+ },
43
+ "scripts": {
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run",
46
+ "profile": "vitest run --config vitest.profile.config.ts",
47
+ "check": "npm run typecheck && npm test",
48
+ "check:package": "vitest run test/package.integration.test.ts"
49
+ },
50
+ "peerDependencies": {
51
+ "@earendil-works/pi-coding-agent": "*",
52
+ "@earendil-works/pi-tui": "*"
53
+ },
54
+ "devDependencies": {
55
+ "@earendil-works/pi-coding-agent": "1.1.0",
56
+ "@earendil-works/pi-tui": "1.1.0",
57
+ "@types/node": "^24.0.4",
58
+ "pi-coding-agent-test": "0.1.3",
59
+ "typescript": "5.9.3",
60
+ "vitest": "4.1.11"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public",
64
+ "registry": "https://registry.npmjs.org"
65
+ }
66
+ }
package/src/index.ts ADDED
@@ -0,0 +1,182 @@
1
+ import path from "node:path";
2
+ import { getAgentDir, getSettingsListTheme, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { Box, Container, isKeyRelease, matchesKey, SettingsList, Text, type TUI } from "@earendil-works/pi-tui";
4
+ import { TranscriptLayout } from "./layout.js";
5
+ import { TOOL_START_ENTRY, TranscriptTimestamps } from "./timestamps.js";
6
+ import { defaults, loadPreferences, savePreferences, type Preferences } from "./preferences.js";
7
+
8
+ /** Split native fullscreen history without changing message content or the input dock. */
9
+ export default function transcript(pi: ExtensionAPI): void {
10
+ let layout: TranscriptLayout | undefined;
11
+ let host: TUI | undefined;
12
+ let timestamps: TranscriptTimestamps | undefined;
13
+ let unsubscribeInput: (() => void) | undefined;
14
+ let writes = Promise.resolve();
15
+ let preferences: Preferences = { ...defaults };
16
+ const file = path.join(getAgentDir(), "transcript.json");
17
+
18
+ let settingsOpen = false;
19
+ const closeSearch = (): void => { layout?.closeSearch(false); };
20
+
21
+ const openSettings = async (ctx: ExtensionContext): Promise<void> => {
22
+ if (settingsOpen) return;
23
+ settingsOpen = true;
24
+ closeSearch();
25
+ try {
26
+ await ctx.ui.custom<void>((tui, _theme, _keys, done) => {
27
+ const list = new SettingsList([{
28
+ id: "timestamps", label: "Show timestamps",
29
+ description: "Hide clock rows without changing messages or recorded tool starts.",
30
+ currentValue: preferences.showTimestamps ? "on" : "off", values: ["on", "off"],
31
+ }, {
32
+ id: "height", label: "Conversation height",
33
+ description: "Auto grows to half the history area. Drag sets a manual maximum.",
34
+ currentValue: preferences.chatHeight === null ? "auto" : "manual", values: ["auto", "manual"],
35
+ }], 3, getSettingsListTheme(), (id, value) => {
36
+ if (id === "timestamps") layout?.setTimestamps(value === "on");
37
+ else if (id === "height") layout?.setAutomaticHeight(value === "auto");
38
+ tui.requestRender();
39
+ }, () => done());
40
+ const content = new Container();
41
+ content.addChild({
42
+ render: () => [ctx.ui.theme.fg("accent", ctx.ui.theme.bold("pi-stereo-view settings")), ""],
43
+ invalidate() {},
44
+ });
45
+ content.addChild(list);
46
+ const box = new Box(1, 1, text => ctx.ui.theme.bg("customMessageBg", text));
47
+ box.addChild(content);
48
+ return {
49
+ render: (width: number) => box.render(width),
50
+ invalidate: () => box.invalidate(),
51
+ handleMouse: (event) => box.handleMouse(event),
52
+ handleInput: (data: string) => {
53
+ list.handleInput(data);
54
+ tui.requestRender();
55
+ },
56
+ };
57
+ }, { overlay: true, overlayOptions: { width: 60, margin: 1 } });
58
+ } finally {
59
+ settingsOpen = false;
60
+ }
61
+ await writes;
62
+ };
63
+
64
+ const ensureLayout = (): void => {
65
+ if (host) layout?.attach(host);
66
+ };
67
+ const persist = (ctx: ExtensionContext): void => {
68
+ const snapshot = { ...preferences };
69
+ writes = writes.then(() => savePreferences(file, snapshot)).catch(error => {
70
+ ctx.ui.notify("Could not save pi-stereo-view preferences: " + String(error), "error");
71
+ });
72
+ };
73
+
74
+ pi.on("session_start", async (_event, ctx) => {
75
+ if (ctx.mode !== "tui") return;
76
+ closeSearch();
77
+ layout?.dispose();
78
+ unsubscribeInput?.();
79
+ timestamps = new TranscriptTimestamps(ctx.sessionManager);
80
+ try {
81
+ preferences = await loadPreferences(file);
82
+ } catch (error) {
83
+ ctx.ui.notify("Could not read pi-stereo-view preferences; using defaults: " + String(error), "warning");
84
+ preferences = { ...defaults };
85
+ }
86
+ let finish: () => void = () => {};
87
+ await ctx.ui.custom<void>((tui, _theme, _keys, done) => {
88
+ host = tui;
89
+ finish = done;
90
+ layout = new TranscriptLayout(preferences, () => ctx.ui.theme,
91
+ message => ctx.ui.notify(message, "warning"), () => persist(ctx), timestamps!,
92
+ () => { void openSettings(ctx).catch(error => ctx.ui.notify(String(error), "error")); });
93
+ ensureLayout();
94
+ return new Text("", 0, 0);
95
+ }, {
96
+ overlay: true,
97
+ overlayOptions: { visible: () => false, nonCapturing: true },
98
+ onHandle: () => finish(),
99
+ });
100
+ unsubscribeInput = ctx.ui.onTerminalInput(data => {
101
+ ensureLayout();
102
+ const pane = layout?.searchPane();
103
+ if (pane && !settingsOpen && matchesKey(data, "ctrl+f")) {
104
+ if (isKeyRelease(data) || layout?.openSearch(pane)) return { consume: true };
105
+ }
106
+ return undefined;
107
+ });
108
+ });
109
+ pi.on("message_start", event => {
110
+ timestamps?.observe(event.message);
111
+ layout?.refreshSearch();
112
+ });
113
+ pi.on("message_update", event => {
114
+ timestamps?.observe(event.message);
115
+ layout?.refreshSearch();
116
+ ensureLayout();
117
+ });
118
+ pi.on("message_end", event => {
119
+ timestamps?.observe(event.message, true);
120
+ layout?.refreshSearch();
121
+ });
122
+ pi.on("tool_execution_update", () => layout?.refreshSearch());
123
+ pi.on("tool_execution_end", () => layout?.refreshSearch());
124
+ pi.on("tool_execution_start", event => {
125
+ if (!timestamps || event.parentToolCallId) return;
126
+ pi.appendEntry(TOOL_START_ENTRY, { toolCallId: event.toolCallId, time: Date.now() });
127
+ host?.requestRender();
128
+ });
129
+ pi.on("session_tree", () => { closeSearch(); timestamps?.reset(); });
130
+ pi.on("session_compact", () => { closeSearch(); timestamps?.reset(); });
131
+ pi.on("session_shutdown", async () => {
132
+ closeSearch();
133
+ unsubscribeInput?.();
134
+ unsubscribeInput = undefined;
135
+ layout?.dispose();
136
+ layout = undefined;
137
+ timestamps = undefined;
138
+ host = undefined;
139
+ await writes;
140
+ });
141
+
142
+ const toggle = (): void => {
143
+ layout?.toggle();
144
+ ensureLayout();
145
+ };
146
+ pi.registerCommand("stereo-view", {
147
+ description: "History: settings, toggle, swap, layout <columns|rows>, zoom <chat|activity>, width, height, scroll",
148
+ handler: async (args, ctx) => {
149
+ if (ctx.mode !== "tui" || !layout) {
150
+ ctx.ui.notify("pi-stereo-view layout is available only in interactive Pi.", "warning");
151
+ return;
152
+ }
153
+ const [action = "toggle", target, amount] = args.trim().split(/\s+/);
154
+ if (action === "toggle" || action === "") toggle();
155
+ else if (action === "settings") await openSettings(ctx);
156
+ else if (action === "help") {
157
+ ctx.ui.notify("pi-stereo-view icons: ▣ combined, ◫ split, ▤ rows, ▥ columns, ↕/↔ swap, ↗ expand, ↙ restore, ⌕ search, ≡ settings. Ctrl+F searches the active panel. Drag the ═ or ║ divider to resize.", "info");
158
+ } else if (action === "swap") layout.swap();
159
+ else if (action === "width" && /^\d+$/.test(target ?? "")) layout.setWidth(Number(target));
160
+ else if (action === "height" && target === "auto") layout.setHeight(null);
161
+ else if (action === "height" && /^\d+$/.test(target ?? "")) layout.setHeight(Number(target));
162
+ else if (action === "layout" && (target === "columns" || target === "rows")) layout.setLayout(target);
163
+ else if (action === "zoom" && (target === "chat" || target === "activity")) layout.maximize(target);
164
+ else if (action === "scroll" && (target === "chat" || target === "activity") && /^-?\d+$/.test(amount ?? "")) {
165
+ layout.scroll(target, Number(amount));
166
+ } else {
167
+ ctx.ui.notify("Use /stereo-view [help|settings|toggle|swap|layout <columns|rows>|zoom <chat|activity>|width <columns>|height <rows|auto>|scroll <chat|activity> <lines>]", "warning");
168
+ return;
169
+ }
170
+ ensureLayout();
171
+ await writes;
172
+ },
173
+ });
174
+ pi.registerShortcut("ctrl+alt+t", { description: "Toggle split history", handler: async () => toggle() });
175
+ pi.registerShortcut("ctrl+alt+s", { description: "Swap stereo-view panels", handler: async () => layout?.swap() });
176
+ pi.registerShortcut("ctrl+alt+left", { description: "Shrink conversation along the split axis", handler: async () => {
177
+ layout?.resize(-1);
178
+ } });
179
+ pi.registerShortcut("ctrl+alt+right", { description: "Grow conversation along the split axis", handler: async () => {
180
+ layout?.resize(1);
181
+ } });
182
+ }