pi-demo-mode 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/LICENSE +21 -0
- package/README.md +53 -0
- package/extensions/demo-mode.ts +233 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dutifuldev
|
|
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
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# pi-demo-mode
|
|
2
|
+
|
|
3
|
+
pi-demo-mode is a [Pi](https://github.com/earendil-works/pi) extension package
|
|
4
|
+
that puts Pi into a self-driving demo mode. It sends a story prompt when the
|
|
5
|
+
session starts, keeps asking the model to continue after every turn (compacting
|
|
6
|
+
the session when the context fills up), and strips the TUI down to the chat
|
|
7
|
+
plus a single status line — no input bar, no directory line, no context meter.
|
|
8
|
+
The result is a terminal that endlessly writes a story on its own, which is
|
|
9
|
+
what you want for screen recordings and demo walls.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Into a plain Pi setup:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi install github:osolmaz/pi-demo-mode
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or as an npm git dependency of a Pi launcher, pointing Pi at
|
|
20
|
+
`extensions/demo-mode.ts` inside the installed package.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
The extension is inert until it is enabled, so it can stay installed for
|
|
25
|
+
interactive sessions. Configuration is entirely through environment
|
|
26
|
+
variables:
|
|
27
|
+
|
|
28
|
+
| Variable | Meaning |
|
|
29
|
+
| ------------------------------ | --------------------------------------------------------------- |
|
|
30
|
+
| `PI_DEMO_MODE=1` | enable demo mode |
|
|
31
|
+
| `PI_DEMO_INITIAL_PROMPT` | first prompt text |
|
|
32
|
+
| `PI_DEMO_INITIAL_PROMPT_FILE` | UTF-8 file with the first prompt (wins over the inline text) |
|
|
33
|
+
| `PI_DEMO_FOLLOWUP_PROMPT` | repeated prompt after each turn |
|
|
34
|
+
| `PI_DEMO_FOLLOWUP_PROMPT_FILE` | UTF-8 file with the repeated prompt (wins over the inline text) |
|
|
35
|
+
|
|
36
|
+
Without prompt overrides, a built-in never-ending sci-fi story prompt is
|
|
37
|
+
used. Run Pi with tools disabled so nothing interrupts the story:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
PI_DEMO_MODE=1 pi --no-tools --no-approve
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The demo stops on its own when a turn is aborted or errors, and treats any
|
|
44
|
+
message you type as a live director note for the story.
|
|
45
|
+
|
|
46
|
+
## Used by
|
|
47
|
+
|
|
48
|
+
- [localpi](https://github.com/osolmaz/localpi) (`localpi --demo`)
|
|
49
|
+
- [diffusionpi](https://github.com/osolmaz/diffusionpi) (`diffusionpi demo`)
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Self-driving demo mode for Pi: sends a story prompt on startup, keeps
|
|
2
|
+
// asking the model to continue after every turn, and strips the TUI down to
|
|
3
|
+
// the chat plus a single status line. Everything is gated on PI_DEMO_MODE=1
|
|
4
|
+
// so the extension can stay installed in interactive sessions without doing
|
|
5
|
+
// anything.
|
|
6
|
+
//
|
|
7
|
+
// Configuration (all optional):
|
|
8
|
+
// PI_DEMO_MODE=1 enable demo mode
|
|
9
|
+
// PI_DEMO_INITIAL_PROMPT first prompt text
|
|
10
|
+
// PI_DEMO_INITIAL_PROMPT_FILE UTF-8 file with the first prompt
|
|
11
|
+
// PI_DEMO_FOLLOWUP_PROMPT repeated prompt text after each turn
|
|
12
|
+
// PI_DEMO_FOLLOWUP_PROMPT_FILE UTF-8 file with the repeated prompt
|
|
13
|
+
//
|
|
14
|
+
// A *_FILE variable wins over its inline counterpart; without either, the
|
|
15
|
+
// built-in never-ending sci-fi story prompts are used.
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
|
|
18
|
+
import type { ExtensionAPI, ExtensionContext, TurnEndEvent } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
|
|
20
|
+
const demoEnabled = process.env["PI_DEMO_MODE"] === "1";
|
|
21
|
+
|
|
22
|
+
const fallbackInitialPrompt =
|
|
23
|
+
"You are narrating a never-ending sci-fi adventure. Continue in short paragraphs. Whenever the user sends a message, treat it as a live director note and incorporate it immediately. Never end the story.";
|
|
24
|
+
const fallbackFollowupPrompt = "Continue. Try to write as long as possible.";
|
|
25
|
+
|
|
26
|
+
const compactAtContextPercent = 70;
|
|
27
|
+
const demoCompactionInstructions = [
|
|
28
|
+
"Preserve the demo narrative state, named entities, current setting,",
|
|
29
|
+
"unresolved plot threads, and latest user direction.",
|
|
30
|
+
"Keep the summary concise so the story can continue after compaction."
|
|
31
|
+
].join(" ");
|
|
32
|
+
|
|
33
|
+
async function promptText(inlineVar: string, fileVar: string, fallback: string): Promise<string> {
|
|
34
|
+
const file = process.env[fileVar];
|
|
35
|
+
if (file !== undefined && file !== "") {
|
|
36
|
+
try {
|
|
37
|
+
const text = (await readFile(file, "utf8")).trim();
|
|
38
|
+
if (text !== "") {
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Fall through to the inline value or fallback below.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const inline = process.env[inlineVar];
|
|
46
|
+
return inline === undefined || inline === "" ? fallback : inline;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Demo chrome: recordings only need the chat and the perf status line, so the
|
|
50
|
+
// footer is replaced with a single dim line (extension statuses + model name)
|
|
51
|
+
// and the input editor is hidden. The built-in footer's cwd and context lines
|
|
52
|
+
// are intentionally dropped.
|
|
53
|
+
type DemoFooterTheme = { fg(color: string, text: string): string };
|
|
54
|
+
type DemoFooterData = { getExtensionStatuses(): ReadonlyMap<string, string> };
|
|
55
|
+
type DemoFooterComponent = { render(width: number): string[] };
|
|
56
|
+
type DemoChromeUi = {
|
|
57
|
+
setFooter(
|
|
58
|
+
factory:
|
|
59
|
+
| ((tui: unknown, theme: DemoFooterTheme, footerData: DemoFooterData) => DemoFooterComponent)
|
|
60
|
+
| undefined
|
|
61
|
+
): void;
|
|
62
|
+
setEditorComponent(
|
|
63
|
+
factory: ((tui: unknown, theme: unknown, keybindings: unknown) => unknown) | undefined
|
|
64
|
+
): void;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const ansiPattern = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*m", "gu");
|
|
68
|
+
|
|
69
|
+
function visibleLength(text: string): number {
|
|
70
|
+
return text.replace(ansiPattern, "").length;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function demoFooterLines(left: string, right: string, width: number): string[] {
|
|
74
|
+
const leftWidth = visibleLength(left);
|
|
75
|
+
const rightWidth = visibleLength(right);
|
|
76
|
+
if (left === "") {
|
|
77
|
+
return [" ".repeat(Math.max(0, width - rightWidth)) + right];
|
|
78
|
+
}
|
|
79
|
+
if (leftWidth + 2 + rightWidth <= width) {
|
|
80
|
+
return [left + " ".repeat(width - leftWidth - rightWidth) + right];
|
|
81
|
+
}
|
|
82
|
+
return [left, " ".repeat(Math.max(0, width - rightWidth)) + right];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function applyDemoChrome(ctx: ExtensionContext): void {
|
|
86
|
+
const ui = ctx.ui as unknown as Partial<DemoChromeUi>;
|
|
87
|
+
if (typeof ui.setFooter !== "function" || typeof ui.setEditorComponent !== "function") {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const modelName = ctx.model?.id ?? "";
|
|
91
|
+
ui.setFooter((_tui, theme, footerData) => ({
|
|
92
|
+
render(width: number): string[] {
|
|
93
|
+
const left = Array.from(footerData.getExtensionStatuses().entries())
|
|
94
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
95
|
+
.map(([, text]) => text.replace(/[\r\n\t]+/gu, " ").trim())
|
|
96
|
+
.filter((text) => text !== "")
|
|
97
|
+
.join(" ");
|
|
98
|
+
return demoFooterLines(left, theme.fg("dim", modelName), width);
|
|
99
|
+
}
|
|
100
|
+
}));
|
|
101
|
+
void hideEditor(ui as DemoChromeUi);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function hideEditor(ui: DemoChromeUi): Promise<void> {
|
|
105
|
+
try {
|
|
106
|
+
const pkg = (await import("@earendil-works/pi-coding-agent")) as unknown as {
|
|
107
|
+
CustomEditor: new (
|
|
108
|
+
tui: unknown,
|
|
109
|
+
theme: unknown,
|
|
110
|
+
keybindings: unknown
|
|
111
|
+
) => { render(width: number): string[] };
|
|
112
|
+
};
|
|
113
|
+
class HiddenEditor extends pkg.CustomEditor {
|
|
114
|
+
override render(): string[] {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
ui.setEditorComponent((tui, theme, keybindings) => new HiddenEditor(tui, theme, keybindings));
|
|
119
|
+
} catch {
|
|
120
|
+
// Keep the default editor when the package cannot be imported (e.g. the
|
|
121
|
+
// extension is loaded outside Pi's module loader in tests).
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default function piDemoMode(pi: ExtensionAPI): void {
|
|
126
|
+
if (!demoEnabled) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let started = false;
|
|
130
|
+
let stopped = false;
|
|
131
|
+
let compacting = false;
|
|
132
|
+
|
|
133
|
+
function queueInitialPrompt(): void {
|
|
134
|
+
void promptText(
|
|
135
|
+
"PI_DEMO_INITIAL_PROMPT",
|
|
136
|
+
"PI_DEMO_INITIAL_PROMPT_FILE",
|
|
137
|
+
fallbackInitialPrompt
|
|
138
|
+
).then((prompt) => {
|
|
139
|
+
if (!stopped) {
|
|
140
|
+
pi.sendUserMessage(prompt);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function queueFollowup(): void {
|
|
146
|
+
void promptText(
|
|
147
|
+
"PI_DEMO_FOLLOWUP_PROMPT",
|
|
148
|
+
"PI_DEMO_FOLLOWUP_PROMPT_FILE",
|
|
149
|
+
fallbackFollowupPrompt
|
|
150
|
+
).then((prompt) => {
|
|
151
|
+
if (!stopped && !compacting) {
|
|
152
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function compactThenFollowup(ctx: ExtensionContext): void {
|
|
158
|
+
if (compacting) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
compacting = true;
|
|
162
|
+
ctx.compact({
|
|
163
|
+
customInstructions: demoCompactionInstructions,
|
|
164
|
+
onComplete: () => {
|
|
165
|
+
compacting = false;
|
|
166
|
+
queueFollowup();
|
|
167
|
+
},
|
|
168
|
+
onError: (error) => {
|
|
169
|
+
compacting = false;
|
|
170
|
+
stopped = true;
|
|
171
|
+
ctx.ui.notify("Demo compaction failed: " + error.message, "error");
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
pi.on("session_start", (event, ctx) => {
|
|
177
|
+
if (ctx.mode !== "tui") {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
applyDemoChrome(ctx);
|
|
181
|
+
if (started || stopped || event.reason !== "startup") {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
started = true;
|
|
185
|
+
queueInitialPrompt();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
pi.on("turn_end", (event, ctx) => {
|
|
189
|
+
if (!started || stopped || compacting || ctx.mode !== "tui") {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (event.message.role !== "assistant") {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
switch (event.message.stopReason) {
|
|
196
|
+
case "aborted":
|
|
197
|
+
case "error":
|
|
198
|
+
stopped = true;
|
|
199
|
+
return;
|
|
200
|
+
case "toolUse":
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (shouldCompactBeforeFollowup(event, ctx)) {
|
|
204
|
+
compactThenFollowup(ctx);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
queueFollowup();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
pi.on("session_shutdown", () => {
|
|
211
|
+
stopped = true;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function shouldCompactBeforeFollowup(event: TurnEndEvent, ctx: ExtensionContext): boolean {
|
|
216
|
+
const contextPercent = currentContextPercent(event, ctx);
|
|
217
|
+
return contextPercent !== undefined && contextPercent >= compactAtContextPercent;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function currentContextPercent(event: TurnEndEvent, ctx: ExtensionContext): number | undefined {
|
|
221
|
+
const usage = ctx.getContextUsage();
|
|
222
|
+
if (usage?.percent !== undefined && usage.percent !== null) {
|
|
223
|
+
return usage.percent;
|
|
224
|
+
}
|
|
225
|
+
if (event.message.role !== "assistant") {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
const contextWindow = ctx.model?.contextWindow;
|
|
229
|
+
if (contextWindow === undefined || contextWindow <= 0) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
return (event.message.usage.totalTokens / contextWindow) * 100;
|
|
233
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-demo-mode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Self-driving demo mode for Pi: continuous story generation with a stripped-down TUI",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"demo",
|
|
8
|
+
"extension"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/osolmaz/pi-demo-mode.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/osolmaz/pi-demo-mode/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/osolmaz/pi-demo-mode#readme",
|
|
20
|
+
"files": [
|
|
21
|
+
"extensions/**",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"pi": {
|
|
26
|
+
"extensions": [
|
|
27
|
+
"./extensions"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"format": "prettier --check .",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"check": "npm run format && npm run typecheck && npm test"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@earendil-works/pi-coding-agent": "^0.82.1",
|
|
41
|
+
"@types/node": "^22.0.0",
|
|
42
|
+
"prettier": "^3.0.0",
|
|
43
|
+
"typescript": "^5.0.0",
|
|
44
|
+
"vitest": "^3.0.0"
|
|
45
|
+
}
|
|
46
|
+
}
|