@herbertgao/pi-extensions 2026.8.1 → 2026.8.2
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.md +5 -2
- package/node_modules/@czottmann/pi-automode/README.md +16 -2
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +35 -0
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +18 -9
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +6 -2
- package/node_modules/@czottmann/pi-automode/examples/automode.local.json +6 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +11 -7
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +100 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +32 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +78 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +16 -4
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +23 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +22 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +18 -2
- package/node_modules/@czottmann/pi-automode/package.json +4 -4
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +23 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +99 -0
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +30 -0
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +16 -0
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +32 -0
- package/node_modules/@dietrichgebert/ponytail/LICENSE +21 -0
- package/node_modules/@dietrichgebert/ponytail/README.es.md +298 -0
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +315 -0
- package/node_modules/@dietrichgebert/ponytail/README.md +351 -0
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +21 -0
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +62 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +115 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +27 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +27 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +44 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +21 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +96 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +169 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +98 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +130 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +85 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-statusline.ps1 +24 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-statusline.sh +18 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +77 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +26 -0
- package/node_modules/@dietrichgebert/ponytail/package.json +47 -0
- package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +211 -0
- package/node_modules/@dietrichgebert/ponytail/pi-extension/package.json +8 -0
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +60 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +120 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +41 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +44 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +50 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +71 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +57 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -3
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +10 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary.ts +6 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -83
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/markdown-enhance.ts +119 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +73 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-hover.ts +130 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-interaction.ts +65 -542
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-layout.ts +160 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-packets.ts +143 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-scroll.ts +184 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool-diff/diff-renderer.ts +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool-grouping.ts +9 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +91 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/agent-summary.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +1 -0
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +2 -2
- package/node_modules/@herbertgao/pi-stash/README.md +9 -9
- package/node_modules/@herbertgao/pi-stash/package.json +1 -1
- package/node_modules/@herbertgao/pi-stash/src/index.ts +2 -1
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +14 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +6 -2
- package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +85 -11
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +28 -4
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +12 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +2 -0
- package/node_modules/@narumitw/pi-btw/LICENSE +21 -0
- package/node_modules/@narumitw/pi-btw/README.md +210 -0
- package/node_modules/@narumitw/pi-btw/package.json +53 -0
- package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +632 -0
- package/node_modules/@narumitw/pi-btw/src/btw.ts +996 -0
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +339 -0
- package/node_modules/@narumitw/pi-btw/src/index.ts +1 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +296 -0
- package/node_modules/@narumitw/pi-btw/src/settings.ts +237 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +238 -0
- package/node_modules/@narumitw/pi-btw/src/text.ts +10 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +620 -0
- package/node_modules/@pi-plugins/fast-mode/LICENSE +21 -0
- package/node_modules/@pi-plugins/fast-mode/README.md +92 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts +6 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +2026 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -0
- package/node_modules/@pi-plugins/fast-mode/package.json +73 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +59 -0
- package/node_modules/pi-mcp-adapter/README.md +62 -13
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +377 -0
- package/node_modules/pi-mcp-adapter/commands.ts +9 -7
- package/node_modules/pi-mcp-adapter/config.ts +110 -49
- package/node_modules/pi-mcp-adapter/direct-tools.ts +34 -19
- package/node_modules/pi-mcp-adapter/elicitation-handler.ts +37 -36
- package/node_modules/pi-mcp-adapter/errors.ts +21 -17
- package/node_modules/pi-mcp-adapter/host-html-template.ts +13 -7
- package/node_modules/pi-mcp-adapter/index.ts +37 -14
- package/node_modules/pi-mcp-adapter/init.ts +9 -7
- package/node_modules/pi-mcp-adapter/json-schema-validator.ts +2 -2
- package/node_modules/pi-mcp-adapter/lifecycle.ts +8 -8
- package/node_modules/pi-mcp-adapter/logger.ts +1 -1
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +36 -22
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +120 -2
- package/node_modules/pi-mcp-adapter/mcp-code.ts +13 -11
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +19 -13
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +15 -11
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +34 -15
- package/node_modules/pi-mcp-adapter/mcp-probe.ts +131 -34
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +2 -2
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +7 -4
- package/node_modules/pi-mcp-adapter/mcp-trace.ts +59 -64
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +34 -26
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +6 -4
- package/node_modules/pi-mcp-adapter/oauth-handler.ts +1 -1
- package/node_modules/pi-mcp-adapter/onboarding-state.ts +19 -11
- package/node_modules/pi-mcp-adapter/package.json +13 -8
- package/node_modules/pi-mcp-adapter/panel-keys.ts +20 -4
- package/node_modules/pi-mcp-adapter/prompts.ts +2 -2
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +48 -11
- package/node_modules/pi-mcp-adapter/sampling-handler.ts +22 -15
- package/node_modules/pi-mcp-adapter/search-ranking.ts +2 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +165 -113
- package/node_modules/pi-mcp-adapter/session-recovery.ts +11 -12
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +3 -3
- package/node_modules/pi-mcp-adapter/state.ts +3 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +67 -4
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -5
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +14 -2
- package/node_modules/pi-mcp-adapter/ts-shape.ts +9 -2
- package/node_modules/pi-mcp-adapter/types.ts +109 -22
- package/node_modules/pi-mcp-adapter/ui-app-bridge-helpers.ts +40 -0
- package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +28 -19
- package/node_modules/pi-mcp-adapter/ui-server.ts +90 -58
- package/node_modules/pi-mcp-adapter/ui-session.ts +16 -14
- package/node_modules/pi-mcp-adapter/unix-socket-transport.ts +4 -4
- package/node_modules/pi-mcp-adapter/utils.ts +23 -9
- package/package.json +22 -7
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@narumitw/pi-btw",
|
|
3
|
+
"version": "0.50.0",
|
|
4
|
+
"description": "Pi extension that adds a /btw side-question command.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"private": false,
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"pi",
|
|
12
|
+
"btw",
|
|
13
|
+
"side-question"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"pi": {
|
|
21
|
+
"extensions": [
|
|
22
|
+
"./src/index.ts"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"piExtension": {
|
|
26
|
+
"lifecycle": "stable"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"check": "biome check . && npm run typecheck",
|
|
30
|
+
"format": "biome check --write .",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@biomejs/biome": "2.5.7",
|
|
35
|
+
"@earendil-works/pi-ai": "0.84.1",
|
|
36
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
37
|
+
"@earendil-works/pi-tui": "0.84.1",
|
|
38
|
+
"typescript": "7.0.2"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/narumiruna/pi-extensions",
|
|
43
|
+
"directory": "packages/pi-btw"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@narumitw/pi-tui-kit": "^0.49.1"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@earendil-works/pi-ai": "*",
|
|
50
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
51
|
+
"@earendil-works/pi-tui": "*"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import type { KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
type Component,
|
|
4
|
+
Key,
|
|
5
|
+
matchesKey,
|
|
6
|
+
type TUI,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
visibleWidth,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
10
|
+
import type { SideThreadTurn } from "./side-thread.js";
|
|
11
|
+
|
|
12
|
+
const RESERVED_APP_ROWS = 3;
|
|
13
|
+
const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
14
|
+
|
|
15
|
+
export interface BtwBringToMainSegment {
|
|
16
|
+
role: "user" | "assistant";
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BtwBringToMainSummary {
|
|
21
|
+
lines: number;
|
|
22
|
+
messages: number;
|
|
23
|
+
tokens: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BtwSelectionLine {
|
|
27
|
+
role: BtwBringToMainSegment["role"];
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BtwTextPosition {
|
|
32
|
+
line: number;
|
|
33
|
+
column: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BtwTextRangeSelectorState {
|
|
37
|
+
cursor: BtwTextPosition;
|
|
38
|
+
anchor?: BtwTextPosition;
|
|
39
|
+
lineAnchor?: number;
|
|
40
|
+
preferredColumn: number;
|
|
41
|
+
scrollOffset: number;
|
|
42
|
+
horizontalOffset: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type BtwQuickBringToMainScope =
|
|
46
|
+
| { kind: "latest" }
|
|
47
|
+
| { kind: "from"; answeredTurnIndex: number }
|
|
48
|
+
| { kind: "entire" };
|
|
49
|
+
|
|
50
|
+
export type BtwTextRangeSelectorAction =
|
|
51
|
+
| { kind: "confirm"; segments: BtwBringToMainSegment[] }
|
|
52
|
+
| { kind: "back" }
|
|
53
|
+
| { kind: "close" };
|
|
54
|
+
|
|
55
|
+
export function getAnsweredTurns(
|
|
56
|
+
turns: readonly SideThreadTurn[],
|
|
57
|
+
): Array<Extract<SideThreadTurn, { kind: "answered" }>> {
|
|
58
|
+
return turns.filter(
|
|
59
|
+
(turn): turn is Extract<SideThreadTurn, { kind: "answered" }> => turn.kind === "answered",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildQuickBringToMainSegments(
|
|
64
|
+
turns: readonly SideThreadTurn[],
|
|
65
|
+
scope: BtwQuickBringToMainScope,
|
|
66
|
+
): BtwBringToMainSegment[] {
|
|
67
|
+
const answered = getAnsweredTurns(turns);
|
|
68
|
+
const selected =
|
|
69
|
+
scope.kind === "latest"
|
|
70
|
+
? answered.slice(-1)
|
|
71
|
+
: scope.kind === "from"
|
|
72
|
+
? answered.slice(Math.max(0, scope.answeredTurnIndex))
|
|
73
|
+
: answered;
|
|
74
|
+
return selected.flatMap((turn) => [
|
|
75
|
+
{ role: "user" as const, text: turn.question },
|
|
76
|
+
{ role: "assistant" as const, text: turn.answer },
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildBtwSelectionLines(turns: readonly SideThreadTurn[]): BtwSelectionLine[] {
|
|
81
|
+
return buildQuickBringToMainSegments(turns, { kind: "entire" }).flatMap((segment) =>
|
|
82
|
+
segment.text.split("\n").map((text) => ({ role: segment.role, text })),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function segmentsFromLineRange(
|
|
87
|
+
lines: readonly BtwSelectionLine[],
|
|
88
|
+
anchor: number,
|
|
89
|
+
cursor: number,
|
|
90
|
+
): BtwBringToMainSegment[] {
|
|
91
|
+
if (lines.length === 0) return [];
|
|
92
|
+
const start = Math.max(0, Math.min(anchor, cursor, lines.length - 1));
|
|
93
|
+
const end = Math.max(0, Math.min(Math.max(anchor, cursor), lines.length - 1));
|
|
94
|
+
const segments: BtwBringToMainSegment[] = [];
|
|
95
|
+
for (const line of lines.slice(start, end + 1)) {
|
|
96
|
+
const previous = segments.at(-1);
|
|
97
|
+
if (previous?.role === line.role) {
|
|
98
|
+
previous.text += `\n${line.text}`;
|
|
99
|
+
} else {
|
|
100
|
+
segments.push({ role: line.role, text: line.text });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return segments;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function segmentsFromTextRange(
|
|
107
|
+
lines: readonly BtwSelectionLine[],
|
|
108
|
+
anchor: BtwTextPosition,
|
|
109
|
+
cursor: BtwTextPosition,
|
|
110
|
+
): BtwBringToMainSegment[] {
|
|
111
|
+
if (lines.length === 0) return [];
|
|
112
|
+
const first = clampTextPosition(lines, anchor);
|
|
113
|
+
const second = clampTextPosition(lines, cursor);
|
|
114
|
+
const [start, end] = compareTextPositions(first, second) <= 0 ? [first, second] : [second, first];
|
|
115
|
+
if (compareTextPositions(start, end) === 0) return [];
|
|
116
|
+
|
|
117
|
+
const segments: BtwBringToMainSegment[] = [];
|
|
118
|
+
for (let lineIndex = start.line; lineIndex <= end.line; lineIndex += 1) {
|
|
119
|
+
const line = lines[lineIndex];
|
|
120
|
+
if (!line) continue;
|
|
121
|
+
const characters = splitGraphemes(line.text);
|
|
122
|
+
const from = lineIndex === start.line ? start.column : 0;
|
|
123
|
+
const to = lineIndex === end.line ? end.column : characters.length;
|
|
124
|
+
const text = characters.slice(from, to).join("");
|
|
125
|
+
if (text) {
|
|
126
|
+
const previous = segments.at(-1);
|
|
127
|
+
if (previous?.role === line.role) previous.text += text;
|
|
128
|
+
else segments.push({ role: line.role, text });
|
|
129
|
+
}
|
|
130
|
+
const crossesSameRoleLine = lineIndex < end.line && lines[lineIndex + 1]?.role === line.role;
|
|
131
|
+
if (crossesSameRoleLine) {
|
|
132
|
+
const current = segments.at(-1);
|
|
133
|
+
if (current?.role === line.role) current.text += "\n";
|
|
134
|
+
else segments.push({ role: line.role, text: "\n" });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return segments;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function estimateBringToMainTokens(segments: readonly BtwBringToMainSegment[]): number {
|
|
141
|
+
return Math.ceil(
|
|
142
|
+
Buffer.byteLength(segments.map((segment) => segment.text).join("\n"), "utf8") / 4,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function summarizeBringToMain(
|
|
147
|
+
segments: readonly BtwBringToMainSegment[],
|
|
148
|
+
): BtwBringToMainSummary {
|
|
149
|
+
return {
|
|
150
|
+
lines: segments.reduce((count, segment) => count + segment.text.split("\n").length, 0),
|
|
151
|
+
messages: segments.length,
|
|
152
|
+
tokens: estimateBringToMainTokens(segments),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatBtwBringToMain(segments: readonly BtwBringToMainSegment[]): string {
|
|
157
|
+
const body = segments
|
|
158
|
+
.map(
|
|
159
|
+
(segment) =>
|
|
160
|
+
`${segment.role === "user" ? "User" : "Assistant"}:\n${escapeBringToMainText(segment.text)}`,
|
|
161
|
+
)
|
|
162
|
+
.join("\n\n");
|
|
163
|
+
return [
|
|
164
|
+
"The following context was brought back from a /btw side discussion.",
|
|
165
|
+
"Treat it as discussion context, not as work already completed.",
|
|
166
|
+
"",
|
|
167
|
+
"<btw_context>",
|
|
168
|
+
body,
|
|
169
|
+
"</btw_context>",
|
|
170
|
+
].join("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export class BtwTextRangeSelector implements Component {
|
|
174
|
+
private readonly lines: BtwSelectionLine[];
|
|
175
|
+
private cursor: BtwTextPosition = { line: 0, column: 0 };
|
|
176
|
+
private anchor: BtwTextPosition | undefined;
|
|
177
|
+
private lineAnchor: number | undefined;
|
|
178
|
+
private preferredColumn = 0;
|
|
179
|
+
private scrollOffset = 0;
|
|
180
|
+
private horizontalOffset = 0;
|
|
181
|
+
private warning: string | undefined;
|
|
182
|
+
private finished = false;
|
|
183
|
+
|
|
184
|
+
constructor(
|
|
185
|
+
private readonly tui: TUI,
|
|
186
|
+
private readonly theme: Theme,
|
|
187
|
+
private readonly keybindings: KeybindingsManager,
|
|
188
|
+
turns: readonly SideThreadTurn[],
|
|
189
|
+
private readonly onAction: (action: BtwTextRangeSelectorAction) => void,
|
|
190
|
+
initialState?: BtwTextRangeSelectorState,
|
|
191
|
+
) {
|
|
192
|
+
this.lines = buildBtwSelectionLines(turns);
|
|
193
|
+
if (initialState) {
|
|
194
|
+
this.cursor = clampTextPosition(this.lines, initialState.cursor);
|
|
195
|
+
this.anchor = initialState.anchor
|
|
196
|
+
? clampTextPosition(this.lines, initialState.anchor)
|
|
197
|
+
: undefined;
|
|
198
|
+
this.lineAnchor =
|
|
199
|
+
initialState.lineAnchor === undefined
|
|
200
|
+
? undefined
|
|
201
|
+
: Math.max(0, Math.min(this.lines.length - 1, initialState.lineAnchor));
|
|
202
|
+
this.preferredColumn = Math.max(0, initialState.preferredColumn);
|
|
203
|
+
this.scrollOffset = Math.max(0, initialState.scrollOffset);
|
|
204
|
+
this.horizontalOffset = Math.max(0, initialState.horizontalOffset);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
getState(): BtwTextRangeSelectorState {
|
|
209
|
+
return {
|
|
210
|
+
cursor: { ...this.cursor },
|
|
211
|
+
anchor: this.anchor ? { ...this.anchor } : undefined,
|
|
212
|
+
lineAnchor: this.lineAnchor,
|
|
213
|
+
preferredColumn: this.preferredColumn,
|
|
214
|
+
scrollOffset: this.scrollOffset,
|
|
215
|
+
horizontalOffset: this.horizontalOffset,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
render(width: number): string[] {
|
|
220
|
+
const safeWidth = Math.max(1, width);
|
|
221
|
+
const availableRows = Math.max(1, this.tui.terminal.rows - RESERVED_APP_ROWS);
|
|
222
|
+
const showStatus = availableRows >= 4;
|
|
223
|
+
const showFooter = availableRows >= 3;
|
|
224
|
+
const viewportHeight = Math.max(
|
|
225
|
+
1,
|
|
226
|
+
availableRows - 1 - (showStatus ? 1 : 0) - (showFooter ? 1 : 0),
|
|
227
|
+
);
|
|
228
|
+
this.keepCursorVisible(viewportHeight);
|
|
229
|
+
const textWidth = Math.max(1, safeWidth - visibleWidth("●> Assistant │ "));
|
|
230
|
+
this.keepCursorHorizontallyVisible(textWidth);
|
|
231
|
+
const range = this.getSelectionRange();
|
|
232
|
+
const lineRange = this.getLineSelectionRange();
|
|
233
|
+
const visible = this.lines.slice(this.scrollOffset, this.scrollOffset + viewportHeight);
|
|
234
|
+
const rows = visible.map((line, visibleIndex) => {
|
|
235
|
+
const lineIndex = this.scrollOffset + visibleIndex;
|
|
236
|
+
const role = line.role === "user" ? "User" : "Assistant";
|
|
237
|
+
const lineSelected = lineRange
|
|
238
|
+
? lineIndex >= lineRange.start && lineIndex <= lineRange.end
|
|
239
|
+
: false;
|
|
240
|
+
const prefix = `${lineSelected ? "●" : " "}${lineIndex === this.cursor.line ? ">" : " "} ${role.padEnd(9)} │ `;
|
|
241
|
+
const text = this.renderTextLine(line, lineIndex, range, lineSelected);
|
|
242
|
+
return truncateToWidth(
|
|
243
|
+
lineIndex === this.cursor.line ? this.theme.fg("accent", prefix) + text : prefix + text,
|
|
244
|
+
safeWidth,
|
|
245
|
+
"",
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
const selected = this.getSelectedSegments();
|
|
249
|
+
const summary = summarizeBringToMain(selected);
|
|
250
|
+
const status =
|
|
251
|
+
selected.length === 0
|
|
252
|
+
? "Selected: none"
|
|
253
|
+
: `Selected: ${summary.lines} ${summary.lines === 1 ? "line" : "lines"} · ${summary.messages} ${summary.messages === 1 ? "message" : "messages"} · ~${summary.tokens} ${summary.tokens === 1 ? "token" : "tokens"}`;
|
|
254
|
+
const confirm = confirmKeyLabel(this.keybindings);
|
|
255
|
+
const back = keybindingLabel(this.keybindings, "tui.select.cancel", ["ctrl+c"]);
|
|
256
|
+
const vertical = `${keybindingLabel(this.keybindings, "tui.select.up")}/${keybindingLabel(this.keybindings, "tui.select.down")}`;
|
|
257
|
+
const confirmUsesSpace = this.keybindings.matches(" ", "tui.select.confirm");
|
|
258
|
+
const detailedFooter = this.warning
|
|
259
|
+
? `${this.warning} • ${confirmUsesSpace ? "Shift+Arrows select" : "Space lines • Shift+Arrows text"} • ${back} back • Ctrl+C close`
|
|
260
|
+
: this.lineAnchor !== undefined
|
|
261
|
+
? `${confirmUsesSpace ? `${vertical} extend lines` : `Space clear • ${vertical} extend lines`} • Shift+Arrows text • ${confirm} bring • ${back} back • Ctrl+C close`
|
|
262
|
+
: `Shift+Arrows select • Arrows move${confirmUsesSpace ? "" : " • Space lines"} • ${confirm} bring • ${back} back • Ctrl+C close`;
|
|
263
|
+
const criticalFooter = `${confirm} bring • ${back} back • Ctrl+C close`;
|
|
264
|
+
const footer = visibleWidth(detailedFooter) <= safeWidth ? detailedFooter : criticalFooter;
|
|
265
|
+
return fitRows(
|
|
266
|
+
[
|
|
267
|
+
truncateToWidth(
|
|
268
|
+
this.theme.fg("accent", this.theme.bold("Select text to bring to main")),
|
|
269
|
+
safeWidth,
|
|
270
|
+
"",
|
|
271
|
+
),
|
|
272
|
+
...(showStatus ? [truncateToWidth(this.theme.fg("muted", status), safeWidth, "")] : []),
|
|
273
|
+
...rows,
|
|
274
|
+
...(showFooter
|
|
275
|
+
? [
|
|
276
|
+
truncateToWidth(
|
|
277
|
+
this.theme.fg(this.warning ? "warning" : "muted", footer),
|
|
278
|
+
safeWidth,
|
|
279
|
+
"",
|
|
280
|
+
),
|
|
281
|
+
]
|
|
282
|
+
: []),
|
|
283
|
+
],
|
|
284
|
+
availableRows,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
handleInput(data: string): void {
|
|
289
|
+
if (this.finished) return;
|
|
290
|
+
if (matchesKey(data, Key.ctrl("c"))) {
|
|
291
|
+
this.finish({ kind: "close" });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
295
|
+
this.finish({ kind: "back" });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (matchesConfirm(data, this.keybindings)) {
|
|
299
|
+
if (this.lines.length > 0) {
|
|
300
|
+
const segments = this.getSelectedSegments();
|
|
301
|
+
if (segments.length === 0) {
|
|
302
|
+
this.warning = "Select text first";
|
|
303
|
+
this.tui.requestRender();
|
|
304
|
+
} else {
|
|
305
|
+
this.finish({ kind: "confirm", segments });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (matchesKey(data, Key.space)) {
|
|
311
|
+
this.anchor = undefined;
|
|
312
|
+
this.lineAnchor = this.lineAnchor === undefined ? this.cursor.line : undefined;
|
|
313
|
+
this.afterMove();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (matchesKey(data, Key.shift("left"))) {
|
|
317
|
+
this.moveHorizontal(-1, true);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (matchesKey(data, Key.shift("right"))) {
|
|
321
|
+
this.moveHorizontal(1, true);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (matchesKey(data, Key.shift("up"))) {
|
|
325
|
+
this.moveVertical(-1, true);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (matchesKey(data, Key.shift("down"))) {
|
|
329
|
+
this.moveVertical(1, true);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (matchesKey(data, Key.left)) {
|
|
333
|
+
this.moveHorizontal(-1, false);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (matchesKey(data, Key.right)) {
|
|
337
|
+
this.moveHorizontal(1, false);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (this.keybindings.matches(data, "tui.select.up")) {
|
|
341
|
+
this.moveVertical(-1, false);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (this.keybindings.matches(data, "tui.select.down")) {
|
|
345
|
+
this.moveVertical(1, false);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (this.keybindings.matches(data, "tui.select.pageUp")) {
|
|
349
|
+
this.moveVertical(-10, false);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (this.keybindings.matches(data, "tui.select.pageDown")) {
|
|
353
|
+
this.moveVertical(10, false);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
invalidate(): void {}
|
|
359
|
+
|
|
360
|
+
private renderTextLine(
|
|
361
|
+
line: BtwSelectionLine,
|
|
362
|
+
lineIndex: number,
|
|
363
|
+
range: { start: BtwTextPosition; end: BtwTextPosition } | undefined,
|
|
364
|
+
lineSelected: boolean,
|
|
365
|
+
): string {
|
|
366
|
+
const characters = splitGraphemes(line.text);
|
|
367
|
+
let rendered = this.horizontalOffset > 0 ? this.theme.fg("muted", "…") : "";
|
|
368
|
+
let buffer = "";
|
|
369
|
+
let bufferSelected = false;
|
|
370
|
+
const flush = () => {
|
|
371
|
+
if (!buffer) return;
|
|
372
|
+
rendered += bufferSelected
|
|
373
|
+
? this.theme.bg("selectedBg", this.theme.fg("text", buffer))
|
|
374
|
+
: buffer;
|
|
375
|
+
buffer = "";
|
|
376
|
+
};
|
|
377
|
+
for (let column = this.horizontalOffset; column <= characters.length; column += 1) {
|
|
378
|
+
if (range && lineIndex === range.start.line && column === range.start.column) {
|
|
379
|
+
flush();
|
|
380
|
+
rendered += this.theme.fg("accent", "[");
|
|
381
|
+
}
|
|
382
|
+
if (lineIndex === this.cursor.line && column === this.cursor.column) {
|
|
383
|
+
flush();
|
|
384
|
+
rendered += this.theme.fg("accent", "│");
|
|
385
|
+
}
|
|
386
|
+
if (range && lineIndex === range.end.line && column === range.end.column) {
|
|
387
|
+
flush();
|
|
388
|
+
rendered += this.theme.fg("accent", "]");
|
|
389
|
+
}
|
|
390
|
+
const character = characters[column];
|
|
391
|
+
if (character === undefined) continue;
|
|
392
|
+
const selected =
|
|
393
|
+
lineSelected || (range ? positionFallsInside(lineIndex, column, range) : false);
|
|
394
|
+
if (buffer && selected !== bufferSelected) flush();
|
|
395
|
+
bufferSelected = selected;
|
|
396
|
+
buffer += escapeTerminalControls(character);
|
|
397
|
+
}
|
|
398
|
+
flush();
|
|
399
|
+
return rendered;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private getSelectionRange(): { start: BtwTextPosition; end: BtwTextPosition } | undefined {
|
|
403
|
+
if (!this.anchor || compareTextPositions(this.anchor, this.cursor) === 0) return undefined;
|
|
404
|
+
return compareTextPositions(this.anchor, this.cursor) < 0
|
|
405
|
+
? { start: this.anchor, end: this.cursor }
|
|
406
|
+
: { start: this.cursor, end: this.anchor };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private getLineSelectionRange(): { start: number; end: number } | undefined {
|
|
410
|
+
return this.lineAnchor === undefined
|
|
411
|
+
? undefined
|
|
412
|
+
: {
|
|
413
|
+
start: Math.min(this.lineAnchor, this.cursor.line),
|
|
414
|
+
end: Math.max(this.lineAnchor, this.cursor.line),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private getSelectedSegments(): BtwBringToMainSegment[] {
|
|
419
|
+
if (this.lineAnchor !== undefined) {
|
|
420
|
+
return segmentsFromLineRange(this.lines, this.lineAnchor, this.cursor.line);
|
|
421
|
+
}
|
|
422
|
+
return this.anchor ? segmentsFromTextRange(this.lines, this.anchor, this.cursor) : [];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private moveHorizontal(delta: -1 | 1, extend: boolean): void {
|
|
426
|
+
if (this.lines.length === 0) return;
|
|
427
|
+
if (!extend) this.lineAnchor = undefined;
|
|
428
|
+
if (!extend && this.anchor) {
|
|
429
|
+
const range = this.getSelectionRange();
|
|
430
|
+
if (range) this.cursor = delta < 0 ? range.start : range.end;
|
|
431
|
+
this.anchor = undefined;
|
|
432
|
+
this.preferredColumn = this.cursor.column;
|
|
433
|
+
this.afterMove();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
this.beginOrClearSelection(extend);
|
|
437
|
+
const line = this.lines[this.cursor.line];
|
|
438
|
+
const length = line ? splitGraphemes(line.text).length : 0;
|
|
439
|
+
if (delta < 0) {
|
|
440
|
+
if (this.cursor.column > 0) this.cursor = { ...this.cursor, column: this.cursor.column - 1 };
|
|
441
|
+
else if (this.cursor.line > 0) {
|
|
442
|
+
const previousLine = this.lines[this.cursor.line - 1];
|
|
443
|
+
this.cursor = {
|
|
444
|
+
line: this.cursor.line - 1,
|
|
445
|
+
column: previousLine ? splitGraphemes(previousLine.text).length : 0,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
} else if (this.cursor.column < length) {
|
|
449
|
+
this.cursor = { ...this.cursor, column: this.cursor.column + 1 };
|
|
450
|
+
} else if (this.cursor.line < this.lines.length - 1) {
|
|
451
|
+
this.cursor = { line: this.cursor.line + 1, column: 0 };
|
|
452
|
+
}
|
|
453
|
+
this.preferredColumn = this.cursor.column;
|
|
454
|
+
this.afterMove();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private moveVertical(delta: number, extend: boolean): void {
|
|
458
|
+
if (this.lines.length === 0) return;
|
|
459
|
+
if (extend || this.lineAnchor === undefined) this.beginOrClearSelection(extend);
|
|
460
|
+
const line = Math.max(0, Math.min(this.lines.length - 1, this.cursor.line + delta));
|
|
461
|
+
const target = this.lines[line];
|
|
462
|
+
this.cursor = {
|
|
463
|
+
line,
|
|
464
|
+
column: Math.min(this.preferredColumn, target ? splitGraphemes(target.text).length : 0),
|
|
465
|
+
};
|
|
466
|
+
this.afterMove();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private beginOrClearSelection(extend: boolean): void {
|
|
470
|
+
if (extend) this.lineAnchor = undefined;
|
|
471
|
+
if (extend && !this.anchor) this.anchor = { ...this.cursor };
|
|
472
|
+
if (!extend) this.anchor = undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private afterMove(): void {
|
|
476
|
+
this.warning = undefined;
|
|
477
|
+
this.tui.requestRender();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private keepCursorVisible(height: number): void {
|
|
481
|
+
if (height <= 0) return;
|
|
482
|
+
if (this.cursor.line < this.scrollOffset) this.scrollOffset = this.cursor.line;
|
|
483
|
+
if (this.cursor.line >= this.scrollOffset + height) {
|
|
484
|
+
this.scrollOffset = this.cursor.line - height + 1;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private keepCursorHorizontallyVisible(width: number): void {
|
|
489
|
+
const characters = splitGraphemes(this.lines[this.cursor.line]?.text ?? "");
|
|
490
|
+
const displayWidths = characters.map((character) =>
|
|
491
|
+
visibleWidth(escapeTerminalControls(character)),
|
|
492
|
+
);
|
|
493
|
+
const currentWidth = displayWidths[this.cursor.column] ?? 0;
|
|
494
|
+
let usedWidth = 1 + Math.min(currentWidth, Math.max(0, width - 1));
|
|
495
|
+
let offset = this.cursor.column;
|
|
496
|
+
for (let index = this.cursor.column - 1; index >= 0; index -= 1) {
|
|
497
|
+
const nextWidth = usedWidth + (displayWidths[index] ?? 0) + (index > 0 ? 1 : 0);
|
|
498
|
+
if (nextWidth > width) break;
|
|
499
|
+
usedWidth += displayWidths[index] ?? 0;
|
|
500
|
+
offset = index;
|
|
501
|
+
}
|
|
502
|
+
this.horizontalOffset = offset;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private finish(action: BtwTextRangeSelectorAction): void {
|
|
506
|
+
if (this.finished) return;
|
|
507
|
+
this.finished = true;
|
|
508
|
+
this.onAction(action);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function clampTextPosition(
|
|
513
|
+
lines: readonly BtwSelectionLine[],
|
|
514
|
+
position: BtwTextPosition,
|
|
515
|
+
): BtwTextPosition {
|
|
516
|
+
const line = Math.max(0, Math.min(lines.length - 1, position.line));
|
|
517
|
+
const text = lines[line]?.text ?? "";
|
|
518
|
+
return {
|
|
519
|
+
line,
|
|
520
|
+
column: Math.max(0, Math.min(splitGraphemes(text).length, position.column)),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function confirmKeyLabel(keybindings: KeybindingsManager): string {
|
|
525
|
+
return keybindingLabel(keybindings, "tui.select.confirm", ["ctrl+c"], "enter");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function matchesConfirm(data: string, keybindings: KeybindingsManager): boolean {
|
|
529
|
+
if (matchesKey(data, Key.ctrl("c"))) return false;
|
|
530
|
+
const hasUsableBinding = keybindings
|
|
531
|
+
.getKeys("tui.select.confirm")
|
|
532
|
+
.map(String)
|
|
533
|
+
.some((key) => key.toLowerCase() !== "ctrl+c");
|
|
534
|
+
return (
|
|
535
|
+
keybindings.matches(data, "tui.select.confirm") ||
|
|
536
|
+
(!hasUsableBinding && matchesKey(data, Key.enter))
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function keybindingLabel(
|
|
541
|
+
keybindings: KeybindingsManager,
|
|
542
|
+
keybinding:
|
|
543
|
+
| "tui.select.confirm"
|
|
544
|
+
| "tui.select.cancel"
|
|
545
|
+
| "tui.select.up"
|
|
546
|
+
| "tui.select.down"
|
|
547
|
+
| "tui.select.pageUp"
|
|
548
|
+
| "tui.select.pageDown",
|
|
549
|
+
excluded: readonly string[] = [],
|
|
550
|
+
fallback?: string,
|
|
551
|
+
): string {
|
|
552
|
+
const key = keybindings
|
|
553
|
+
.getKeys(keybinding)
|
|
554
|
+
.map(String)
|
|
555
|
+
.find((candidate) => !excluded.includes(candidate.toLowerCase()));
|
|
556
|
+
return formatKeyLabel(key ?? fallback ?? keybinding);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function formatKeyLabel(key: string): string {
|
|
560
|
+
return key
|
|
561
|
+
.split("+")
|
|
562
|
+
.map((part) => {
|
|
563
|
+
const lower = part.toLowerCase();
|
|
564
|
+
if (lower === "ctrl") return "Ctrl";
|
|
565
|
+
if (lower === "alt") return "Alt";
|
|
566
|
+
if (lower === "shift") return "Shift";
|
|
567
|
+
if (lower === "escape" || lower === "esc") return "Esc";
|
|
568
|
+
if (lower === "enter" || lower === "return") return "Enter";
|
|
569
|
+
if (lower === "pageup") return "PgUp";
|
|
570
|
+
if (lower === "pagedown") return "PgDn";
|
|
571
|
+
return part.length === 1
|
|
572
|
+
? part.toUpperCase()
|
|
573
|
+
: `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`;
|
|
574
|
+
})
|
|
575
|
+
.join("+");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function splitGraphemes(text: string): string[] {
|
|
579
|
+
return [...GRAPHEME_SEGMENTER.segment(text)].map(({ segment }) => segment);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function compareTextPositions(first: BtwTextPosition, second: BtwTextPosition): number {
|
|
583
|
+
return first.line === second.line ? first.column - second.column : first.line - second.line;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function positionFallsInside(
|
|
587
|
+
line: number,
|
|
588
|
+
column: number,
|
|
589
|
+
range: { start: BtwTextPosition; end: BtwTextPosition },
|
|
590
|
+
): boolean {
|
|
591
|
+
const position = { line, column };
|
|
592
|
+
return (
|
|
593
|
+
compareTextPositions(position, range.start) >= 0 &&
|
|
594
|
+
compareTextPositions(position, range.end) < 0
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function fitRows(rows: string[], availableRows: number): string[] {
|
|
599
|
+
if (rows.length <= availableRows) return rows;
|
|
600
|
+
if (availableRows <= 1) return rows.slice(0, 1);
|
|
601
|
+
return [rows[0] ?? "", ...rows.slice(rows.length - availableRows + 1)];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function escapeBringToMainText(text: string): string {
|
|
605
|
+
return [...text]
|
|
606
|
+
.map((character) => {
|
|
607
|
+
if (character === "\n") return character;
|
|
608
|
+
if (character === "\t") return " ";
|
|
609
|
+
const code = character.charCodeAt(0);
|
|
610
|
+
if (code <= 31 || (code >= 127 && code <= 159)) {
|
|
611
|
+
return `\\x${code.toString(16).padStart(2, "0")}`;
|
|
612
|
+
}
|
|
613
|
+
return character;
|
|
614
|
+
})
|
|
615
|
+
.join("")
|
|
616
|
+
.replace(/<btw_context(?=[ \t\r\n>])/g, "<btw_context")
|
|
617
|
+
.replace(/<\/btw_context[ \t\r\n]*>/g, (terminator) =>
|
|
618
|
+
terminator.replaceAll("<", "<").replaceAll(">", ">"),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function escapeTerminalControls(text: string): string {
|
|
623
|
+
return [...text]
|
|
624
|
+
.map((character) => {
|
|
625
|
+
const code = character.charCodeAt(0);
|
|
626
|
+
if (code <= 31 || (code >= 127 && code <= 159)) {
|
|
627
|
+
return `\\x${code.toString(16).padStart(2, "0")}`;
|
|
628
|
+
}
|
|
629
|
+
return character;
|
|
630
|
+
})
|
|
631
|
+
.join("");
|
|
632
|
+
}
|