pi-ui-extend 0.1.19 → 0.1.20

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.
@@ -73,7 +73,7 @@ export const SUBAGENTS_WIDGET_MAX_ROWS = 8;
73
73
  export const DEFAULT_THINKING_TOOL_RULE = {
74
74
  previewLines: 0,
75
75
  direction: "head",
76
- color: "accent",
76
+ color: "thinkingForeground",
77
77
  };
78
78
  export const TERMINAL_COMMAND_MODIFIER_FLAG = 8;
79
79
  export const GIT_BRANCH_CACHE_MS = 30_000;
@@ -9,14 +9,14 @@ export function renderConversationEntry(entry, width, options) {
9
9
  const { left: userContentLeft, contentWidth: userContentWidth } = horizontalPaddingLayout(width);
10
10
  const userLine = (text, entryId, syntaxHighlight, segments) => ({
11
11
  text: padHorizontalText(text, width),
12
- colorOverride: options.colors.warning,
12
+ colorOverride: options.colors.userForeground,
13
13
  ...(segments && segments.length > 0 ? { segments: segments.map((segment) => ({ ...segment, start: segment.start + userContentLeft, end: segment.end + userContentLeft })) } : {}),
14
14
  ...(syntaxHighlight === undefined ? {} : { syntaxHighlight }),
15
15
  ...(entryId === undefined ? {} : { target: { kind: "user-message", id: entryId } }),
16
16
  });
17
17
  const queuedLine = (text, entryId, segments) => ({
18
18
  text,
19
- colorOverride: options.colors.warning,
19
+ colorOverride: options.colors.userForeground,
20
20
  ...(segments && segments.length > 0 ? { segments } : {}),
21
21
  target: { kind: "queue-message", id: entryId },
22
22
  });
@@ -8,7 +8,7 @@ export class EditorLayoutRenderer {
8
8
  this.host = host;
9
9
  }
10
10
  computeLayout(width, rows) {
11
- const maxAvailableInputRows = Math.max(1, rows - 5);
11
+ const maxAvailableInputRows = Math.max(1, rows - 4);
12
12
  const renderedInput = this.renderInput(width, Math.min(INPUT_MAX_ROWS, maxAvailableInputRows), maxAvailableInputRows);
13
13
  const maxEntityRows = Math.max(0, rows - renderedInput.lines.length - 4);
14
14
  const editorEntityWidth = inputFrameContentWidth(width);
@@ -18,9 +18,9 @@ export class EditorLayoutRenderer {
18
18
  aboveEditorLines = [...aboveEditorLines, { text: "", variant: "normal" }];
19
19
  }
20
20
  const belowEditorLines = this.limitEntityLines(this.renderExtensionWidgets("belowEditor", editorEntityWidth), maxEntityRows - aboveEditorLines.length);
21
- const inputBottomSeparatorRow = rows - 1;
22
- const belowEditorStartRow = inputBottomSeparatorRow - belowEditorLines.length;
23
- const inputStartRow = belowEditorStartRow - renderedInput.lines.length;
21
+ const belowEditorStartRow = rows - belowEditorLines.length;
22
+ const inputBottomSeparatorRow = belowEditorStartRow - 1;
23
+ const inputStartRow = inputBottomSeparatorRow - renderedInput.lines.length;
24
24
  const inputSeparatorRow = inputStartRow - aboveEditorLines.length - 1;
25
25
  return {
26
26
  renderedInput,
@@ -150,7 +150,7 @@ export class AppRenderController {
150
150
  if (row < statusRow) {
151
151
  this.deps.mouseController.renderedRowTexts.set(row, separatorText);
152
152
  appendFrameOutput("inputStatus", row, this.renderFrameRow(row, this.deps.screenStyler.styleLine(row, separatorText, columns, {
153
- foreground: this.deps.theme.colors.inputBorder,
153
+ foreground: this.deps.theme.colors.tabBorder,
154
154
  })));
155
155
  }
156
156
  }
@@ -176,7 +176,15 @@ export class AppRenderController {
176
176
  })}`);
177
177
  }
178
178
  }
179
- const belowEditorStartRow = inputStartRow + renderedInput.lines.length;
179
+ if (inputBottomSeparatorRow && inputBottomSeparatorRow > inputSeparatorRow && inputBottomSeparatorRow < statusRow) {
180
+ const separatorText = inputFrameLine(columns, "bottom");
181
+ const row = toScreenRow(inputBottomSeparatorRow);
182
+ this.deps.mouseController.renderedRowTexts.set(row, separatorText);
183
+ appendFrameOutput("inputStatus", row, this.renderFrameRow(row, this.deps.screenStyler.styleLine(row, separatorText, columns, {
184
+ foreground: this.deps.theme.colors.tabBorder,
185
+ })));
186
+ }
187
+ const belowEditorStartRow = (inputBottomSeparatorRow ?? (inputStartRow + renderedInput.lines.length - 1)) + 1;
180
188
  for (let index = 0; index < belowEditorLines.length; index += 1) {
181
189
  const rendered = frameRenderedLine(belowEditorLines[index], columns, this.deps.theme, this.deps.screenStyler);
182
190
  const row = toScreenRow(belowEditorStartRow + index);
@@ -191,25 +199,7 @@ export class AppRenderController {
191
199
  appendFrameOutput("inputStatus", row, this.renderFrameRow(row, rendered.output(row)));
192
200
  }
193
201
  const statusLayout = this.deps.statusLineRenderer.layout(columns);
194
- const statusLineRenderer = this.deps.statusLineRenderer;
195
- const inputBorderWidgetsLayout = statusLineRenderer.inputBorderWidgetsLayout?.(columns);
196
- if (inputBottomSeparatorRow > 1) {
197
- const separatorText = inputFrameLine(columns, "bottom");
198
- const row = toScreenRow(inputBottomSeparatorRow);
199
- if (row < statusRow) {
200
- const text = inputBorderWidgetsLayout
201
- ? overlayText(separatorText, inputBorderWidgetsLayout.inputBorderWidgetStartColumn ?? 1, inputBorderWidgetsLayout.text)
202
- : separatorText;
203
- this.deps.mouseController.renderedRowTexts.set(row, text);
204
- const output = inputBorderWidgetsLayout && statusLineRenderer.renderInputBorderWidgets
205
- ? statusLineRenderer.renderInputBorderWidgets(row, inputBorderWidgetsLayout, separatorText, columns)
206
- : this.deps.screenStyler.styleLine(row, separatorText, columns, {
207
- foreground: this.deps.theme.colors.inputBorder,
208
- });
209
- appendFrameOutput("inputStatus", row, this.renderFrameRow(row, output));
210
- }
211
- }
212
- this.updateStatusMouseState(statusLayout, statusRow, inputBorderWidgetsLayout, toScreenRow(inputBottomSeparatorRow));
202
+ this.updateStatusMouseState(statusLayout, statusRow);
213
203
  appendFrameOutput("inputStatus", statusRow, this.renderFrameRow(statusRow, this.deps.statusLineRenderer.render(statusRow, statusLayout, columns)));
214
204
  const voiceProgressOverlay = this.renderVoiceProgressOverlay(this.deps.voiceProgressOverlayText(), columns, statusRow);
215
205
  if (voiceProgressOverlay) {
@@ -287,9 +277,9 @@ export class AppRenderController {
287
277
  const text = fixedCellText(flash.text, width);
288
278
  return `\x1b[${flash.y};${flash.startColumn}H\x1b[7m${text}${ANSI_RESET}`;
289
279
  }
290
- updateStatusMouseState(statusLayout, statusRow, inputBorderWidgetsLayout, inputBorderWidgetsRow) {
291
- const widgetLayout = inputBorderWidgetsLayout;
292
- const widgetRow = inputBorderWidgetsRow ?? statusRow;
280
+ updateStatusMouseState(statusLayout, statusRow) {
281
+ const widgetLayout = statusLayout;
282
+ const widgetRow = statusRow;
293
283
  this.deps.mouseController.statusModelTarget = this.deps.statusLineRenderer.modelTarget(statusLayout.text, statusRow);
294
284
  this.deps.mouseController.statusThinkingTarget = this.deps.statusLineRenderer.thinkingTarget(statusLayout.text, statusRow);
295
285
  this.deps.mouseController.statusContextTarget = this.deps.statusLineRenderer.contextTarget(statusLayout.text, statusRow, statusLayout);
@@ -53,6 +53,8 @@ export declare class StatusLineRenderer {
53
53
  terminalBellSoundTarget(layout: StatusLineLayout, row: number): StatusTerminalBellSoundTarget | undefined;
54
54
  sessionTarget(statusText: string, row: number, label: string, workspaceLabel: string): StatusSessionTarget | undefined;
55
55
  private segments;
56
+ private fitWorkspaceLabel;
57
+ private pushStatusWidgetSegments;
56
58
  private pushPromptEnhancerWidgetSegment;
57
59
  private pushUserJumpWidgetSegment;
58
60
  private pushDraftQueueWidgetSegment;
@@ -1,12 +1,10 @@
1
1
  import { compactProgressBarSegments, formatCompactProgressBar } from "../../context-progress-bar.js";
2
- import { padOrTrimPlain } from "./render-text.js";
2
+ import { ellipsizeDisplay, padOrTrimPlain } from "./render-text.js";
3
3
  import { displayIndexForColumn, stringDisplayWidth } from "../../terminal-width.js";
4
4
  import { APP_ICONS } from "../icons.js";
5
5
  import { resolveColor, resolveModelColor } from "../../config.js";
6
6
  const MODEL_USAGE_PROGRESS_BAR_WIDTH = stringDisplayWidth(formatCompactProgressBar(100));
7
- const INPUT_BORDER_WIDGET_INSET = 2;
8
- const INPUT_BORDER_WIDGET_SEPARATOR = "─";
9
- const STATUS_ICON_BUTTON_WIDTH = 3;
7
+ const STATUS_WIDGET_GAP = " ";
10
8
  export class StatusLineRenderer {
11
9
  host;
12
10
  constructor(host) {
@@ -18,17 +16,29 @@ export class StatusLineRenderer {
18
16
  const baseStatus = this.host.currentStatus();
19
17
  const workspaceLabel = this.host.statusWorkspaceLabel();
20
18
  const modelUsageLabel = this.host.modelUsageStatusLabel();
21
- const workspaceDetailsLabel = modelUsageLabel ? `${workspaceLabel} ${modelUsageLabel}` : workspaceLabel;
22
- const contextBarLabel = this.contextBarLabel(baseStatus, contentWidth, workspaceDetailsLabel);
19
+ const widgetsLayout = this.inputBorderWidgetsLayout(contentWidth);
20
+ const leftWidth = widgetsLayout?.inputBorderWidgetStartColumn
21
+ ? Math.max(0, widgetsLayout.inputBorderWidgetStartColumn - 2)
22
+ : contentWidth;
23
+ const fullWorkspaceDetailsLabel = modelUsageLabel ? `${workspaceLabel} ${modelUsageLabel}` : workspaceLabel;
24
+ const contextBarLabel = this.contextBarLabel(baseStatus, leftWidth, fullWorkspaceDetailsLabel);
23
25
  const status = contextBarLabel ? `${baseStatus} ${contextBarLabel}` : baseStatus;
24
26
  const sessionLabel = "";
25
- const details = `${status} ${workspaceDetailsLabel}`;
26
- const text = padOrTrimPlain(`${statusDot} ${details}`, width);
27
+ const fittedWorkspaceLabel = this.fitWorkspaceLabel(statusDot, status, workspaceLabel, modelUsageLabel, leftWidth);
28
+ const workspaceDetailsLabel = modelUsageLabel
29
+ ? `${fittedWorkspaceLabel ? `${fittedWorkspaceLabel} ` : ""}${modelUsageLabel}`
30
+ : fittedWorkspaceLabel;
31
+ const details = workspaceDetailsLabel ? `${status} ${workspaceDetailsLabel}` : status;
32
+ const leftText = padOrTrimPlain(`${statusDot} ${details}`, leftWidth);
33
+ const text = widgetsLayout?.inputBorderWidgetStartColumn
34
+ ? overlayText(padOrTrimPlain(leftText, contentWidth), widgetsLayout.inputBorderWidgetStartColumn, widgetsLayout.text)
35
+ : padOrTrimPlain(leftText, contentWidth);
27
36
  return {
37
+ ...widgetsLayout,
28
38
  details,
29
39
  text,
30
40
  sessionLabel,
31
- workspaceLabel,
41
+ workspaceLabel: fittedWorkspaceLabel,
32
42
  ...(modelUsageLabel ? { modelUsageLabel } : {}),
33
43
  ...(contextBarLabel ? { contextBarLabel } : {}),
34
44
  };
@@ -76,17 +86,18 @@ export class StatusLineRenderer {
76
86
  if (!hasParts)
77
87
  return undefined;
78
88
  const parts = [];
79
- const totalWidth = widgets.reduce((total, widget, index) => total + (index > 0 ? 1 : 0) + stringDisplayWidth(widget.text), 0);
80
- const endColumn = Math.max(1, width - INPUT_BORDER_WIDGET_INSET);
89
+ const gapWidth = stringDisplayWidth(STATUS_WIDGET_GAP);
90
+ const totalWidth = widgets.reduce((total, widget, index) => total + stringDisplayWidth(widget.text) + (index > 0 ? gapWidth : 0), 0);
91
+ const endColumn = Math.max(1, width + 1);
81
92
  const startColumn = endColumn - totalWidth;
82
- if (startColumn < 2)
93
+ if (startColumn < 1)
83
94
  return undefined;
84
95
  layout.inputBorderWidgetStartColumn = startColumn;
85
96
  let nextColumn = startColumn;
86
97
  for (const [index, widget] of widgets.entries()) {
87
98
  if (index > 0) {
88
- parts.push(INPUT_BORDER_WIDGET_SEPARATOR);
89
- nextColumn += 1;
99
+ parts.push(STATUS_WIDGET_GAP);
100
+ nextColumn += gapWidth;
90
101
  }
91
102
  parts.push(widget.text);
92
103
  widget.assign(nextColumn, widget.text);
@@ -110,7 +121,6 @@ export class StatusLineRenderer {
110
121
  }
111
122
  inputBorderWidgetSegments(layout, text) {
112
123
  const colors = this.host.theme.colors;
113
- const background = colors.inputBorderWidgetBackground;
114
124
  const segments = [];
115
125
  const pushWidgetSegment = (widget, foreground) => {
116
126
  if (!widget)
@@ -119,7 +129,6 @@ export class StatusLineRenderer {
119
129
  start: displayIndexForColumn(text, widget.startColumn),
120
130
  end: displayIndexForColumn(text, widget.endColumn),
121
131
  foreground,
122
- background,
123
132
  });
124
133
  };
125
134
  pushWidgetSegment(layout.draftQueueWidget, colors.info);
@@ -138,14 +147,12 @@ export class StatusLineRenderer {
138
147
  start: displayIndexForColumn(text, voiceWidget.startColumn),
139
148
  end: displayIndexForColumn(text, voiceWidget.micEndColumn),
140
149
  foreground: this.host.voiceStatusWidgetActive() ? colors.error : colors.muted,
141
- background,
142
150
  });
143
151
  if (voiceWidget.languageEndColumn > voiceWidget.languageStartColumn) {
144
152
  segments.push({
145
153
  start: displayIndexForColumn(text, voiceWidget.languageStartColumn),
146
154
  end: displayIndexForColumn(text, voiceWidget.languageEndColumn),
147
155
  foreground: colors.statusForeground,
148
- background,
149
156
  });
150
157
  }
151
158
  }
@@ -271,16 +278,10 @@ export class StatusLineRenderer {
271
278
  end: statusDotStart + APP_ICONS.record.length,
272
279
  foreground: this.statusDotColor(),
273
280
  }] : [];
274
- this.pushDraftQueueWidgetSegment(segments, statusText);
275
- this.pushUserJumpWidgetSegment(segments, statusText);
276
- this.pushThinkingExpandWidgetSegment(segments, statusText, layout);
277
- this.pushCompactToolsWidgetSegment(segments, statusText);
278
- this.pushTerminalBellSoundWidgetSegment(segments, statusText);
281
+ this.pushStatusWidgetSegments(segments, statusText, layout);
279
282
  this.pushWorkspaceSegments(segments, statusText, layout.workspaceLabel);
280
283
  if (layout.modelUsageLabel)
281
284
  this.pushModelUsageSegments(segments, statusText, layout.modelUsageLabel);
282
- this.pushPromptEnhancerWidgetSegment(segments, statusText);
283
- this.pushVoiceWidgetSegment(segments, statusText);
284
285
  const session = this.host.session;
285
286
  if (!session)
286
287
  return segments;
@@ -304,6 +305,50 @@ export class StatusLineRenderer {
304
305
  this.pushSegment(segments, statusText.lastIndexOf(layout.sessionLabel), layout.sessionLabel.length, this.host.theme.colors.selectionForeground);
305
306
  return segments;
306
307
  }
308
+ fitWorkspaceLabel(statusDot, status, workspaceLabel, modelUsageLabel, width) {
309
+ const modelUsageSuffix = modelUsageLabel ? ` ${modelUsageLabel}` : "";
310
+ const available = width - stringDisplayWidth(`${statusDot} ${status} `) - stringDisplayWidth(modelUsageSuffix);
311
+ if (available <= 0)
312
+ return "";
313
+ return ellipsizeDisplay(workspaceLabel, available);
314
+ }
315
+ pushStatusWidgetSegments(segments, statusText, layout) {
316
+ const colors = this.host.theme.colors;
317
+ const widgets = [
318
+ { widget: layout.draftQueueWidget, foreground: colors.info },
319
+ { widget: layout.promptEnhancerWidget, foreground: this.host.promptEnhancerStatusWidgetActive()
320
+ ? colors.warning
321
+ : this.host.promptEnhancerStatusWidgetEnabled()
322
+ ? colors.info
323
+ : colors.muted },
324
+ { widget: layout.userJumpWidget, foreground: this.host.userMessageJumpMenuActive?.() ? colors.info : colors.muted },
325
+ { widget: layout.terminalBellSoundWidget, foreground: this.host.terminalBellSoundStatusWidgetEnabled() ? colors.info : colors.muted },
326
+ { widget: layout.thinkingExpandWidget, foreground: this.host.allThinkingExpandedActive?.() ? colors.info : colors.muted },
327
+ { widget: layout.compactToolsWidget, foreground: this.host.superCompactToolsActive?.() ? colors.info : colors.muted },
328
+ ].filter((entry) => Boolean(entry.widget));
329
+ for (const { widget, foreground } of widgets) {
330
+ segments.push({
331
+ start: displayIndexForColumn(statusText, widget.startColumn),
332
+ end: displayIndexForColumn(statusText, widget.endColumn),
333
+ foreground,
334
+ });
335
+ }
336
+ const voiceWidget = layout.voiceWidget;
337
+ if (voiceWidget) {
338
+ segments.push({
339
+ start: displayIndexForColumn(statusText, voiceWidget.startColumn),
340
+ end: displayIndexForColumn(statusText, voiceWidget.micEndColumn),
341
+ foreground: this.host.voiceStatusWidgetActive() ? colors.error : colors.muted,
342
+ });
343
+ if (voiceWidget.languageEndColumn > voiceWidget.languageStartColumn) {
344
+ segments.push({
345
+ start: displayIndexForColumn(statusText, voiceWidget.languageStartColumn),
346
+ end: displayIndexForColumn(statusText, voiceWidget.languageEndColumn),
347
+ foreground: colors.statusForeground,
348
+ });
349
+ }
350
+ }
351
+ }
307
352
  pushPromptEnhancerWidgetSegment(segments, statusText) {
308
353
  const widgetText = this.host.promptEnhancerStatusWidgetText();
309
354
  const start = statusText.lastIndexOf(widgetText);
@@ -497,7 +542,7 @@ export class StatusLineRenderer {
497
542
  };
498
543
  }
499
544
  iconButtonText(icon) {
500
- return padOrTrimPlain(` ${icon} `, STATUS_ICON_BUTTON_WIDTH);
545
+ return icon;
501
546
  }
502
547
  voiceBorderWidgetText(widgetText) {
503
548
  const parts = this.voiceBorderWidgetParts(widgetText);
@@ -505,21 +550,22 @@ export class StatusLineRenderer {
505
550
  return "";
506
551
  const micButton = this.iconButtonText(parts.buttonIconText);
507
552
  if (parts.languageText) {
508
- return `${micButton}${INPUT_BORDER_WIDGET_SEPARATOR}${parts.languageText}`;
553
+ return `${micButton}${STATUS_WIDGET_GAP}${parts.languageText}`;
509
554
  }
510
555
  return micButton;
511
556
  }
512
557
  voiceWidgetLayout(startColumn, sourceText, widgetText) {
513
558
  const parts = this.voiceBorderWidgetParts(sourceText);
559
+ const micWidth = parts?.buttonIconText ? stringDisplayWidth(parts.buttonIconText) : stringDisplayWidth(widgetText);
514
560
  const languageStartOffset = parts?.languageText
515
- ? STATUS_ICON_BUTTON_WIDTH + stringDisplayWidth(INPUT_BORDER_WIDGET_SEPARATOR)
561
+ ? micWidth + stringDisplayWidth(STATUS_WIDGET_GAP)
516
562
  : stringDisplayWidth(widgetText);
517
563
  const languageEndOffset = parts?.languageText
518
564
  ? languageStartOffset + stringDisplayWidth(parts.languageText)
519
565
  : languageStartOffset;
520
566
  return {
521
567
  startColumn,
522
- micEndColumn: startColumn + STATUS_ICON_BUTTON_WIDTH,
568
+ micEndColumn: startColumn + micWidth,
523
569
  languageStartColumn: startColumn + languageStartOffset,
524
570
  languageEndColumn: startColumn + languageEndOffset,
525
571
  endColumn: startColumn + stringDisplayWidth(widgetText),
@@ -20,7 +20,7 @@ export type InputFrameCopyRows = {
20
20
  inputStartRow: number;
21
21
  inputEndRow: number;
22
22
  inputSeparatorRow: number;
23
- inputBottomSeparatorRow: number;
23
+ inputBottomSeparatorRow?: number;
24
24
  };
25
25
  export type AppMouseControllerHost = {
26
26
  terminalColumns(): number;
@@ -784,12 +784,15 @@ export class AppMouseController {
784
784
  const topOffset = editorLayoutTopOffset(tabPanelRows);
785
785
  const toScreenRow = (layoutRow) => Math.max(1, Math.min(terminalRows, topOffset + layoutRow));
786
786
  const toScreenRowExclusive = (layoutRow) => Math.max(1, Math.min(terminalRows + 1, topOffset + layoutRow));
787
- return {
787
+ const inputFrame = {
788
788
  inputStartRow: toScreenRow(layout.inputStartRow),
789
789
  inputEndRow: toScreenRowExclusive(layout.inputStartRow + layout.renderedInput.lines.length),
790
790
  inputSeparatorRow: toScreenRow(layout.inputSeparatorRow),
791
- inputBottomSeparatorRow: toScreenRow(layout.inputBottomSeparatorRow),
792
791
  };
792
+ if (layout.inputBottomSeparatorRow !== undefined) {
793
+ inputFrame.inputBottomSeparatorRow = toScreenRow(layout.inputBottomSeparatorRow);
794
+ }
795
+ return inputFrame;
793
796
  }
794
797
  getSelectedConversationText(anchor, current) {
795
798
  const range = orderedConversationSelection(anchor, current);
@@ -935,7 +938,10 @@ function orderedConversationSelection(anchor, current) {
935
938
  return anchor.x <= current.x ? { start: anchor, end: current } : { start: current, end: anchor };
936
939
  }
937
940
  export function screenSelectionLineText(row, text, startColumn, endColumn, inputFrame) {
938
- if (inputFrame && (row === inputFrame.inputSeparatorRow || row === inputFrame.inputBottomSeparatorRow)) {
941
+ if (inputFrame && row === inputFrame.inputSeparatorRow) {
942
+ return undefined;
943
+ }
944
+ if (inputFrame?.inputBottomSeparatorRow !== undefined && row === inputFrame.inputBottomSeparatorRow) {
939
945
  return undefined;
940
946
  }
941
947
  return sliceByDisplayColumns(text, startColumn, endColumn);
@@ -75,7 +75,7 @@ export class ScreenStyler {
75
75
  }
76
76
  styleInputLine(row, text, tagSpans, suggestionSpans, width, tagColor, suggestionColor, frameColor) {
77
77
  const colors = this.host.theme.colors;
78
- const baseOptions = { foreground: colors.warning };
78
+ const baseOptions = { foreground: colors.userForeground };
79
79
  if (this.selectionRangeForRow(row, width, text))
80
80
  return this.styleLine(row, text, width, baseOptions);
81
81
  const plain = padOrTrimPlain(text, width);
@@ -447,7 +447,7 @@ export type EditorLayout = {
447
447
  belowEditorLines: readonly RenderedLine[];
448
448
  inputStartRow: number;
449
449
  inputSeparatorRow: number;
450
- inputBottomSeparatorRow: number;
450
+ inputBottomSeparatorRow?: number;
451
451
  bodyHeight: number;
452
452
  };
453
453
  export type MouseEvent = {
package/dist/theme.d.ts CHANGED
@@ -36,6 +36,14 @@ export type Theme = {
36
36
  toolMutation: string;
37
37
  toolSearch: string;
38
38
  toolTitle: string;
39
+ toolBash: string;
40
+ toolRead: string;
41
+ toolIndex: string;
42
+ toolEdit: string;
43
+ toolWeb: string;
44
+ toolMeta: string;
45
+ thinkingForeground: string;
46
+ userForeground: string;
39
47
  thinkingXHigh: string;
40
48
  modelOpenAI: string;
41
49
  statusDotBase: string;
package/dist/theme.js CHANGED
@@ -10,7 +10,7 @@ export const THEMES = {
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",
@@ -37,6 +37,14 @@ export const THEMES = {
37
37
  toolMutation: "#d47aa2",
38
38
  toolSearch: "#a889d6",
39
39
  toolTitle: "#9aa7b4",
40
+ toolBash: "#c99670",
41
+ toolRead: "#6daa8a",
42
+ toolIndex: "#7a9ec7",
43
+ toolEdit: "#c76a8a",
44
+ toolWeb: "#8a9cc7",
45
+ toolMeta: "#8b8fa3",
46
+ thinkingForeground: "#b8a0d4",
47
+ userForeground: "#88b4dc",
40
48
  thinkingXHigh: "#ff8a86",
41
49
  modelOpenAI: "#c8b45a",
42
50
  statusDotBase: "#30363d",
@@ -55,7 +63,7 @@ export const THEMES = {
55
63
  headerForeground: "#0f172a",
56
64
  headerBackground: "#e2e8f0",
57
65
  statusForeground: "#475569",
58
- statusBackground: "#f8fafc",
66
+ statusBackground: "#edf0f4",
59
67
  inputForeground: "#0f172a",
60
68
  inputBackground: "#f8fafc",
61
69
  inputBorder: "#334155",
@@ -82,6 +90,14 @@ export const THEMES = {
82
90
  toolMutation: "#a33a68",
83
91
  toolSearch: "#6d52a5",
84
92
  toolTitle: "#526070",
93
+ toolBash: "#8a6535",
94
+ toolRead: "#3d7a56",
95
+ toolIndex: "#3a6d96",
96
+ toolEdit: "#963a5e",
97
+ toolWeb: "#4a6096",
98
+ toolMeta: "#6b7280",
99
+ thinkingForeground: "#6b5491",
100
+ userForeground: "#4a78b5",
85
101
  thinkingXHigh: "#cf333d",
86
102
  modelOpenAI: "#75671f",
87
103
  statusDotBase: "#334155",
@@ -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
  }
@@ -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.20",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {