@oh-my-pi/pi-coding-agent 16.4.3 → 16.4.4
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 +13 -0
- package/dist/cli.js +3014 -2996
- package/dist/types/tiny/message-preproc.d.ts +63 -0
- package/dist/types/tiny/text.d.ts +1 -27
- package/package.json +12 -12
- package/scripts/bench-title-models.ts +332 -0
- package/scripts/build-binary.ts +12 -12
- package/src/auto-thinking/classifier.ts +2 -16
- package/src/cli.ts +11 -3
- package/src/prompts/system/title-system.md +11 -12
- package/src/session/agent-session.ts +1 -1
- package/src/tiny/message-preproc.ts +155 -0
- package/src/tiny/text.ts +12 -70
- package/src/tiny/worker.ts +6 -3
- package/src/tools/image-gen.ts +3 -4
- package/src/utils/title-generator.ts +5 -4
- package/src/prompts/system/tiny-title-system.md +0 -8
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts raw user text into bounded, low-noise input for tiny models.
|
|
3
|
+
*
|
|
4
|
+
* Tiny models copy literal noise verbatim and lose the task when only the head
|
|
5
|
+
* of a long message survives. The shared pipeline strips ANSI escapes, paired
|
|
6
|
+
* XML/tool envelopes, full commit hashes, and fenced code blocks, then preserves
|
|
7
|
+
* both ends with an explicit omission marker. Title generation, auto-thinking,
|
|
8
|
+
* and the title benchmark MUST use this same policy.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Maximum characters emitted by {@link preprocessTinyMessage}. */
|
|
12
|
+
export const MAX_TINY_MESSAGE_CHARS = 2000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Minimum length of code-stripped input below which we fall back to the
|
|
16
|
+
* original message. Guards against messages that are (almost) entirely a code
|
|
17
|
+
* block — stripping would otherwise leave the model nothing to title from.
|
|
18
|
+
*/
|
|
19
|
+
const MIN_STRIPPED_TITLE_CHARS = 12;
|
|
20
|
+
/** Matches a fenced code block (3+ backticks), including an unterminated trailing fence. */
|
|
21
|
+
const FENCED_CODE_BLOCK = /```+[\s\S]*?(?:```+|$)/g;
|
|
22
|
+
/** Matches SGR ANSI escape sequences (colors/styles) that leak in from pasted terminal output. */
|
|
23
|
+
const ANSI_ESCAPE = /\u001b\[[0-9;]*m/g;
|
|
24
|
+
/** Matches a paired XML/HTML-ish block, e.g. `<user>…</user>` or a tool envelope. */
|
|
25
|
+
const XML_BLOCK = /<([a-zA-Z][\w-]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g;
|
|
26
|
+
/** Matches a hex run long enough to be a full commit SHA rather than an ordinary word. */
|
|
27
|
+
const LONG_HEX_RUN = /\b[0-9a-fA-F]{12,}\b/g;
|
|
28
|
+
/** Short-hash prefix length kept after truncating a long hex run. */
|
|
29
|
+
const SHORT_HASH_CHARS = 7;
|
|
30
|
+
|
|
31
|
+
/** Drop SGR ANSI escape sequences. */
|
|
32
|
+
export function stripAnsi(message: string): string {
|
|
33
|
+
return message.replace(ANSI_ESCAPE, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Remove paired XML/HTML-ish blocks (`<user>…</user>`, `<think>…</think>`,
|
|
38
|
+
* tool envelopes). Self-closing and unpaired inline tags (`<Header/>`, a lone
|
|
39
|
+
* `<div>`) are left in place — only fully paired blocks, whose contents would
|
|
40
|
+
* otherwise dominate the title, are dropped.
|
|
41
|
+
*/
|
|
42
|
+
export function stripXmlBlocks(message: string): string {
|
|
43
|
+
return message.replace(XML_BLOCK, " ");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Truncate full commit-hash-like hex runs (≥12 chars) to a short 7-char prefix. */
|
|
47
|
+
export function shortenHashes(message: string): string {
|
|
48
|
+
return message.replace(LONG_HEX_RUN, match => match.slice(0, SHORT_HASH_CHARS));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Middle-truncate cleaned text, preserving 2/3 of the available space from the
|
|
53
|
+
* head and 1/3 from the tail. The omission marker counts toward the bound.
|
|
54
|
+
*/
|
|
55
|
+
export function truncateTinyMessage(message: string): string {
|
|
56
|
+
if (message.length <= MAX_TINY_MESSAGE_CHARS) return message;
|
|
57
|
+
let omitted = message.length - MAX_TINY_MESSAGE_CHARS;
|
|
58
|
+
let marker = "";
|
|
59
|
+
let headChars = 0;
|
|
60
|
+
let tailChars = 0;
|
|
61
|
+
// The omitted count changes the marker width; two passes converge because
|
|
62
|
+
// only the decimal digit count can change.
|
|
63
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
64
|
+
marker = `\n[… ${omitted} chars omitted …]\n`;
|
|
65
|
+
const keptChars = Math.max(0, MAX_TINY_MESSAGE_CHARS - marker.length);
|
|
66
|
+
headChars = Math.ceil((keptChars * 2) / 3);
|
|
67
|
+
tailChars = keptChars - headChars;
|
|
68
|
+
omitted = message.length - headChars - tailChars;
|
|
69
|
+
}
|
|
70
|
+
marker = `\n[… ${omitted} chars omitted …]\n`;
|
|
71
|
+
return `${message.slice(0, headChars)}${marker}${message.slice(-tailChars)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Strip fenced code blocks from a message before titling.
|
|
76
|
+
*
|
|
77
|
+
* Small title models latch onto literal text inside code blocks — e.g. a pasted
|
|
78
|
+
* UI mockup containing "Welcome to Claude Code v2.1.158" yields that string as
|
|
79
|
+
* the title instead of the surrounding intent. Removing fenced blocks leaves the
|
|
80
|
+
* prose that actually describes the task. Inline code (single backticks) is kept
|
|
81
|
+
* — it is short, high-signal context like `/login`.
|
|
82
|
+
*
|
|
83
|
+
* Falls back to the original message when stripping leaves too little to title
|
|
84
|
+
* (a message that is essentially just a code block).
|
|
85
|
+
*/
|
|
86
|
+
export function stripCodeBlocks(message: string): string {
|
|
87
|
+
const cleaned = message
|
|
88
|
+
.replace(FENCED_CODE_BLOCK, " ")
|
|
89
|
+
.replace(/[ \t]+/g, " ")
|
|
90
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
91
|
+
.trim();
|
|
92
|
+
return cleaned.length >= MIN_STRIPPED_TITLE_CHARS ? cleaned : message;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Clean noise from message content without applying the length bound. */
|
|
96
|
+
export function cleanTinyMessage(message: string): string {
|
|
97
|
+
return stripCodeBlocks(shortenHashes(stripXmlBlocks(stripAnsi(message))));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Apply the shared tiny-model cleanup and middle-truncation policy. */
|
|
101
|
+
export function preprocessTinyMessage(message: string): string {
|
|
102
|
+
return truncateTinyMessage(cleanTinyMessage(message));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Envelope produced by {@link formatTitleConversationContext}. Anchored to both
|
|
106
|
+
* ends so ordinary user text merely containing a chat snippet never matches. */
|
|
107
|
+
const CHAT_CONTEXT_ENVELOPE = /^\s*<chat>[\s\S]*<\/chat>\s*$/;
|
|
108
|
+
/** Structural tags emitted by {@link formatTitleConversationContext}. */
|
|
109
|
+
const CHAT_SCAFFOLD_TAG = /<\/?(?:chat|user|assistant|think)>/g;
|
|
110
|
+
|
|
111
|
+
/** True when `message` is a preformatted replan context from
|
|
112
|
+
* {@link formatTitleConversationContext} — already cleaned per turn and
|
|
113
|
+
* bounded, so it must bypass {@link preprocessTinyMessage} (whose paired-tag
|
|
114
|
+
* stripping would consume the entire envelope). */
|
|
115
|
+
export function isPreformattedChatContext(message: string): boolean {
|
|
116
|
+
return CHAT_CONTEXT_ENVELOPE.test(message);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Drop the `<chat>`/`<user>`/`<assistant>`/`<think>` scaffolding, keeping turn
|
|
120
|
+
* text. Used for token-level signal checks on preformatted contexts. */
|
|
121
|
+
export function stripChatScaffolding(message: string): string {
|
|
122
|
+
return message.replace(CHAT_SCAFFOLD_TAG, " ");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Wrap a preprocessed user message for title generation. Preformatted replan
|
|
126
|
+
* contexts pass through untouched. */
|
|
127
|
+
export function formatTitleUserMessage(message: string): string {
|
|
128
|
+
if (isPreformattedChatContext(message)) return message;
|
|
129
|
+
return `<user>\n${preprocessTinyMessage(message)}\n</user>`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** One recent conversation turn supplied to title refresh after replanning. */
|
|
133
|
+
export interface TitleConversationTurn {
|
|
134
|
+
role: "user" | "assistant";
|
|
135
|
+
text?: string;
|
|
136
|
+
thinking?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Format preprocessed recent context for title generation after a todo replan. */
|
|
140
|
+
export function formatTitleConversationContext(turns: readonly TitleConversationTurn[]): string {
|
|
141
|
+
const formattedTurns: string[] = [];
|
|
142
|
+
for (const turn of turns) {
|
|
143
|
+
const sections: string[] = [];
|
|
144
|
+
// Clean raw content before adding structural tags so paired-tag stripping
|
|
145
|
+
// cannot consume the `<user>`/`<assistant>` scaffolding added below.
|
|
146
|
+
const text = cleanTinyMessage(turn.text ?? "").trim();
|
|
147
|
+
if (text) sections.push(text);
|
|
148
|
+
const thinking = turn.role === "assistant" ? cleanTinyMessage(turn.thinking ?? "").trim() : "";
|
|
149
|
+
if (thinking) sections.push(`<think>\n${thinking}\n</think>`);
|
|
150
|
+
if (sections.length === 0) continue;
|
|
151
|
+
formattedTurns.push(`<${turn.role}>\n${sections.join("\n\n")}\n</${turn.role}>`);
|
|
152
|
+
}
|
|
153
|
+
if (formattedTurns.length === 0) return "";
|
|
154
|
+
return truncateTinyMessage(`<chat>\n${formattedTurns.join("\n\n")}\n</chat>`);
|
|
155
|
+
}
|
package/src/tiny/text.ts
CHANGED
|
@@ -1,70 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Minimum length of code-stripped input below which we fall back to the
|
|
5
|
-
* original message. Guards against messages that are (almost) entirely a code
|
|
6
|
-
* block — stripping would otherwise leave the model nothing to title from.
|
|
7
|
-
*/
|
|
8
|
-
const MIN_STRIPPED_TITLE_CHARS = 12;
|
|
9
|
-
/** Matches a fenced code block (3+ backticks), including an unterminated trailing fence. */
|
|
10
|
-
const FENCED_CODE_BLOCK = /```+[\s\S]*?(?:```+|$)/g;
|
|
11
|
-
|
|
12
|
-
export function truncateTitleInput(message: string): string {
|
|
13
|
-
return message.length > MAX_TITLE_INPUT_CHARS ? `${message.slice(0, MAX_TITLE_INPUT_CHARS)}…` : message;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Strip fenced code blocks from a message before titling.
|
|
18
|
-
*
|
|
19
|
-
* Small title models latch onto literal text inside code blocks — e.g. a pasted
|
|
20
|
-
* UI mockup containing "Welcome to Claude Code v2.1.158" yields that string as
|
|
21
|
-
* the title instead of the surrounding intent. Removing fenced blocks leaves the
|
|
22
|
-
* prose that actually describes the task. Inline code (single backticks) is kept
|
|
23
|
-
* — it is short, high-signal context like `/login`.
|
|
24
|
-
*
|
|
25
|
-
* Falls back to the original message when stripping leaves too little to title
|
|
26
|
-
* (a message that is essentially just a code block).
|
|
27
|
-
*/
|
|
28
|
-
export function stripCodeBlocks(message: string): string {
|
|
29
|
-
const cleaned = message
|
|
30
|
-
.replace(FENCED_CODE_BLOCK, " ")
|
|
31
|
-
.replace(/[ \t]+/g, " ")
|
|
32
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
33
|
-
.trim();
|
|
34
|
-
return cleaned.length >= MIN_STRIPPED_TITLE_CHARS ? cleaned : message;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Prepare a raw user message for titling: drop code blocks, then bound length. */
|
|
38
|
-
export function prepareTitleInput(message: string): string {
|
|
39
|
-
return truncateTitleInput(stripCodeBlocks(message));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function formatTitleUserMessage(message: string): string {
|
|
43
|
-
return `<user-message>\n${prepareTitleInput(message)}\n</user-message>`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Single recent conversation turn supplied to title refresh after replanning. */
|
|
47
|
-
export interface TitleConversationTurn {
|
|
48
|
-
role: "user" | "assistant";
|
|
49
|
-
text?: string;
|
|
50
|
-
thinking?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Format recent user/assistant context for title generation after a todo replan. */
|
|
54
|
-
export function formatTitleConversationContext(turns: readonly TitleConversationTurn[]): string {
|
|
55
|
-
const formattedTurns: string[] = [];
|
|
56
|
-
for (const turn of turns) {
|
|
57
|
-
const sections: string[] = [];
|
|
58
|
-
const text = turn.text?.trim();
|
|
59
|
-
if (text) sections.push(text);
|
|
60
|
-
const thinking = turn.role === "assistant" ? turn.thinking?.trim() : undefined;
|
|
61
|
-
if (thinking) sections.push(`<thinking>\n${thinking}\n</thinking>`);
|
|
62
|
-
if (sections.length === 0) continue;
|
|
63
|
-
formattedTurns.push(`<${turn.role}>\n${sections.join("\n\n")}\n</${turn.role}>`);
|
|
64
|
-
}
|
|
65
|
-
if (formattedTurns.length === 0) return "";
|
|
66
|
-
return prepareTitleInput(`<conversation>\n${formattedTurns.join("\n\n")}\n</conversation>`);
|
|
67
|
-
}
|
|
1
|
+
import { cleanTinyMessage, isPreformattedChatContext, stripChatScaffolding } from "./message-preproc";
|
|
68
2
|
|
|
69
3
|
/**
|
|
70
4
|
* Greeting / acknowledgement / filler tokens. A first user message composed
|
|
@@ -191,7 +125,11 @@ const COMMON_TITLE_ACRONYMS = new Set<string>([
|
|
|
191
125
|
* the next message instead.
|
|
192
126
|
*/
|
|
193
127
|
export function isLowSignalTitleInput(message: string): boolean {
|
|
194
|
-
|
|
128
|
+
// Preformatted replan contexts are already cleaned per turn; only the
|
|
129
|
+
// scaffolding tags are dropped so the turn text drives the signal check
|
|
130
|
+
// (cleanTinyMessage would strip the paired <chat> envelope to nothing).
|
|
131
|
+
const cleaned = isPreformattedChatContext(message) ? stripChatScaffolding(message) : cleanTinyMessage(message);
|
|
132
|
+
const tokens = cleaned.toLowerCase().match(TITLE_WORD);
|
|
195
133
|
if (!tokens) return true;
|
|
196
134
|
return tokens.every(token => FILLER_TITLE_TOKENS.has(token) || /^\d+$/.test(token));
|
|
197
135
|
}
|
|
@@ -200,14 +138,18 @@ export function isLowSignalTitleInput(message: string): boolean {
|
|
|
200
138
|
* Sentinel a capable title model may emit when a message carries no concrete
|
|
201
139
|
* task. Treated as "no title yet" so the caller can defer titling. Backstop for
|
|
202
140
|
* the deterministic {@link isLowSignalTitleInput} filter; kept in sync with the
|
|
203
|
-
*
|
|
141
|
+
* `<title/>` instruction in `prompts/system/title-system.md`.
|
|
204
142
|
*/
|
|
205
143
|
export const NO_TITLE_SENTINEL = "none";
|
|
206
144
|
|
|
207
145
|
export function normalizeGeneratedTitle(value: string | null | undefined, sourceText?: string): string | null {
|
|
208
146
|
const firstLine = value?.trim().split(/\r?\n/, 1)[0]?.trim();
|
|
209
147
|
if (!firstLine) return null;
|
|
210
|
-
const
|
|
148
|
+
const unquoted = firstLine.replace(/^["']|["']$/g, "").trim();
|
|
149
|
+
if (/^<title\s*\/>$/i.test(unquoted)) return null;
|
|
150
|
+
const title = unquoted
|
|
151
|
+
.replace(/^<title>/i, "")
|
|
152
|
+
.replace(/<\/title>$/i, "")
|
|
211
153
|
.replace(/^["']|["']$/g, "")
|
|
212
154
|
.replace(/[.!?]$/, "")
|
|
213
155
|
.trim();
|
package/src/tiny/worker.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
StoppingCriteria as TransformersStoppingCriteria,
|
|
7
7
|
} from "@huggingface/transformers";
|
|
8
8
|
import { getTinyModelsCacheDir, prompt } from "@oh-my-pi/pi-utils";
|
|
9
|
-
import
|
|
9
|
+
import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
|
|
10
10
|
import {
|
|
11
11
|
errorMessage,
|
|
12
12
|
errorText,
|
|
@@ -21,13 +21,14 @@ import {
|
|
|
21
21
|
} from "../subprocess/worker-runtime";
|
|
22
22
|
import { resolveTinyModelDevicePreference, type TinyModelDevice, tinyModelDeviceLoadOrder } from "./device";
|
|
23
23
|
import { resolveTinyModelDtypeOverride, type TinyModelDtype } from "./dtype";
|
|
24
|
+
import { formatTitleUserMessage } from "./message-preproc";
|
|
24
25
|
import {
|
|
25
26
|
getTinyLocalModelSpec,
|
|
26
27
|
type TinyLocalModelKey,
|
|
27
28
|
type TinyTitleLocalModelKey,
|
|
28
29
|
type TinyTitleLocalModelSpec,
|
|
29
30
|
} from "./models";
|
|
30
|
-
import {
|
|
31
|
+
import { normalizeGeneratedTitle } from "./text";
|
|
31
32
|
import type { TinyTitleTransport, TinyTitleWorkerInbound } from "./title-protocol";
|
|
32
33
|
|
|
33
34
|
const TITLE_PREFILL = "<title>";
|
|
@@ -36,7 +37,7 @@ const TITLE_MAX_NEW_TOKENS = 20;
|
|
|
36
37
|
const STOP_DECODE_WINDOW_TOKENS = 32;
|
|
37
38
|
const MEMORY_COMPLETION_DEFAULT_MAX_NEW_TOKENS = 256;
|
|
38
39
|
const COMPLETION_MAX_NEW_TOKENS = 1024;
|
|
39
|
-
const TINY_TITLE_SYSTEM_PROMPT = prompt.render(
|
|
40
|
+
const TINY_TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
|
|
40
41
|
|
|
41
42
|
const tinyModelDevicePreference = resolveTinyModelDevicePreference();
|
|
42
43
|
const tinyModelDtypeOverride = resolveTinyModelDtypeOverride();
|
|
@@ -230,6 +231,8 @@ function buildPrompt(generator: TextGenerationPipeline, message: string, systemP
|
|
|
230
231
|
function extractTinyTitle(text: string, sourceText: string): string | null {
|
|
231
232
|
const titleStart = text.lastIndexOf(TITLE_PREFILL);
|
|
232
233
|
const withoutPrefix = titleStart >= 0 ? text.slice(titleStart + TITLE_PREFILL.length) : text;
|
|
234
|
+
// Self-closing tag: <title/> or <title /> (only when the prefill is present).
|
|
235
|
+
if (titleStart >= 0 && /^\s*\/>/.test(withoutPrefix)) return null;
|
|
233
236
|
const closeIndex = withoutPrefix.indexOf(TITLE_CLOSE);
|
|
234
237
|
const withoutClose = closeIndex >= 0 ? withoutPrefix.slice(0, closeIndex) : withoutPrefix;
|
|
235
238
|
const tagIndex = withoutClose.indexOf("<");
|
package/src/tools/image-gen.ts
CHANGED
|
@@ -831,11 +831,10 @@ function buildOpenAIImageHeaders(model: Model, apiKey: string, sessionId: string
|
|
|
831
831
|
|
|
832
832
|
if (model.api === "openai-codex-responses" || model.provider === "openai-codex") {
|
|
833
833
|
const accountId = getCodexAccountId(apiKey);
|
|
834
|
-
if (!accountId) {
|
|
835
|
-
throw new Error("Failed to extract accountId from OpenAI Codex token");
|
|
836
|
-
}
|
|
837
834
|
headers.delete("x-api-key");
|
|
838
|
-
|
|
835
|
+
if (accountId) {
|
|
836
|
+
headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
|
|
837
|
+
}
|
|
839
838
|
headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES);
|
|
840
839
|
headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
|
841
840
|
headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`);
|
|
@@ -12,8 +12,9 @@ import { resolveRoleSelection } from "../config/model-resolver";
|
|
|
12
12
|
import type { Settings } from "../config/settings";
|
|
13
13
|
import titleMarkerInstruction from "../prompts/system/title-marker-instruction.md" with { type: "text" };
|
|
14
14
|
import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
|
|
15
|
+
import { formatTitleUserMessage } from "../tiny/message-preproc";
|
|
15
16
|
import { isTinyTitleLocalModelKey, ONLINE_TINY_TITLE_MODEL_KEY } from "../tiny/models";
|
|
16
|
-
import {
|
|
17
|
+
import { isLowSignalTitleInput, normalizeGeneratedTitle } from "../tiny/text";
|
|
17
18
|
import { tinyTitleClient } from "../tiny/title-client";
|
|
18
19
|
|
|
19
20
|
const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
|
|
@@ -33,7 +34,7 @@ const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
|
33
34
|
const TITLE_MAX_TOKENS = 1024;
|
|
34
35
|
|
|
35
36
|
/** Matches the title the model wraps in `<title>...</title>`. */
|
|
36
|
-
const TITLE_MARKER_GLOBAL_RE = /<title>([\s\S]*?)<\/title
|
|
37
|
+
const TITLE_MARKER_GLOBAL_RE = /<title>([\s\S]*?)<\/title>|<title\s*\/>|<title>\s*$/gi;
|
|
37
38
|
const TITLE_VISIBILITY_SENTINEL = "\uE000omp-title-visible\uE000";
|
|
38
39
|
const THINKING_TAG_ENVELOPE_RE = /<(think|thinking|reasoning)>\s*[\s\S]*?<\/\1>/gi;
|
|
39
40
|
const THINKING_FENCE_ENVELOPE_RE = /```(?:thinking|reasoning)\b[\s\S]*?```/gi;
|
|
@@ -263,8 +264,8 @@ function extractVisibleMarkedTitle(text: string): string | undefined {
|
|
|
263
264
|
TITLE_MARKER_GLOBAL_RE.lastIndex = 0;
|
|
264
265
|
let marker: RegExpExecArray | null = TITLE_MARKER_GLOBAL_RE.exec(text);
|
|
265
266
|
while (marker !== null) {
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
267
|
+
const content = marker[1];
|
|
268
|
+
if (isVisibleTitleMarker(text, marker.index)) return content?.trim() ?? "";
|
|
268
269
|
marker = TITLE_MARKER_GLOBAL_RE.exec(text);
|
|
269
270
|
}
|
|
270
271
|
return undefined;
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
You generate concise terminal session titles.
|
|
2
|
-
|
|
3
|
-
Input is one user message inside `<user-message>` tags.
|
|
4
|
-
|
|
5
|
-
Return one specific 3-7 word title in sentence case (capitalize only the first word and proper nouns; keep ALL-CAPS acronyms like `CNPG`, `API`, `JWT` verbatim).
|
|
6
|
-
Continue the assistant response after `<title>` and close it with `</title>`.
|
|
7
|
-
|
|
8
|
-
NEVER include quotes, punctuation, markdown, commentary, or a second line.
|