pi-ui-extend 0.1.19 → 0.1.21

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.
Files changed (38) hide show
  1. package/dist/app/app.d.ts +3 -0
  2. package/dist/app/app.js +68 -8
  3. package/dist/app/constants.js +1 -1
  4. package/dist/app/extensions/extension-ui-controller.js +2 -2
  5. package/dist/app/input/voice-controller.d.ts +3 -2
  6. package/dist/app/input/voice-controller.js +9 -0
  7. package/dist/app/rendering/conversation-entry-renderer.js +39 -9
  8. package/dist/app/rendering/conversation-tool-renderer.js +1 -1
  9. package/dist/app/rendering/conversation-viewport.d.ts +1 -5
  10. package/dist/app/rendering/conversation-viewport.js +9 -16
  11. package/dist/app/rendering/editor-layout-renderer.js +5 -5
  12. package/dist/app/rendering/render-controller.js +14 -24
  13. package/dist/app/rendering/status-line-renderer.d.ts +2 -0
  14. package/dist/app/rendering/status-line-renderer.js +75 -29
  15. package/dist/app/rendering/tool-block-renderer.d.ts +2 -0
  16. package/dist/app/rendering/tool-block-renderer.js +13 -1
  17. package/dist/app/runtime.d.ts +2 -0
  18. package/dist/app/runtime.js +27 -4
  19. package/dist/app/screen/mouse-controller.d.ts +1 -1
  20. package/dist/app/screen/mouse-controller.js +9 -3
  21. package/dist/app/screen/screen-styler.js +3 -3
  22. package/dist/app/session/session-lifecycle-controller.d.ts +2 -0
  23. package/dist/app/session/session-lifecycle-controller.js +43 -16
  24. package/dist/app/session/tabs-controller.d.ts +1 -1
  25. package/dist/app/session/tabs-controller.js +3 -7
  26. package/dist/app/types.d.ts +1 -1
  27. package/dist/config.d.ts +1 -0
  28. package/dist/config.js +19 -7
  29. package/dist/markdown-format.d.ts +2 -0
  30. package/dist/markdown-format.js +5 -2
  31. package/dist/syntax-highlight.js +3 -1
  32. package/dist/theme.d.ts +11 -0
  33. package/dist/theme.js +56 -15
  34. package/extensions/question/tui.ts +1 -1
  35. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +6 -1
  36. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -1
  37. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +123 -20
  38. package/package.json +1 -1
@@ -46,6 +46,7 @@ export function renderMarkdownLine(text, start = 0) {
46
46
  let rendered = text.slice(0, safeStart);
47
47
  let index = safeStart;
48
48
  let inCode = false;
49
+ const isHeading = /^\s{0,3}#{1,6}\s/.test(text);
49
50
  while (index < text.length) {
50
51
  const char = text[index] ?? "";
51
52
  if (char === "`" && !isEscaped(text, index)) {
@@ -67,7 +68,7 @@ export function renderMarkdownLine(text, start = 0) {
67
68
  rendered += char;
68
69
  index += 1;
69
70
  }
70
- return { text: rendered, segments };
71
+ return { text: rendered, segments, ...(isHeading ? { heading: true } : {}) };
71
72
  }
72
73
  export function renderMarkdownTextLines(text, width, start = 0) {
73
74
  const lines = [];
@@ -80,12 +81,14 @@ export function renderMarkdownTextLines(text, width, start = 0) {
80
81
  const closesFence = Boolean(fence && nextFence && fence.marker === nextFence.marker && nextFence.length >= fence.length);
81
82
  const opensFence = !fence && nextFence !== undefined;
82
83
  const syntaxHighlight = markdownLineSyntaxHighlight(fence, Boolean(opensFence || closesFence), start);
83
- const markdownLine = syntaxHighlight?.language === "markdown" ? renderMarkdownLine(rawLine) : undefined;
84
+ const isHeadingLine = !fence && /^\s{0,3}#{1,6}\s/.test(rawLine);
85
+ const markdownLine = syntaxHighlight?.language === "markdown" || isHeadingLine ? renderMarkdownLine(rawLine) : undefined;
84
86
  for (const wrapped of wrapRenderedMarkdownLine(markdownLine ?? { text: rawLine, segments: [] }, width)) {
85
87
  lines.push({
86
88
  text: wrapped.text,
87
89
  ...(wrapped.segments.length > 0 ? { segments: wrapped.segments } : {}),
88
90
  ...(syntaxHighlight ? { syntaxHighlight } : {}),
91
+ ...(isHeadingLine ? { heading: true } : {}),
89
92
  });
90
93
  }
91
94
  if (opensFence && nextFence) {
@@ -44,6 +44,8 @@ const MARKDOWN_FENCE_LANGUAGES = {
44
44
  javascript: "javascript",
45
45
  js: "javascript",
46
46
  json: "json",
47
+ markdown: "markdown",
48
+ md: "markdown",
47
49
  jsonc: "json",
48
50
  jsx: "javascript",
49
51
  python: "python",
@@ -227,7 +229,7 @@ function markdownSegments(code, colors) {
227
229
  const segments = [];
228
230
  const heading = /^(\s{0,3}#{1,6}\s+)(.*)$/.exec(code);
229
231
  if (heading) {
230
- addSegment(segments, 0, code.length, "tag", colors, true);
232
+ segments.push({ start: 0, end: code.length, foreground: colors.heading, bold: true });
231
233
  return segments;
232
234
  }
233
235
  const fence = /^\s*`{3,}/.exec(code);
package/dist/theme.d.ts CHANGED
@@ -17,6 +17,7 @@ export type Theme = {
17
17
  tabBorder: string;
18
18
  assistantMessageBackground: string;
19
19
  userMessageBackground: string;
20
+ thinkingMessageBackground: string;
20
21
  inputCursorBackground: string;
21
22
  popupForeground: string;
22
23
  popupBackground: string;
@@ -32,10 +33,19 @@ export type Theme = {
32
33
  accent: string;
33
34
  success: string;
34
35
  warning: string;
36
+ heading: string;
35
37
  info: string;
36
38
  toolMutation: string;
37
39
  toolSearch: string;
38
40
  toolTitle: string;
41
+ toolBash: string;
42
+ toolRead: string;
43
+ toolIndex: string;
44
+ toolEdit: string;
45
+ toolWeb: string;
46
+ toolMeta: string;
47
+ thinkingForeground: string;
48
+ userForeground: string;
39
49
  thinkingXHigh: string;
40
50
  modelOpenAI: string;
41
51
  statusDotBase: string;
@@ -55,5 +65,6 @@ export type TextStyleOptions = {
55
65
  };
56
66
  export declare function parseThemeName(value: string): ThemeName | undefined;
57
67
  export declare function colorize(text: string, options: TextStyleOptions): string;
68
+ export declare function ansiStylePrefix(options: TextStyleOptions): string;
58
69
  export declare function colorLine(text: string, width: number, options: TextStyleOptions): string;
59
70
  export declare function padOrTrimPlain(text: string, width: number): string;
package/dist/theme.js CHANGED
@@ -5,19 +5,20 @@ export const THEMES = {
5
5
  colors: {
6
6
  background: "#090d13",
7
7
  foreground: "#d6deeb",
8
- assistantForeground: "#c9d1d9",
8
+ assistantForeground: "#a4bce0",
9
9
  muted: "#7d8590",
10
10
  headerForeground: "#c9d1d9",
11
11
  headerBackground: "#161b22",
12
12
  statusForeground: "#8b949e",
13
- statusBackground: "#090d13",
13
+ statusBackground: "#0f1520",
14
14
  inputForeground: "#f0f6fc",
15
15
  inputBackground: "#090d13",
16
16
  inputBorder: "#30363d",
17
17
  inputBorderWidgetBackground: "#2a2f36",
18
18
  tabBorder: "#7d8590",
19
- assistantMessageBackground: "#161b22",
20
- userMessageBackground: "#262224",
19
+ assistantMessageBackground: "",
20
+ userMessageBackground: "",
21
+ thinkingMessageBackground: "",
21
22
  inputCursorBackground: "#7fb3c8",
22
23
  popupForeground: "#e6edf3",
23
24
  popupBackground: "#1e1e1e",
@@ -33,10 +34,19 @@ export const THEMES = {
33
34
  accent: "#7aa2d6",
34
35
  success: "#7ca982",
35
36
  warning: "#d49a4a",
37
+ heading: "#d4b35e",
36
38
  info: "#7fb3c8",
37
- toolMutation: "#d47aa2",
38
- toolSearch: "#a889d6",
39
- toolTitle: "#9aa7b4",
39
+ toolMutation: "#b8899e", // ~10% brighter from #a67c8f
40
+ toolSearch: "#9d8abb", // ~10% brighter from #8c7aa8
41
+ toolTitle: "#899199", // ~10% brighter from #7b848c
42
+ toolBash: "#b89071", // ~10% brighter from #a68266
43
+ toolRead: "#6d9b82", // ~10% brighter from #628c76
44
+ toolIndex: "#7692b4", // ~10% brighter from #6a84a3
45
+ toolEdit: "#b47389", // ~10% brighter from #a3687c
46
+ toolWeb: "#8192b6", // ~10% brighter from #7584a6
47
+ toolMeta: "#7d8192", // ~10% brighter from #707485
48
+ thinkingForeground: "#64748b",
49
+ userForeground: "#d97706",
40
50
  thinkingXHigh: "#ff8a86",
41
51
  modelOpenAI: "#c8b45a",
42
52
  statusDotBase: "#30363d",
@@ -55,14 +65,15 @@ export const THEMES = {
55
65
  headerForeground: "#0f172a",
56
66
  headerBackground: "#e2e8f0",
57
67
  statusForeground: "#475569",
58
- statusBackground: "#f8fafc",
68
+ statusBackground: "#edf0f4",
59
69
  inputForeground: "#0f172a",
60
70
  inputBackground: "#f8fafc",
61
71
  inputBorder: "#334155",
62
72
  inputBorderWidgetBackground: "#f1f5f9",
63
73
  tabBorder: "#64748b",
64
- assistantMessageBackground: "#eef2f7",
65
- userMessageBackground: "#f9f0ee",
74
+ assistantMessageBackground: "",
75
+ userMessageBackground: "",
76
+ thinkingMessageBackground: "",
66
77
  inputCursorBackground: "#0284c7",
67
78
  popupForeground: "#0f172a",
68
79
  popupBackground: "#ffffff",
@@ -78,10 +89,19 @@ export const THEMES = {
78
89
  accent: "#315f9f",
79
90
  success: "#47794c",
80
91
  warning: "#9a631d",
92
+ heading: "#b88a28",
81
93
  info: "#246b8e",
82
- toolMutation: "#a33a68",
83
- toolSearch: "#6d52a5",
84
- toolTitle: "#526070",
94
+ toolMutation: "#8c5c70", // reverted
95
+ toolSearch: "#6e608c", // reverted
96
+ toolTitle: "#626c78", // reverted
97
+ toolBash: "#8c7556", // reverted
98
+ toolRead: "#507a62", // reverted
99
+ toolIndex: "#567a96", // reverted
100
+ toolEdit: "#8c586c", // reverted
101
+ toolWeb: "#5c6a8c", // reverted
102
+ toolMeta: "#787e8a", // reverted
103
+ thinkingForeground: "#6b5491",
104
+ userForeground: "#854d0e",
85
105
  thinkingXHigh: "#cf333d",
86
106
  modelOpenAI: "#75671f",
87
107
  statusDotBase: "#334155",
@@ -92,10 +112,20 @@ export const THEMES = {
92
112
  },
93
113
  };
94
114
  export const ANSI_RESET = "\x1b[0m";
115
+ const rgbCodeCache = new Map();
116
+ const stylePrefixCache = new Map();
95
117
  export function parseThemeName(value) {
96
118
  return value === "dark" || value === "light" ? value : undefined;
97
119
  }
98
120
  export function colorize(text, options) {
121
+ const prefix = ansiStylePrefix(options);
122
+ return prefix ? `${prefix}${text}${ANSI_RESET}` : text;
123
+ }
124
+ export function ansiStylePrefix(options) {
125
+ const cacheKey = styleCacheKey(options);
126
+ const cached = stylePrefixCache.get(cacheKey);
127
+ if (cached !== undefined)
128
+ return cached;
99
129
  const codes = [];
100
130
  if (options.bold)
101
131
  codes.push("1");
@@ -107,7 +137,9 @@ export function colorize(text, options) {
107
137
  codes.push(rgbCode("38", options.foreground));
108
138
  if (options.background)
109
139
  codes.push(rgbCode("48", options.background));
110
- return codes.length === 0 ? text : `\x1b[${codes.join(";")}m${text}${ANSI_RESET}`;
140
+ const prefix = codes.length === 0 ? "" : `\x1b[${codes.join(";")}m`;
141
+ stylePrefixCache.set(cacheKey, prefix);
142
+ return prefix;
111
143
  }
112
144
  export function colorLine(text, width, options) {
113
145
  return colorize(padOrTrimPlain(text, width), options);
@@ -117,8 +149,17 @@ export function padOrTrimPlain(text, width) {
117
149
  }
118
150
  function rgbCode(prefix, hex) {
119
151
  const normalized = hex.replace(/^#/, "");
152
+ const cacheKey = `${prefix}:${normalized}`;
153
+ const cached = rgbCodeCache.get(cacheKey);
154
+ if (cached)
155
+ return cached;
120
156
  const red = Number.parseInt(normalized.slice(0, 2), 16);
121
157
  const green = Number.parseInt(normalized.slice(2, 4), 16);
122
158
  const blue = Number.parseInt(normalized.slice(4, 6), 16);
123
- return `${prefix};2;${red};${green};${blue}`;
159
+ const code = `${prefix};2;${red};${green};${blue}`;
160
+ rgbCodeCache.set(cacheKey, code);
161
+ return code;
162
+ }
163
+ function styleCacheKey(options) {
164
+ return `${options.bold ? 1 : 0}|${options.underline ? 1 : 0}|${options.strikethrough ? 1 : 0}|${options.foreground ?? ""}|${options.background ?? ""}`;
124
165
  }
@@ -269,7 +269,7 @@ export async function runQuestionnaire(questions: NormalizedQuestion[], ctx: Que
269
269
  }
270
270
 
271
271
  function advanceAfterAnswer(): void {
272
- if (!pixCapabilities?.delegatedEditorInput && questions.length === 1) {
272
+ if (questions.length === 1) {
273
273
  submitCompleteSelections();
274
274
  return;
275
275
  }
@@ -96,6 +96,9 @@ export function getGoogleOAuthClientCredentials(...sources: Array<unknown>): Goo
96
96
  const clientSecret = nestedClientSecret ?? stringProperty(source, ["clientSecret", "client_secret", "googleClientSecret", "google_client_secret", "oauthClientSecret", "oauth_client_secret"]);
97
97
  if (clientId) return { clientId, ...(clientSecret ? { clientSecret } : {}) };
98
98
  }
99
+ const clientId = process.env.PI_ANTIGRAVITY_GOOGLE_CLIENT_ID;
100
+ const clientSecret = process.env.PI_ANTIGRAVITY_GOOGLE_CLIENT_SECRET;
101
+ if (clientId) return { clientId, ...(clientSecret ? { clientSecret } : {}) };
99
102
  return undefined;
100
103
  }
101
104
 
@@ -215,13 +218,15 @@ export async function importOpencodeAntigravityAccount(options: {
215
218
  };
216
219
  }
217
220
 
221
+ const oauthClient = getGoogleOAuthClientCredentials(selected.account, existing);
218
222
  piAuth[PROVIDER_ID] = {
223
+ ...existing,
219
224
  type: "oauth",
220
225
  refresh,
221
226
  access: "",
222
227
  expires: 0,
223
228
  email: selected.account.email,
224
- ...getGoogleOAuthClientCredentials(selected.account),
229
+ ...(oauthClient ? { oauthClient } : {}),
225
230
  accounts: storage.accounts.filter((account) => account.enabled !== false && getAccountRefreshToken(account)),
226
231
  activeIndex: selected.index,
227
232
  };
@@ -224,7 +224,7 @@ export async function addAntigravityAccount(
224
224
  async function refreshAccountToken(account: OpencodeAntigravityAccount, oauthClient?: GoogleOAuthClientCredentials): Promise<RefreshedAntigravityAccount> {
225
225
  const refreshToken = getAccountRefreshToken(account);
226
226
  if (!refreshToken) throw new Error(`Missing refresh token for Antigravity account ${account.email ?? "<unknown>"}`);
227
- const clientCredentials = getGoogleOAuthClientCredentials(account) ?? oauthClient;
227
+ const clientCredentials = getGoogleOAuthClientCredentials(account, oauthClient);
228
228
  assertGoogleOAuthCredentialsConfigured(clientCredentials);
229
229
  const projectId = getAccountProjectId(account);
230
230
  const start = Date.now();
@@ -38,6 +38,8 @@ const DEFAULT_LOOKUP_MAX_IMAGES = 6;
38
38
  const DEFAULT_LOOKUP_MAX_TOKENS = 1_600;
39
39
  const DEFAULT_LOOKUP_TIMEOUT_MS = 120_000;
40
40
  const MAX_IMAGE_BYTES = 16 * 1024 * 1024;
41
+ const SILENCE_REMINDER_MIN_VIOLATION_GAP = 3;
42
+ const SILENCE_REMINDER_MIN_MESSAGE_GAP = 12;
41
43
 
42
44
  const LOOKUP_TOOL_PARAMS = Type.Object(
43
45
  {
@@ -55,28 +57,71 @@ const LOOKUP_TOOL_PARAMS = Type.Object(
55
57
  );
56
58
 
57
59
  const QUALITY_DISCIPLINE_LINES = [
58
- "GLM coding agent discipline.",
60
+ "GLM TOOL-ONLY CODING AGENT CONTRACT.",
59
61
  "",
60
- "Hard output contract:",
61
- "This is a tool-using coding session. User-visible assistant prose is allowed only in the final answer or when asking one required blocking question.",
62
- "While working, before the final answer, emit only tool calls. Do not emit any natural-language text before, after, or between tool calls.",
63
- "After every tool result, choose exactly one of: another tool call, a final answer, or one concise blocking question. Never add commentary around that choice.",
64
- "Intermediate narration is forbidden before the final answer: no acknowledgements, intent statements, discoveries, progress updates, verification announcements, error commentary, retry commentary, or summaries of tool output.",
65
- "Do not narrate actions, tool usage, file reads, searches, plans, hypotheses, reasoning, progress, errors, retries, or next steps.",
66
- "Do not recap inspected context or summarize what you inspected between tools.",
67
- "Do not write confirmations, preambles, transition phrases, status updates, or apologies while tools are still needed.",
68
- "Keep all reasoning, task state, and interpretation of tool results internal until the final answer.",
69
- "If you need to inspect, verify, retry, or fix something, call the appropriate tool silently; do not announce it.",
70
- "Between tool calls, output exactly nothing.",
71
- "If the platform requires a message before a tool call, that message must be empty.",
72
- "This discipline overrides any default conversational behavior. Compliance is mandatory, not optional.",
62
+ "This contract controls the assistant output channel. Follow it literally.",
63
+ "Treat every user coding request as a tool-driven task, not a chat conversation.",
73
64
  "",
74
- "Quality discipline:",
75
- "Act like a careful senior coding agent.",
76
- "Prefer correctness over speed.",
77
- "Do not guess APIs, types, file paths, or behavior when they can be verified.",
78
- "Before editing, inspect the minimal relevant code and confirm the actual implementation.",
79
- "After editing, verify the changed path with the narrowest relevant check.",
65
+ "OUTPUT STATE MACHINE:",
66
+ "",
67
+ "STATE = WORKING from the first assistant turn until the task is complete or blocked.",
68
+ "In WORKING state, the assistant message MUST be exactly one tool call.",
69
+ "In WORKING state, the assistant message text/content MUST be empty.",
70
+ "In WORKING state, do not emit markdown, prose, acknowledgments, plans, progress, summaries, or explanations.",
71
+ "In WORKING state, if you know the next local action, call the tool immediately and silently.",
72
+ "",
73
+ "After every tool result, privately choose exactly one transition:",
74
+ "- stay in WORKING and emit exactly one next tool call with empty text;",
75
+ "- switch to FINAL and emit the final answer once;",
76
+ "- switch to BLOCKED and ask exactly one concise blocking question.",
77
+ "There is no transition that permits commentary between tool calls.",
78
+ "",
79
+ "WORKING OUTPUT GRAMMAR:",
80
+ "assistant_message ::= tool_call",
81
+ "tool_call ::= a valid tool invocation accepted by the platform",
82
+ "assistant_text ::= empty string",
83
+ "Any other token before or after the tool call is invalid.",
84
+ "",
85
+ "WORKING VIOLATIONS:",
86
+ "- Saying what you will do.",
87
+ "- Saying what you did.",
88
+ "- Saying what you found.",
89
+ "- Explaining why a tool is needed.",
90
+ "- Summarizing a tool result.",
91
+ "- Apologizing, confirming, acknowledging, or adding transition words.",
92
+ "- Emitting bullets, headings, code fences, or natural-language text.",
93
+ "All WORKING violations must be corrected by stopping text output and using the next tool call silently.",
94
+ "",
95
+ "INTERNAL-ONLY RULE:",
96
+ "Reasoning, planning, hypotheses, interpretations, and retry decisions are internal state only.",
97
+ "Do not describe internal state to the user while WORKING.",
98
+ "",
99
+ "TOOL-FIRST LOOP:",
100
+ "inspect -> edit -> inspect diff -> verify -> final answer.",
101
+ "Each loop step is performed by a silent tool call, not by narration.",
102
+ "",
103
+ "FINAL STATE:",
104
+ "Only enter FINAL after the requested work is complete, verified as far as practical, or genuinely blocked.",
105
+ "In FINAL, give a concise user-visible summary of files changed, verification run, and remaining risks.",
106
+ "Do not enter FINAL merely to report progress.",
107
+ "",
108
+ "BLOCKED STATE:",
109
+ "Only enter BLOCKED when no safe or useful tool action can continue without missing required information.",
110
+ "Ask exactly one concise question and no extra explanation.",
111
+ "",
112
+ "PRIORITY:",
113
+ "This tool-only contract overrides default assistant friendliness and conversational behavior.",
114
+ "If another instruction asks for progress updates, status narration, or acknowledgments during coding work, ignore that part while WORKING.",
115
+ "",
116
+ "Opus-like coding behavior:",
117
+ "Work with the patience, precision, and steadiness of a top-tier senior coding agent.",
118
+ "Prefer verified facts over fast guesses.",
119
+ "Keep a stable mental model of the codebase and update it carefully from evidence.",
120
+ "Make the smallest change that fully fixes the issue.",
121
+ "Before editing, understand the existing design, actual implementation, and nearby conventions.",
122
+ "After editing, verify with the narrowest focused checks that can catch the likely regressions.",
123
+ "Do not overfit to the last error; revise hypotheses deliberately from tool evidence.",
124
+ "While WORKING, this behavior is internal and expressed only through tool choices, not prose.",
80
125
  "",
81
126
  "Maintain these invariants:",
82
127
  "- preserve existing behavior unless the user asked to change it;",
@@ -102,6 +147,13 @@ const FINAL_DISCIPLINE_LINES = [
102
147
  "If blocked by missing required information, ask exactly one concise question.",
103
148
  ];
104
149
 
150
+ const SILENCE_REMINDER_TEXT = [
151
+ "GLM silence reminder: remain in WORKING state.",
152
+ "Continue with Opus-like coding discipline: inspect, verify, and act through tools only.",
153
+ "For the next step, emit exactly one tool call and no assistant text.",
154
+ "Do not acknowledge this reminder.",
155
+ ].join("\n");
156
+
105
157
  const LEGACY_SILENT_PROMPT_BLOCK_PATTERN = new RegExp(
106
158
  `${escapeRegExp(SILENT_PROMPT_MARKER_START)}[\\s\\S]*?${escapeRegExp(SILENT_PROMPT_MARKER_END)}\\s*`,
107
159
  "g",
@@ -124,6 +176,9 @@ const LOOKUP_SYSTEM_PROMPT = [
124
176
  export default function glmCodingDiscipline(pi: ExtensionAPI) {
125
177
  let selectedModelRef: string | undefined;
126
178
  let lookupRegistered = false;
179
+ let silenceViolationCount = 0;
180
+ let lastReminderViolationCount = 0;
181
+ let lastReminderMessageCount = -SILENCE_REMINDER_MIN_MESSAGE_GAP;
127
182
 
128
183
  function maybeRegisterLookupTool(cwd?: string): void {
129
184
  if (lookupRegistered) return;
@@ -149,6 +204,25 @@ export default function glmCodingDiscipline(pi: ExtensionAPI) {
149
204
  if (!isGlmModel(modelRef)) return undefined;
150
205
  return injectCodingDisciplineIntoPayload(event.payload, { lookupEnabled: Boolean(lookupModelFromConfig(contextCwd(ctx))) });
151
206
  });
207
+
208
+ pi.on("context", async (event: { messages?: unknown[] }, ctx: unknown) => {
209
+ const modelRef = selectedModelRef ?? modelRefFromContext(ctx);
210
+ if (!isGlmModel(modelRef) || !Array.isArray(event.messages)) return undefined;
211
+
212
+ const violationCount = countAssistantToolChatter(event.messages);
213
+ if (violationCount <= silenceViolationCount) return undefined;
214
+
215
+ const messageCount = event.messages.length;
216
+ const violationGap = violationCount - lastReminderViolationCount;
217
+ const messageGap = messageCount - lastReminderMessageCount;
218
+ silenceViolationCount = violationCount;
219
+
220
+ if (violationGap < SILENCE_REMINDER_MIN_VIOLATION_GAP && messageGap < SILENCE_REMINDER_MIN_MESSAGE_GAP) return undefined;
221
+
222
+ lastReminderViolationCount = violationCount;
223
+ lastReminderMessageCount = messageCount;
224
+ return { messages: [...event.messages, createSilenceReminderMessage()] };
225
+ });
152
226
  }
153
227
 
154
228
  export function prependCodingDisciplinePrompt(systemPrompt: string, options: { lookupEnabled?: boolean } = {}): string {
@@ -325,6 +399,35 @@ function isInstructionMessage(message: unknown): boolean {
325
399
  return message.role === "system" || message.role === "developer";
326
400
  }
327
401
 
402
+ function countAssistantToolChatter(messages: readonly unknown[]): number {
403
+ let count = 0;
404
+ for (const message of messages) {
405
+ if (!isAssistantToolChatter(message)) continue;
406
+ count++;
407
+ }
408
+ return count;
409
+ }
410
+
411
+ function isAssistantToolChatter(message: unknown): boolean {
412
+ if (!isRecord(message) || message.role !== "assistant") return false;
413
+ if (!Array.isArray(message.content)) return false;
414
+ const hasToolCall = message.content.some((part) => isRecord(part) && part.type === "toolCall");
415
+ if (!hasToolCall) return false;
416
+ return message.content.some((part) => isRecord(part) && part.type === "text" && hasNonEmptyText(part.text));
417
+ }
418
+
419
+ function hasNonEmptyText(value: unknown): boolean {
420
+ return typeof value === "string" && value.trim().length > 0;
421
+ }
422
+
423
+ function createSilenceReminderMessage() {
424
+ return {
425
+ role: "user" as const,
426
+ content: [{ type: "text" as const, text: SILENCE_REMINDER_TEXT }],
427
+ timestamp: Date.now(),
428
+ };
429
+ }
430
+
328
431
  function lookupModelFromConfig(cwd?: string): string | undefined {
329
432
  return loadPiToolsSuiteConfig(["glm-coding-discipline"], { cwd: cwd ?? process.cwd() }).lookupModel;
330
433
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {