pi-cc-extensions 0.8.31 → 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.
Files changed (52) hide show
  1. package/README.en.md +164 -174
  2. package/README.md +113 -115
  3. package/extensions/claude-code-style.ts +314 -180
  4. package/extensions/compact-thinking.ts +58 -0
  5. package/extensions/fixed-editor.ts +185 -0
  6. package/extensions/index.ts +10 -4
  7. package/node_modules/@tifan/pi-fixed-editor/README.md +36 -0
  8. package/node_modules/@tifan/pi-fixed-editor/package.json +46 -0
  9. package/node_modules/@tifan/pi-fixed-editor/src/cluster.ts +113 -0
  10. package/node_modules/@tifan/pi-fixed-editor/src/index.ts +259 -0
  11. package/node_modules/@tifan/pi-fixed-editor/src/terminal-split.ts +1337 -0
  12. package/package.json +9 -5
  13. package/extensions/ask-user-question/LICENSE +0 -21
  14. package/extensions/ask-user-question/README.md +0 -63
  15. package/extensions/ask-user-question/ask-user-question.ts +0 -266
  16. package/extensions/ask-user-question/config.ts +0 -79
  17. package/extensions/ask-user-question/events.ts +0 -55
  18. package/extensions/ask-user-question/index.ts +0 -17
  19. package/extensions/ask-user-question/reconcile.ts +0 -47
  20. package/extensions/ask-user-question/rpc-fallback.ts +0 -172
  21. package/extensions/ask-user-question/state/build-questionnaire.ts +0 -314
  22. package/extensions/ask-user-question/state/dialog-height.ts +0 -16
  23. package/extensions/ask-user-question/state/key-router.ts +0 -291
  24. package/extensions/ask-user-question/state/questionnaire-session.ts +0 -193
  25. package/extensions/ask-user-question/state/row-intent.ts +0 -143
  26. package/extensions/ask-user-question/state/selectors/contract.ts +0 -24
  27. package/extensions/ask-user-question/state/selectors/derivations.ts +0 -40
  28. package/extensions/ask-user-question/state/selectors/focus.ts +0 -17
  29. package/extensions/ask-user-question/state/selectors/projections.ts +0 -102
  30. package/extensions/ask-user-question/state/state-reducer.ts +0 -299
  31. package/extensions/ask-user-question/state/state.ts +0 -49
  32. package/extensions/ask-user-question/tool/format-answer.ts +0 -29
  33. package/extensions/ask-user-question/tool/response-envelope.ts +0 -50
  34. package/extensions/ask-user-question/tool/types.ts +0 -149
  35. package/extensions/ask-user-question/tool/validate-questionnaire.ts +0 -64
  36. package/extensions/ask-user-question/view/component-binding.ts +0 -45
  37. package/extensions/ask-user-question/view/components/inline-input.ts +0 -109
  38. package/extensions/ask-user-question/view/components/multi-select-view.ts +0 -198
  39. package/extensions/ask-user-question/view/components/option-list-view.ts +0 -77
  40. package/extensions/ask-user-question/view/components/preview/markdown-content-cache.ts +0 -76
  41. package/extensions/ask-user-question/view/components/preview/preview-block-renderer.ts +0 -114
  42. package/extensions/ask-user-question/view/components/preview/preview-box-renderer.ts +0 -86
  43. package/extensions/ask-user-question/view/components/preview/preview-layout-decider.ts +0 -219
  44. package/extensions/ask-user-question/view/components/preview/preview-pane.ts +0 -230
  45. package/extensions/ask-user-question/view/components/submit-picker.ts +0 -66
  46. package/extensions/ask-user-question/view/components/tab-bar.ts +0 -57
  47. package/extensions/ask-user-question/view/components/wrapping-select.ts +0 -307
  48. package/extensions/ask-user-question/view/dialog-builder.ts +0 -225
  49. package/extensions/ask-user-question/view/props-adapter.ts +0 -123
  50. package/extensions/ask-user-question/view/stateful-view.ts +0 -24
  51. package/extensions/ask-user-question/view/tab-components.ts +0 -16
  52. 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
+ }
@@ -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 askUserQuestion from "./ask-user-question/index.ts";
4
- import claudeCodeStyle from "./claude-code-style.ts";
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
- claudeCodeStyle(pi);
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
  }
@@ -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
+ }