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.
- package/dist/app/constants.js +1 -1
- package/dist/app/rendering/conversation-entry-renderer.js +2 -2
- package/dist/app/rendering/editor-layout-renderer.js +4 -4
- package/dist/app/rendering/render-controller.js +14 -24
- package/dist/app/rendering/status-line-renderer.d.ts +2 -0
- package/dist/app/rendering/status-line-renderer.js +75 -29
- package/dist/app/screen/mouse-controller.d.ts +1 -1
- package/dist/app/screen/mouse-controller.js +9 -3
- package/dist/app/screen/screen-styler.js +1 -1
- package/dist/app/types.d.ts +1 -1
- package/dist/theme.d.ts +8 -0
- package/dist/theme.js +18 -2
- package/extensions/question/tui.ts +1 -1
- package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +123 -20
- package/package.json +1 -1
package/dist/app/constants.js
CHANGED
|
@@ -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: "
|
|
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.
|
|
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.
|
|
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 -
|
|
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
|
|
22
|
-
const
|
|
23
|
-
const inputStartRow =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
291
|
-
const widgetLayout =
|
|
292
|
-
const widgetRow =
|
|
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
|
|
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
|
|
22
|
-
const
|
|
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
|
|
26
|
-
const
|
|
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
|
|
80
|
-
const
|
|
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 <
|
|
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(
|
|
89
|
-
nextColumn +=
|
|
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.
|
|
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
|
|
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}${
|
|
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
|
-
?
|
|
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 +
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
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.
|
|
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);
|
package/dist/app/types.d.ts
CHANGED
|
@@ -447,7 +447,7 @@ export type EditorLayout = {
|
|
|
447
447
|
belowEditorLines: readonly RenderedLine[];
|
|
448
448
|
inputStartRow: number;
|
|
449
449
|
inputSeparatorRow: number;
|
|
450
|
-
inputBottomSeparatorRow
|
|
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: "#
|
|
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: "#
|
|
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 (
|
|
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
|
|
60
|
+
"GLM TOOL-ONLY CODING AGENT CONTRACT.",
|
|
59
61
|
"",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
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
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
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
|
}
|