pi-cc-extensions 1.0.3 → 1.0.5
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.en.md +6 -6
- package/README.md +6 -6
- package/extensions/config/config.ts +16 -27
- package/extensions/config/panel.ts +49 -48
- package/extensions/feature/compact-thinking.ts +2 -3
- package/extensions/feature/context.ts +291 -136
- package/extensions/feature/shell/footer-layout.ts +108 -23
- package/extensions/feature/shell/footer.ts +59 -43
- package/extensions/feature/shell/prompt-editor.ts +77 -0
- package/extensions/index.ts +6 -0
- package/extensions/renderer/compact-mode.ts +124 -54
- package/extensions/renderer/default-mode.ts +7 -4
- package/extensions/renderer/mouse/interaction.ts +69 -28
- package/extensions/renderer/mouse/scroll.ts +23 -8
- package/extensions/renderer/mouse/sticky-prompt.ts +2 -2
- package/extensions/renderer/tool/ccstyle-tool.ts +24 -12
- package/extensions/renderer/tool/codemode.ts +121 -6
- package/extensions/renderer/tool/diff/diff-limits.ts +3 -2
- package/extensions/renderer/tool/diff/diff-palette.ts +39 -8
- package/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
- package/extensions/renderer/tool/grouping.ts +13 -13
- package/extensions/renderer/tool/message-display.ts +3 -2
- package/extensions/renderer/tool/names.ts +49 -49
- package/extensions/renderer/tool/result.ts +24 -0
- package/extensions/renderer/tool/tool-state.ts +51 -2
- package/package.json +1 -1
- package/themes/cc-dark.json +72 -71
- package/themes/cc-light.json +72 -71
|
@@ -68,7 +68,7 @@ export function setScrollButtonVisible(visible: boolean): void {
|
|
|
68
68
|
export function noteNewTranscriptItem(): void {
|
|
69
69
|
const state = scrollButtonState();
|
|
70
70
|
const tui = getToolMouseTui();
|
|
71
|
-
if (!state.visible || !tui) return;
|
|
71
|
+
if (!dockScrollButtonActive() || !state.visible || !tui) return;
|
|
72
72
|
// 提交新消息时官方可能直接跳回底部(不经过滚动输入):先按实时跟随状态校正,
|
|
73
73
|
// 否则跟随期间的内容会被当成“离开底部时的新消息”计入。
|
|
74
74
|
if (isAtTranscriptBottom(tui)) {
|
|
@@ -107,6 +107,11 @@ export function toolMouseInteractionActive(): boolean {
|
|
|
107
107
|
return config.mode !== "off";
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/** dock 模式才用扩展的独占行;native 或 ccstyle off 时交还 Pi 原生浮层。 */
|
|
111
|
+
export function dockScrollButtonActive(): boolean {
|
|
112
|
+
return toolMouseInteractionActive() && config.scrollButtonMode === "dock";
|
|
113
|
+
}
|
|
114
|
+
|
|
110
115
|
/** 惰性 Proxy 官方 fullscreen(TuiAltScreen)判定。 */
|
|
111
116
|
export function fullscreenLazyTui(tui: any): boolean {
|
|
112
117
|
return isLazyProxyTui(tui) && tui.mode === "fullscreen";
|
|
@@ -132,10 +137,15 @@ export function restoreOfficialScrollToEnd(tui: any): void {
|
|
|
132
137
|
tui[OFFICIAL_SCROLL_TO_END_KEY] = undefined;
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
/**
|
|
140
|
+
/** dock 模式替换官方浮层;native 或 ccstyle off 时保留 Pi 原生 Jump to latest。 */
|
|
136
141
|
export function syncOfficialScrollToEnd(tui: any): void {
|
|
137
|
-
if (
|
|
138
|
-
|
|
142
|
+
if (dockScrollButtonActive()) {
|
|
143
|
+
disableOfficialScrollToEnd(tui);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
restoreOfficialScrollToEnd(tui);
|
|
147
|
+
setScrollButtonVisible(false);
|
|
148
|
+
setScrollButtonHovered(false);
|
|
139
149
|
}
|
|
140
150
|
|
|
141
151
|
/** 官方 fullscreen:是否已跟随 transcript 底部(按钮隐藏条件)。 */
|
|
@@ -202,7 +212,7 @@ export function hideScrollButton(tui: any): void {
|
|
|
202
212
|
export function scheduleScrollButtonSync(tui: any, data: string): void {
|
|
203
213
|
if (
|
|
204
214
|
!fullscreenLazyTui(tui) ||
|
|
205
|
-
!
|
|
215
|
+
!dockScrollButtonActive() ||
|
|
206
216
|
!isScrollNavigationInput(data) ||
|
|
207
217
|
scrollButtonSyncScheduled
|
|
208
218
|
)
|
|
@@ -211,7 +221,7 @@ export function scheduleScrollButtonSync(tui: any, data: string): void {
|
|
|
211
221
|
const previousLines = tui.previousLines;
|
|
212
222
|
const check = (attempt: number) => {
|
|
213
223
|
scrollButtonSyncScheduled = false;
|
|
214
|
-
if (getToolMouseTui() !== tui) return;
|
|
224
|
+
if (getToolMouseTui() !== tui || !dockScrollButtonActive()) return;
|
|
215
225
|
// Pi renders on its own frame timer. Inspect the resulting viewport before
|
|
216
226
|
// showing the button so empty or non-scrollable transcripts never flash it.
|
|
217
227
|
const rendered = tui.previousLines !== previousLines;
|
|
@@ -234,7 +244,7 @@ export function scheduleScrollButtonSync(tui: any, data: string): void {
|
|
|
234
244
|
}
|
|
235
245
|
|
|
236
246
|
export function updateScrollButtonFromInput(tui: any, data: string): void {
|
|
237
|
-
if (!fullscreenLazyTui(tui) || !
|
|
247
|
+
if (!fullscreenLazyTui(tui) || !dockScrollButtonActive()) return;
|
|
238
248
|
if (matchesKey(data, "enter") || matchesKey(data, "return")) hideScrollButton(tui);
|
|
239
249
|
}
|
|
240
250
|
|
|
@@ -247,7 +257,12 @@ function scrollButtonText(): string {
|
|
|
247
257
|
}
|
|
248
258
|
|
|
249
259
|
export function renderScrollButton(width: number, theme: any): string[] {
|
|
250
|
-
if (
|
|
260
|
+
if (
|
|
261
|
+
!dockScrollButtonActive() ||
|
|
262
|
+
!getScrollButtonVisible() ||
|
|
263
|
+
!fullscreenLazyTui(getToolMouseTui())
|
|
264
|
+
)
|
|
265
|
+
return [];
|
|
251
266
|
const label = theme.fg(
|
|
252
267
|
getScrollButtonHovered() ? "text" : "accent",
|
|
253
268
|
`[ ↓ ${scrollButtonText()} ]`,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "../../utils/patch-keys.ts";
|
|
28
28
|
import { isToolTuiFullscreen } from "../tool/names.ts";
|
|
29
29
|
import type { SgrMousePacket } from "./packets.ts";
|
|
30
|
-
import { setScrollButtonVisible } from "./scroll.ts";
|
|
30
|
+
import { dockScrollButtonActive, setScrollButtonVisible } from "./scroll.ts";
|
|
31
31
|
|
|
32
32
|
/** 消息正文首行/末行的零宽 APC 标记;终端忽略,合成前剥离。 */
|
|
33
33
|
const MESSAGE_START = "\x1b_cc:p\x07";
|
|
@@ -338,7 +338,7 @@ export function handleStickyPromptClick(tui: any, packet: SgrMousePacket): boole
|
|
|
338
338
|
const view = tui.getPrimaryScrollView?.();
|
|
339
339
|
if (typeof view?.scrollTo === "function") {
|
|
340
340
|
view.scrollTo(state.jumpRow, { disableFollow: true });
|
|
341
|
-
if (view.isFollowingEnd === false) setScrollButtonVisible(true);
|
|
341
|
+
if (view.isFollowingEnd === false && dockScrollButtonActive()) setScrollButtonVisible(true);
|
|
342
342
|
}
|
|
343
343
|
// 落点已到消息头,这一帧的粘性行立即失效;等下一帧按新位置重算。
|
|
344
344
|
state.visible = false;
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "./names.ts";
|
|
28
28
|
import {
|
|
29
29
|
collapsedResultSummary,
|
|
30
|
+
expandedBodyComponent,
|
|
30
31
|
formatToolDuration,
|
|
31
32
|
insetComponent,
|
|
32
33
|
renderCollapsedToolResultToWidth,
|
|
@@ -154,6 +155,23 @@ function renderExpandedTaskResult(
|
|
|
154
155
|
return formatted ? new Text(`↳ ${formatted}`, 0, 0) : undefined;
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
/**
|
|
159
|
+
* 工具耗时(毫秒)。结束时优先用 pi 记在结果上的 durationMs(单调,restore 出来的行也带得到);
|
|
160
|
+
* 运行中 pi 不给(partial 期间渲染上下文里恒为 undefined),退回 renderCall 记下的时钟。
|
|
161
|
+
* 拿不到返回 undefined。时钟回拨时钳到 0。
|
|
162
|
+
*/
|
|
163
|
+
function toolElapsedMs(context: any, isPartial: boolean): number | undefined {
|
|
164
|
+
const recorded = context?.durationMs;
|
|
165
|
+
if (!isPartial && typeof recorded === "number" && Number.isFinite(recorded) && recorded >= 0) {
|
|
166
|
+
return recorded;
|
|
167
|
+
}
|
|
168
|
+
const startedAt = context?.state?.ccstyleShellStartedAt;
|
|
169
|
+
if (typeof startedAt !== "number") return undefined;
|
|
170
|
+
const endedAt = context?.state?.ccstyleShellEndedAt;
|
|
171
|
+
const end = typeof endedAt === "number" ? endedAt : Date.now();
|
|
172
|
+
return Math.max(0, end - startedAt);
|
|
173
|
+
}
|
|
174
|
+
|
|
157
175
|
/**
|
|
158
176
|
* shell 的耗时文本。Pi 原生 shell 渲染器会在结果底部报一行,ccstyle 的通用折叠行把它盖掉了。
|
|
159
177
|
*
|
|
@@ -163,16 +181,8 @@ function renderExpandedTaskResult(
|
|
|
163
181
|
*/
|
|
164
182
|
function shellDurationText(toolName: string, context: any, isPartial: boolean): string {
|
|
165
183
|
if (!SHELL_TOOL_NAMES.has(toolName)) return "";
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
return formatToolDuration(recorded);
|
|
169
|
-
}
|
|
170
|
-
const startedAt = context?.state?.ccstyleShellStartedAt;
|
|
171
|
-
if (typeof startedAt !== "number") return "";
|
|
172
|
-
const endedAt = context?.state?.ccstyleShellEndedAt;
|
|
173
|
-
const end = typeof endedAt === "number" ? endedAt : Date.now();
|
|
174
|
-
// 时钟回拨时别吐负数;快照固定时钟也会走到这。
|
|
175
|
-
return formatToolDuration(Math.max(0, end - startedAt));
|
|
184
|
+
const elapsed = toolElapsedMs(context, isPartial);
|
|
185
|
+
return elapsed === undefined ? "" : formatToolDuration(elapsed);
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
/** 用 ccstyle call/result 包装任意工具定义。 */
|
|
@@ -328,6 +338,7 @@ export function createCcstyleTool(
|
|
|
328
338
|
running: Boolean(options?.isPartial),
|
|
329
339
|
isError,
|
|
330
340
|
isHovered: () => isToolCallHovered(toolCallId),
|
|
341
|
+
code: context?.args?.code,
|
|
331
342
|
});
|
|
332
343
|
}
|
|
333
344
|
// ask_user_question:折叠态摊成问答树,展开态按问题逐条列出选择。
|
|
@@ -405,8 +416,9 @@ export function createCcstyleTool(
|
|
|
405
416
|
writeExecutionMetadata,
|
|
406
417
|
getToolDisplayConfig,
|
|
407
418
|
);
|
|
408
|
-
// 展开态由 Box(1,1)
|
|
409
|
-
if (richResult)
|
|
419
|
+
// 展开态由 Box(1,1) 提供内边距,首行再缩 2 格与标题文字列对齐;折叠态 inset 一级缩进
|
|
420
|
+
if (richResult)
|
|
421
|
+
return expanded ? expandedBodyComponent(richResult) : insetComponent(richResult);
|
|
410
422
|
}
|
|
411
423
|
|
|
412
424
|
const text = textFromResult(result, expanded);
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* - 输出以 "Script completed\nWall time …\nOutput:\n" 开头(pi 原生也切掉),行数统计与展开正文都得去掉它,
|
|
9
9
|
* 否则折叠摘要会多算 3 行。
|
|
10
10
|
*/
|
|
11
|
+
import { highlightCode } from "@earendil-works/pi-coding-agent";
|
|
11
12
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
13
|
+
import { config } from "../../config/config.ts";
|
|
12
14
|
import { stripAnsi } from "../../utils/ansi-text.ts";
|
|
15
|
+
import { oneLine } from "../../utils/format.ts";
|
|
13
16
|
import { toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
14
17
|
import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
|
|
15
18
|
import { mcpToolTitle } from "./mcp-title.ts";
|
|
@@ -94,6 +97,105 @@ function fitSummaryParts(parts: readonly string[], width: number): string {
|
|
|
94
97
|
return visibleWidth(text) <= width ? text : headTruncateToWidth(text, width);
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
/**
|
|
101
|
+
* 摘要行的打分:调用最能说明脚本在干什么,字符串次之,行长只在前两者相同时决胜。
|
|
102
|
+
* 调用权重必须大于「字符串分 + 行长上限」,否则长赋值会压过短调用。
|
|
103
|
+
*/
|
|
104
|
+
const CODE_SUMMARY_CALL_SCORE = 4;
|
|
105
|
+
const CODE_SUMMARY_STRING_SCORE = 2;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 无子调用时的脚本摘要:取信息量最大的一行语句,压成单行。
|
|
109
|
+
*
|
|
110
|
+
* 要的是一段真实的 JavaScript 片段,折叠行才能用 highlightCode 着色;
|
|
111
|
+
* 标识符链(`a → b → c`)着色器认不出,染不上。跳过注释、空行和 `// @options`。
|
|
112
|
+
* 只是折叠行的提示,不求还原语义;看代码还是展开 Input。
|
|
113
|
+
*/
|
|
114
|
+
export function codemodeCodeSummary(code: unknown): string | undefined {
|
|
115
|
+
if (typeof code !== "string" || !code.trim()) return undefined;
|
|
116
|
+
let best: { text: string; score: number } | undefined;
|
|
117
|
+
let skippingBlock = false;
|
|
118
|
+
for (const raw of code.split("\n")) {
|
|
119
|
+
const line = raw.trim();
|
|
120
|
+
if (skippingBlock) {
|
|
121
|
+
if (line.includes("*/")) skippingBlock = false;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (line.startsWith("/*")) {
|
|
125
|
+
if (!line.includes("*/")) skippingBlock = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!line || line.startsWith("//") || line.startsWith("*")) continue;
|
|
129
|
+
const text = oneLine(line);
|
|
130
|
+
if (!text) continue;
|
|
131
|
+
const score =
|
|
132
|
+
(text.includes("(") ? CODE_SUMMARY_CALL_SCORE : 0) +
|
|
133
|
+
(/["'`]/.test(text) ? CODE_SUMMARY_STRING_SCORE : 0) +
|
|
134
|
+
Math.min(text.length, 100) / 100;
|
|
135
|
+
if (!best || score > best.score) best = { text, score };
|
|
136
|
+
}
|
|
137
|
+
return best?.text;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 没有子调用时的脚本行:语句逐行用 │ 树线列出,行数跟 expandedInputMaxLines。
|
|
142
|
+
* 超出的收成一行 `… N more lines`。注释、空行和 `// @options` 不占行。
|
|
143
|
+
*/
|
|
144
|
+
export function codemodeCodeLines(
|
|
145
|
+
code: unknown,
|
|
146
|
+
fg: (color: string, text: string) => string,
|
|
147
|
+
rowWidth: number,
|
|
148
|
+
): string[] {
|
|
149
|
+
if (typeof code !== "string" || !code.trim()) return [];
|
|
150
|
+
const statements: string[] = [];
|
|
151
|
+
let skippingBlock = false;
|
|
152
|
+
for (const raw of code.split("\n")) {
|
|
153
|
+
const line = raw.trim();
|
|
154
|
+
if (skippingBlock) {
|
|
155
|
+
if (line.includes("*/")) skippingBlock = false;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (line.startsWith("/*")) {
|
|
159
|
+
if (!line.includes("*/")) skippingBlock = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!line || line.startsWith("//") || line.startsWith("*")) continue;
|
|
163
|
+
const text = oneLine(line);
|
|
164
|
+
if (text) statements.push(text);
|
|
165
|
+
}
|
|
166
|
+
if (statements.length === 0) return [];
|
|
167
|
+
const limit = Math.max(1, config.expandedInputMaxLines);
|
|
168
|
+
const visible = statements.slice(0, limit);
|
|
169
|
+
const rail = `${INDENT}${fg("dim", "│")} `;
|
|
170
|
+
const lines = visible.map((statement) =>
|
|
171
|
+
truncateToWidth(`${rail}${highlightCodeSummary(statement)}`, rowWidth, ""),
|
|
172
|
+
);
|
|
173
|
+
const hidden = statements.length - visible.length;
|
|
174
|
+
if (hidden > 0) {
|
|
175
|
+
lines.push(
|
|
176
|
+
truncateToWidth(
|
|
177
|
+
`${rail}${fg("dim", `… ${hidden} more ${hidden === 1 ? "line" : "lines"}`)}`,
|
|
178
|
+
rowWidth,
|
|
179
|
+
"",
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return lines;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 所以包一层 `() => { … }` 再取中间那一行。失败或没着色时退回原文。
|
|
188
|
+
*/
|
|
189
|
+
export function highlightCodeSummary(summary: string): string {
|
|
190
|
+
try {
|
|
191
|
+
const lines = highlightCode(`() => { ${summary} }`, "javascript");
|
|
192
|
+
const colored = lines[0]?.replace(/^\(\) => \{ /, "").replace(/ \}$/, "");
|
|
193
|
+
return colored && stripAnsi(colored) === summary ? colored : summary;
|
|
194
|
+
} catch {
|
|
195
|
+
return summary;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
97
199
|
/** 与 pi 原生一致:不到 1 分显示两位有效数字。 */
|
|
98
200
|
function formatCost(cost: number): string {
|
|
99
201
|
return `$${cost >= 0.01 ? cost.toFixed(2) : cost.toPrecision(2)}`;
|
|
@@ -159,9 +261,12 @@ function callSummary(call: CodemodeNestedCall): ToolCallSummary {
|
|
|
159
261
|
const parsed = parseCallArgs(raw);
|
|
160
262
|
if (parsed) {
|
|
161
263
|
const summary = toolCallSummary(name, parsed.args, { title, variant: "default" });
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
264
|
+
// 截断串只恢复出部分字段:字段链认出来了才用(payload 是字段链挑出的值,
|
|
265
|
+
// path/main 的兜底标题不算);认不出仍旧贴原文,免得把不完整的 JSON 当成完整入参摊开。
|
|
266
|
+
const recognized = summary.payload !== undefined || summary.path !== undefined;
|
|
267
|
+
// 完全还原的入参:标题与载荷分色后的 summary 直接用;恢复了字段的也用结构化
|
|
268
|
+
// 结果(字段链认出的值比截断原文更可读)。认不出的才退回贴原文。
|
|
269
|
+
if (parsed.complete || recognized) return summary;
|
|
165
270
|
}
|
|
166
271
|
return { main: title, detail: "", payload: raw };
|
|
167
272
|
}
|
|
@@ -176,7 +281,7 @@ function statusIcon(status: unknown, theme: any): string {
|
|
|
176
281
|
return theme.fg("muted", "⊘");
|
|
177
282
|
default:
|
|
178
283
|
// 运行中:帧在 render() 内取,动画定时器只负责 requestRender
|
|
179
|
-
return theme.fg("
|
|
284
|
+
return theme.fg("dim", toolLoadingIcon());
|
|
180
285
|
}
|
|
181
286
|
}
|
|
182
287
|
|
|
@@ -209,6 +314,8 @@ export function codemodeCollapsedLines(options: {
|
|
|
209
314
|
width: number;
|
|
210
315
|
/** 鼠标悬停整卡时展开提示用 text 色,与其它折叠卡一致。 */
|
|
211
316
|
hovered?: boolean;
|
|
317
|
+
/** 脚本源码;没有子调用时用来补一行 `code:` 摘要。 */
|
|
318
|
+
code?: unknown;
|
|
212
319
|
}): string[] {
|
|
213
320
|
const { result, theme, running, isError } = options;
|
|
214
321
|
const fg = theme.fg.bind(theme);
|
|
@@ -216,6 +323,11 @@ export function codemodeCollapsedLines(options: {
|
|
|
216
323
|
const rowWidth = toolViewportWidth(options.width);
|
|
217
324
|
const shown = calls.slice(-COLLAPSED_CALL_LIMIT);
|
|
218
325
|
const lines: string[] = [];
|
|
326
|
+
// 没有子调用时脚本本身是唯一线索:逐行用 │ 树线列出,行数跟 expandedInputMaxLines,
|
|
327
|
+
// 汇总行仍用 └ 收尾。
|
|
328
|
+
if (shown.length === 0) {
|
|
329
|
+
lines.push(...codemodeCodeLines(options.code, fg, rowWidth));
|
|
330
|
+
}
|
|
219
331
|
if (shown.length < calls.length) {
|
|
220
332
|
const hidden = calls.length - shown.length;
|
|
221
333
|
lines.push(
|
|
@@ -229,8 +341,8 @@ export function codemodeCollapsedLines(options: {
|
|
|
229
341
|
for (const call of shown) lines.push(callRow(call, theme, rowWidth));
|
|
230
342
|
|
|
231
343
|
const summary = codemodeSummaryParts(calls, codemodeOutputLineCount(result), running);
|
|
232
|
-
//
|
|
233
|
-
const prefix =
|
|
344
|
+
// 上面已有行(子调用或代码摘要)时用 └ 收尾,否则不画衔接符
|
|
345
|
+
const prefix = lines.length > 0 ? `${INDENT}${fg("dim", "└")} ` : INDENT;
|
|
234
346
|
// 圆点保持 dim;hover 只高亮可点击文字。
|
|
235
347
|
const hint = running
|
|
236
348
|
? ""
|
|
@@ -281,6 +393,8 @@ export function createCodemodeResultComponent(options: {
|
|
|
281
393
|
isError: boolean;
|
|
282
394
|
/** 折叠态 hover 查询,与 diff 卡同一套(mouse/hover)。 */
|
|
283
395
|
isHovered?: () => boolean;
|
|
396
|
+
/** 脚本源码;没有子调用时用来补一行 `code:` 摘要。 */
|
|
397
|
+
code?: unknown;
|
|
284
398
|
}): any {
|
|
285
399
|
const { result, theme, running, isError } = options;
|
|
286
400
|
return {
|
|
@@ -292,6 +406,7 @@ export function createCodemodeResultComponent(options: {
|
|
|
292
406
|
isError,
|
|
293
407
|
width,
|
|
294
408
|
hovered: options.isHovered?.() ?? false,
|
|
409
|
+
code: options.code,
|
|
295
410
|
});
|
|
296
411
|
},
|
|
297
412
|
invalidate(): void {},
|
|
@@ -135,10 +135,11 @@ export function applyLineLimit(
|
|
|
135
135
|
const clickLabel = showMoreHintText();
|
|
136
136
|
const clickIndex = hintText.lastIndexOf(clickLabel);
|
|
137
137
|
// 展开态不会再截断,剩下的分支必是折叠态提示。
|
|
138
|
+
// 提示文本与其余折叠卡一致用 dim,hover 时 clickLabel 提到 text。
|
|
138
139
|
const styledHint =
|
|
139
|
-
|
|
140
|
+
clickIndex >= 0
|
|
140
141
|
? theme.fg("muted", hintText.slice(0, clickIndex)) +
|
|
141
|
-
theme.fg("text", clickLabel) +
|
|
142
|
+
theme.fg(hovered ? "text" : "dim", clickLabel) +
|
|
142
143
|
theme.fg("muted", hintText.slice(clickIndex + clickLabel.length))
|
|
143
144
|
: theme.fg("muted", hintText);
|
|
144
145
|
return {
|
|
@@ -31,12 +31,18 @@ export interface DiffPalette {
|
|
|
31
31
|
removeEmphasisBgAnsi: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const ADD_ROW_BACKGROUND_MIX_RATIO = 0.
|
|
34
|
+
const ADD_ROW_BACKGROUND_MIX_RATIO = 0.04;
|
|
35
35
|
const REMOVE_ROW_BACKGROUND_MIX_RATIO = 0.12;
|
|
36
36
|
const ADD_INLINE_EMPHASIS_MIX_RATIO = 0.26;
|
|
37
|
-
const REMOVE_INLINE_EMPHASIS_MIX_RATIO = 0.
|
|
37
|
+
const REMOVE_INLINE_EMPHASIS_MIX_RATIO = 0.5;
|
|
38
38
|
const ADDITION_TINT_TARGET: RgbColor = { r: 84, g: 190, b: 118 };
|
|
39
39
|
const DELETION_TINT_TARGET: RgbColor = { r: 232, g: 95, b: 122 };
|
|
40
|
+
// GitHub dark 删除行底 ~#67060c:混向深酒红目标,压掉主题基底的粉/灰感。
|
|
41
|
+
const DELETION_ROW_TARGET: RgbColor = { r: 103, g: 6, b: 12 };
|
|
42
|
+
const DELETION_EMPHASIS_TARGET: RgbColor = { r: 160, g: 17, b: 23 };
|
|
43
|
+
// 行底/强调底混完再向自身亮度去饱和,留灰感,避免纯色底过艳。
|
|
44
|
+
const ROW_DESATURATE_RATIO = 0.3;
|
|
45
|
+
const EMPHASIS_DESATURATE_RATIO = 0.2;
|
|
40
46
|
export const ANSI_BG_RESET = "\x1b[49m";
|
|
41
47
|
|
|
42
48
|
export function emphasis(theme: DiffTheme, text: string): string {
|
|
@@ -202,6 +208,11 @@ function rgbToBgAnsi(color: RgbColor): string {
|
|
|
202
208
|
return `\x1b[48;2;${r};${g};${b}m`;
|
|
203
209
|
}
|
|
204
210
|
|
|
211
|
+
function desaturateRgb(color: RgbColor, ratio: number): RgbColor {
|
|
212
|
+
const luma = (color.r * 299 + color.g * 587 + color.b * 114) / 1000;
|
|
213
|
+
return mixRgb(color, { r: luma, g: luma, b: luma }, ratio);
|
|
214
|
+
}
|
|
215
|
+
|
|
205
216
|
function mixRgb(base: RgbColor, tint: RgbColor, ratio: number): RgbColor {
|
|
206
217
|
const clamped = Math.max(0, Math.min(1, ratio));
|
|
207
218
|
return {
|
|
@@ -263,9 +274,11 @@ export function readThemeAnsi(
|
|
|
263
274
|
}
|
|
264
275
|
|
|
265
276
|
export function resolveDiffPalette(theme: DiffTheme): DiffPalette {
|
|
266
|
-
const
|
|
267
|
-
parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolPendingBg")) ??
|
|
277
|
+
const fallbackBg = parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolPendingBg")) ??
|
|
268
278
|
parseAnsiColorCode(readThemeAnsi(theme, "bg", "userMessageBg")) ?? { r: 32, g: 35, b: 42 };
|
|
279
|
+
// 删除行用 toolErrorBg 做基底(主题里是 diffDelBg 暗红);不定义时退回通用基底混 tint。
|
|
280
|
+
const addBaseBg = parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolSuccessBg")) ?? fallbackBg;
|
|
281
|
+
const removeBaseBg = parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolErrorBg")) ?? addBaseBg;
|
|
269
282
|
const addFg = parseAnsiColorCode(readThemeAnsi(theme, "fg", "toolDiffAdded")) ?? {
|
|
270
283
|
r: 88,
|
|
271
284
|
g: 173,
|
|
@@ -279,10 +292,28 @@ export function resolveDiffPalette(theme: DiffTheme): DiffPalette {
|
|
|
279
292
|
const addTint = mixRgb(addFg, ADDITION_TINT_TARGET, 0.35);
|
|
280
293
|
const removeTint = mixRgb(removeFg, DELETION_TINT_TARGET, 0.65);
|
|
281
294
|
|
|
282
|
-
const addRowBg =
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
const addRowBg = desaturateRgb(
|
|
296
|
+
mixRgb(addBaseBg, addTint, ADD_ROW_BACKGROUND_MIX_RATIO),
|
|
297
|
+
ROW_DESATURATE_RATIO,
|
|
298
|
+
);
|
|
299
|
+
const addEmphasisBg = desaturateRgb(
|
|
300
|
+
mixRgb(addBaseBg, addTint, ADD_INLINE_EMPHASIS_MIX_RATIO),
|
|
301
|
+
EMPHASIS_DESATURATE_RATIO,
|
|
302
|
+
);
|
|
303
|
+
// 暗色基底下向 GitHub 深酒红(~#67060c)加权,压掉主题基底的粉/灰感;
|
|
304
|
+
// 浅色基底保持原 tint 口径,免得行底深得盖过文字。
|
|
305
|
+
const removeBaseLuminance =
|
|
306
|
+
(removeBaseBg.r * 299 + removeBaseBg.g * 587 + removeBaseBg.b * 114) / 1000;
|
|
307
|
+
const removeRowTarget = removeBaseLuminance < 128 ? DELETION_ROW_TARGET : removeTint;
|
|
308
|
+
const removeEmphasisTarget = removeBaseLuminance < 128 ? DELETION_EMPHASIS_TARGET : removeTint;
|
|
309
|
+
const removeRowBg = desaturateRgb(
|
|
310
|
+
mixRgb(removeBaseBg, removeRowTarget, REMOVE_ROW_BACKGROUND_MIX_RATIO),
|
|
311
|
+
ROW_DESATURATE_RATIO,
|
|
312
|
+
);
|
|
313
|
+
const removeEmphasisBg = desaturateRgb(
|
|
314
|
+
mixRgb(removeBaseBg, removeEmphasisTarget, REMOVE_INLINE_EMPHASIS_MIX_RATIO),
|
|
315
|
+
EMPHASIS_DESATURATE_RATIO,
|
|
316
|
+
);
|
|
286
317
|
|
|
287
318
|
return {
|
|
288
319
|
addRowBgAnsi: rgbToBgAnsi(addRowBg),
|
|
@@ -71,7 +71,7 @@ export function buildDiffSummaryText(stats: DiffSummaryStats, width: number): st
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* 折叠摘要行样式:整行 muted,`+A -D` 用 diff
|
|
74
|
+
* 折叠摘要行样式:整行 muted,`+A -D` 用 diff 头行同一套绿/红,`click to show more` 与其余折叠卡一致用 dim,hover 提到 text。
|
|
75
75
|
* 0 行预览与多行预览共用这一套颜色:改折叠行数时同一处信息不变色。
|
|
76
76
|
*/
|
|
77
77
|
export function styleDiffSummaryLine(
|
|
@@ -103,7 +103,7 @@ export function styleDiffSummaryLine(
|
|
|
103
103
|
}
|
|
104
104
|
if (hintIndex >= cursor) {
|
|
105
105
|
pushMuted(hintIndex);
|
|
106
|
-
pieces.push(theme.fg(hovered ? "text" : "
|
|
106
|
+
pieces.push(theme.fg(hovered ? "text" : "dim", hint));
|
|
107
107
|
cursor = hintIndex + hint.length;
|
|
108
108
|
}
|
|
109
109
|
pushMuted(text.length);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type Component,
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
9
|
import { config } from "../../config/config.ts";
|
|
10
|
+
import { aggregateToolCardBgSlot, toolCardStatus } from "./tool-state.ts";
|
|
10
11
|
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
11
12
|
import {
|
|
12
13
|
ASK_USER_QUESTION_TOOL_NAME,
|
|
@@ -105,9 +106,7 @@ function previousSibling(
|
|
|
105
106
|
type ToolStatus = "pending" | "success" | "error";
|
|
106
107
|
|
|
107
108
|
function status(tool: any): ToolStatus {
|
|
108
|
-
|
|
109
|
-
if (tool?.isPartial === true || (tool?.executionStarted && !tool?.result)) return "pending";
|
|
110
|
-
return tool?.result ? "success" : "pending";
|
|
109
|
+
return toolCardStatus(tool);
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
function statusIcon(value: ToolStatus): string {
|
|
@@ -235,14 +234,15 @@ function askUserResultSummary(tool: any): ToolCallSummary | undefined {
|
|
|
235
234
|
.map((entry) => entry.answer);
|
|
236
235
|
if (answers.length === 0) {
|
|
237
236
|
const label = tool.result?.details?.cancelled === true ? "Declined" : "Done";
|
|
238
|
-
return { main:
|
|
237
|
+
return { main: toolTitle(tool), detail: "", payload: label };
|
|
239
238
|
}
|
|
240
239
|
const first = answers[0]!;
|
|
241
240
|
const question = String(first.question ?? "").trim() || "Question";
|
|
242
241
|
const more = answers.length > 1 ? ` (+${answers.length - 1})` : "";
|
|
243
242
|
return {
|
|
244
|
-
main:
|
|
243
|
+
main: toolTitle(tool),
|
|
245
244
|
detail: "",
|
|
245
|
+
payload: `${question} -> ${askUserAnswerText(first)}${more}`,
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
248
|
|
|
@@ -253,7 +253,7 @@ function codemodeResultSummary(tool: any): ToolCallSummary | undefined {
|
|
|
253
253
|
codemodeOutputLineCount(tool.result),
|
|
254
254
|
tool?.isPartial === true,
|
|
255
255
|
);
|
|
256
|
-
return { main:
|
|
256
|
+
return { main: toolTitle(tool), detail: "", payload: parts.join(" · ") };
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
/**
|
|
@@ -283,7 +283,7 @@ function collapsedGroupToolLine(
|
|
|
283
283
|
width: number,
|
|
284
284
|
): string {
|
|
285
285
|
const toolStatus = status(tool);
|
|
286
|
-
const color = toolStatus === "pending" ? "
|
|
286
|
+
const color = toolStatus === "pending" ? "dim" : toolStatus;
|
|
287
287
|
const summary = askUserResultSummary(tool) ?? codemodeResultSummary(tool) ?? toolSummary(tool);
|
|
288
288
|
const prefix = ` ${fg("dim", last ? "└" : "├")} ${fg(color, statusIcon(toolStatus))} `;
|
|
289
289
|
const detail = fg("dim", summary.detail);
|
|
@@ -311,7 +311,7 @@ function groupCountText(
|
|
|
311
311
|
.filter((key) => counts[key])
|
|
312
312
|
.map((key) => {
|
|
313
313
|
const label = key === "pending" ? "running" : key === "success" ? "done" : "failed";
|
|
314
|
-
const color = key === "pending" ? "
|
|
314
|
+
const color = key === "pending" ? "dim" : key;
|
|
315
315
|
return `${fg(color, String(counts[key]))} ${label}`;
|
|
316
316
|
})
|
|
317
317
|
.join(` ${fg("dim", "·")} `);
|
|
@@ -499,7 +499,7 @@ export class ToolGroupComponent extends Container {
|
|
|
499
499
|
const label = names.size === 1 ? toolTitle(this.children[0]) : "Multiple Tools";
|
|
500
500
|
const overall = groupOverallStatus(counts);
|
|
501
501
|
if ((this.children as any[]).some(isAnimatingTool)) scheduleGroupAnimation(this.patch);
|
|
502
|
-
const overallColor = overall === "pending" ? "
|
|
502
|
+
const overallColor = overall === "pending" ? "dim" : overall;
|
|
503
503
|
// 圆点保持 dim;hover 只高亮可点击文字。
|
|
504
504
|
const hintText = this._expanded ? collapseHintText() : showMoreHintText();
|
|
505
505
|
const hint = `${fg("dim", "·")} ${fg(this.hintHovered ? "text" : "dim", hintText)}`;
|
|
@@ -527,7 +527,7 @@ export class ToolGroupComponent extends Container {
|
|
|
527
527
|
expandedHit.hover === this.hintHovered &&
|
|
528
528
|
expandedHit.theme === this.patch.theme &&
|
|
529
529
|
expandedHit.fullscreen === isToolTuiFullscreen() &&
|
|
530
|
-
expandedHit.bgSlot ===
|
|
530
|
+
expandedHit.bgSlot === aggregateToolCardBgSlot(this.children) &&
|
|
531
531
|
expandedHit.paints.length === childPaints.length &&
|
|
532
532
|
expandedHit.paints.every((paint, index) => paint === childPaints[index])
|
|
533
533
|
) {
|
|
@@ -538,7 +538,7 @@ export class ToolGroupComponent extends Container {
|
|
|
538
538
|
for (let index = 0; index < total; index++) {
|
|
539
539
|
const tool = this.children[index];
|
|
540
540
|
const toolStatus = status(tool);
|
|
541
|
-
const color = toolStatus === "pending" ? "
|
|
541
|
+
const color = toolStatus === "pending" ? "dim" : toolStatus;
|
|
542
542
|
const branch = index === total - 1 ? "└" : "├";
|
|
543
543
|
const continuation = index === total - 1 ? " " : "│ ";
|
|
544
544
|
if (!this._expanded) {
|
|
@@ -564,8 +564,8 @@ export class ToolGroupComponent extends Container {
|
|
|
564
564
|
}
|
|
565
565
|
}
|
|
566
566
|
if (this._expanded) {
|
|
567
|
-
//
|
|
568
|
-
const backgroundSlot =
|
|
567
|
+
// 有一个失败整卡用失败底色;没有失败但还有进行中用进行中底色。
|
|
568
|
+
const backgroundSlot = aggregateToolCardBgSlot(this.children);
|
|
569
569
|
for (const line of expandedLines) {
|
|
570
570
|
lines.push(paddedBackgroundRow(theme, backgroundSlot, line, width));
|
|
571
571
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
7
7
|
import { MESSAGE_DISPLAY_PATCH, patchRegistry } from "../../utils/patch-keys.ts";
|
|
8
8
|
import { config } from "../../config/config.ts";
|
|
9
|
+
import { TOOL_CARD_BG_SLOT } from "./tool-state.ts";
|
|
9
10
|
import { showMoreHintText } from "./names.ts";
|
|
10
11
|
import { walkComponentTree } from "../../utils/component-tree.ts";
|
|
11
12
|
|
|
@@ -82,11 +83,11 @@ function renderCcstyle(component: any, kind: DisplayKind): void {
|
|
|
82
83
|
component.addChild(new Text(`${icon} ${title}${hint}`, 0, 0));
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
|
-
//
|
|
86
|
+
// 展开卡与成功的工具卡同底色,上下左右各 1 格。
|
|
86
87
|
component.paddingX = 1;
|
|
87
88
|
component.paddingY = 1;
|
|
88
89
|
if (typeof theme.bg === "function" && typeof component.setBgFn === "function") {
|
|
89
|
-
component.setBgFn((text: string) => theme.bg(
|
|
90
|
+
component.setBgFn((text: string) => theme.bg(TOOL_CARD_BG_SLOT.success, text));
|
|
90
91
|
}
|
|
91
92
|
component.addChild(new Text(`${icon} ${title}`, 0, 0));
|
|
92
93
|
component.addChild(new Spacer(1));
|