pi-one-ui 0.7.0 → 0.7.1

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 (49) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.en.md +19 -6
  3. package/README.md +19 -6
  4. package/extensions/app/config/context.ts +42 -0
  5. package/extensions/app/config/editor.ts +80 -0
  6. package/extensions/app/config/footer.ts +291 -0
  7. package/extensions/app/config/panel.ts +1 -5
  8. package/extensions/app/config/renderer.ts +1 -6
  9. package/extensions/app/config/shell.ts +358 -1435
  10. package/extensions/app/config/store.ts +22 -1
  11. package/extensions/app/config/values.ts +33 -0
  12. package/extensions/app/config/working-line.ts +100 -0
  13. package/extensions/app/overlay/input-router.ts +56 -32
  14. package/extensions/app/overlay/overlay-manager.ts +183 -21
  15. package/extensions/app/runtime/event-coordinator.ts +90 -42
  16. package/extensions/{features → app/runtime}/flush-docked-bash.ts +12 -2
  17. package/extensions/app/runtime/render-scheduler.ts +35 -31
  18. package/extensions/app/runtime/tui-runtime.ts +315 -312
  19. package/extensions/app/settings/controller.ts +202 -0
  20. package/extensions/app/settings/panel.ts +81 -0
  21. package/extensions/app/settings/presets.ts +43 -0
  22. package/extensions/features/context-inspector/index.ts +83 -662
  23. package/extensions/layouts/context/index.ts +58 -161
  24. package/extensions/layouts/context/message/user-message.ts +1 -1
  25. package/extensions/layouts/context/renderer/index.ts +31 -13
  26. package/extensions/layouts/context/renderer/mouse/interaction.ts +29 -14
  27. package/extensions/layouts/editor/controller.ts +9 -18
  28. package/extensions/layouts/editor/factory.ts +3 -3
  29. package/extensions/layouts/footer/controller.ts +30 -19
  30. package/extensions/layouts/footer/footer.ts +2 -2
  31. package/extensions/layouts/overlay/context-inspector.ts +497 -0
  32. package/extensions/{app → layouts}/overlay/selector-border.ts +5 -5
  33. package/extensions/{app → layouts}/overlay/selector-controller.ts +1 -1
  34. package/extensions/layouts/overlay/settings-panel.ts +359 -0
  35. package/extensions/{app/commands → layouts/overlay}/settings-previews.ts +4 -4
  36. package/extensions/layouts/working-line/controller.ts +3 -29
  37. package/extensions/layouts/working-line/working-line.ts +17 -114
  38. package/extensions/shared/working-line-text.ts +108 -0
  39. package/package.json +1 -1
  40. package/extensions/app/host/pi-extension-port.ts +0 -25
  41. package/extensions/app/host/pi-ui-port.ts +0 -52
  42. package/extensions/app/host/tui-capabilities.ts +0 -27
  43. package/extensions/app/ownership/layout-registry.ts +0 -65
  44. package/extensions/app/ownership.ts +0 -32
  45. package/extensions/app/panel.ts +0 -770
  46. package/extensions/app/presets.ts +0 -35
  47. package/extensions/app/runtime/runtime-state.ts +0 -83
  48. /package/extensions/{layouts/working-line → app/config}/working-line-messages.ts +0 -0
  49. /package/extensions/app/{ownership → runtime}/prototype-patch-registry.ts +0 -0
@@ -0,0 +1,108 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import { visibleWidth } from "@earendil-works/pi-tui";
3
+
4
+ export const MAX_WORKING_LINE_MESSAGES = 48;
5
+ export const MAX_WORKING_LINE_MESSAGE_CELLS = 43;
6
+ export const MAX_WORKING_LINE_RAW_CODE_UNITS = 4096;
7
+ export const MAX_WORKING_LINE_NORMALIZED_CODE_UNITS = 256;
8
+ export const MAX_WORKING_LINE_ENTRIES_EXAMINED = 256;
9
+
10
+ export function* segmentGraphemes(value: string): Iterable<string> {
11
+ for (const part of new Intl.Segmenter(undefined, {
12
+ granularity: "grapheme",
13
+ }).segment(value)) {
14
+ yield part.segment;
15
+ }
16
+ }
17
+
18
+ export function truncateGraphemes(
19
+ value: string,
20
+ maximumCells: number,
21
+ maximumCodeUnits = Number.POSITIVE_INFINITY,
22
+ ): string {
23
+ let width = 0;
24
+ let codeUnits = 0;
25
+ const output: string[] = [];
26
+ for (const grapheme of segmentGraphemes(value)) {
27
+ const nextWidth = visibleWidth(grapheme);
28
+ if (
29
+ width >= maximumCells ||
30
+ width + nextWidth > maximumCells ||
31
+ codeUnits + grapheme.length > maximumCodeUnits
32
+ ) {
33
+ break;
34
+ }
35
+ output.push(grapheme);
36
+ width += nextWidth;
37
+ codeUnits += grapheme.length;
38
+ }
39
+ while (output.length > 0 && /^\s+$/u.test(output.at(-1) ?? "")) {
40
+ output.pop();
41
+ }
42
+ return output.join("");
43
+ }
44
+
45
+ /** 配置与渲染共同使用的文本限制,保留完整 grapheme。 */
46
+ export function normalizeWorkingLineMessage(value: unknown): string {
47
+ if (typeof value !== "string") {
48
+ return "";
49
+ }
50
+ let bounded = value;
51
+ if (value.length > MAX_WORKING_LINE_RAW_CODE_UNITS) {
52
+ const graphemes = [
53
+ ...segmentGraphemes(value.slice(0, MAX_WORKING_LINE_RAW_CODE_UNITS)),
54
+ ];
55
+ graphemes.pop();
56
+ bounded = graphemes.join("");
57
+ }
58
+ const withoutTerminalSequences = stripVTControlCharacters(
59
+ bounded
60
+ .replaceAll(
61
+ /[\u0090\u0098\u009d\u009e\u009f][\s\S]*?(?:\u0007|\u009c|\x1b\\|$)/g,
62
+ "",
63
+ )
64
+ .replaceAll(/\u009b[0-?]*[ -/]*[@-~]/g, ""),
65
+ );
66
+ const normalized = withoutTerminalSequences
67
+ .replaceAll(/[\u0000-\u001f\u007f-\u009f]/g, " ")
68
+ .replaceAll(
69
+ /[\u034f\u061c\u200b\u200e\u200f\u202a-\u202e\u2060\u2066-\u206f]/g,
70
+ "",
71
+ )
72
+ .normalize("NFC")
73
+ .replaceAll(/\s+/gu, " ");
74
+ const graphemes = [...segmentGraphemes(normalized)];
75
+ while (graphemes.length > 0 && /^\s+$/u.test(graphemes[0] ?? "")) {
76
+ graphemes.shift();
77
+ }
78
+ while (graphemes.length > 0 && /^\s+$/u.test(graphemes.at(-1) ?? "")) {
79
+ graphemes.pop();
80
+ }
81
+ const truncated = truncateGraphemes(
82
+ graphemes.join(""),
83
+ MAX_WORKING_LINE_MESSAGE_CELLS,
84
+ MAX_WORKING_LINE_NORMALIZED_CODE_UNITS,
85
+ );
86
+ return visibleWidth(truncated) > 0 ? truncated : "";
87
+ }
88
+
89
+ export function normalizeWorkingLineMessages(values: unknown): string[] {
90
+ if (!Array.isArray(values)) {
91
+ return [];
92
+ }
93
+ const output: string[] = [];
94
+ const seen = new Set<string>();
95
+ const examined = Math.min(values.length, MAX_WORKING_LINE_ENTRIES_EXAMINED);
96
+ for (let index = 0; index < examined; index += 1) {
97
+ const normalized = normalizeWorkingLineMessage(values[index]);
98
+ if (!normalized || seen.has(normalized)) {
99
+ continue;
100
+ }
101
+ seen.add(normalized);
102
+ output.push(normalized);
103
+ if (output.length === MAX_WORKING_LINE_MESSAGES) {
104
+ break;
105
+ }
106
+ }
107
+ return output;
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-one-ui",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "A single, customizable Pi UI package with layout-oriented Context and Shell rendering for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,25 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
-
3
- export type PiExtensionPort = Pick<
4
- ExtensionAPI,
5
- | "appendEntry"
6
- | "on"
7
- | "registerCommand"
8
- | "registerEntryRenderer"
9
- | "registerMarkdownTransformer"
10
- | "registerMessageRenderer"
11
- >;
12
-
13
- /**
14
- * Binds the host extension API once so modules do not pass the raw object around.
15
- */
16
- export function createPiExtensionPort(pi: ExtensionAPI): PiExtensionPort {
17
- return {
18
- appendEntry: pi.appendEntry.bind(pi),
19
- on: pi.on.bind(pi) as PiExtensionPort["on"],
20
- registerCommand: pi.registerCommand.bind(pi),
21
- registerEntryRenderer: pi.registerEntryRenderer.bind(pi),
22
- registerMarkdownTransformer: pi.registerMarkdownTransformer.bind(pi),
23
- registerMessageRenderer: pi.registerMessageRenderer.bind(pi),
24
- };
25
- }
@@ -1,52 +0,0 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
-
3
- type PiUi = ExtensionContext["ui"];
4
-
5
- export type PiUiPort = Pick<
6
- PiUi,
7
- | "custom"
8
- | "getEditorComponent"
9
- | "getToolsExpanded"
10
- | "notify"
11
- | "onTerminalInput"
12
- | "setEditorComponent"
13
- | "setFooter"
14
- | "setHeader"
15
- | "setStatus"
16
- | "setToolsExpanded"
17
- | "setWidget"
18
- | "setWorkingIndicator"
19
- | "setWorkingMessage"
20
- | "setWorkingVisible"
21
- > & {
22
- readonly theme: PiUi["theme"];
23
- requestRender(force?: boolean): void;
24
- };
25
-
26
- /**
27
- * Adapts a session UI context to the narrow port consumed by layouts.
28
- */
29
- export function createPiUiPort(
30
- ctx: ExtensionContext,
31
- requestRender: (force?: boolean) => void = () => {},
32
- ): PiUiPort {
33
- const ui = ctx.ui;
34
- return {
35
- custom: (...args) => ui.custom(...args),
36
- getEditorComponent: () => ui.getEditorComponent(),
37
- getToolsExpanded: () => ui.getToolsExpanded(),
38
- notify: (...args) => ui.notify(...args),
39
- onTerminalInput: (handler) => ui.onTerminalInput(handler),
40
- setEditorComponent: (factory) => ui.setEditorComponent(factory),
41
- setFooter: (factory) => ui.setFooter(factory),
42
- setHeader: (factory) => ui.setHeader(factory),
43
- setStatus: (key, text) => ui.setStatus(key, text),
44
- setToolsExpanded: (expanded) => ui.setToolsExpanded(expanded),
45
- setWidget: ui.setWidget.bind(ui) as PiUiPort["setWidget"],
46
- setWorkingIndicator: (options) => ui.setWorkingIndicator(options),
47
- setWorkingMessage: (message) => ui.setWorkingMessage(message),
48
- setWorkingVisible: (visible) => ui.setWorkingVisible(visible),
49
- theme: ui.theme,
50
- requestRender,
51
- };
52
- }
@@ -1,27 +0,0 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
-
3
- export type TuiCapabilities = Readonly<{
4
- interactive: boolean;
5
- customUi: boolean;
6
- editorFactory: boolean;
7
- footer: boolean;
8
- header: boolean;
9
- workingIndicator: boolean;
10
- overlays: boolean;
11
- }>;
12
-
13
- /**
14
- * Detects which interactive UI seams are available in the current host context.
15
- */
16
- export function detectTuiCapabilities(ctx: ExtensionContext): TuiCapabilities {
17
- const ui = ctx.ui as Partial<ExtensionContext["ui"]>;
18
- return {
19
- interactive: ctx.mode === "tui" && ctx.hasUI,
20
- customUi: typeof ui.custom === "function",
21
- editorFactory: typeof ui.setEditorComponent === "function",
22
- footer: typeof ui.setFooter === "function",
23
- header: typeof ui.setHeader === "function",
24
- workingIndicator: typeof ui.setWorkingIndicator === "function",
25
- overlays: typeof ui.custom === "function",
26
- };
27
- }
@@ -1,65 +0,0 @@
1
- export const LAYOUT_OWNERSHIP = {
2
- header: "header",
3
- context: "context",
4
- workingLine: "working-line",
5
- editor: "editor",
6
- footer: "footer",
7
- overlay: "overlay",
8
- input: "overlay",
9
- } as const;
10
-
11
- export type LayoutId = keyof typeof LAYOUT_OWNERSHIP;
12
- export type LayoutOwner = (typeof LAYOUT_OWNERSHIP)[LayoutId];
13
-
14
- type Claim = {
15
- owner: LayoutOwner;
16
- token: object;
17
- };
18
-
19
- /**
20
- * Tracks ownership of host UI seams. A layout can be claimed once and its
21
- * release callback is safe to call repeatedly or after another owner wins.
22
- */
23
- export class LayoutRegistry {
24
- private readonly claims = new Map<LayoutId, Claim>();
25
-
26
- /**
27
- * Claims one layout for a runtime token.
28
- *
29
- * @returns A release function that only removes this token's claim.
30
- */
31
- claim(layout: LayoutId, token: object): () => void {
32
- const owner = LAYOUT_OWNERSHIP[layout];
33
- const current = this.claims.get(layout);
34
- if (current && current.token !== token) {
35
- throw new Error(
36
- `UI layout ${layout} is already owned by ${current.owner}`,
37
- );
38
- }
39
- this.claims.set(layout, { owner, token });
40
- return () => {
41
- if (this.claims.get(layout)?.token === token) this.claims.delete(layout);
42
- };
43
- }
44
-
45
- /**
46
- * Returns the canonical owner assigned to a claimed layout.
47
- */
48
- ownerOf(layout: LayoutId): LayoutOwner | undefined {
49
- return this.claims.get(layout)?.owner;
50
- }
51
-
52
- /**
53
- * Reports whether a runtime token currently claims the layout.
54
- */
55
- isClaimed(layout: LayoutId): boolean {
56
- return this.claims.has(layout);
57
- }
58
-
59
- /**
60
- * Releases all claims, normally during runtime disposal.
61
- */
62
- clear(): void {
63
- this.claims.clear();
64
- }
65
- }
@@ -1,32 +0,0 @@
1
- /** Legacy slot map retained for existing callers during Layout extraction. */
2
- export const UI_OWNERSHIP = {
3
- editor: "editor",
4
- userMessage: "context",
5
- footer: "shell",
6
- workingLine: "shell",
7
- selector: "overlay",
8
- toolRenderer: "context",
9
- diffRenderer: "context",
10
- thinking: "context",
11
- agentSummary: "context",
12
- } as const;
13
-
14
- export type UiSlot = keyof typeof UI_OWNERSHIP;
15
- export type UiOwner = (typeof UI_OWNERSHIP)[UiSlot];
16
-
17
- /**
18
- * Returns the canonical visual owner for a Pi UI slot.
19
- *
20
- * @param slot UI slot to resolve.
21
- * @returns Canonical owner identifier.
22
- */
23
- export function ownerFor(slot: UiSlot): UiOwner {
24
- return UI_OWNERSHIP[slot];
25
- }
26
-
27
- export {
28
- LAYOUT_OWNERSHIP,
29
- type LayoutId,
30
- type LayoutOwner,
31
- LayoutRegistry,
32
- } from "./ownership/layout-registry.ts";