@tian.zuo/pi-beautify-style 0.2.0 → 0.3.0
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @tian.zuo/pi-beautify-style
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#201](https://github.com/TianZuo555/pi-extensions/pull/201) [`fad61ea`](https://github.com/TianZuo555/pi-extensions/commit/fad61ea8cd833d5efc22c2e171507414f87e179a) Thanks [@TianZuo555](https://github.com/TianZuo555)! - Use a leading `## Heading` (or `**Bold**` line) from the reasoning as the thinking card title on every provider, not only OpenAI Responses. The title shows while the reasoning streams and stays on the finished card (`Plan · Thought for 8s`). Headings quoted from tool output mid-reasoning are ignored.
|
|
8
|
+
|
|
9
|
+
- [#201](https://github.com/TianZuo555/pi-extensions/pull/201) [`fad61ea`](https://github.com/TianZuo555/pi-extensions/commit/fad61ea8cd833d5efc22c2e171507414f87e179a) Thanks [@TianZuo555](https://github.com/TianZuo555)! - Show the current reasoning step instead of `Thinking...` when the reasoning has no heading (DeepSeek, GLM, Kimi, and other `reasoning_content` models): `Step 3 · Check the CI config · 6s`. A step comes from an intent sentence (`Let me …`, `I'll …`, `让我…`), a lead such as `Plan:` or `Decision:`, or a rethink (`Actually, let me …`). Each label stays at least 800 ms, so fast thoughts do not flicker. The finished card keeps the last step and the count: `Check the CI config · Thought for 8s · 3 steps`.
|
|
10
|
+
|
|
3
11
|
## 0.2.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -90,6 +90,13 @@ and restart Pi to change behavior.
|
|
|
90
90
|
> summary title — that heading becomes the card title while it streams, and stays
|
|
91
91
|
> on the finished card (`Plan · Thought for 8s`). A heading copied out of tool
|
|
92
92
|
> output mid-reasoning is ignored.
|
|
93
|
+
>
|
|
94
|
+
> Raw reasoning with no heading (DeepSeek, GLM, Kimi, and other
|
|
95
|
+
> `reasoning_content` models) shows its current step instead of `Thinking...`:
|
|
96
|
+
> `Step 3 · Check the CI config · 6s`. A step comes from an intent sentence
|
|
97
|
+
> (`Let me …`, `I'll …`, `让我…`), a lead such as `Plan:` or `Decision:`, or a
|
|
98
|
+
> rethink (`Actually, let me …`). The finished card keeps the last step and the
|
|
99
|
+
> count: `Check the CI config · Thought for 8s · 3 steps`.
|
|
93
100
|
|
|
94
101
|
> [!NOTE]
|
|
95
102
|
> For Mermaid diagrams, set `markdown.mermaid` to `final` in
|
|
@@ -54,9 +54,11 @@ export type Config = {
|
|
|
54
54
|
toolLabelClip: number;
|
|
55
55
|
/**
|
|
56
56
|
* Title the reasoning card from a heading the reasoning leads with: an OpenAI
|
|
57
|
-
* Responses summary heading, or a leading `# Heading` / `**Bold**` line.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* Responses summary heading, or a leading `# Heading` / `**Bold**` line. Reasoning
|
|
58
|
+
* without a heading shows its current step while it streams (`Step 3 · Check the
|
|
59
|
+
* CI config`), taken from intent sentences such as `Let me …` or leads such as
|
|
60
|
+
* `Plan:`. The title stays on the finished card (`Plan · Thought for 8s`, or
|
|
61
|
+
* `Check the CI config · Thought for 8s · 3 steps`).
|
|
60
62
|
*/
|
|
61
63
|
useSummaryTitlesAsThinkingTitle: boolean;
|
|
62
64
|
/**
|
|
@@ -29,6 +29,12 @@ import {
|
|
|
29
29
|
styleCompactThinkingText,
|
|
30
30
|
} from "../renderer/compact-mode.ts";
|
|
31
31
|
import { refreshMountedTranscript } from "../renderer/transcript-refresh.ts";
|
|
32
|
+
import {
|
|
33
|
+
formatStepCount,
|
|
34
|
+
formatThinkingStep,
|
|
35
|
+
parseThinkingSteps,
|
|
36
|
+
type ThinkingStep,
|
|
37
|
+
} from "./thinking-steps.ts";
|
|
32
38
|
import {
|
|
33
39
|
COMPACT_THINKING_OWNER,
|
|
34
40
|
COMPACT_THINKING_PATCH_KEY,
|
|
@@ -49,6 +55,9 @@ export type CompactThinkingConfig = {
|
|
|
49
55
|
* Use a title that the reasoning leads with — an OpenAI Responses summary
|
|
50
56
|
* heading or a leading `# Heading` / `**Bold**` line — as the card heading
|
|
51
57
|
* instead of the `Thinking...` label. The title stays on the finished card.
|
|
58
|
+
* Reasoning without a title shows its current step instead
|
|
59
|
+
* (`Step 3 · Check the CI config`); the finished card keeps the last step and
|
|
60
|
+
* the step count.
|
|
52
61
|
*/
|
|
53
62
|
useSummaryTitlesAsThinkingTitle: boolean;
|
|
54
63
|
/**
|
|
@@ -132,6 +141,8 @@ const config: CompactThinkingConfig = {
|
|
|
132
141
|
};
|
|
133
142
|
|
|
134
143
|
const DURATION_ENTRY_TYPE = "compact-thinking-duration";
|
|
144
|
+
/** A live step label stays at least this long before the next one replaces it. */
|
|
145
|
+
const STEP_LABEL_MIN_MS = 800;
|
|
135
146
|
|
|
136
147
|
/** 当前激活 session 的只读查询委托(compact 渲染层使用)。 */
|
|
137
148
|
type ThinkingDurationQuery = (messageTimestamp: number) => number | undefined;
|
|
@@ -690,6 +701,30 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
690
701
|
let animationFrame = 0;
|
|
691
702
|
let compactSummaryActive = false;
|
|
692
703
|
let patchInstalled = true;
|
|
704
|
+
/** Live step label on screen per run (`timestamp:runStartIndex`), for the flicker hold. */
|
|
705
|
+
const shownSteps = new Map<string, { label: string; count: number; shownAt: number }>();
|
|
706
|
+
let stepMemo: { text: string; streaming: boolean; steps: ThinkingStep[] } | undefined;
|
|
707
|
+
|
|
708
|
+
function stepsOf(text: string, streaming: boolean): ThinkingStep[] {
|
|
709
|
+
if (stepMemo?.text !== text || stepMemo.streaming !== streaming) {
|
|
710
|
+
stepMemo = { text, streaming, steps: parseThinkingSteps(text, streaming) };
|
|
711
|
+
}
|
|
712
|
+
return stepMemo.steps;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** Keep the shown step for STEP_LABEL_MIN_MS so fast thoughts do not flicker. */
|
|
716
|
+
function heldStep(key: string, label: string, count: number) {
|
|
717
|
+
const now = Date.now();
|
|
718
|
+
const shown = shownSteps.get(key);
|
|
719
|
+
if (shown && shown.label !== label && now - shown.shownAt < STEP_LABEL_MIN_MS) return shown;
|
|
720
|
+
if (shown?.label === label) {
|
|
721
|
+
shown.count = count;
|
|
722
|
+
return shown;
|
|
723
|
+
}
|
|
724
|
+
const next = { label, count, shownAt: now };
|
|
725
|
+
shownSteps.set(key, next);
|
|
726
|
+
return next;
|
|
727
|
+
}
|
|
693
728
|
|
|
694
729
|
function thinkingStyle(text: string) {
|
|
695
730
|
return styleCompactThinkingText(text, activeTheme);
|
|
@@ -792,30 +827,40 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
792
827
|
(previous.type === "thinking" && previous.thinking.trim()),
|
|
793
828
|
);
|
|
794
829
|
const thinkingBlocks: string[] = [];
|
|
830
|
+
// Untrimmed text: a trailing newline proves the last sentence is complete.
|
|
831
|
+
const rawThinkingBlocks: string[] = [];
|
|
795
832
|
let latestSummary: SummaryPart | undefined;
|
|
833
|
+
const responses = isOpenAiResponsesMessage(message);
|
|
796
834
|
for (; i < message.content.length; i++) {
|
|
797
835
|
const thinkingContent = message.content[i];
|
|
798
836
|
if (thinkingContent.type !== "thinking") break;
|
|
799
837
|
const thinking = thinkingContent.thinking.trim();
|
|
800
838
|
if (!thinking) continue;
|
|
801
839
|
thinkingBlocks.push(thinking);
|
|
840
|
+
rawThinkingBlocks.push(thinkingContent.thinking);
|
|
802
841
|
|
|
803
842
|
if (config.useSummaryTitlesAsThinkingTitle) {
|
|
804
843
|
const contentIsActive =
|
|
805
844
|
activeThinking?.messageTimestamp === message.timestamp &&
|
|
806
845
|
activeThinking.contentIndex === i;
|
|
846
|
+
// Raw reasoning: a block's leading title names the card and leaves that
|
|
847
|
+
// block's body. Every block of the run stays in the preview.
|
|
848
|
+
if (!responses) {
|
|
849
|
+
const leading = parseLeadingTitle(thinking, contentIsActive);
|
|
850
|
+
if (leading) {
|
|
851
|
+
latestSummary = leading;
|
|
852
|
+
thinkingBlocks[thinkingBlocks.length - 1] = leading.body;
|
|
853
|
+
}
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
807
856
|
// During streaming, thinkingSignature may contain only the previously
|
|
808
857
|
// completed summary parts. Prefer the live text so a newly arriving
|
|
809
858
|
// title immediately replaces the old one instead of appearing in its
|
|
810
859
|
// preview body. Once complete, the structured signature is canonical.
|
|
811
860
|
// OpenAI Responses concatenates several summary parts in one block, so
|
|
812
|
-
//
|
|
813
|
-
// title from the leading line of its reasoning.
|
|
861
|
+
// the newest title wins and its summary body is the preview.
|
|
814
862
|
const titleFromText = (streaming: boolean) =>
|
|
815
|
-
|
|
816
|
-
? (parseLatestTitle(thinking, streaming) ??
|
|
817
|
-
parseLeadingTitle(thinking, streaming))
|
|
818
|
-
: parseLeadingTitle(thinking, streaming);
|
|
863
|
+
parseLatestTitle(thinking, streaming) ?? parseLeadingTitle(thinking, streaming);
|
|
819
864
|
const titleFromSignature = getLatestOpenAiSummary(
|
|
820
865
|
thinkingContent.thinkingSignature,
|
|
821
866
|
);
|
|
@@ -842,8 +887,23 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
842
887
|
const durationMs = active ? elapsedMs : completedMs;
|
|
843
888
|
const durationText = durationMs === undefined ? undefined : formatThoughtDuration(durationMs);
|
|
844
889
|
|
|
890
|
+
const steps =
|
|
891
|
+
config.useSummaryTitlesAsThinkingTitle && !latestSummary
|
|
892
|
+
? stepsOf(rawThinkingBlocks.join("\n\n"), active)
|
|
893
|
+
: [];
|
|
894
|
+
const lastStep = steps.at(-1);
|
|
895
|
+
|
|
845
896
|
let heading: string;
|
|
846
|
-
if (active &&
|
|
897
|
+
if (active && lastStep) {
|
|
898
|
+
const shown = heldStep(
|
|
899
|
+
`${message.timestamp}:${runStartIndex}`,
|
|
900
|
+
formatThinkingStep(lastStep),
|
|
901
|
+
steps.length,
|
|
902
|
+
);
|
|
903
|
+
heading =
|
|
904
|
+
animatedText(`Step ${shown.count} · ${shown.label}`, thinkingStyle, true) +
|
|
905
|
+
(durationText ? thinkingStyle(` · ${durationText}`) : "");
|
|
906
|
+
} else if (active && latestSummary) {
|
|
847
907
|
heading =
|
|
848
908
|
animatedText(latestSummary.title, summaryTitleStyle, true) +
|
|
849
909
|
(durationText ? thinkingStyle(` · ${durationText}`) : "");
|
|
@@ -857,13 +917,17 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
857
917
|
// A detected title stays in front of it, so the card keeps its identity
|
|
858
918
|
// after the run. 无时长记录(中断流/旧会话/entry 缺失或索引错位)时不回退到
|
|
859
919
|
// 加载文案 "Thinking...":改用标题或中性 "Thought"。
|
|
860
|
-
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
title
|
|
864
|
-
|
|
920
|
+
// Without a title, the last step names the card and the step count follows.
|
|
921
|
+
const title = latestSummary?.title ?? (lastStep && formatThinkingStep(lastStep));
|
|
922
|
+
const parts = [
|
|
923
|
+
title,
|
|
924
|
+
durationText ? `Thought for ${durationText}` : undefined,
|
|
925
|
+
lastStep ? formatStepCount(steps.length) : undefined,
|
|
926
|
+
].filter((part): part is string => Boolean(part));
|
|
927
|
+
heading = thinkingStyle(parts.length > 0 ? parts.join(" · ") : "Thought");
|
|
865
928
|
}
|
|
866
|
-
const
|
|
929
|
+
const fullRun = thinkingBlocks.filter(Boolean).join("\n\n");
|
|
930
|
+
const previewSource = (responses ? (latestSummary?.body ?? fullRun) : fullRun).trim();
|
|
867
931
|
self.contentContainer.addChild(
|
|
868
932
|
new ThinkingPreviewBlock(
|
|
869
933
|
heading,
|
|
@@ -968,6 +1032,7 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
968
1032
|
startedAt: Date.now(),
|
|
969
1033
|
};
|
|
970
1034
|
streamingComponents.clear();
|
|
1035
|
+
shownSteps.clear();
|
|
971
1036
|
animationFrame = 0;
|
|
972
1037
|
|
|
973
1038
|
// Depending on event-listener order, Pi may have rendered the empty
|
|
@@ -1003,6 +1068,7 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
1003
1068
|
} as DurationEntryData);
|
|
1004
1069
|
|
|
1005
1070
|
activeThinking = undefined;
|
|
1071
|
+
shownSteps.clear();
|
|
1006
1072
|
stopAnimationTimerIfIdle();
|
|
1007
1073
|
|
|
1008
1074
|
const components = [...streamingComponents];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Step labels for raw chain-of-thought reasoning (DeepSeek, GLM, Kimi, Claude, …).
|
|
2
|
+
// These providers stream no headings, so the card would only say `Thinking...`.
|
|
3
|
+
// The reasoning still narrates itself: one paragraph per thought, an intent
|
|
4
|
+
// sentence (`Let me check X.`) for the next action, a pivot word (`Wait`,
|
|
5
|
+
// `Actually`) for a rethink, and a labeled lead (`Plan:`, `Decision:`) for a
|
|
6
|
+
// milestone. Each paragraph gives at most one step.
|
|
7
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
|
|
9
|
+
export type ThinkingStepKind = "intent" | "rethink" | "milestone";
|
|
10
|
+
|
|
11
|
+
export type ThinkingStep = {
|
|
12
|
+
kind: ThinkingStepKind;
|
|
13
|
+
label: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Max columns of a step label, so the duration after it stays visible. */
|
|
17
|
+
const MAX_STEP_WIDTH = 60;
|
|
18
|
+
|
|
19
|
+
const LEAD_WORDS =
|
|
20
|
+
"ok|okay|so|now|next|then|first|finally|good|great|alright|right|actually|wait|hmm+|but|and|also|instead|alternatively";
|
|
21
|
+
const INTENT_EN = new RegExp(
|
|
22
|
+
`^(?:(?:${LEAD_WORDS})[,.!:;—–-]*\\s+)*(?:let me|let's|let us|i'll|i will|i'm going to|i need to)\\s+(?:(?:also|quickly|first|now|just|then|next|still)\\s+)*(.+)$`,
|
|
23
|
+
"i",
|
|
24
|
+
);
|
|
25
|
+
const INTENT_ZH =
|
|
26
|
+
/^(?:好的?|那么|现在|接下来|然后|首先|最后|嗯|不过|其实)?[,,、\s]*(?:让我|我来|我先|我需要|我要|我将|下一步)[,,::\s]*(.+)$/;
|
|
27
|
+
const PIVOT_EN = /^(?:wait|actually|but wait|alternatively|hold on|on second thought|instead)\b/i;
|
|
28
|
+
const PIVOT_ZH = /^(?:不对|等等|其实|或者|换个思路)/;
|
|
29
|
+
const MILESTONE =
|
|
30
|
+
/^(plan|decision|conclusion|confirmed|root cause|key question|approach|goal|summary|fix|findings?|result|answer|计划|结论|决定|方案|原因|根因|目标)[ \t]*[::][ \t]*([\s\S]*)$/i;
|
|
31
|
+
const LIST_MARKER = /^\s*(?:\d+[.)]|[-*•])\s+/;
|
|
32
|
+
/** End of a complete sentence or line. A `.` counts only before whitespace, so `foo.ts` is not cut. */
|
|
33
|
+
const BOUNDARY = /[.!?](?=\s)|[。!?]|\n/g;
|
|
34
|
+
|
|
35
|
+
function completePart(text: string): string {
|
|
36
|
+
let end = 0;
|
|
37
|
+
for (const match of text.matchAll(BOUNDARY)) end = match.index + match[0].length;
|
|
38
|
+
return text.slice(0, end);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sentences(paragraph: string): string[] {
|
|
42
|
+
return paragraph
|
|
43
|
+
.split("\n")
|
|
44
|
+
.flatMap((line) => line.split(/(?<=[.!?])\s+(?=\S)|(?<=[。!?])/))
|
|
45
|
+
.map((sentence) => sentence.trim())
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function clean(text: string): string {
|
|
50
|
+
const plain = text
|
|
51
|
+
.replace(/\s+/g, " ")
|
|
52
|
+
.replace(/[\s.!?:;,。!?:;,…]+$/, "")
|
|
53
|
+
.trim();
|
|
54
|
+
if (visibleWidth(plain) <= MAX_STEP_WIDTH) return plain;
|
|
55
|
+
let out = "";
|
|
56
|
+
let width = 0;
|
|
57
|
+
for (const char of plain) {
|
|
58
|
+
const charWidth = visibleWidth(char);
|
|
59
|
+
if (width + charWidth > MAX_STEP_WIDTH - 1) break;
|
|
60
|
+
out += char;
|
|
61
|
+
width += charWidth;
|
|
62
|
+
}
|
|
63
|
+
return `${out.trimEnd()}…`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function capitalize(text: string): string {
|
|
67
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function milestoneStep(paragraph: string): ThinkingStep | undefined {
|
|
71
|
+
const match = MILESTONE.exec(paragraph);
|
|
72
|
+
if (!match) return undefined;
|
|
73
|
+
const lead = capitalize(match[1]);
|
|
74
|
+
const first = match[2]
|
|
75
|
+
.split("\n")
|
|
76
|
+
.map((line) => line.replace(LIST_MARKER, "").trim())
|
|
77
|
+
.find(Boolean);
|
|
78
|
+
const detail = first ? clean(sentences(first)[0] ?? "") : "";
|
|
79
|
+
return { kind: "milestone", label: detail ? clean(`${lead}: ${detail}`) : lead };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function intentStep(paragraph: string): ThinkingStep | undefined {
|
|
83
|
+
const all = sentences(paragraph);
|
|
84
|
+
for (let i = all.length - 1; i >= 0; i--) {
|
|
85
|
+
const sentence = all[i];
|
|
86
|
+
const english = INTENT_EN.exec(sentence);
|
|
87
|
+
const action = english?.[1] ?? INTENT_ZH.exec(sentence)?.[1];
|
|
88
|
+
if (!action || /^know\b/i.test(action)) continue;
|
|
89
|
+
const label = clean(english ? capitalize(action) : action);
|
|
90
|
+
if (label.length < 3) continue;
|
|
91
|
+
const pivot = PIVOT_EN.test(sentence) || PIVOT_ZH.test(sentence);
|
|
92
|
+
return { kind: pivot ? "rethink" : "intent", label };
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Steps found in reasoning text, oldest first. While the text streams, the
|
|
99
|
+
* unfinished last sentence is ignored, so a label never shows half a word.
|
|
100
|
+
*/
|
|
101
|
+
export function parseThinkingSteps(text: string, streaming: boolean): ThinkingStep[] {
|
|
102
|
+
const source = (streaming ? completePart(text) : text)
|
|
103
|
+
// Code in a fence is quoted material, not narration. An open fence runs to the end.
|
|
104
|
+
.replace(/```[\s\S]*?(?:```|$)/g, "\n")
|
|
105
|
+
.replace(/\*\*|`/g, "");
|
|
106
|
+
const steps: ThinkingStep[] = [];
|
|
107
|
+
for (const raw of source.split(/\n[ \t]*\n/)) {
|
|
108
|
+
const paragraph = raw.trim();
|
|
109
|
+
if (!paragraph) continue;
|
|
110
|
+
const step = milestoneStep(paragraph) ?? intentStep(paragraph);
|
|
111
|
+
if (step) steps.push(step);
|
|
112
|
+
}
|
|
113
|
+
return steps;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Card text of one step. */
|
|
117
|
+
export function formatThinkingStep(step: ThinkingStep): string {
|
|
118
|
+
return step.kind === "rethink" ? clean(`Rethinking: ${step.label}`) : step.label;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function formatStepCount(count: number): string {
|
|
122
|
+
return count === 1 ? "1 step" : `${count} steps`;
|
|
123
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tian.zuo/pi-beautify-style",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Claude Code-style TUI for the pi coding agent: tool cards, rich diffs, compact mode, footer chips, and integrations for this repo's ask-user, background-terminals, and usage extensions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|