pi-cc-extensions 0.8.30 → 0.8.32
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/README.en.md +164 -174
- package/README.md +113 -115
- package/extensions/claude-code-style.ts +371 -208
- package/extensions/compact-thinking.ts +58 -0
- package/extensions/fixed-editor.ts +185 -0
- package/extensions/index.ts +10 -4
- package/extensions/subagent-notification.ts +30 -23
- package/node_modules/@tifan/pi-fixed-editor/README.md +36 -0
- package/node_modules/@tifan/pi-fixed-editor/package.json +46 -0
- package/node_modules/@tifan/pi-fixed-editor/src/cluster.ts +113 -0
- package/node_modules/@tifan/pi-fixed-editor/src/index.ts +259 -0
- package/node_modules/@tifan/pi-fixed-editor/src/terminal-split.ts +1337 -0
- package/package.json +9 -5
- package/extensions/ask-user-question/LICENSE +0 -21
- package/extensions/ask-user-question/README.md +0 -63
- package/extensions/ask-user-question/ask-user-question.ts +0 -266
- package/extensions/ask-user-question/config.ts +0 -79
- package/extensions/ask-user-question/events.ts +0 -55
- package/extensions/ask-user-question/index.ts +0 -17
- package/extensions/ask-user-question/reconcile.ts +0 -47
- package/extensions/ask-user-question/rpc-fallback.ts +0 -172
- package/extensions/ask-user-question/state/build-questionnaire.ts +0 -314
- package/extensions/ask-user-question/state/dialog-height.ts +0 -16
- package/extensions/ask-user-question/state/key-router.ts +0 -291
- package/extensions/ask-user-question/state/questionnaire-session.ts +0 -193
- package/extensions/ask-user-question/state/row-intent.ts +0 -143
- package/extensions/ask-user-question/state/selectors/contract.ts +0 -24
- package/extensions/ask-user-question/state/selectors/derivations.ts +0 -40
- package/extensions/ask-user-question/state/selectors/focus.ts +0 -17
- package/extensions/ask-user-question/state/selectors/projections.ts +0 -102
- package/extensions/ask-user-question/state/state-reducer.ts +0 -299
- package/extensions/ask-user-question/state/state.ts +0 -49
- package/extensions/ask-user-question/tool/format-answer.ts +0 -29
- package/extensions/ask-user-question/tool/response-envelope.ts +0 -50
- package/extensions/ask-user-question/tool/types.ts +0 -149
- package/extensions/ask-user-question/tool/validate-questionnaire.ts +0 -64
- package/extensions/ask-user-question/view/component-binding.ts +0 -45
- package/extensions/ask-user-question/view/components/inline-input.ts +0 -109
- package/extensions/ask-user-question/view/components/multi-select-view.ts +0 -198
- package/extensions/ask-user-question/view/components/option-list-view.ts +0 -77
- package/extensions/ask-user-question/view/components/preview/markdown-content-cache.ts +0 -76
- package/extensions/ask-user-question/view/components/preview/preview-block-renderer.ts +0 -114
- package/extensions/ask-user-question/view/components/preview/preview-box-renderer.ts +0 -86
- package/extensions/ask-user-question/view/components/preview/preview-layout-decider.ts +0 -219
- package/extensions/ask-user-question/view/components/preview/preview-pane.ts +0 -230
- package/extensions/ask-user-question/view/components/submit-picker.ts +0 -66
- package/extensions/ask-user-question/view/components/tab-bar.ts +0 -57
- package/extensions/ask-user-question/view/components/wrapping-select.ts +0 -307
- package/extensions/ask-user-question/view/dialog-builder.ts +0 -225
- package/extensions/ask-user-question/view/props-adapter.ts +0 -123
- package/extensions/ask-user-question/view/stateful-view.ts +0 -24
- package/extensions/ask-user-question/view/tab-components.ts +0 -16
- package/extensions/ask-user-question/view/tab-content-strategy.ts +0 -264
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createJiti } from "jiti";
|
|
6
|
+
|
|
7
|
+
export type CompactThinkingConfig = {
|
|
8
|
+
useSummaryTitlesAsThinkingTitle: boolean;
|
|
9
|
+
previewLines: number;
|
|
10
|
+
animationIntervalMs: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type CompactThinkingController = {
|
|
14
|
+
updateConfig(next: CompactThinkingConfig): void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function installCompactThinking(
|
|
18
|
+
pi: ExtensionAPI,
|
|
19
|
+
initialConfig: CompactThinkingConfig,
|
|
20
|
+
): CompactThinkingController {
|
|
21
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
22
|
+
const configPath = join(agentDir, "compact-thinking.json");
|
|
23
|
+
let hasValidCompatibilityConfig = false;
|
|
24
|
+
try {
|
|
25
|
+
const value = JSON.parse(readFileSync(configPath, "utf8")) as Partial<CompactThinkingConfig>;
|
|
26
|
+
hasValidCompatibilityConfig =
|
|
27
|
+
typeof value.useSummaryTitlesAsThinkingTitle === "boolean" &&
|
|
28
|
+
Number.isInteger(value.previewLines) &&
|
|
29
|
+
(value.previewLines ?? -1) >= 0 &&
|
|
30
|
+
Number.isFinite(value.animationIntervalMs) &&
|
|
31
|
+
(value.animationIntervalMs ?? 0) >= 1;
|
|
32
|
+
} catch {
|
|
33
|
+
// The upstream loader needs a valid compatibility file on first install.
|
|
34
|
+
}
|
|
35
|
+
if (!hasValidCompatibilityConfig) {
|
|
36
|
+
if (!existsSync(agentDir)) mkdirSync(agentDir, { recursive: true });
|
|
37
|
+
writeFileSync(configPath, `${JSON.stringify(initialConfig, null, 2)}\n`, "utf8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The package publishes TypeScript source only, so use the loader already used by fixed-editor.
|
|
41
|
+
const jiti = createJiti(import.meta.url);
|
|
42
|
+
const upstreamConfig = (
|
|
43
|
+
jiti("pi-compact-thinking/lib/config.ts") as { config: CompactThinkingConfig }
|
|
44
|
+
).config;
|
|
45
|
+
const compactThinking = (
|
|
46
|
+
jiti("pi-compact-thinking/index.ts") as {
|
|
47
|
+
default: (api: ExtensionAPI) => void;
|
|
48
|
+
}
|
|
49
|
+
).default;
|
|
50
|
+
Object.assign(upstreamConfig, initialConfig);
|
|
51
|
+
compactThinking(pi);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
updateConfig(next) {
|
|
55
|
+
Object.assign(upstreamConfig, next);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { createJiti } from "jiti";
|
|
4
|
+
|
|
5
|
+
type LifecycleHandler = (event: any, ctx: any) => void;
|
|
6
|
+
type FixedEditorExtension = (pi: ExtensionAPI) => void;
|
|
7
|
+
|
|
8
|
+
// The npm package publishes TypeScript source only, so use Pi's own TS loader.
|
|
9
|
+
const jiti = createJiti(import.meta.url);
|
|
10
|
+
// Match pi-fixed-editor's own `.js` import specifier; Jiti caches `.ts` separately.
|
|
11
|
+
const terminalSplit = jiti("@tifan/pi-fixed-editor/src/terminal-split.js") as {
|
|
12
|
+
TerminalSplitCompositor?: { prototype: Record<PropertyKey, any> };
|
|
13
|
+
};
|
|
14
|
+
export type FixedEditorHitbox = { row: number; startCol: number; endCol: number };
|
|
15
|
+
|
|
16
|
+
type FixedEditorSharedState = { scrollButtonHitbox: FixedEditorHitbox | null };
|
|
17
|
+
const FIXED_EDITOR_STATE_KEY = Symbol.for("pi.ccstyle.fixed-editor.state");
|
|
18
|
+
const fixedEditorState = (() => {
|
|
19
|
+
const host = globalThis as typeof globalThis & {
|
|
20
|
+
[FIXED_EDITOR_STATE_KEY]?: FixedEditorSharedState;
|
|
21
|
+
};
|
|
22
|
+
host[FIXED_EDITOR_STATE_KEY] ??= { scrollButtonHitbox: null };
|
|
23
|
+
return host[FIXED_EDITOR_STATE_KEY];
|
|
24
|
+
})();
|
|
25
|
+
|
|
26
|
+
export function getFixedEditorScrollButtonHitbox(): FixedEditorHitbox | null {
|
|
27
|
+
return fixedEditorState.scrollButtonHitbox;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FIXED_EDITOR_SPACING_PATCH = Symbol.for("pi.ccstyle.fixed-editor-spacing-patch");
|
|
31
|
+
const clusterModule = jiti("@tifan/pi-fixed-editor/src/cluster.js") as {
|
|
32
|
+
renderFixedEditorCluster?: (input: any) => any;
|
|
33
|
+
};
|
|
34
|
+
if (!(clusterModule as any)[FIXED_EDITOR_SPACING_PATCH] && clusterModule.renderFixedEditorCluster) {
|
|
35
|
+
const renderCluster = clusterModule.renderFixedEditorCluster;
|
|
36
|
+
const padWidgets = (lines: string[] | undefined) =>
|
|
37
|
+
lines?.map((line) => (line.length > 0 && !line.includes("Ctrl+End") ? ` ${line}` : line));
|
|
38
|
+
clusterModule.renderFixedEditorCluster = (input) => {
|
|
39
|
+
const aboveWidgetLines = padWidgets(input.aboveWidgetLines);
|
|
40
|
+
const result = renderCluster({
|
|
41
|
+
...input,
|
|
42
|
+
// pi-fixed-editor strips Pi's leading widget spacer; restore it below the spinner.
|
|
43
|
+
aboveWidgetLines:
|
|
44
|
+
input.statusLines?.length && aboveWidgetLines?.length
|
|
45
|
+
? ["", ...aboveWidgetLines]
|
|
46
|
+
: aboveWidgetLines,
|
|
47
|
+
belowWidgetLines: padWidgets(input.belowWidgetLines),
|
|
48
|
+
});
|
|
49
|
+
const buttonRow = result.lines.findIndex((line: string) => line.includes("Ctrl+End"));
|
|
50
|
+
if (buttonRow < 0) {
|
|
51
|
+
fixedEditorState.scrollButtonHitbox = null;
|
|
52
|
+
} else {
|
|
53
|
+
const plain = result.lines[buttonRow]
|
|
54
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
55
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
56
|
+
const start = plain.indexOf("[");
|
|
57
|
+
fixedEditorState.scrollButtonHitbox = {
|
|
58
|
+
row: input.terminalRows - result.lines.length + buttonRow + 1,
|
|
59
|
+
startCol: visibleWidth(plain.slice(0, Math.max(0, start))) + 1,
|
|
60
|
+
endCol: visibleWidth(plain.trimEnd()),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
};
|
|
65
|
+
(clusterModule as any)[FIXED_EDITOR_SPACING_PATCH] = true;
|
|
66
|
+
}
|
|
67
|
+
const loaded = jiti("@tifan/pi-fixed-editor/src/index.ts") as {
|
|
68
|
+
default?: FixedEditorExtension;
|
|
69
|
+
};
|
|
70
|
+
const fixedEditor = loaded.default ?? (loaded as unknown as FixedEditorExtension);
|
|
71
|
+
|
|
72
|
+
const IME_CURSOR_PATCH = Symbol.for("pi.ccstyle.fixed-editor-ime-cursor-patch");
|
|
73
|
+
|
|
74
|
+
function positionHiddenImeCursor(compositor: any): void {
|
|
75
|
+
if (
|
|
76
|
+
compositor.disposed ||
|
|
77
|
+
compositor.writing ||
|
|
78
|
+
compositor.getShowHardwareCursor?.() ||
|
|
79
|
+
compositor.hasVisibleOverlay?.()
|
|
80
|
+
)
|
|
81
|
+
return;
|
|
82
|
+
const rows = compositor.getRawRows?.();
|
|
83
|
+
if (!Number.isFinite(rows)) return;
|
|
84
|
+
const width = Math.max(1, Number(compositor.terminal?.columns) || 80);
|
|
85
|
+
const cluster = compositor.getCluster?.(width, rows);
|
|
86
|
+
if (!cluster?.cursor) return;
|
|
87
|
+
const row = Math.max(1, rows - cluster.lines.length + cluster.cursor.row + 1);
|
|
88
|
+
const col = Math.max(1, cluster.cursor.col + 1);
|
|
89
|
+
compositor.originalWrite?.(`\x1b[?2026h\x1b[${row};${col}H\x1b[?25l\x1b[?2026l`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function installFixedEditorImePatch(compositorClass: {
|
|
93
|
+
prototype: Record<PropertyKey, any>;
|
|
94
|
+
}): void {
|
|
95
|
+
const prototype = compositorClass.prototype;
|
|
96
|
+
if (prototype[IME_CURSOR_PATCH]) return;
|
|
97
|
+
for (const name of ["write", "requestRepaint"] as const) {
|
|
98
|
+
const original = prototype[name];
|
|
99
|
+
if (typeof original !== "function") continue;
|
|
100
|
+
prototype[name] = function (this: any, ...args: any[]) {
|
|
101
|
+
const result = Reflect.apply(original, this, args);
|
|
102
|
+
positionHiddenImeCursor(this);
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
prototype[IME_CURSOR_PATCH] = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (terminalSplit.TerminalSplitCompositor) {
|
|
110
|
+
installFixedEditorImePatch(terminalSplit.TerminalSplitCompositor);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type FixedEditorController = {
|
|
114
|
+
setEnabled(enabled: boolean): void;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
type FixedEditorOwner = {
|
|
118
|
+
owner: object;
|
|
119
|
+
stop: () => void;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const FIXED_EDITOR_OWNER = Symbol.for("pi.ccstyle.fixed-editor-owner");
|
|
123
|
+
|
|
124
|
+
/** Adds runtime on/off control around pi-fixed-editor's session lifecycle. */
|
|
125
|
+
export function installFixedEditor(
|
|
126
|
+
pi: ExtensionAPI,
|
|
127
|
+
initiallyEnabled: boolean,
|
|
128
|
+
): FixedEditorController {
|
|
129
|
+
let start: LifecycleHandler | undefined;
|
|
130
|
+
let shutdown: LifecycleHandler | undefined;
|
|
131
|
+
fixedEditor({
|
|
132
|
+
on(event: string, handler: LifecycleHandler) {
|
|
133
|
+
if (event === "session_start") start = handler;
|
|
134
|
+
if (event === "session_shutdown") shutdown = handler;
|
|
135
|
+
},
|
|
136
|
+
} as unknown as ExtensionAPI);
|
|
137
|
+
|
|
138
|
+
const owner = {};
|
|
139
|
+
let enabled = initiallyEnabled;
|
|
140
|
+
let active = false;
|
|
141
|
+
let session: { event: any; ctx: any } | undefined;
|
|
142
|
+
|
|
143
|
+
const deactivate = () => {
|
|
144
|
+
fixedEditorState.scrollButtonHitbox = null;
|
|
145
|
+
const current = (globalThis as any)[FIXED_EDITOR_OWNER] as FixedEditorOwner | undefined;
|
|
146
|
+
if (current?.owner === owner) current.stop();
|
|
147
|
+
else active = false;
|
|
148
|
+
};
|
|
149
|
+
const activate = () => {
|
|
150
|
+
if (!session || session.ctx?.mode !== "tui" || active) return;
|
|
151
|
+
const previous = (globalThis as any)[FIXED_EDITOR_OWNER] as FixedEditorOwner | undefined;
|
|
152
|
+
if (previous?.owner !== owner) previous?.stop();
|
|
153
|
+
const currentSession = session;
|
|
154
|
+
start?.(currentSession.event, currentSession.ctx);
|
|
155
|
+
active = true;
|
|
156
|
+
(globalThis as any)[FIXED_EDITOR_OWNER] = {
|
|
157
|
+
owner,
|
|
158
|
+
stop: () => {
|
|
159
|
+
if (!active) return;
|
|
160
|
+
shutdown?.(currentSession.event, currentSession.ctx);
|
|
161
|
+
active = false;
|
|
162
|
+
if ((globalThis as any)[FIXED_EDITOR_OWNER]?.owner === owner) {
|
|
163
|
+
delete (globalThis as any)[FIXED_EDITOR_OWNER];
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
} satisfies FixedEditorOwner;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
pi.on("session_start", (event, ctx) => {
|
|
170
|
+
session = { event, ctx };
|
|
171
|
+
if (enabled) activate();
|
|
172
|
+
});
|
|
173
|
+
pi.on("session_shutdown", () => {
|
|
174
|
+
deactivate();
|
|
175
|
+
session = undefined;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
setEnabled(nextEnabled) {
|
|
180
|
+
enabled = nextEnabled;
|
|
181
|
+
if (nextEnabled) activate();
|
|
182
|
+
else deactivate();
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import agentAutocomplete from "./agent-autocomplete.ts";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import claudeCodeStyle, { getCompactThinkingConfig } from "./claude-code-style.ts";
|
|
4
|
+
import { installCompactThinking, type CompactThinkingController } from "./compact-thinking.ts";
|
|
5
5
|
import context from "./context.ts";
|
|
6
6
|
import piAliases from "./pi-aliases.ts";
|
|
7
7
|
import piStartupHeader from "./pi-startup-header.ts";
|
|
@@ -12,11 +12,17 @@ import workingMessage from "./working-message.ts";
|
|
|
12
12
|
export default function (pi: ExtensionAPI): void {
|
|
13
13
|
piAliases(pi);
|
|
14
14
|
piStartupHeader(pi);
|
|
15
|
-
|
|
15
|
+
let compactThinking: CompactThinkingController | undefined;
|
|
16
|
+
const compactThinkingBridge: CompactThinkingController = {
|
|
17
|
+
updateConfig(next) {
|
|
18
|
+
compactThinking?.updateConfig(next);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
claudeCodeStyle(pi, undefined, compactThinkingBridge);
|
|
22
|
+
compactThinking = installCompactThinking(pi, getCompactThinkingConfig());
|
|
16
23
|
subagentNotification(pi);
|
|
17
24
|
workingMessage(pi);
|
|
18
25
|
context(pi);
|
|
19
26
|
sessionReference(pi);
|
|
20
27
|
agentAutocomplete(pi);
|
|
21
|
-
askUserQuestion(pi);
|
|
22
28
|
}
|
|
@@ -84,31 +84,38 @@ export function formatSubagentNotificationLines(lines: string[], width: number):
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
export default function subagentNotificationExtension(pi: ExtensionAPI): void {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
87
|
+
let patch: Patch | undefined;
|
|
88
|
+
|
|
89
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
90
|
+
if (patch || ctx?.mode !== "tui" || !ctx?.hasUI) return;
|
|
91
|
+
const prototype = CustomMessageComponent.prototype as any;
|
|
92
|
+
const host = globalThis as any;
|
|
93
|
+
const previous = host[PATCH_KEY] as Patch | undefined;
|
|
94
|
+
if (previous) previous.active = false;
|
|
95
|
+
const originalRender =
|
|
96
|
+
previous && prototype.render === previous.installedRender
|
|
97
|
+
? previous.originalRender
|
|
98
|
+
: prototype.render;
|
|
99
|
+
patch = {
|
|
100
|
+
active: true,
|
|
101
|
+
prototype,
|
|
102
|
+
originalRender,
|
|
103
|
+
installedRender: undefined as any,
|
|
104
|
+
};
|
|
105
|
+
const installed = patch;
|
|
106
|
+
installed.installedRender = function (this: any, width: number): string[] {
|
|
107
|
+
const lines = originalRender.call(this, width);
|
|
108
|
+
if (!installed.active || this?.message?.customType !== "subagent-notification") return lines;
|
|
109
|
+
return formatSubagentNotificationLines(lines, width);
|
|
110
|
+
};
|
|
111
|
+
prototype.render = installed.installedRender;
|
|
112
|
+
host[PATCH_KEY] = installed;
|
|
113
|
+
});
|
|
108
114
|
|
|
109
115
|
pi.on("session_shutdown", async () => {
|
|
110
|
-
if (!patch
|
|
116
|
+
if (!patch?.active) return;
|
|
111
117
|
patch.active = false;
|
|
112
|
-
if (prototype.render === patch.installedRender)
|
|
118
|
+
if (patch.prototype.render === patch.installedRender)
|
|
119
|
+
patch.prototype.render = patch.originalRender;
|
|
113
120
|
});
|
|
114
121
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @tifan/pi-fixed-editor
|
|
2
|
+
|
|
3
|
+
Keep Pi's editor, footer, and editor-adjacent widgets fixed at the bottom of the terminal while the transcript scrolls.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@tifan/pi-fixed-editor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- Pins Pi's existing editor/footer area to the bottom of the terminal.
|
|
14
|
+
- Keeps `aboveEditor` and `belowEditor` widgets with the editor.
|
|
15
|
+
- Preserves existing editor behavior, footer content, and autocomplete.
|
|
16
|
+
- Routes mouse wheel and PageUp/PageDown scrolling to the transcript.
|
|
17
|
+
- Jumps the transcript back to the bottom when you submit a query with Enter.
|
|
18
|
+
|
|
19
|
+
## What it does not do
|
|
20
|
+
|
|
21
|
+
- No styling.
|
|
22
|
+
- No powerline footer.
|
|
23
|
+
- No commands.
|
|
24
|
+
- No settings.
|
|
25
|
+
- No custom shortcuts.
|
|
26
|
+
- No print or RPC mode behavior.
|
|
27
|
+
|
|
28
|
+
Uninstall or disable the extension to return to Pi's default scrolling behavior.
|
|
29
|
+
|
|
30
|
+
## Credits
|
|
31
|
+
|
|
32
|
+
Fixed terminal-region behavior is adapted from [pi-powerline-footer](https://github.com/nicobailon/pi-powerline-footer) by Nico Bailon.
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tifan/pi-fixed-editor",
|
|
3
|
+
"version": "0.2.5",
|
|
4
|
+
"description": "Keep the pi editor and footer fixed while the transcript scrolls.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fixed-editor",
|
|
7
|
+
"pi",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"pi-package",
|
|
10
|
+
"terminal"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/tifandotme/pi-extensions/tree/master/packages/pi-fixed-editor#readme",
|
|
13
|
+
"bugs": "https://github.com/tifandotme/pi-extensions/issues",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Tifan Dwi Avianto <hi@tifan.me>",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/tifandotme/pi-extensions.git",
|
|
19
|
+
"directory": "packages/pi-fixed-editor"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src/**/*.ts",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"provenance": true
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "node --experimental-strip-types --test tests/**/*.test.ts"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@earendil-works/pi-coding-agent": ">=0.78.0",
|
|
36
|
+
"@earendil-works/pi-tui": ">=0.78.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20"
|
|
40
|
+
},
|
|
41
|
+
"pi": {
|
|
42
|
+
"extensions": [
|
|
43
|
+
"./src/index.ts"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CURSOR_MARKER,
|
|
3
|
+
truncateToWidth,
|
|
4
|
+
visibleWidth,
|
|
5
|
+
} from "@earendil-works/pi-tui"
|
|
6
|
+
|
|
7
|
+
export interface FixedEditorClusterInput {
|
|
8
|
+
width: number
|
|
9
|
+
terminalRows: number
|
|
10
|
+
statusLines?: string[]
|
|
11
|
+
aboveWidgetLines?: string[]
|
|
12
|
+
editorLines: string[]
|
|
13
|
+
belowWidgetLines?: string[]
|
|
14
|
+
footerLines?: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FixedEditorCursor {
|
|
18
|
+
row: number
|
|
19
|
+
col: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FixedEditorClusterRender {
|
|
23
|
+
lines: string[]
|
|
24
|
+
cursor: FixedEditorCursor | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeLines(lines: string[] | undefined, width: number): string[] {
|
|
28
|
+
if (!lines || width <= 0) return []
|
|
29
|
+
|
|
30
|
+
return lines
|
|
31
|
+
.filter((line) => line !== undefined && line !== null)
|
|
32
|
+
.map((line) =>
|
|
33
|
+
visibleWidth(line) > width
|
|
34
|
+
? truncateToWidth(line, width, "", true)
|
|
35
|
+
: line,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function takeTail(lines: string[], count: number): string[] {
|
|
40
|
+
if (count <= 0) return []
|
|
41
|
+
return lines.length <= count ? lines : lines.slice(lines.length - count)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function capEditorLines(lines: string[], count: number): string[] {
|
|
45
|
+
if (count <= 0) return []
|
|
46
|
+
if (lines.length <= count) return lines
|
|
47
|
+
|
|
48
|
+
const cursorRow = lines.findIndex((line) => line.includes(CURSOR_MARKER))
|
|
49
|
+
if (cursorRow !== -1) {
|
|
50
|
+
const start = Math.max(
|
|
51
|
+
0,
|
|
52
|
+
Math.min(cursorRow - count + 1, lines.length - count),
|
|
53
|
+
)
|
|
54
|
+
return lines.slice(start, start + count)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return lines.slice(lines.length - count)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractCursor(lines: string[]): FixedEditorClusterRender {
|
|
61
|
+
let cursor: FixedEditorCursor | null = null
|
|
62
|
+
const cleaned = lines.map((line, row) => {
|
|
63
|
+
const markerIndex = line.indexOf(CURSOR_MARKER)
|
|
64
|
+
if (markerIndex === -1) return line
|
|
65
|
+
|
|
66
|
+
cursor ??= {
|
|
67
|
+
row,
|
|
68
|
+
col: visibleWidth(line.slice(0, markerIndex)),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
line.slice(0, markerIndex) +
|
|
73
|
+
line.slice(markerIndex + CURSOR_MARKER.length)
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return { lines: cleaned, cursor }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function renderFixedEditorCluster(
|
|
81
|
+
input: FixedEditorClusterInput,
|
|
82
|
+
): FixedEditorClusterRender {
|
|
83
|
+
const width = Math.max(1, input.width)
|
|
84
|
+
const maxRows = Math.max(1, input.terminalRows - 1)
|
|
85
|
+
|
|
86
|
+
const statusLines = normalizeLines(input.statusLines, width)
|
|
87
|
+
const aboveWidgetLines = normalizeLines(input.aboveWidgetLines, width)
|
|
88
|
+
const editorSource = normalizeLines(input.editorLines, width)
|
|
89
|
+
const belowWidgetLines = normalizeLines(input.belowWidgetLines, width)
|
|
90
|
+
const footerLines = normalizeLines(input.footerLines, width)
|
|
91
|
+
|
|
92
|
+
const editorLines = capEditorLines(editorSource, maxRows)
|
|
93
|
+
let remaining = maxRows - editorLines.length
|
|
94
|
+
|
|
95
|
+
const footer = takeTail(footerLines, remaining)
|
|
96
|
+
remaining -= footer.length
|
|
97
|
+
|
|
98
|
+
const belowWidgets = takeTail(belowWidgetLines, remaining)
|
|
99
|
+
remaining -= belowWidgets.length
|
|
100
|
+
|
|
101
|
+
const aboveWidgets = takeTail(aboveWidgetLines, remaining)
|
|
102
|
+
remaining -= aboveWidgets.length
|
|
103
|
+
|
|
104
|
+
const status = takeTail(statusLines, remaining)
|
|
105
|
+
|
|
106
|
+
return extractCursor([
|
|
107
|
+
...status,
|
|
108
|
+
...aboveWidgets,
|
|
109
|
+
...editorLines,
|
|
110
|
+
...belowWidgets,
|
|
111
|
+
...footer,
|
|
112
|
+
])
|
|
113
|
+
}
|