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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/extensions/app/config/renderer.ts +452 -0
- package/extensions/app/config/shell.ts +2484 -0
- package/extensions/app/ownership.ts +18 -0
- package/extensions/app/panel.ts +533 -0
- package/extensions/app/presets.ts +36 -0
- package/extensions/features/agent-summary/core.ts +194 -0
- package/extensions/features/agent-summary/index.ts +37 -0
- package/extensions/features/compact-thinking.ts +1262 -0
- package/extensions/features/context.ts +857 -0
- package/extensions/features/reference/index.ts +643 -0
- package/extensions/features/reference/session.ts +278 -0
- package/extensions/features/reference/subagent.ts +218 -0
- package/extensions/features/shell/aliases.ts +28 -0
- package/extensions/features/shell/flush-docked-bash.ts +39 -0
- package/extensions/features/shell/startup-header.ts +357 -0
- package/extensions/features/shell/working-message.ts +226 -0
- package/extensions/index.ts +52 -0
- package/extensions/shell/accent-rail-editor.ts +112 -0
- package/extensions/shell/accent-rail-layout-patch.ts +530 -0
- package/extensions/shell/completion-menu.ts +186 -0
- package/extensions/shell/editor-metadata-format.ts +287 -0
- package/extensions/shell/editor-transfer.ts +73 -0
- package/extensions/shell/extension-status.ts +102 -0
- package/extensions/shell/footer-format.ts +388 -0
- package/extensions/shell/footer-layout.ts +144 -0
- package/extensions/shell/footer.ts +1035 -0
- package/extensions/shell/format.ts +577 -0
- package/extensions/shell/git.ts +434 -0
- package/extensions/shell/icons.ts +261 -0
- package/extensions/shell/index.ts +1659 -0
- package/extensions/shell/interaction-summary.ts +1194 -0
- package/extensions/shell/live-context.ts +82 -0
- package/extensions/shell/minimalist-editor.ts +548 -0
- package/extensions/shell/package-version.ts +675 -0
- package/extensions/shell/project-refresh.ts +130 -0
- package/extensions/shell/project-state.ts +60 -0
- package/extensions/shell/prototype-patch-registry.ts +183 -0
- package/extensions/shell/runtime.ts +915 -0
- package/extensions/shell/selector-border.ts +109 -0
- package/extensions/shell/session-lifecycle.ts +60 -0
- package/extensions/shell/settings-command.ts +2144 -0
- package/extensions/shell/settings-previews.ts +137 -0
- package/extensions/shell/state.ts +83 -0
- package/extensions/shell/style.ts +480 -0
- package/extensions/shell/telemetry.ts +90 -0
- package/extensions/shell/ui.ts +1326 -0
- package/extensions/shell/user-message-osc.ts +279 -0
- package/extensions/shell/user-message-styles.ts +266 -0
- package/extensions/shell/user-message.ts +226 -0
- package/extensions/shell/working-line-messages.ts +24 -0
- package/extensions/shell/working-line-spinners.ts +54 -0
- package/extensions/shell/working-line.ts +1603 -0
- package/extensions/tools/ansi-color.ts +44 -0
- package/extensions/tools/ansi-text.ts +52 -0
- package/extensions/tools/component-tree.ts +37 -0
- package/extensions/tools/format.ts +28 -0
- package/extensions/tools/fullscreen-detect.ts +11 -0
- package/extensions/tools/patch-keys.ts +158 -0
- package/extensions/tools/sgr-mouse.ts +59 -0
- package/extensions/tools/tool-loading-icon.ts +20 -0
- package/extensions/tools/tool-result-sanitize.ts +27 -0
- package/extensions/transcript/index.ts +65 -0
- package/extensions/transcript/renderer/compact-mode.ts +1443 -0
- package/extensions/transcript/renderer/default-mode.ts +735 -0
- package/extensions/transcript/renderer/index.ts +286 -0
- package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
- package/extensions/transcript/renderer/mouse/hover.ts +175 -0
- package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
- package/extensions/transcript/renderer/mouse/layout.ts +207 -0
- package/extensions/transcript/renderer/mouse/packets.ts +96 -0
- package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
- package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
- package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
- package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
- package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
- package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
- package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
- package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
- package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
- package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
- package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
- package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
- package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
- package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
- package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
- package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
- package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
- package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
- package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
- package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
- package/extensions/transcript/renderer/tool/grouping.ts +619 -0
- package/extensions/transcript/renderer/tool/message-display.ts +183 -0
- package/extensions/transcript/renderer/tool/names.ts +195 -0
- package/extensions/transcript/renderer/tool/result.ts +842 -0
- package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
- package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
- package/package.json +58 -0
- package/themes/cc-dark.json +91 -0
- package/themes/cc-light.json +91 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** ANSI 16 色 → RGB(标准 xterm 色序)。 */
|
|
2
|
+
const ANSI_16_RGB: readonly [number, number, number][] = [
|
|
3
|
+
[0, 0, 0],
|
|
4
|
+
[128, 0, 0],
|
|
5
|
+
[0, 128, 0],
|
|
6
|
+
[128, 128, 0],
|
|
7
|
+
[0, 0, 128],
|
|
8
|
+
[128, 0, 128],
|
|
9
|
+
[0, 128, 128],
|
|
10
|
+
[192, 192, 192],
|
|
11
|
+
[128, 128, 128],
|
|
12
|
+
[255, 0, 0],
|
|
13
|
+
[0, 255, 0],
|
|
14
|
+
[255, 255, 0],
|
|
15
|
+
[0, 0, 255],
|
|
16
|
+
[255, 0, 255],
|
|
17
|
+
[0, 255, 255],
|
|
18
|
+
[255, 255, 255],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/** ANSI 16 色码(0-15)→ RGB;越界回退白。 */
|
|
22
|
+
export function ansi16ToRgb(index: number): [number, number, number] {
|
|
23
|
+
return ANSI_16_RGB[index] ?? [255, 255, 255];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** ANSI 256 色码 → RGB(0-15 标准色 + 232-255 灰阶 + 16-231 六阶立方体)。 */
|
|
27
|
+
export function ansi256ToRgb(code: number): [number, number, number] {
|
|
28
|
+
// SGR 正则可解析任意位数字,入口先规范:取整并钳制到合法区间,
|
|
29
|
+
// 否则灰阶分支对 code > 255 会算出超出 0-255 的非法 RGB。
|
|
30
|
+
if (!Number.isFinite(code)) return [0, 0, 0];
|
|
31
|
+
code = Math.min(255, Math.max(0, Math.trunc(code)));
|
|
32
|
+
if (code < 16) return ansi16ToRgb(code);
|
|
33
|
+
if (code >= 232) {
|
|
34
|
+
const gray = 8 + (code - 232) * 10;
|
|
35
|
+
return [gray, gray, gray];
|
|
36
|
+
}
|
|
37
|
+
const cubeIndex = code - 16;
|
|
38
|
+
const levels = [0, 95, 135, 175, 215, 255];
|
|
39
|
+
return [
|
|
40
|
+
levels[Math.floor(cubeIndex / 36)]!,
|
|
41
|
+
levels[Math.floor((cubeIndex % 36) / 6)]!,
|
|
42
|
+
levels[cubeIndex % 6]!,
|
|
43
|
+
];
|
|
44
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 顶层共享的 ANSI/终端控制序列处理。
|
|
3
|
+
*
|
|
4
|
+
* 收敛原先散落在 renderer/grouping.ts / renderer/mouse/packets.ts /
|
|
5
|
+
* renderer/compact-mode.ts 的重复剥离逻辑,供 renderer 与 feature 共用。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 单个 CSI 序列(颜色、光标等 SGR/CUP/ED 等)。 */
|
|
9
|
+
const CSI_SEQUENCE_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
10
|
+
/** OSC 序列(如 \x1b]8;;url\x07 或 ST 结尾)。 */
|
|
11
|
+
const OSC_SEQUENCE_RE = /\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
|
|
12
|
+
/** compact-mode 原有 hasVisibleText 用的更严格 OSC(内容不允许内嵌 ESC)。 */
|
|
13
|
+
const OSC_SEQUENCE_STRICT_RE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
14
|
+
|
|
15
|
+
/** 剥离一行内所有 CSI 序列。 */
|
|
16
|
+
export function stripAnsi(line: string): string {
|
|
17
|
+
return line.replace(CSI_SEQUENCE_RE, "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 仅剥离终端序列、保留原布局(换行/空白不动),用于命中区间计算。 */
|
|
21
|
+
export function stripTerminalSequencesPreservingLayout(value: string): string {
|
|
22
|
+
return value.replace(OSC_SEQUENCE_RE, "").replace(CSI_SEQUENCE_RE, "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 剥离终端序列并折叠空白(用于纯文本比较)。 */
|
|
26
|
+
export function stripTerminalSequences(value: string): string {
|
|
27
|
+
return stripTerminalSequencesPreservingLayout(value)
|
|
28
|
+
.replace(/\s+/g, " ")
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 去掉行内所有 CSI/OSC 序列后是否仍有可见文本(判断工具卡首尾内容行)。 */
|
|
33
|
+
export function hasVisibleText(line: string): boolean {
|
|
34
|
+
// OSC 用更严格的变体:内容内嵌 ESC 时不吞并后续文本,保持 compact-mode 原有语义。
|
|
35
|
+
return (
|
|
36
|
+
line.replace(CSI_SEQUENCE_RE, "").replace(OSC_SEQUENCE_STRICT_RE, "").trim()
|
|
37
|
+
.length > 0
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 剥离背景色 ANSI(用于重新铺背景行)。 */
|
|
42
|
+
export function stripBackgroundAnsi(line: string): string {
|
|
43
|
+
return line.replace(/\x1b\[(?:4[0-9]|10[0-7]|48(?:(?:;|:)[0-9]+)+|49)m/g, "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 剥离行首状态图标(展开组内工具首行复用)。 */
|
|
47
|
+
export function stripLeadingStatusIcon(line: string): string {
|
|
48
|
+
return line.replace(
|
|
49
|
+
/^((?:\x1b\[[0-9;]*m|[ \t]|[├└│─])*)(?:\x1b\[[0-9;]*m)*(?:[✓✗●○■⬤•·])(?:\x1b\[[0-9;]*m)*\s+/,
|
|
50
|
+
"$1",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 组件树递归遍历共享实现。
|
|
3
|
+
*
|
|
4
|
+
* 收敛原先散落在 renderer 的 compact-mode / message-display / transcript-refresh /
|
|
5
|
+
* grouping / mouse/packets 的「children + getMountedRoots + seen 防环」同款遍历。
|
|
6
|
+
*
|
|
7
|
+
* visitor 在每个非数组对象节点上调用一次;返回 `false` 表示不再下钻该节点的
|
|
8
|
+
* children 与 mounted roots(对应各处原有的提前 return 语义)。
|
|
9
|
+
*/
|
|
10
|
+
export function walkComponentTree(
|
|
11
|
+
root: any,
|
|
12
|
+
visitor: (value: any) => boolean | void,
|
|
13
|
+
): void {
|
|
14
|
+
const seen = new Set<any>();
|
|
15
|
+
const visit = (value: any): void => {
|
|
16
|
+
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
17
|
+
seen.add(value);
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
for (const child of value) visit(child);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (visitor(value) === false) return;
|
|
23
|
+
const children = value.children;
|
|
24
|
+
if (Array.isArray(children)) {
|
|
25
|
+
for (const child of children) visit(child);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const mounted = value.getMountedRoots?.();
|
|
29
|
+
if (Array.isArray(mounted)) {
|
|
30
|
+
for (const root of mounted) visit(root);
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// renderer 切换中的惰性 Proxy 可能暂时没有 mounted roots。
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
visit(root);
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { sanitizeToolResultText } from "./tool-result-sanitize.ts";
|
|
3
|
+
|
|
4
|
+
/** 毫秒 → "1h 2m 3s"/"2m 3s"/"3s";低于 1 秒返回 ""(省略)。 */
|
|
5
|
+
export function formatDuration(ms: number): string {
|
|
6
|
+
const totalSec = Math.floor(ms / 1000);
|
|
7
|
+
if (totalSec < 1) return "";
|
|
8
|
+
const hours = Math.floor(totalSec / 3600);
|
|
9
|
+
const minutes = Math.floor((totalSec % 3600) / 60);
|
|
10
|
+
const seconds = totalSec % 60;
|
|
11
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
12
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
13
|
+
return `${seconds}s`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 文本单行化:去换行空白,超宽截断加省略号。 */
|
|
17
|
+
export function oneLine(value: unknown, max = 96): string {
|
|
18
|
+
const text = sanitizeToolResultText(String(value ?? ""), 4096)
|
|
19
|
+
.replace(/\s+/g, " ")
|
|
20
|
+
.trim();
|
|
21
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 把一行文本补齐到固定可见宽度,超宽时截断并追加省略号。 */
|
|
25
|
+
export function padLine(text: string, width: number): string {
|
|
26
|
+
const truncated = truncateToWidth(text, width, "…");
|
|
27
|
+
return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
|
|
28
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 官方 0.84+ 的 tui 引用是惰性 Proxy(createInteractiveTuiReference):
|
|
3
|
+
* 函数属性每次 get 都返回新包装,执行时才解析到当前实现。
|
|
4
|
+
* 通过它捕获 doRender/render/handleInput 会解析到自身形成无限递归,
|
|
5
|
+
* 因此检测到惰性 Proxy 时必须跳过所有"捕获后包装"类 patch。
|
|
6
|
+
*/
|
|
7
|
+
export function isLazyProxyTui(tui: any): boolean {
|
|
8
|
+
if (!tui || typeof tui !== "object") return false;
|
|
9
|
+
const probe = tui.requestRender;
|
|
10
|
+
return typeof probe === "function" && probe !== tui.requestRender;
|
|
11
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ccstyle 补丁所有权与跨模块协调键的单一来源(single source of truth)。
|
|
3
|
+
*
|
|
4
|
+
* 全部使用 Symbol.for(全局注册表):/reload 后新模块实例拿到的是同一个 symbol,
|
|
5
|
+
* 这是跨 reload 识别/让渡补丁所有权的关键。切勿把这些 Symbol.for 改成模块局部 Symbol。
|
|
6
|
+
*
|
|
7
|
+
* 两类用途:
|
|
8
|
+
* 1. globalThis 槽位:安装补丁时写 host[KEY] = patch,dispose 时按所有权判断还原。
|
|
9
|
+
* 2. 实例/函数标记:把补丁包装器或组件实例标记在某个 symbol 属性上,供跨模块识别。
|
|
10
|
+
*
|
|
11
|
+
* PatchRegistry 把第 1 类的读写收口为带所有权语义的 install/dispose 生命周期;
|
|
12
|
+
* 底层仍是 globalThis[symbol],因此外部(含测试)直接读 globalThis 槽不受影响。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ── compact / compact-thinking 协调 ──
|
|
16
|
+
export const COMPACT_MODE_PATCH_KEY = Symbol.for(
|
|
17
|
+
"pi.ccstyle.compact-mode-patch",
|
|
18
|
+
);
|
|
19
|
+
export const COMPACT_THINKING_PATCH_KEY = Symbol.for(
|
|
20
|
+
"pi.ccstyle.compact-thinking-update",
|
|
21
|
+
);
|
|
22
|
+
export const PROTOTYPE_ORIGINAL_KEY = Symbol.for(
|
|
23
|
+
"pi.ccstyle.prototype-original",
|
|
24
|
+
);
|
|
25
|
+
export const COMPACT_THINKING_OWNER = Symbol.for(
|
|
26
|
+
"pi.ccstyle.compact-thinking-owner",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// ── assistant 展开状态(实例标记;description 供旧模块按 symbol.description 清理) ──
|
|
30
|
+
export const ASSISTANT_SET_EXPANDED_DESCRIPTION =
|
|
31
|
+
"pi.ccstyle.compact-assistant-set-expanded";
|
|
32
|
+
export const ASSISTANT_TOGGLE_ROUND_DESCRIPTION =
|
|
33
|
+
"pi.ccstyle.compact-assistant-toggle-round";
|
|
34
|
+
export const ASSISTANT_REENTRY_DESCRIPTION =
|
|
35
|
+
"pi.ccstyle.compact-assistant-reentry";
|
|
36
|
+
export const ASSISTANT_SET_EXPANDED_KEY = Symbol.for(
|
|
37
|
+
ASSISTANT_SET_EXPANDED_DESCRIPTION,
|
|
38
|
+
);
|
|
39
|
+
export const ASSISTANT_TOGGLE_ROUND_KEY = Symbol.for(
|
|
40
|
+
ASSISTANT_TOGGLE_ROUND_DESCRIPTION,
|
|
41
|
+
);
|
|
42
|
+
export const ASSISTANT_REENTRY_KEY = Symbol.for(ASSISTANT_REENTRY_DESCRIPTION);
|
|
43
|
+
|
|
44
|
+
// ── 工具渲染补丁 ──
|
|
45
|
+
export const GLOBAL_TOOL_RENDER_PATCH = Symbol.for(
|
|
46
|
+
"pi.ccstyle.global-tool-render-patch",
|
|
47
|
+
);
|
|
48
|
+
export const COMPONENT_TOOL_RENDER_MODE = Symbol.for(
|
|
49
|
+
"pi.ccstyle.component-tool-render-mode",
|
|
50
|
+
);
|
|
51
|
+
export const TOOL_EXPANDED_BACKGROUND_PATCH = Symbol.for(
|
|
52
|
+
"pi.ccstyle.tool-expanded-background-patch",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// ── 消息组件补丁 ──
|
|
56
|
+
export const MESSAGE_DISPLAY_PATCH = Symbol.for(
|
|
57
|
+
"pi.ccstyle.message-display-patch",
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// ── 压缩渲染补丁(/reload 残留 deactivate 用) ──
|
|
61
|
+
export const GLOBAL_COMPACTION_RENDER_PATCH = Symbol.for(
|
|
62
|
+
"pi.ccstyle.compaction-render-patch",
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// ── 工具分组 ──
|
|
66
|
+
export const TOOL_GROUPING_PATCH_KEY = Symbol.for(
|
|
67
|
+
"pi.ccstyle.tool-grouping-patch",
|
|
68
|
+
);
|
|
69
|
+
export const TOOL_GROUPING_PARENT_KEY = Symbol.for(
|
|
70
|
+
"pi.ccstyle.tool-grouping-parent",
|
|
71
|
+
);
|
|
72
|
+
export const TOOL_GROUPING_GENERATION_KEY = Symbol.for(
|
|
73
|
+
"pi.ccstyle.tool-grouping-generation",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// ── 鼠标交互 ──
|
|
77
|
+
export const TOOL_MOUSE_OWNER_KEY = Symbol.for("pi.ccstyle.tool-mouse-owner");
|
|
78
|
+
export const TOOL_MOUSE_TUI_SLOT = Symbol.for("pi.ccstyle.tool-mouse-tui");
|
|
79
|
+
export const SCROLL_BUTTON_STATE_SLOT = Symbol.for(
|
|
80
|
+
"pi.ccstyle.scroll-button-state",
|
|
81
|
+
);
|
|
82
|
+
export const FLUSH_DOCKED_BASH_PATCH = Symbol.for(
|
|
83
|
+
"pi.ccstyle.flush-docked-bash-patch",
|
|
84
|
+
);
|
|
85
|
+
export const TOOL_HOVER_STATE_KEY = Symbol.for("pi.ccstyle.tool-hover-state");
|
|
86
|
+
|
|
87
|
+
// ── rich diff 组件标记 ──
|
|
88
|
+
export const RICH_DIFF_COMPONENT = Symbol.for("pi.ccstyle.rich-diff-component");
|
|
89
|
+
|
|
90
|
+
// ── PatchRegistry:所有权生命周期统一收口 ──
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* host 上的补丁槽位读写器。把散落各处的
|
|
94
|
+
* `host[KEY] = patch` / `if (host[KEY] === patch) delete host[KEY]` 收口为
|
|
95
|
+
* 带所有权语义的 install/dispose 生命周期:
|
|
96
|
+
*
|
|
97
|
+
* - install:覆盖写入并返回被替换的旧值(不自动让旧值失效——部分安装器
|
|
98
|
+
* 需要先读旧补丁以链接下游方法,由调用方决定如何 inactivate)。
|
|
99
|
+
* - dispose:所有权守卫删除——仅当槽位仍 === 该值才删除,防止 /reload 后
|
|
100
|
+
* 旧模块误删新模块的补丁。
|
|
101
|
+
* - owns:判断槽位是否仍由某值持有(恒等比较)。
|
|
102
|
+
* - ensure:惰性状态槽(??= 语义),用于无所有权的跨模块共享状态。
|
|
103
|
+
*/
|
|
104
|
+
export class PatchRegistry {
|
|
105
|
+
readonly host: Record<PropertyKey, unknown>;
|
|
106
|
+
|
|
107
|
+
constructor(
|
|
108
|
+
host: Record<PropertyKey, unknown> = globalThis as unknown as Record<
|
|
109
|
+
PropertyKey,
|
|
110
|
+
unknown
|
|
111
|
+
>,
|
|
112
|
+
) {
|
|
113
|
+
this.host = host;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 读取槽位当前值。 */
|
|
117
|
+
get<T = unknown>(key: symbol): T | undefined {
|
|
118
|
+
return this.host[key] as T | undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 是否仍由 value 持有(恒等比较)。 */
|
|
122
|
+
owns(key: symbol, value: unknown): boolean {
|
|
123
|
+
return this.host[key] === value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 覆盖写入并返回被替换的旧值;不自动让旧值失效。 */
|
|
127
|
+
install<T = unknown>(key: symbol, value: T): T | undefined {
|
|
128
|
+
const previous = this.host[key] as T | undefined;
|
|
129
|
+
this.host[key] = value;
|
|
130
|
+
return previous;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 所有权守卫删除:仅当槽位仍 === value 时删除。返回是否删除。 */
|
|
134
|
+
dispose(key: symbol, value: unknown): boolean {
|
|
135
|
+
if (!this.owns(key, value)) return false;
|
|
136
|
+
delete this.host[key];
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 惰性槽位:无值(null/undefined)时用 init() 初始化并缓存。 */
|
|
141
|
+
ensure<T>(key: symbol, init: () => T): T {
|
|
142
|
+
const current = this.host[key];
|
|
143
|
+
if (current !== null && current !== undefined) return current as T;
|
|
144
|
+
const value = init();
|
|
145
|
+
this.host[key] = value;
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 无条件删除槽位(不判断所有权)。返回是否实际删除了键。 */
|
|
150
|
+
delete(key: symbol): boolean {
|
|
151
|
+
if (!(key in this.host)) return false;
|
|
152
|
+
delete this.host[key];
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 全局单例:host 固定为 globalThis,跨 /reload 与 jiti 重载稳定。 */
|
|
158
|
+
export const patchRegistry = new PatchRegistry();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SGR 鼠标协议解析(共享)。
|
|
3
|
+
*
|
|
4
|
+
* 收敛 feature/_util.ts 的单包解析与 renderer/mouse/packets.ts 的批量解析。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** SGR 鼠标协议包(code;col;row + M/m 终结符)。 */
|
|
8
|
+
export type SgrMousePacket = {
|
|
9
|
+
code: number;
|
|
10
|
+
col: number;
|
|
11
|
+
row: number;
|
|
12
|
+
final: "M" | "m";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** 解析单个完整 SGR 鼠标包(整段数据必须恰好是一个包)。 */
|
|
16
|
+
export function parseSgrMousePacket(data: string): SgrMousePacket | null {
|
|
17
|
+
const match = data.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
return {
|
|
20
|
+
code: Number(match[1]),
|
|
21
|
+
col: Number(match[2]),
|
|
22
|
+
row: Number(match[3]),
|
|
23
|
+
final: match[4] as "M" | "m",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 解析整段终端输入为 SGR 鼠标包序列;数据必须完全由连续 SGR 包组成
|
|
29
|
+
* (夹杂其他字节返回 null,交由常规输入链处理)。
|
|
30
|
+
*/
|
|
31
|
+
export function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
|
|
32
|
+
const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
33
|
+
const packets: SgrMousePacket[] = [];
|
|
34
|
+
let offset = 0;
|
|
35
|
+
|
|
36
|
+
for (const match of data.matchAll(pattern)) {
|
|
37
|
+
if (match.index !== offset) return null;
|
|
38
|
+
offset = match.index + match[0].length;
|
|
39
|
+
packets.push({
|
|
40
|
+
code: Number(match[1]),
|
|
41
|
+
col: Number(match[2]),
|
|
42
|
+
row: Number(match[3]),
|
|
43
|
+
final: match[4] as "M" | "m",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return packets.length > 0 && offset === data.length ? packets : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 是否为左键按下(排除修饰键、32 表示 motion 事件)。 */
|
|
51
|
+
export function isSgrLeftPress(packet: SgrMousePacket): boolean {
|
|
52
|
+
const baseButton = mouseBaseButton(packet.code);
|
|
53
|
+
return packet.final === "M" && baseButton === 0 && (packet.code & 32) === 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 剥离修饰键位(4/8/16/32:shift/meta/ctrl/motion),得到基础按键码。 */
|
|
57
|
+
export function mouseBaseButton(code: number): number {
|
|
58
|
+
return code & ~(4 | 8 | 16 | 32);
|
|
59
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const TOOL_LOADING_INTERVAL_MS = 80;
|
|
2
|
+
|
|
3
|
+
const BRAILLE_LOADING_FRAMES = [
|
|
4
|
+
"⠋",
|
|
5
|
+
"⠙",
|
|
6
|
+
"⠹",
|
|
7
|
+
"⠸",
|
|
8
|
+
"⠼",
|
|
9
|
+
"⠴",
|
|
10
|
+
"⠦",
|
|
11
|
+
"⠧",
|
|
12
|
+
"⠇",
|
|
13
|
+
"⠏",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function toolLoadingIcon(now = Date.now()): string {
|
|
17
|
+
return BRAILLE_LOADING_FRAMES[
|
|
18
|
+
Math.floor(now / TOOL_LOADING_INTERVAL_MS) % BRAILLE_LOADING_FRAMES.length
|
|
19
|
+
]!;
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const UNSAFE_TERMINAL_ESCAPE = new RegExp(
|
|
2
|
+
"(?:\\u001B\\]|\\u009D)[\\s\\S]*?(?:\\u0007|\\u001B\\x5C|\\u009C)" +
|
|
3
|
+
"|(?:\\u001B[PX^_]|\\u0090)[\\s\\S]*?(?:\\u001B\\x5C|\\u009C)" +
|
|
4
|
+
"|(?:\\u001B\\[|\\u009B)[0-?]*[ -/]*[@-~]" +
|
|
5
|
+
"|\\u001B[@-_]",
|
|
6
|
+
"g",
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Prevent captured terminal control responses from being replayed by tool renderers.
|
|
11
|
+
* Optional `maxChars` trims the input first so preview/one-line paths never scan multi-MB outputs.
|
|
12
|
+
*/
|
|
13
|
+
export function sanitizeToolResultText(
|
|
14
|
+
value: string,
|
|
15
|
+
maxChars?: number,
|
|
16
|
+
): string {
|
|
17
|
+
const source =
|
|
18
|
+
typeof maxChars === "number" && maxChars >= 0 && value.length > maxChars
|
|
19
|
+
? value.slice(0, maxChars)
|
|
20
|
+
: value;
|
|
21
|
+
return source
|
|
22
|
+
.replace(UNSAFE_TERMINAL_ESCAPE, "")
|
|
23
|
+
.replace(/\x1B/g, "")
|
|
24
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, "")
|
|
25
|
+
.replace(/\r\n/g, "\n")
|
|
26
|
+
.replace(/\r/g, "\n");
|
|
27
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { config } from "../app/config/renderer.ts";
|
|
6
|
+
|
|
7
|
+
// shell
|
|
8
|
+
import piAliases from "../features/shell/aliases.ts";
|
|
9
|
+
import { installFlushDockedBash } from "../features/shell/flush-docked-bash.ts";
|
|
10
|
+
import piStartupHeader from "../features/shell/startup-header.ts";
|
|
11
|
+
import workingMessage from "../features/shell/working-message.ts";
|
|
12
|
+
|
|
13
|
+
// feature
|
|
14
|
+
import agentAutocomplete from "../features/reference/subagent.ts";
|
|
15
|
+
import agentSummary from "../features/agent-summary/index.ts";
|
|
16
|
+
import context from "../features/context.ts";
|
|
17
|
+
import sessionReference from "../features/reference/index.ts";
|
|
18
|
+
import { installCompactThinking } from "../features/compact-thinking.ts";
|
|
19
|
+
|
|
20
|
+
// renderer
|
|
21
|
+
import registerTranscriptRenderer, {
|
|
22
|
+
getCompactThinkingConfig,
|
|
23
|
+
} from "./renderer/index.ts";
|
|
24
|
+
import markdownEnhance from "./renderer/markdown-enhance.ts";
|
|
25
|
+
|
|
26
|
+
export type TranscriptRuntimeController = {
|
|
27
|
+
setMode: (mode: "on" | "compact" | "off", ctx: ExtensionContext) => void;
|
|
28
|
+
updateConfig: (
|
|
29
|
+
partial: Partial<typeof config>,
|
|
30
|
+
ctx: ExtensionContext,
|
|
31
|
+
) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type TranscriptExtensionOptions = {
|
|
35
|
+
/** Expose live transcript refresh to the unified settings panel. */
|
|
36
|
+
onRuntimeController?: (controller: TranscriptRuntimeController) => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default function (
|
|
40
|
+
pi: ExtensionAPI,
|
|
41
|
+
options: TranscriptExtensionOptions = {},
|
|
42
|
+
): void {
|
|
43
|
+
// shell chrome
|
|
44
|
+
if (config.enableAliases) piAliases(pi);
|
|
45
|
+
installFlushDockedBash();
|
|
46
|
+
piStartupHeader(pi);
|
|
47
|
+
if (config.enableWorkingMessage) workingMessage(pi);
|
|
48
|
+
|
|
49
|
+
// render stack:thinking controller 直接交给 style 作 query
|
|
50
|
+
markdownEnhance(pi);
|
|
51
|
+
registerTranscriptRenderer(
|
|
52
|
+
pi,
|
|
53
|
+
undefined,
|
|
54
|
+
installCompactThinking(pi, getCompactThinkingConfig()),
|
|
55
|
+
{
|
|
56
|
+
onRuntimeController: options.onRuntimeController,
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// features
|
|
61
|
+
if (config.enableContextCommand) context(pi);
|
|
62
|
+
if (config.enableSessionReference) sessionReference(pi);
|
|
63
|
+
if (config.enableSubagentAutocomplete) agentAutocomplete(pi);
|
|
64
|
+
if (config.enableAgentSummary) agentSummary(pi);
|
|
65
|
+
}
|