pi-one-ui 0.1.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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/extensions/app/config/renderer.ts +452 -0
  4. package/extensions/app/config/shell.ts +2484 -0
  5. package/extensions/app/ownership.ts +18 -0
  6. package/extensions/app/panel.ts +533 -0
  7. package/extensions/app/presets.ts +36 -0
  8. package/extensions/features/agent-summary/core.ts +194 -0
  9. package/extensions/features/agent-summary/index.ts +37 -0
  10. package/extensions/features/compact-thinking.ts +1262 -0
  11. package/extensions/features/context.ts +857 -0
  12. package/extensions/features/reference/index.ts +643 -0
  13. package/extensions/features/reference/session.ts +278 -0
  14. package/extensions/features/reference/subagent.ts +218 -0
  15. package/extensions/features/shell/aliases.ts +28 -0
  16. package/extensions/features/shell/flush-docked-bash.ts +39 -0
  17. package/extensions/features/shell/startup-header.ts +357 -0
  18. package/extensions/features/shell/working-message.ts +226 -0
  19. package/extensions/index.ts +52 -0
  20. package/extensions/shell/accent-rail-editor.ts +112 -0
  21. package/extensions/shell/accent-rail-layout-patch.ts +530 -0
  22. package/extensions/shell/completion-menu.ts +186 -0
  23. package/extensions/shell/editor-metadata-format.ts +287 -0
  24. package/extensions/shell/editor-transfer.ts +73 -0
  25. package/extensions/shell/extension-status.ts +102 -0
  26. package/extensions/shell/footer-format.ts +388 -0
  27. package/extensions/shell/footer-layout.ts +144 -0
  28. package/extensions/shell/footer.ts +1035 -0
  29. package/extensions/shell/format.ts +577 -0
  30. package/extensions/shell/git.ts +434 -0
  31. package/extensions/shell/icons.ts +261 -0
  32. package/extensions/shell/index.ts +1659 -0
  33. package/extensions/shell/interaction-summary.ts +1194 -0
  34. package/extensions/shell/live-context.ts +82 -0
  35. package/extensions/shell/minimalist-editor.ts +548 -0
  36. package/extensions/shell/package-version.ts +675 -0
  37. package/extensions/shell/project-refresh.ts +130 -0
  38. package/extensions/shell/project-state.ts +60 -0
  39. package/extensions/shell/prototype-patch-registry.ts +183 -0
  40. package/extensions/shell/runtime.ts +915 -0
  41. package/extensions/shell/selector-border.ts +109 -0
  42. package/extensions/shell/session-lifecycle.ts +60 -0
  43. package/extensions/shell/settings-command.ts +2144 -0
  44. package/extensions/shell/settings-previews.ts +137 -0
  45. package/extensions/shell/state.ts +83 -0
  46. package/extensions/shell/style.ts +480 -0
  47. package/extensions/shell/telemetry.ts +90 -0
  48. package/extensions/shell/ui.ts +1326 -0
  49. package/extensions/shell/user-message-osc.ts +279 -0
  50. package/extensions/shell/user-message-styles.ts +266 -0
  51. package/extensions/shell/user-message.ts +226 -0
  52. package/extensions/shell/working-line-messages.ts +24 -0
  53. package/extensions/shell/working-line-spinners.ts +54 -0
  54. package/extensions/shell/working-line.ts +1603 -0
  55. package/extensions/tools/ansi-color.ts +44 -0
  56. package/extensions/tools/ansi-text.ts +52 -0
  57. package/extensions/tools/component-tree.ts +37 -0
  58. package/extensions/tools/format.ts +28 -0
  59. package/extensions/tools/fullscreen-detect.ts +11 -0
  60. package/extensions/tools/patch-keys.ts +158 -0
  61. package/extensions/tools/sgr-mouse.ts +59 -0
  62. package/extensions/tools/tool-loading-icon.ts +20 -0
  63. package/extensions/tools/tool-result-sanitize.ts +27 -0
  64. package/extensions/transcript/index.ts +65 -0
  65. package/extensions/transcript/renderer/compact-mode.ts +1443 -0
  66. package/extensions/transcript/renderer/default-mode.ts +735 -0
  67. package/extensions/transcript/renderer/index.ts +286 -0
  68. package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
  69. package/extensions/transcript/renderer/mouse/hover.ts +175 -0
  70. package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
  71. package/extensions/transcript/renderer/mouse/layout.ts +207 -0
  72. package/extensions/transcript/renderer/mouse/packets.ts +96 -0
  73. package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
  74. package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
  75. package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
  76. package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
  77. package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
  78. package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
  79. package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
  80. package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
  81. package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
  82. package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
  83. package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
  84. package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
  85. package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
  86. package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
  87. package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
  88. package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
  89. package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
  90. package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
  91. package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
  92. package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
  93. package/extensions/transcript/renderer/tool/grouping.ts +619 -0
  94. package/extensions/transcript/renderer/tool/message-display.ts +183 -0
  95. package/extensions/transcript/renderer/tool/names.ts +195 -0
  96. package/extensions/transcript/renderer/tool/result.ts +842 -0
  97. package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
  98. package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
  99. package/package.json +58 -0
  100. package/themes/cc-dark.json +91 -0
  101. package/themes/cc-light.json +91 -0
@@ -0,0 +1,147 @@
1
+ import {
2
+ createWriteToolDefinition,
3
+ type ExtensionAPI,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { truncateToWidth } from "@earendil-works/pi-tui";
6
+ import {
7
+ renderEditDiffResult,
8
+ renderWriteDiffResult,
9
+ type DisplayConfigInput,
10
+ } from "./diff-renderer.ts";
11
+ import { DEFAULT_TOOL_DISPLAY_CONFIG } from "../../../../app/config/renderer.ts";
12
+ import {
13
+ executeWriteWithMetadata,
14
+ WriteExecutionMetadataStore,
15
+ } from "./write-execution.ts";
16
+
17
+ function resultText(result: any): string {
18
+ const blocks = Array.isArray(result?.content) ? result.content : [];
19
+ return blocks
20
+ .filter(
21
+ (block: any) => block?.type === "text" && typeof block.text === "string",
22
+ )
23
+ .map((block: any) => block.text)
24
+ .join("\n");
25
+ }
26
+
27
+ function unavailableComponent(reason: string, theme: any) {
28
+ return {
29
+ render(width: number): string[] {
30
+ return [
31
+ truncateToWidth(
32
+ theme.fg("warning", `↳ diff unavailable: ${reason}`),
33
+ Math.max(0, width),
34
+ "",
35
+ ),
36
+ ];
37
+ },
38
+ invalidate() {},
39
+ };
40
+ }
41
+
42
+ export function renderRichToolResult(
43
+ toolName: string,
44
+ result: any,
45
+ options: any,
46
+ theme: any,
47
+ context: any,
48
+ writeMetadata: WriteExecutionMetadataStore,
49
+ /** Plain snapshot or live getter — getter lets /ccstyle panel changes repaint existing diffs. */
50
+ displayConfig: DisplayConfigInput = DEFAULT_TOOL_DISPLAY_CONFIG,
51
+ ): any | undefined {
52
+ if (options?.isPartial || options?.isError || context?.isError)
53
+ return undefined;
54
+ const expanded = options?.expanded === true || context?.expanded === true;
55
+ const filePath = context?.args?.file_path ?? context?.args?.path;
56
+ if (toolName === "edit") {
57
+ return renderEditDiffResult(
58
+ result?.details,
59
+ {
60
+ expanded,
61
+ filePath,
62
+ isHovered: options?.isHovered,
63
+ invalidate: () => context?.invalidate?.(),
64
+ },
65
+ displayConfig,
66
+ theme,
67
+ resultText(result),
68
+ );
69
+ }
70
+ if (toolName !== "write") return undefined;
71
+
72
+ const metadata = writeMetadata.get(context?.toolCallId);
73
+ if (!metadata) {
74
+ return unavailableComponent("execution metadata is unavailable", theme);
75
+ }
76
+ if (metadata.diffUnavailableReason) {
77
+ return unavailableComponent(metadata.diffUnavailableReason, theme);
78
+ }
79
+ return renderWriteDiffResult(
80
+ typeof context?.args?.content === "string"
81
+ ? context.args.content
82
+ : undefined,
83
+ {
84
+ expanded,
85
+ filePath,
86
+ previousContent: metadata.previousContent,
87
+ fileExistedBeforeWrite: metadata.fileExistedBeforeWrite,
88
+ isHovered: options?.isHovered,
89
+ invalidate: () => context?.invalidate?.(),
90
+ },
91
+ displayConfig,
92
+ theme,
93
+ resultText(result),
94
+ );
95
+ }
96
+
97
+ function hasExternalWriteOwner(pi: ExtensionAPI): boolean {
98
+ try {
99
+ const tools = pi.getAllTools();
100
+ const write = tools.find((tool: any) => tool?.name === "write") as any;
101
+ const source = write?.sourceInfo?.source;
102
+ return Boolean(write && typeof source === "string" && source !== "builtin");
103
+ } catch {
104
+ // getAllTools is unavailable before the extension runtime is bound.
105
+ return false;
106
+ }
107
+ }
108
+
109
+ export function installWriteOverride(
110
+ pi: ExtensionAPI,
111
+ store = new WriteExecutionMetadataStore(),
112
+ ): WriteExecutionMetadataStore {
113
+ if (
114
+ typeof (pi as any).registerTool !== "function" ||
115
+ hasExternalWriteOwner(pi)
116
+ )
117
+ return store;
118
+ const nativeWrite = createWriteToolDefinition(process.cwd()) as any;
119
+ pi.registerTool({
120
+ ...nativeWrite,
121
+ async execute(
122
+ toolCallId: string,
123
+ params: { path: string; content: string },
124
+ signal: AbortSignal | undefined,
125
+ _onUpdate: unknown,
126
+ ctx: { cwd: string },
127
+ ) {
128
+ return executeWriteWithMetadata(
129
+ store,
130
+ toolCallId,
131
+ params,
132
+ signal,
133
+ ctx.cwd,
134
+ );
135
+ },
136
+ });
137
+ return store;
138
+ }
139
+
140
+ export {
141
+ DEFAULT_TOOL_DISPLAY_CONFIG,
142
+ type ToolDisplayConfig,
143
+ type DiffViewMode,
144
+ type DiffIndicatorMode,
145
+ } from "../../../../app/config/renderer.ts";
146
+ export type { DisplayConfigInput } from "./diff-renderer.ts";
147
+ export { WriteExecutionMetadataStore } from "./write-execution.ts";
@@ -0,0 +1,100 @@
1
+ import { normalizeDiffRenderWidth } from "./diff-presentation.ts";
2
+ import { pluralize } from "./diff-text.ts";
3
+ import { showMoreHintText } from "../show-more-hint.ts";
4
+
5
+ export interface WidthMeasurementOps {
6
+ measure(text: string): number;
7
+ truncate(text: string, maxWidth: number): string;
8
+ }
9
+
10
+ export interface CollapsedDiffHintOptions {
11
+ remainingLines: number;
12
+ hiddenHunks: number;
13
+ }
14
+
15
+ function guardSafeWidth(width: number): number | undefined {
16
+ const safe = normalizeDiffRenderWidth(width);
17
+ return safe === 0 ? undefined : safe;
18
+ }
19
+
20
+ function renderWithSafeWidth<T>(
21
+ width: number,
22
+ fallback: T,
23
+ render: (safeWidth: number) => T,
24
+ ): T {
25
+ const safeWidth = guardSafeWidth(width);
26
+ if (safeWidth === undefined) {
27
+ return fallback;
28
+ }
29
+ return render(safeWidth);
30
+ }
31
+
32
+ export function clampRenderedLineToWidth(
33
+ text: string,
34
+ width: number,
35
+ ops: WidthMeasurementOps,
36
+ ): string {
37
+ return renderWithSafeWidth(width, "", (safeWidth) => {
38
+ if (ops.measure(text) <= safeWidth) {
39
+ return text;
40
+ }
41
+
42
+ for (let targetWidth = safeWidth; targetWidth >= 0; targetWidth--) {
43
+ const candidate = ops.truncate(text, targetWidth);
44
+ if (ops.measure(candidate) <= safeWidth) {
45
+ return candidate;
46
+ }
47
+ }
48
+
49
+ return "";
50
+ });
51
+ }
52
+
53
+ export function clampRenderedLinesToWidth(
54
+ lines: string[],
55
+ width: number,
56
+ ops: WidthMeasurementOps,
57
+ ): string[] {
58
+ return lines.map((line) => clampRenderedLineToWidth(line, width, ops));
59
+ }
60
+
61
+ export function buildCollapsedDiffHintText(
62
+ options: CollapsedDiffHintOptions,
63
+ width: number,
64
+ ops: WidthMeasurementOps,
65
+ ): string {
66
+ return renderWithSafeWidth(width, "", (safeWidth) => {
67
+ const remainingText = `${options.remainingLines} more ${pluralize(options.remainingLines, "diff line")}`;
68
+ const hiddenHunksText =
69
+ options.hiddenHunks > 0
70
+ ? `${options.hiddenHunks} more ${pluralize(options.hiddenHunks, "hunk")}`
71
+ : undefined;
72
+ const shortRemainingText = `${options.remainingLines} more ${pluralize(options.remainingLines, "line")}`;
73
+ const shortHiddenHunksText =
74
+ options.hiddenHunks > 0
75
+ ? `${options.hiddenHunks} ${pluralize(options.hiddenHunks, "hunk")}`
76
+ : undefined;
77
+
78
+ const candidates = [
79
+ `… (${[remainingText, hiddenHunksText, showMoreHintText()].filter(Boolean).join(" • ")})`,
80
+ `… (${[remainingText, hiddenHunksText].filter(Boolean).join(" • ")})`,
81
+ `… (${[shortRemainingText, shortHiddenHunksText].filter(Boolean).join(" • ")})`,
82
+ options.hiddenHunks > 0
83
+ ? `… (+${options.remainingLines} • +${options.hiddenHunks}h)`
84
+ : `… (+${options.remainingLines})`,
85
+ "…",
86
+ ];
87
+
88
+ for (const candidate of candidates) {
89
+ if (ops.measure(candidate) <= safeWidth) {
90
+ return candidate;
91
+ }
92
+ }
93
+
94
+ return clampRenderedLineToWidth(
95
+ candidates[candidates.length - 1] ?? "",
96
+ safeWidth,
97
+ ops,
98
+ );
99
+ });
100
+ }
@@ -0,0 +1,104 @@
1
+ export const MAX_HL_CHARS = 32_000;
2
+ const MAX_CACHE_ENTRIES = 48;
3
+
4
+ type CodeToAnsi = (code: string, lang: any, theme: any) => Promise<string>;
5
+ type Loader = () => Promise<CodeToAnsi>;
6
+ type Subscriber = { fallback: readonly string[]; invalidate: () => void };
7
+ type Pending = { work: Promise<void>; subscribers: Set<Subscriber> };
8
+
9
+ let defaultLoad: Promise<CodeToAnsi> | undefined;
10
+ const loadDefault: Loader = () => {
11
+ if (!defaultLoad) {
12
+ defaultLoad = import("@shikijs/cli")
13
+ .then((module) => module.codeToANSI as CodeToAnsi)
14
+ .catch((error) => {
15
+ defaultLoad = undefined;
16
+ throw error;
17
+ });
18
+ }
19
+ return defaultLoad;
20
+ };
21
+
22
+ function differs(
23
+ lines: readonly string[],
24
+ fallback: readonly string[],
25
+ ): boolean {
26
+ return (
27
+ lines.length !== fallback.length ||
28
+ lines.some((line, index) => line !== fallback[index])
29
+ );
30
+ }
31
+
32
+ export class ShikiHighlightCache {
33
+ private readonly cache = new Map<string, string[]>();
34
+ private readonly pending = new Map<string, Pending>();
35
+ private readonly loader: Loader;
36
+
37
+ constructor(loader: Loader = loadDefault) {
38
+ this.loader = loader;
39
+ }
40
+
41
+ get(
42
+ code: string,
43
+ lang: string | undefined,
44
+ theme: string,
45
+ fallback: readonly string[],
46
+ invalidate?: () => void,
47
+ ): string[] | undefined {
48
+ if (!lang || code.length > MAX_HL_CHARS) return undefined;
49
+ const key = `${theme}\0${lang}\0${code}`;
50
+ const cached = this.cache.get(key);
51
+ if (cached) {
52
+ this.cache.delete(key);
53
+ this.cache.set(key, cached);
54
+ return cached;
55
+ }
56
+
57
+ const existing = this.pending.get(key);
58
+ if (existing) {
59
+ if (
60
+ invalidate &&
61
+ ![...existing.subscribers].some(
62
+ (item) => item.invalidate === invalidate,
63
+ )
64
+ ) {
65
+ existing.subscribers.add({ fallback, invalidate });
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ const pending: Pending = {
71
+ work: undefined as never,
72
+ subscribers: new Set(),
73
+ };
74
+ if (invalidate) pending.subscribers.add({ fallback, invalidate });
75
+ pending.work = this.loader()
76
+ .then((highlight) => highlight(code, lang, theme))
77
+ .then((ansi) => {
78
+ const lines = ansi.replace(/\r/g, "").replace(/\n$/, "").split("\n");
79
+ this.cache.set(key, lines);
80
+ while (this.cache.size > MAX_CACHE_ENTRIES) {
81
+ const oldest = this.cache.keys().next().value;
82
+ if (oldest !== undefined) this.cache.delete(oldest);
83
+ }
84
+ for (const subscriber of pending.subscribers) {
85
+ if (!differs(lines, subscriber.fallback)) continue;
86
+ try {
87
+ subscriber.invalidate();
88
+ } catch {
89
+ // One component must not prevent the other subscribers from repainting.
90
+ }
91
+ }
92
+ })
93
+ .catch(() => {})
94
+ .finally(() => this.pending.delete(key));
95
+ this.pending.set(key, pending);
96
+ return undefined;
97
+ }
98
+
99
+ clear(): void {
100
+ this.cache.clear();
101
+ }
102
+ }
103
+
104
+ export const shikiHighlightCache = new ShikiHighlightCache();
@@ -0,0 +1,124 @@
1
+ import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
2
+ import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, resolve } from "node:path";
4
+
5
+ export const MAX_COMPARABLE_WRITE_BYTES = 512_000;
6
+ export const MAX_WRITE_METADATA_ENTRIES = 100;
7
+
8
+ export type WriteExecutionMeta = {
9
+ fileExistedBeforeWrite: boolean;
10
+ previousContent?: string;
11
+ diffUnavailableReason?: string;
12
+ };
13
+
14
+ export class WriteExecutionMetadataStore {
15
+ readonly entries = new Map<string, WriteExecutionMeta>();
16
+
17
+ set(toolCallId: string, metadata: WriteExecutionMeta): void {
18
+ this.entries.delete(toolCallId);
19
+ this.entries.set(toolCallId, metadata);
20
+ while (this.entries.size > MAX_WRITE_METADATA_ENTRIES) {
21
+ const oldest = this.entries.keys().next().value;
22
+ if (oldest === undefined) break;
23
+ this.entries.delete(oldest);
24
+ }
25
+ }
26
+
27
+ get(toolCallId: unknown): WriteExecutionMeta | undefined {
28
+ return typeof toolCallId === "string"
29
+ ? this.entries.get(toolCallId)
30
+ : undefined;
31
+ }
32
+
33
+ delete(toolCallId: string): void {
34
+ this.entries.delete(toolCallId);
35
+ }
36
+
37
+ clear(): void {
38
+ this.entries.clear();
39
+ }
40
+ }
41
+
42
+ async function capturePreviousContent(
43
+ absolutePath: string,
44
+ ): Promise<WriteExecutionMeta> {
45
+ let info;
46
+ try {
47
+ info = await lstat(absolutePath);
48
+ } catch (error: any) {
49
+ if (error?.code === "ENOENT") return { fileExistedBeforeWrite: false };
50
+ return {
51
+ fileExistedBeforeWrite: true,
52
+ diffUnavailableReason: "unable to inspect the previous file",
53
+ };
54
+ }
55
+
56
+ if (!info.isFile()) {
57
+ return {
58
+ fileExistedBeforeWrite: true,
59
+ diffUnavailableReason: "previous path is not a regular file",
60
+ };
61
+ }
62
+ if (info.size > MAX_COMPARABLE_WRITE_BYTES) {
63
+ return {
64
+ fileExistedBeforeWrite: true,
65
+ diffUnavailableReason: `previous file exceeds ${MAX_COMPARABLE_WRITE_BYTES} bytes`,
66
+ };
67
+ }
68
+
69
+ try {
70
+ const bytes = await readFile(absolutePath);
71
+ const previousContent = new TextDecoder("utf-8", { fatal: true }).decode(
72
+ bytes,
73
+ );
74
+ return { fileExistedBeforeWrite: true, previousContent };
75
+ } catch {
76
+ return {
77
+ fileExistedBeforeWrite: true,
78
+ diffUnavailableReason: "previous file is not comparable UTF-8 text",
79
+ };
80
+ }
81
+ }
82
+
83
+ export async function executeWriteWithMetadata(
84
+ store: WriteExecutionMetadataStore,
85
+ toolCallId: string,
86
+ params: { path: string; content: string },
87
+ signal: AbortSignal | undefined,
88
+ cwd: string,
89
+ ): Promise<{
90
+ content: Array<{ type: "text"; text: string }>;
91
+ details: undefined;
92
+ }> {
93
+ const absolutePath = isAbsolute(params.path)
94
+ ? params.path
95
+ : resolve(cwd, params.path);
96
+ store.delete(toolCallId);
97
+ try {
98
+ return await withFileMutationQueue(absolutePath, async () => {
99
+ const throwIfAborted = () => {
100
+ if (signal?.aborted) throw new Error("Operation aborted");
101
+ };
102
+ throwIfAborted();
103
+ const metadata = await capturePreviousContent(absolutePath);
104
+ throwIfAborted();
105
+ await mkdir(dirname(absolutePath), { recursive: true });
106
+ throwIfAborted();
107
+ await writeFile(absolutePath, params.content, "utf8");
108
+ throwIfAborted();
109
+ store.set(toolCallId, metadata);
110
+ return {
111
+ content: [
112
+ {
113
+ type: "text",
114
+ text: `Successfully wrote ${params.content.length} bytes to ${params.path}`,
115
+ },
116
+ ],
117
+ details: undefined,
118
+ };
119
+ });
120
+ } catch (error) {
121
+ store.delete(toolCallId);
122
+ throw error;
123
+ }
124
+ }