pi-ui-extend 0.1.26 → 0.1.28
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/dist/app/icons.d.ts +3 -0
- package/dist/app/icons.js +6 -0
- package/dist/app/rendering/conversation-tool-renderer.js +3 -3
- package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
- package/dist/app/rendering/tool-block-renderer.js +76 -21
- package/dist/app/screen/file-link-opener.js +5 -0
- package/dist/app/screen/file-links.js +13 -1
- package/dist/app/screen/mouse-controller.d.ts +3 -0
- package/dist/app/screen/mouse-controller.js +57 -7
- package/extensions/session-title/index.ts +8 -19
- package/package.json +1 -1
package/dist/app/icons.d.ts
CHANGED
|
@@ -25,6 +25,9 @@ declare const NERD_FONT_ICONS: {
|
|
|
25
25
|
readonly thinkingExpanded: "";
|
|
26
26
|
readonly stopCircle: "";
|
|
27
27
|
readonly timerSand: "";
|
|
28
|
+
readonly toolBodyEnd: "└";
|
|
29
|
+
readonly toolBodyGutter: "│";
|
|
30
|
+
readonly toolPreviewTruncated: "⊞";
|
|
28
31
|
readonly down: "";
|
|
29
32
|
};
|
|
30
33
|
export type AppIconName = keyof typeof NERD_FONT_ICONS;
|
package/dist/app/icons.js
CHANGED
|
@@ -31,6 +31,9 @@ const NERD_FONT_ICONS = {
|
|
|
31
31
|
thinkingExpanded: "\u{f0335}",
|
|
32
32
|
stopCircle: "\u{f0665}",
|
|
33
33
|
timerSand: "\u{f051f}",
|
|
34
|
+
toolBodyEnd: "└",
|
|
35
|
+
toolBodyGutter: "│",
|
|
36
|
+
toolPreviewTruncated: "⊞",
|
|
34
37
|
down: "\u{f0045}",
|
|
35
38
|
};
|
|
36
39
|
const FALLBACK_ICONS = {
|
|
@@ -58,6 +61,9 @@ const FALLBACK_ICONS = {
|
|
|
58
61
|
thinkingExpanded: ">",
|
|
59
62
|
stopCircle: "■",
|
|
60
63
|
timerSand: "⏳",
|
|
64
|
+
toolBodyEnd: "`",
|
|
65
|
+
toolBodyGutter: "|",
|
|
66
|
+
toolPreviewTruncated: "+",
|
|
61
67
|
down: "v",
|
|
62
68
|
};
|
|
63
69
|
export const APP_ICON_THEMES = {
|
|
@@ -42,7 +42,7 @@ export function renderConversationToolEntry(entry, width, options) {
|
|
|
42
42
|
collapsedBody: display.collapsedBody,
|
|
43
43
|
expandedText: display.expandedText,
|
|
44
44
|
syntaxHighlight: display.syntaxHighlight,
|
|
45
|
-
}, rule, width, options.colors, { superCompact: Boolean(options.superCompactTools) });
|
|
45
|
+
}, rule, width, options.colors, { superCompact: Boolean(options.superCompactTools), showGutter: true });
|
|
46
46
|
return attachImageClickTargets(lines, entry.id, entry.images, { foreground: options.colors.info, underline: true });
|
|
47
47
|
}
|
|
48
48
|
export function renderThinkingEntry(entry, width, options) {
|
|
@@ -63,7 +63,7 @@ export function renderThinkingEntry(entry, width, options) {
|
|
|
63
63
|
expandedText: compactExpandedText || "(empty)",
|
|
64
64
|
bodyWrap: "word",
|
|
65
65
|
syntaxHighlight: compactExpandedText ? markdownSyntaxHighlightsForText(compactExpandedText) : undefined,
|
|
66
|
-
}, rule, width, options.colors, { superCompact: Boolean(options.superCompactTools && !forceExpanded), backgroundOverride: options.colors.thinkingMessageBackground, skipHeaderBackground: true });
|
|
66
|
+
}, rule, width, options.colors, { superCompact: Boolean(options.superCompactTools && !forceExpanded), backgroundOverride: options.colors.thinkingMessageBackground, skipHeaderBackground: true, showGutter: false });
|
|
67
67
|
}
|
|
68
68
|
function trimTrailingBlankLines(text) {
|
|
69
69
|
return text.replace(/(?:\r?\n[ \t]*)+$/u, "");
|
|
@@ -88,7 +88,7 @@ function renderTodoToolEntry(entry, width, options) {
|
|
|
88
88
|
output: body,
|
|
89
89
|
collapsedBody: body,
|
|
90
90
|
expandedText: body,
|
|
91
|
-
}, rule, width, options.colors, { superCompact: Boolean(options.superCompactTools) });
|
|
91
|
+
}, rule, width, options.colors, { superCompact: Boolean(options.superCompactTools), showGutter: true });
|
|
92
92
|
}
|
|
93
93
|
function todoDetailsText(details) {
|
|
94
94
|
const lines = [];
|
|
@@ -25,5 +25,6 @@ export type ToolBlockRenderOptions = {
|
|
|
25
25
|
superCompact?: boolean;
|
|
26
26
|
backgroundOverride?: string;
|
|
27
27
|
skipHeaderBackground?: boolean;
|
|
28
|
+
showGutter?: boolean;
|
|
28
29
|
};
|
|
29
30
|
export declare function renderToolBlock(entry: ToolBlockEntry, rule: ResolvedToolRule, width: number, colors: Theme["colors"], options?: ToolBlockRenderOptions): RenderedLine[];
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { resolveColor } from "../../config.js";
|
|
2
2
|
import { expandTabs, sliceByDisplayWidth, stringDisplayWidth, wrapDisplayLineByWords } from "../../terminal-width.js";
|
|
3
3
|
import { alertIconPrefixLength, hasToolLspDiagnosticsAfterMutation, lspDiagnosticSeverityForLine, sanitizeText, toolStatusIcon, toolStatusIconColor, wrapLine } from "./render-text.js";
|
|
4
|
-
|
|
4
|
+
import { APP_ICONS } from "../icons.js";
|
|
5
|
+
const TOOL_BODY_PREFIX = " ";
|
|
6
|
+
function truncatedPreviewMarker() {
|
|
7
|
+
return `${APP_ICONS.toolPreviewTruncated} `;
|
|
8
|
+
}
|
|
9
|
+
function toolBodyGutterPrefix() {
|
|
10
|
+
return `${APP_ICONS.toolBodyGutter} `;
|
|
11
|
+
}
|
|
12
|
+
function toolBodyEndPrefix() {
|
|
13
|
+
return `${APP_ICONS.toolBodyEnd} `;
|
|
14
|
+
}
|
|
5
15
|
export function renderToolBlock(entry, rule, width, colors, options = {}) {
|
|
6
16
|
if (rule.hidden)
|
|
7
17
|
return [];
|
|
@@ -19,6 +29,7 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
|
|
|
19
29
|
const headerArgsWidth = width - stringDisplayWidth(headerPrefix) - 1;
|
|
20
30
|
const clippedHeaderArgs = headerArgsWidth > 0 ? sliceByDisplayWidth(headerArgs, headerArgsWidth) : "";
|
|
21
31
|
const target = { kind: "tool", id: entry.id };
|
|
32
|
+
const showGutter = options.showGutter ?? true;
|
|
22
33
|
const header = clippedHeaderArgs ? `${headerPrefix} ${clippedHeaderArgs}` : headerPrefix;
|
|
23
34
|
const headerArgsStart = clippedHeaderArgs ? headerPrefix.length + 1 : header.length;
|
|
24
35
|
const headerLine = {
|
|
@@ -34,7 +45,7 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
|
|
|
34
45
|
};
|
|
35
46
|
const headerLines = [headerLine];
|
|
36
47
|
if (expanded) {
|
|
37
|
-
headerLines.push(...renderToolBodyLines(entry.expandedText, width, target, toolOutputColor, entry.bodyStyle, colors, entry.syntaxHighlight, entry.bodyWrap, hasLspDiagnostics, entry.bodyLineStyles, entry.preserveAnsi));
|
|
48
|
+
headerLines.push(...renderToolBodyLines(entry.expandedText, width, target, toolOutputColor, entry.bodyStyle, colors, entry.syntaxHighlight, entry.bodyWrap, hasLspDiagnostics, entry.bodyLineStyles, entry.preserveAnsi, showGutter));
|
|
38
49
|
if (options.skipHeaderBackground && headerLines.length > 1) {
|
|
39
50
|
applyBackground(headerLines.slice(1));
|
|
40
51
|
}
|
|
@@ -49,7 +60,7 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
|
|
|
49
60
|
if (!body || rule.previewLines === 0)
|
|
50
61
|
return headerLines;
|
|
51
62
|
if (!options.superCompact) {
|
|
52
|
-
headerLines.push(...renderCollapsedPreviewLines(entry, body, rule, width, target, toolOutputColor, colors, hasLspDiagnostics));
|
|
63
|
+
headerLines.push(...renderCollapsedPreviewLines(entry, body, rule, width, target, toolOutputColor, colors, hasLspDiagnostics, showGutter));
|
|
53
64
|
applyBackground(headerLines);
|
|
54
65
|
return headerLines;
|
|
55
66
|
}
|
|
@@ -60,13 +71,14 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
|
|
|
60
71
|
const availablePreviewWidth = width - stringDisplayWidth(header) - stringDisplayWidth(separator);
|
|
61
72
|
if (availablePreviewWidth <= 0)
|
|
62
73
|
return headerLines;
|
|
63
|
-
const
|
|
74
|
+
const markerPrefix = truncatedPreviewMarker();
|
|
75
|
+
const previewText = preview.overflow ? `${markerPrefix}${preview.text}` : preview.text;
|
|
64
76
|
const clippedPreview = sliceByDisplayWidth(previewText, availablePreviewWidth);
|
|
65
77
|
if (!clippedPreview)
|
|
66
78
|
return headerLines;
|
|
67
79
|
headerLine.text = `${header}${separator}${clippedPreview}`;
|
|
68
80
|
const previewStart = header.length + separator.length;
|
|
69
|
-
const previewTextStart = previewStart + (preview.overflow ?
|
|
81
|
+
const previewTextStart = previewStart + (preview.overflow ? markerPrefix.length : 0);
|
|
70
82
|
headerLine.segments = [
|
|
71
83
|
...(headerLine.segments ?? []),
|
|
72
84
|
...(preview.overflow ? [{ start: previewStart, end: previewStart + 1, foreground: colors.statusDotBase }] : []),
|
|
@@ -74,8 +86,8 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
|
|
|
74
86
|
];
|
|
75
87
|
return headerLines;
|
|
76
88
|
}
|
|
77
|
-
function renderCollapsedPreviewLines(entry, body, rule, width, target, color, colors, hasLspDiagnostics) {
|
|
78
|
-
const allLines = renderToolBodyLines(body, width, target, color, entry.bodyStyle, colors, undefined, entry.bodyWrap, hasLspDiagnostics, entry.bodyLineStyles, entry.preserveAnsi);
|
|
89
|
+
function renderCollapsedPreviewLines(entry, body, rule, width, target, color, colors, hasLspDiagnostics, showGutter) {
|
|
90
|
+
const allLines = renderToolBodyLines(body, width, target, color, entry.bodyStyle, colors, undefined, entry.bodyWrap, hasLspDiagnostics, entry.bodyLineStyles, entry.preserveAnsi, showGutter);
|
|
79
91
|
if (rule.previewLines >= allLines.length)
|
|
80
92
|
return allLines;
|
|
81
93
|
const previewLines = rule.direction === "tail" ? allLines.slice(-rule.previewLines) : allLines.slice(0, rule.previewLines);
|
|
@@ -92,20 +104,33 @@ function markTruncatedPreviewLine(lines, direction, markerColor) {
|
|
|
92
104
|
if (lines.length === 0)
|
|
93
105
|
return lines;
|
|
94
106
|
const markerIndex = direction === "tail" ? 0 : lines.length - 1;
|
|
107
|
+
const gutterPrefix = toolBodyGutterPrefix();
|
|
108
|
+
const markerPrefix = truncatedPreviewMarker();
|
|
95
109
|
return lines.map((line, index) => {
|
|
96
110
|
if (index !== markerIndex)
|
|
97
111
|
return line;
|
|
112
|
+
const text = line.text.startsWith(gutterPrefix)
|
|
113
|
+
? `${markerPrefix}${line.text.slice(gutterPrefix.length)}`
|
|
114
|
+
: line.text.startsWith(TOOL_BODY_PREFIX)
|
|
115
|
+
? `${markerPrefix}${line.text.slice(TOOL_BODY_PREFIX.length)}`
|
|
116
|
+
: `${markerPrefix}${line.text}`;
|
|
117
|
+
const existingSegments = (line.segments ?? []).filter((segment) => !(segment.start === 0 && segment.end === 1 && segment.foreground === markerColor));
|
|
98
118
|
return {
|
|
99
119
|
...line,
|
|
100
|
-
text
|
|
101
|
-
segments: [{ start: 0, end: 1, foreground: markerColor }, ...
|
|
120
|
+
text,
|
|
121
|
+
segments: [{ start: 0, end: 1, foreground: markerColor }, ...existingSegments],
|
|
102
122
|
};
|
|
103
123
|
});
|
|
104
124
|
}
|
|
105
|
-
function renderToolBodyLines(text, width, target, color, style, colors, syntaxHighlight, bodyWrap = "char", hasLspDiagnostics = false, bodyLineStyles, preserveAnsi = false) {
|
|
106
|
-
const
|
|
125
|
+
function renderToolBodyLines(text, width, target, color, style, colors, syntaxHighlight, bodyWrap = "char", hasLspDiagnostics = false, bodyLineStyles, preserveAnsi = false, showGutter = false) {
|
|
126
|
+
const displayPrefix = showGutter ? toolBodyGutterPrefix() : TOOL_BODY_PREFIX;
|
|
127
|
+
const prefixWidth = stringDisplayWidth(displayPrefix);
|
|
128
|
+
const bodyWidth = Math.max(1, width - prefixWidth);
|
|
107
129
|
const lines = [];
|
|
108
130
|
const wrapBodyLine = bodyWrap === "word" ? wrapDisplayLineByWords : wrapLine;
|
|
131
|
+
const gutterSegment = showGutter
|
|
132
|
+
? { start: 0, end: 1, foreground: colors.statusDotBase }
|
|
133
|
+
: undefined;
|
|
109
134
|
for (const [rawLineIndex, rawLine] of sanitizeToolBodyText(text, preserveAnsi).split("\n").entries()) {
|
|
110
135
|
const ansiLine = preserveAnsi ? ansiStyledLine(rawLine) : undefined;
|
|
111
136
|
const displayLine = ansiLine?.text ?? rawLine;
|
|
@@ -118,37 +143,67 @@ function renderToolBodyLines(text, width, target, color, style, colors, syntaxHi
|
|
|
118
143
|
: wrapBodyLine(displayLine, bodyWidth).map((wrapped) => ({ text: wrapped, segments: [] }));
|
|
119
144
|
for (const [wrapIndex, wrapped] of wrappedLines.entries()) {
|
|
120
145
|
const line = {
|
|
121
|
-
text:
|
|
122
|
-
copyText:
|
|
146
|
+
text: `${displayPrefix}${wrapped.text}`,
|
|
147
|
+
copyText: `${TOOL_BODY_PREFIX}${wrapped.text}`,
|
|
123
148
|
...(wrapIndex < wrappedLines.length - 1 ? { continuesOnNextLine: true } : {}),
|
|
124
149
|
target,
|
|
125
150
|
colorOverride: color,
|
|
126
151
|
};
|
|
127
152
|
if (diffStyle) {
|
|
128
|
-
const segment = { start:
|
|
153
|
+
const segment = { start: displayPrefix.length, end: line.text.length, foreground: diffStyle.foreground };
|
|
129
154
|
if (diffStyle.bold != null)
|
|
130
155
|
segment.bold = diffStyle.bold;
|
|
131
|
-
line.segments = [segment];
|
|
156
|
+
line.segments = gutterSegment ? [gutterSegment, segment] : [segment];
|
|
132
157
|
}
|
|
133
158
|
else if (lspDiagnosticStyle?.kind === "alert" && wrapIndex === 0) {
|
|
134
|
-
line.segments = [
|
|
159
|
+
line.segments = [
|
|
160
|
+
...(gutterSegment ? [gutterSegment] : []),
|
|
161
|
+
{ start: displayPrefix.length, end: displayPrefix.length + lspDiagnosticStyle.length, foreground: colors.warning, bold: true },
|
|
162
|
+
];
|
|
135
163
|
}
|
|
136
164
|
else if (lspDiagnosticStyle?.kind === "severity") {
|
|
137
|
-
line.segments = [
|
|
165
|
+
line.segments = [
|
|
166
|
+
...(gutterSegment ? [gutterSegment] : []),
|
|
167
|
+
{ start: displayPrefix.length, end: line.text.length, foreground: lspDiagnosticStyle.foreground },
|
|
168
|
+
];
|
|
138
169
|
}
|
|
139
|
-
else if (bodyLineStyle && line.text.length >
|
|
140
|
-
line.segments = [
|
|
170
|
+
else if (bodyLineStyle && line.text.length > displayPrefix.length) {
|
|
171
|
+
line.segments = [
|
|
172
|
+
...(gutterSegment ? [gutterSegment] : []),
|
|
173
|
+
{ start: displayPrefix.length, end: line.text.length, ...bodyLineStyle },
|
|
174
|
+
];
|
|
141
175
|
}
|
|
142
176
|
else if (lineSyntaxHighlight) {
|
|
143
177
|
const rawStart = wrapIndex === 0 ? lineSyntaxHighlight.startColumn ?? 0 : 0;
|
|
144
|
-
line.syntaxHighlight = { language: lineSyntaxHighlight.language, start: Math.min(line.text.length,
|
|
178
|
+
line.syntaxHighlight = { language: lineSyntaxHighlight.language, start: Math.min(line.text.length, displayPrefix.length + rawStart) };
|
|
179
|
+
if (gutterSegment)
|
|
180
|
+
line.segments = [gutterSegment];
|
|
145
181
|
}
|
|
146
182
|
else if (wrapped.segments.length > 0) {
|
|
147
|
-
line.segments =
|
|
183
|
+
line.segments = [
|
|
184
|
+
...(gutterSegment ? [gutterSegment] : []),
|
|
185
|
+
...wrapped.segments.map((segment) => ({
|
|
186
|
+
...segment,
|
|
187
|
+
start: segment.start + displayPrefix.length,
|
|
188
|
+
end: segment.end + displayPrefix.length,
|
|
189
|
+
})),
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
else if (gutterSegment) {
|
|
193
|
+
line.segments = [gutterSegment];
|
|
148
194
|
}
|
|
149
195
|
lines.push(line);
|
|
150
196
|
}
|
|
151
197
|
}
|
|
198
|
+
if (showGutter && lines.length > 0) {
|
|
199
|
+
const lastLine = lines.at(-1);
|
|
200
|
+
if (!lastLine)
|
|
201
|
+
return lines;
|
|
202
|
+
const gutterPrefix = toolBodyGutterPrefix();
|
|
203
|
+
if (lastLine.text.startsWith(gutterPrefix)) {
|
|
204
|
+
lastLine.text = `${toolBodyEndPrefix()}${lastLine.text.slice(gutterPrefix.length)}`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
152
207
|
return lines;
|
|
153
208
|
}
|
|
154
209
|
const ANSI_STANDARD_COLORS = ["#000000", "#cd3131", "#0dbc79", "#e5e510", "#2472c8", "#bc3fbc", "#11a8cd", "#e5e5e5"];
|
|
@@ -11,6 +11,8 @@ export function setFileLinkOpenerTestDeps(overrides) {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
export function openFileLink(link) {
|
|
14
|
+
if (isWebUrl(link.url))
|
|
15
|
+
return openPathWithSystemViewer(link.url);
|
|
14
16
|
const filePath = link.filePath ?? filePathFromUrl(link.url);
|
|
15
17
|
if (!filePath)
|
|
16
18
|
return false;
|
|
@@ -19,6 +21,9 @@ export function openFileLink(link) {
|
|
|
19
21
|
return true;
|
|
20
22
|
return openPathWithSystemViewer(filePath);
|
|
21
23
|
}
|
|
24
|
+
function isWebUrl(url) {
|
|
25
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
26
|
+
}
|
|
22
27
|
function filePathFromUrl(url) {
|
|
23
28
|
if (!url.startsWith("file://"))
|
|
24
29
|
return undefined;
|
|
@@ -3,10 +3,11 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { isAbsolute, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
const FILE_PATH_CANDIDATE = /(?<![\p{L}\p{N}_:])((?:file:\/\/\/|~[\\/]|\.{1,2}[\\/]|[A-Za-z]:[\\/]|[\\/]|[A-Za-z0-9_.@-]+[\\/])[^\s"'`<>]*)/gu;
|
|
6
|
+
const WEB_URL_CANDIDATE = /https?:\/\/[^\s"'`<>]+/gu;
|
|
6
7
|
const TRAILING_PUNCTUATION = new Set([".", ",", ";", ")", "]", "}"]);
|
|
7
8
|
export function detectFileLinks(text, cwd) {
|
|
8
9
|
const links = [];
|
|
9
|
-
if (!text.includes("/") && !text.includes("\\"))
|
|
10
|
+
if (!text.includes("/") && !text.includes("\\") && !text.includes("http://") && !text.includes("https://"))
|
|
10
11
|
return links;
|
|
11
12
|
for (const match of text.matchAll(FILE_PATH_CANDIDATE)) {
|
|
12
13
|
const raw = match[1];
|
|
@@ -28,6 +29,17 @@ export function detectFileLinks(text, cwd) {
|
|
|
28
29
|
column: location.column,
|
|
29
30
|
});
|
|
30
31
|
}
|
|
32
|
+
for (const match of text.matchAll(WEB_URL_CANDIDATE)) {
|
|
33
|
+
const raw = match[0];
|
|
34
|
+
const candidate = trimTrailingPunctuation(raw);
|
|
35
|
+
if (!candidate)
|
|
36
|
+
continue;
|
|
37
|
+
links.push({
|
|
38
|
+
start: match.index,
|
|
39
|
+
end: match.index + candidate.length,
|
|
40
|
+
url: candidate,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
31
43
|
return mergeOverlappingLinks(links);
|
|
32
44
|
}
|
|
33
45
|
export function hyperlink(text, url) {
|
|
@@ -124,6 +124,7 @@ export declare class AppMouseController {
|
|
|
124
124
|
private renderedConversationFrame;
|
|
125
125
|
constructor(host: AppMouseControllerHost, popupMenus: AppPopupMenuController, popupActions: AppPopupActionController, scrollController: AppScrollController, commandController: AppCommandController);
|
|
126
126
|
handleMouse(event: MouseEvent): void;
|
|
127
|
+
private toolTargetContainsEvent;
|
|
127
128
|
activeClickFlash(): ClickFlash | undefined;
|
|
128
129
|
consumeClickFlashDirty(): boolean;
|
|
129
130
|
private withClickFlash;
|
|
@@ -133,10 +134,12 @@ export declare class AppMouseController {
|
|
|
133
134
|
private clickFlashForEvent;
|
|
134
135
|
private clickFlashForRegion;
|
|
135
136
|
private clickFlashRegionForEvent;
|
|
137
|
+
private toolClickFlashRegionForEvent;
|
|
136
138
|
private normalizedClickFlashRegion;
|
|
137
139
|
private inputClickFlashRegionForEvent;
|
|
138
140
|
private imageTargetAt;
|
|
139
141
|
private fileLinkAt;
|
|
142
|
+
private fileLinkTargetAt;
|
|
140
143
|
private statusTargetAt;
|
|
141
144
|
private handleImageClick;
|
|
142
145
|
private handleFileLinkClick;
|
|
@@ -7,6 +7,7 @@ import { sliceByDisplayColumns, stringDisplayWidth } from "../../terminal-width.
|
|
|
7
7
|
import { formatDcpStatsToast } from "../rendering/dcp-stats.js";
|
|
8
8
|
import { detectFileLinks } from "./file-links.js";
|
|
9
9
|
import { openFileLink as openDetectedFileLink } from "./file-link-opener.js";
|
|
10
|
+
import { APP_ICONS } from "../icons.js";
|
|
10
11
|
const CLICK_FLASH_MS = 100;
|
|
11
12
|
const LOST_MOUSE_RELEASE_SETTLE_MS = 180;
|
|
12
13
|
export class AppMouseController {
|
|
@@ -59,6 +60,8 @@ export class AppMouseController {
|
|
|
59
60
|
this.showClickFlashOnPress(event);
|
|
60
61
|
if (event.button === 0 && !event.released && this.handleInputBorderStatusClick(event))
|
|
61
62
|
return;
|
|
63
|
+
if (event.button === 0 && !event.released && this.fileLinkAt(event))
|
|
64
|
+
return;
|
|
62
65
|
if (this.handleMouseSelection(event))
|
|
63
66
|
return;
|
|
64
67
|
if (this.withClickFlash(event, () => this.handleImageClick(event)))
|
|
@@ -151,6 +154,8 @@ export class AppMouseController {
|
|
|
151
154
|
return;
|
|
152
155
|
}
|
|
153
156
|
if (target?.kind === "tool") {
|
|
157
|
+
if (!this.toolTargetContainsEvent(event))
|
|
158
|
+
return;
|
|
154
159
|
const entry = this.host.findEntry(target.id);
|
|
155
160
|
if (entry?.kind === "tool" || entry?.kind === "thinking" || entry?.kind === "shell") {
|
|
156
161
|
entry.expanded = !entry.expanded;
|
|
@@ -159,6 +164,16 @@ export class AppMouseController {
|
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
166
|
}
|
|
167
|
+
toolTargetContainsEvent(event) {
|
|
168
|
+
const text = this.renderedRowTexts.get(event.y) ?? "";
|
|
169
|
+
const gutter = toolGutterGlyphForLine(text);
|
|
170
|
+
if (gutter) {
|
|
171
|
+
const gutterWidth = Math.max(1, stringDisplayWidth(gutter));
|
|
172
|
+
return event.x >= 1 && event.x < 1 + gutterWidth;
|
|
173
|
+
}
|
|
174
|
+
const bounds = nonBlankLineBounds(text, event.x);
|
|
175
|
+
return event.x >= bounds.startColumn && event.x < bounds.endColumn;
|
|
176
|
+
}
|
|
162
177
|
activeClickFlash() {
|
|
163
178
|
return this.clickFlash;
|
|
164
179
|
}
|
|
@@ -221,16 +236,19 @@ export class AppMouseController {
|
|
|
221
236
|
const imageTarget = this.imageTargetAt(event);
|
|
222
237
|
if (imageTarget)
|
|
223
238
|
return { y: event.y, startColumn: imageTarget.start + 1, endColumn: imageTarget.end + 1 };
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
226
|
-
return { y: event.y, startColumn:
|
|
239
|
+
const linkTarget = this.fileLinkTargetAt(event);
|
|
240
|
+
if (linkTarget)
|
|
241
|
+
return { y: event.y, startColumn: linkTarget.startColumn, endColumn: linkTarget.endColumn };
|
|
227
242
|
const tabTarget = this.tabLineTargetAt(event);
|
|
228
243
|
if (tabTarget)
|
|
229
244
|
return { y: tabTarget.row, startColumn: tabTarget.startColumn, endColumn: tabTarget.endColumn };
|
|
230
245
|
const statusTarget = this.statusTargetAt(event);
|
|
231
246
|
if (statusTarget)
|
|
232
247
|
return statusTarget;
|
|
233
|
-
const
|
|
248
|
+
const target = this.renderedTargets.get(event.y);
|
|
249
|
+
if (target?.kind === "tool")
|
|
250
|
+
return this.toolClickFlashRegionForEvent(event);
|
|
251
|
+
const toastTarget = target;
|
|
234
252
|
if (toastTarget?.kind === "toast" && toastTargetContainsEvent(toastTarget, event)) {
|
|
235
253
|
return {
|
|
236
254
|
y: event.y,
|
|
@@ -244,6 +262,19 @@ export class AppMouseController {
|
|
|
244
262
|
}
|
|
245
263
|
return undefined;
|
|
246
264
|
}
|
|
265
|
+
toolClickFlashRegionForEvent(event) {
|
|
266
|
+
const text = this.renderedRowTexts.get(event.y) ?? "";
|
|
267
|
+
const gutter = toolGutterGlyphForLine(text);
|
|
268
|
+
if (gutter) {
|
|
269
|
+
const gutterWidth = Math.max(1, stringDisplayWidth(gutter));
|
|
270
|
+
const region = { y: event.y, startColumn: 1, endColumn: 1 + gutterWidth };
|
|
271
|
+
return event.x >= region.startColumn && event.x < region.endColumn ? region : undefined;
|
|
272
|
+
}
|
|
273
|
+
const bounds = nonBlankLineBounds(text, event.x);
|
|
274
|
+
return event.x >= bounds.startColumn && event.x < bounds.endColumn
|
|
275
|
+
? { y: event.y, startColumn: bounds.startColumn, endColumn: bounds.endColumn }
|
|
276
|
+
: undefined;
|
|
277
|
+
}
|
|
247
278
|
normalizedClickFlashRegion(region) {
|
|
248
279
|
const columns = Math.max(1, this.host.terminalColumns());
|
|
249
280
|
const y = Math.max(1, region.y);
|
|
@@ -261,10 +292,19 @@ export class AppMouseController {
|
|
|
261
292
|
return targets?.find((candidate) => event.x >= candidate.start + 1 && event.x <= candidate.end);
|
|
262
293
|
}
|
|
263
294
|
fileLinkAt(event) {
|
|
295
|
+
return this.fileLinkTargetAt(event)?.link;
|
|
296
|
+
}
|
|
297
|
+
fileLinkTargetAt(event) {
|
|
264
298
|
const text = this.renderedRowTexts.get(event.y);
|
|
265
299
|
if (!text)
|
|
266
300
|
return undefined;
|
|
267
|
-
|
|
301
|
+
for (const link of detectFileLinks(text, this.host.cwd())) {
|
|
302
|
+
const startColumn = stringDisplayWidth(text.slice(0, link.start)) + 1;
|
|
303
|
+
const endColumn = startColumn + stringDisplayWidth(text.slice(link.start, link.end));
|
|
304
|
+
if (event.x >= startColumn && event.x < endColumn)
|
|
305
|
+
return { link, startColumn, endColumn };
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
268
308
|
}
|
|
269
309
|
statusTargetAt(event) {
|
|
270
310
|
const target = [
|
|
@@ -294,7 +334,7 @@ export class AppMouseController {
|
|
|
294
334
|
};
|
|
295
335
|
}
|
|
296
336
|
handleImageClick(event) {
|
|
297
|
-
if (
|
|
337
|
+
if (!isPrimaryButtonRelease(event))
|
|
298
338
|
return false;
|
|
299
339
|
const imageTarget = this.imageTargetAt(event);
|
|
300
340
|
if (!imageTarget)
|
|
@@ -312,7 +352,7 @@ export class AppMouseController {
|
|
|
312
352
|
}
|
|
313
353
|
handleFileLinkClick(event) {
|
|
314
354
|
const modifiedPress = isModifiedPrimaryButton(event.button) && !event.released;
|
|
315
|
-
const plainRelease = event
|
|
355
|
+
const plainRelease = isPrimaryButtonRelease(event);
|
|
316
356
|
if (!modifiedPress && !plainRelease)
|
|
317
357
|
return false;
|
|
318
358
|
const link = this.fileLinkAt(event);
|
|
@@ -995,6 +1035,13 @@ function nonBlankLineBounds(text, fallbackColumn) {
|
|
|
995
1035
|
? { startColumn: fallbackColumn, endColumn: fallbackColumn + 1 }
|
|
996
1036
|
: { startColumn, endColumn };
|
|
997
1037
|
}
|
|
1038
|
+
function toolGutterGlyphForLine(text) {
|
|
1039
|
+
for (const glyph of [APP_ICONS.toolBodyGutter, APP_ICONS.toolBodyEnd, APP_ICONS.toolPreviewTruncated]) {
|
|
1040
|
+
if (text.startsWith(`${glyph} `))
|
|
1041
|
+
return glyph;
|
|
1042
|
+
}
|
|
1043
|
+
return undefined;
|
|
1044
|
+
}
|
|
998
1045
|
function displayCellAtColumn(text, column) {
|
|
999
1046
|
if (column < 1)
|
|
1000
1047
|
return " ";
|
|
@@ -1017,6 +1064,9 @@ function isModifiedPrimaryButton(button) {
|
|
|
1017
1064
|
const modifierBits = button & (8 | 16);
|
|
1018
1065
|
return primaryButton && modifierBits !== 0;
|
|
1019
1066
|
}
|
|
1067
|
+
function isPrimaryButtonRelease(event) {
|
|
1068
|
+
return event.released && (event.button === 0 || (event.button & 3) === 3);
|
|
1069
|
+
}
|
|
1020
1070
|
function editorLayoutRows(terminalRows, tabPanelRows) {
|
|
1021
1071
|
return Math.max(1, terminalRows - tabPanelRows);
|
|
1022
1072
|
}
|
|
@@ -70,6 +70,10 @@ export function firstUserMessageText(ctx: ExtensionContext): string | undefined
|
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function hasExistingUserMessage(ctx: ExtensionContext): boolean {
|
|
74
|
+
return firstUserMessageText(ctx) !== undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
73
77
|
export function fallbackSessionTitleFromInput(input: string, maxTitleChars: number): string | undefined {
|
|
74
78
|
const normalized = input
|
|
75
79
|
.replace(/[\t\r\n]+/gu, " ")
|
|
@@ -371,24 +375,6 @@ export default function sessionTitle(pi: ExtensionAPI) {
|
|
|
371
375
|
})();
|
|
372
376
|
}
|
|
373
377
|
|
|
374
|
-
function primeTitleGenerationFromExistingSession(ctx: ExtensionContext, currentConfig: SessionTitleConfig): void {
|
|
375
|
-
if (currentSessionName(ctx)) return;
|
|
376
|
-
|
|
377
|
-
const input = firstUserMessageText(ctx);
|
|
378
|
-
if (!input) return;
|
|
379
|
-
if (!currentConfig.enabled) {
|
|
380
|
-
applyFallbackSessionTitle(ctx, currentConfig, input);
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
pendingGeneration = {
|
|
385
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
386
|
-
input: truncateInput(input, currentConfig.maxInputChars),
|
|
387
|
-
attempts: 0,
|
|
388
|
-
};
|
|
389
|
-
startTitleGeneration(ctx, currentConfig);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
378
|
function isSameSessionPath(left: string | undefined, right: string | undefined): boolean {
|
|
393
379
|
if (!left || !right) return false;
|
|
394
380
|
if (left === right) return true;
|
|
@@ -447,7 +433,6 @@ export default function sessionTitle(pi: ExtensionAPI) {
|
|
|
447
433
|
await prepareForkTitleState(event, ctx);
|
|
448
434
|
refreshSessionUi(ctx, { force: true });
|
|
449
435
|
scheduleSessionUiRefresh(ctx);
|
|
450
|
-
if (!forkTitleState) primeTitleGenerationFromExistingSession(ctx, config);
|
|
451
436
|
});
|
|
452
437
|
|
|
453
438
|
pi.on("session_shutdown", async () => {
|
|
@@ -480,6 +465,10 @@ export default function sessionTitle(pi: ExtensionAPI) {
|
|
|
480
465
|
sessionId = currentSessionId;
|
|
481
466
|
const currentName = currentSessionName(ctx);
|
|
482
467
|
const activeForkTitleState = forkTitleState?.sessionId === currentSessionId ? forkTitleState : undefined;
|
|
468
|
+
if (!activeForkTitleState && hasExistingUserMessage(ctx)) {
|
|
469
|
+
forkTitleState = undefined;
|
|
470
|
+
return { action: "continue" as const };
|
|
471
|
+
}
|
|
483
472
|
if (currentName && (!activeForkTitleState || currentName !== activeForkTitleState.inheritedSessionName)) {
|
|
484
473
|
forkTitleState = undefined;
|
|
485
474
|
return { action: "continue" as const };
|