pi-ui-extend 0.1.35 → 0.1.37
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/app.d.ts +8 -0
- package/dist/app/app.js +48 -5
- package/dist/app/commands/command-controller.js +1 -0
- package/dist/app/commands/command-host.d.ts +1 -0
- package/dist/app/commands/command-model-actions.d.ts +1 -0
- package/dist/app/commands/command-model-actions.js +32 -0
- package/dist/app/commands/command-navigation-actions.js +3 -0
- package/dist/app/commands/command-registry.d.ts +1 -0
- package/dist/app/commands/command-registry.js +8 -0
- package/dist/app/commands/command-session-actions.d.ts +2 -0
- package/dist/app/commands/command-session-actions.js +81 -1
- package/dist/app/extensions/extension-actions-controller.d.ts +5 -1
- package/dist/app/extensions/extension-actions-controller.js +35 -2
- package/dist/app/input/input-controller.d.ts +2 -0
- package/dist/app/input/input-controller.js +50 -2
- package/dist/app/input/terminal-edit-shortcuts.d.ts +2 -0
- package/dist/app/input/terminal-edit-shortcuts.js +49 -0
- package/dist/app/input/voice-controller.js +1 -1
- package/dist/app/popup/popup-action-controller.d.ts +2 -3
- package/dist/app/popup/popup-action-controller.js +2 -5
- package/dist/app/rendering/message-content.js +4 -3
- package/dist/app/rendering/render-controller.js +21 -38
- package/dist/app/rendering/status-line-renderer.d.ts +1 -0
- package/dist/app/rendering/status-line-renderer.js +14 -2
- package/dist/app/runtime.js +12 -2
- package/dist/app/screen/mouse-controller.js +2 -0
- package/dist/app/session/session-event-controller.d.ts +7 -0
- package/dist/app/session/session-event-controller.js +10 -13
- package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
- package/dist/app/session/session-lifecycle-controller.js +7 -0
- package/dist/app/session/tabs-controller.d.ts +1 -0
- package/dist/app/session/tabs-controller.js +1 -0
- package/dist/app/terminal/terminal-controller.js +1 -0
- package/dist/app/terminal/terminal-output-buffer.d.ts +8 -6
- package/dist/app/terminal/terminal-output-buffer.js +24 -16
- package/dist/app/workspace/workspace-actions-controller.d.ts +1 -0
- package/dist/app/workspace/workspace-actions-controller.js +1 -0
- package/dist/bundled-extensions/terminal-bell/index.js +118 -33
- package/dist/markdown-format.d.ts +1 -0
- package/dist/markdown-format.js +30 -16
- package/dist/schemas/pi-tools-suite-schema.d.ts +5 -0
- package/dist/schemas/pi-tools-suite-schema.js +5 -0
- package/dist/tool-renderers/apply-patch.js +6 -1
- package/dist/tool-renderers/patch-normalize.d.ts +24 -0
- package/dist/tool-renderers/patch-normalize.js +163 -0
- package/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/package.json +5 -5
- package/external/pi-tools-suite/src/antigravity-auth/index.ts +15 -2
- package/external/pi-tools-suite/src/antigravity-auth/status.ts +36 -19
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +5 -2
- package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +8 -3
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +63 -28
- package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +9 -4
- package/external/pi-tools-suite/src/comment-checker/config.ts +98 -0
- package/external/pi-tools-suite/src/comment-checker/detect.ts +215 -0
- package/external/pi-tools-suite/src/comment-checker/index.ts +294 -0
- package/external/pi-tools-suite/src/dcp/commands.ts +29 -15
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +111 -60
- package/external/pi-tools-suite/src/dcp/config.ts +10 -6
- package/external/pi-tools-suite/src/dcp/debug-log.ts +235 -0
- package/external/pi-tools-suite/src/dcp/index.ts +204 -27
- package/external/pi-tools-suite/src/dcp/prompts.ts +25 -28
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +6 -10
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +19 -1
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +36 -58
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +18 -0
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
- package/external/pi-tools-suite/src/dcp/pruner.ts +4 -2
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +31 -2
- package/external/pi-tools-suite/src/dcp/state.ts +62 -4
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +18 -0
- package/external/pi-tools-suite/src/index.ts +1 -0
- package/external/pi-tools-suite/src/model-tools/index.ts +11 -3
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +1 -1
- package/external/pi-tools-suite/src/todo/index.ts +24 -0
- package/external/pi-tools-suite/src/tool-descriptions.ts +3 -3
- package/external/pi-tools-suite/src/usage/index.ts +18 -4
- package/package.json +4 -4
- package/schemas/pi-tools-suite.json +24 -0
|
@@ -4,6 +4,7 @@ const COMMAND_MODIFIER_FLAG = 8;
|
|
|
4
4
|
const LOCK_MODIFIER_MASK = 64 + 128;
|
|
5
5
|
const KEY_CODE_C = 99;
|
|
6
6
|
const KEY_CODE_ENTER = 13;
|
|
7
|
+
const KEY_CODE_ESCAPE = 27;
|
|
7
8
|
const KEY_CODE_V = 118;
|
|
8
9
|
const KEY_CODE_Y = 121;
|
|
9
10
|
const KEY_CODE_Z = 122;
|
|
@@ -11,12 +12,22 @@ const CYRILLIC_SMALL_ES_CODE = 1089;
|
|
|
11
12
|
const CYRILLIC_CAPITAL_ES_CODE = 1057;
|
|
12
13
|
const CYRILLIC_SMALL_EM_CODE = 1084;
|
|
13
14
|
const CYRILLIC_CAPITAL_EM_CODE = 1052;
|
|
15
|
+
const KITTY_ARROW_CODEPOINTS = {
|
|
16
|
+
A: -1,
|
|
17
|
+
B: -2,
|
|
18
|
+
C: -3,
|
|
19
|
+
D: -4,
|
|
20
|
+
};
|
|
14
21
|
const KITTY_CSI_U_SEQUENCE = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u/;
|
|
22
|
+
const KITTY_ARROW_SEQUENCE = /^\x1b\[1;(\d+)(?::(\d+))?([ABCD])/;
|
|
15
23
|
const XTERM_MODIFY_OTHER_KEYS_SEQUENCE = /^\x1b\[27;(\d+);(\d+)~/;
|
|
16
24
|
export function parseTerminalModifiedKeySequence(input) {
|
|
17
25
|
const kitty = parseKittyCsiUSequence(input);
|
|
18
26
|
if (kitty)
|
|
19
27
|
return { kind: "key", key: kitty };
|
|
28
|
+
const kittyArrow = parseKittyArrowSequence(input);
|
|
29
|
+
if (kittyArrow)
|
|
30
|
+
return { kind: "key", key: kittyArrow };
|
|
20
31
|
const xterm = parseXtermModifyOtherKeysSequence(input);
|
|
21
32
|
if (xterm)
|
|
22
33
|
return { kind: "key", key: xterm };
|
|
@@ -57,6 +68,26 @@ export function terminalKeyIsClipboardImagePaste(key) {
|
|
|
57
68
|
export function terminalKeyShouldIgnore(key) {
|
|
58
69
|
return key.eventType === 3;
|
|
59
70
|
}
|
|
71
|
+
export function terminalKeyIsEscape(key) {
|
|
72
|
+
const effectiveModifier = key.modifier & ~LOCK_MODIFIER_MASK;
|
|
73
|
+
if (effectiveModifier !== 0)
|
|
74
|
+
return false;
|
|
75
|
+
return key.codepoint === KEY_CODE_ESCAPE;
|
|
76
|
+
}
|
|
77
|
+
export function terminalKeyArrowDirection(key) {
|
|
78
|
+
const effectiveModifier = key.modifier & ~LOCK_MODIFIER_MASK;
|
|
79
|
+
if (effectiveModifier !== 0)
|
|
80
|
+
return undefined;
|
|
81
|
+
if (key.codepoint === KITTY_ARROW_CODEPOINTS.A)
|
|
82
|
+
return "up";
|
|
83
|
+
if (key.codepoint === KITTY_ARROW_CODEPOINTS.B)
|
|
84
|
+
return "down";
|
|
85
|
+
if (key.codepoint === KITTY_ARROW_CODEPOINTS.C)
|
|
86
|
+
return "right";
|
|
87
|
+
if (key.codepoint === KITTY_ARROW_CODEPOINTS.D)
|
|
88
|
+
return "left";
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
60
91
|
export function terminalEditShortcutForControlChar(char, shiftPressed) {
|
|
61
92
|
if (char === "\u001a")
|
|
62
93
|
return shiftPressed ? "redo" : "undo";
|
|
@@ -82,6 +113,24 @@ function parseKittyCsiUSequence(input) {
|
|
|
82
113
|
length: match[0].length,
|
|
83
114
|
};
|
|
84
115
|
}
|
|
116
|
+
function parseKittyArrowSequence(input) {
|
|
117
|
+
const match = KITTY_ARROW_SEQUENCE.exec(input);
|
|
118
|
+
if (!match)
|
|
119
|
+
return undefined;
|
|
120
|
+
const modifierValue = Number.parseInt(match[1] ?? "", 10);
|
|
121
|
+
const eventType = match[2] ? Number.parseInt(match[2], 10) : undefined;
|
|
122
|
+
const arrow = match[3];
|
|
123
|
+
const codepoint = arrow ? KITTY_ARROW_CODEPOINTS[arrow] : undefined;
|
|
124
|
+
if (!Number.isFinite(modifierValue) || codepoint === undefined)
|
|
125
|
+
return undefined;
|
|
126
|
+
return {
|
|
127
|
+
codepoint,
|
|
128
|
+
baseLayoutKey: undefined,
|
|
129
|
+
modifier: modifierValue - 1,
|
|
130
|
+
eventType: Number.isFinite(eventType) ? eventType : undefined,
|
|
131
|
+
length: match[0].length,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
85
134
|
function parseXtermModifyOtherKeysSequence(input) {
|
|
86
135
|
const match = XTERM_MODIFY_OTHER_KEYS_SEQUENCE.exec(input);
|
|
87
136
|
if (!match)
|
|
@@ -66,7 +66,7 @@ export class AppVoiceController {
|
|
|
66
66
|
case "installing":
|
|
67
67
|
return `${APP_ICONS.microphone}${languageLabel} ${APP_ICONS.timerSand}`;
|
|
68
68
|
case "downloading":
|
|
69
|
-
return `${APP_ICONS.
|
|
69
|
+
return `${APP_ICONS.microphone}${languageLabel} ${APP_ICONS.timerSand}`;
|
|
70
70
|
case "loading":
|
|
71
71
|
return `${APP_ICONS.microphone}${languageLabel} ${APP_ICONS.timerSand}`;
|
|
72
72
|
case "listening":
|
|
@@ -7,6 +7,7 @@ import type { Entry, SlashCommand, UserMessageJumpMenuValue } from "../types.js"
|
|
|
7
7
|
import type { AppWorkspaceActionsController } from "../workspace/workspace-actions-controller.js";
|
|
8
8
|
export type AppPopupActionControllerHost = {
|
|
9
9
|
runtime(): AgentSessionRuntime | undefined;
|
|
10
|
+
awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
|
|
10
11
|
getBuiltinSlashCommands(): readonly SlashCommand[];
|
|
11
12
|
isRunning(): boolean;
|
|
12
13
|
setInput(value: string): void;
|
|
@@ -15,9 +16,7 @@ export type AppPopupActionControllerHost = {
|
|
|
15
16
|
setSessionStatus(session: AgentSession | undefined): void;
|
|
16
17
|
showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
|
|
17
18
|
render(): void;
|
|
18
|
-
|
|
19
|
-
bindCurrentSession(): Promise<void>;
|
|
20
|
-
loadSessionHistory(): void;
|
|
19
|
+
afterSessionReplacement(message?: string): void;
|
|
21
20
|
scrollToConversationEntry(entryId: string): boolean;
|
|
22
21
|
scrollToUserMessageJumpTarget(target: UserMessageJumpMenuValue): Promise<boolean>;
|
|
23
22
|
};
|
|
@@ -228,6 +228,7 @@ export class AppPopupActionController {
|
|
|
228
228
|
this.host.setStatus("switching session");
|
|
229
229
|
this.host.render();
|
|
230
230
|
try {
|
|
231
|
+
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
231
232
|
const result = await runtime.switchSession(session.path);
|
|
232
233
|
if (result.cancelled) {
|
|
233
234
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "Resume cancelled." });
|
|
@@ -235,12 +236,8 @@ export class AppPopupActionController {
|
|
|
235
236
|
this.host.render();
|
|
236
237
|
return true;
|
|
237
238
|
}
|
|
238
|
-
this.host.resetSessionView();
|
|
239
|
-
await this.host.bindCurrentSession();
|
|
240
|
-
this.host.loadSessionHistory();
|
|
241
239
|
const name = runtime.session.sessionName ?? session.id.slice(0, 8);
|
|
242
|
-
this.host.
|
|
243
|
-
this.host.setSessionStatus(runtime.session);
|
|
240
|
+
this.host.afterSessionReplacement(`Resumed session "${name}"`);
|
|
244
241
|
}
|
|
245
242
|
catch (error) {
|
|
246
243
|
this.host.addEntry({ id: createId("error"), kind: "error", text: `Resume failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripDcpControlMetadata } from "../../markdown-format.js";
|
|
1
2
|
import { isRecord } from "../guards.js";
|
|
2
3
|
const MAX_FORMAT_STRING_CHARS = 256 * 1024;
|
|
3
4
|
const MAX_RENDERED_CONTENT_CHARS = 512 * 1024;
|
|
@@ -79,11 +80,11 @@ export function renderContent(content) {
|
|
|
79
80
|
if (!pushPart(stringifyUnknown(item)))
|
|
80
81
|
break;
|
|
81
82
|
}
|
|
82
|
-
return parts.join("\n");
|
|
83
|
+
return stripDcpControlMetadata(parts.join("\n"));
|
|
83
84
|
}
|
|
84
85
|
export function renderUserMessageContent(content) {
|
|
85
86
|
if (typeof content === "string")
|
|
86
|
-
return content;
|
|
87
|
+
return stripDcpControlMetadata(content);
|
|
87
88
|
if (!Array.isArray(content))
|
|
88
89
|
return stringifyUnknown(content);
|
|
89
90
|
const textParts = [];
|
|
@@ -103,7 +104,7 @@ export function renderUserMessageContent(content) {
|
|
|
103
104
|
}
|
|
104
105
|
textParts.push(stringifyUnknown(item));
|
|
105
106
|
}
|
|
106
|
-
const text = textParts.join("\n").replace(/\[Image \d+(?:: [^\]]+)?\]/g, "").trimEnd();
|
|
107
|
+
const text = stripDcpControlMetadata(textParts.join("\n")).replace(/\[Image \d+(?:: [^\]]+)?\]/g, "").trimEnd();
|
|
107
108
|
if (imageCount === 0)
|
|
108
109
|
return text;
|
|
109
110
|
const imageText = userImageLabels(imageCount);
|
|
@@ -75,24 +75,10 @@ export class AppRenderController {
|
|
|
75
75
|
this.deps.mouseController.statusVoiceMicTarget = undefined;
|
|
76
76
|
this.deps.mouseController.statusVoiceLanguageTarget = undefined;
|
|
77
77
|
this.deps.mouseController.tabLineTargets.length = 0;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
conversation: [],
|
|
81
|
-
inputStatus: [],
|
|
82
|
-
};
|
|
83
|
-
const inputStatusStartRow = toScreenRow(inputSeparatorRow);
|
|
84
|
-
const regionForOverlayRow = (row) => {
|
|
85
|
-
if (row >= statusRow)
|
|
86
|
-
return "inputStatus";
|
|
87
|
-
if (topReservedRows > 0 && row <= topReservedRows)
|
|
88
|
-
return "tabs";
|
|
89
|
-
if (row >= inputStatusStartRow)
|
|
90
|
-
return "inputStatus";
|
|
91
|
-
return "conversation";
|
|
92
|
-
};
|
|
93
|
-
const appendFrameOutput = (region, row, output) => {
|
|
78
|
+
const frameRows = new Map();
|
|
79
|
+
const appendFrameOutput = (row, output) => {
|
|
94
80
|
if (row >= 1 && row <= rows)
|
|
95
|
-
|
|
81
|
+
frameRows.set(row, `${frameRows.get(row) ?? ""}${output}`);
|
|
96
82
|
};
|
|
97
83
|
const setRenderedBackground = (row, background) => {
|
|
98
84
|
if (background !== undefined)
|
|
@@ -101,14 +87,14 @@ export class AppRenderController {
|
|
|
101
87
|
if (topReservedRows > 0) {
|
|
102
88
|
this.deps.mouseController.tabLineTargets.push(...tabLayout.targets.map((target) => ({ ...target, row: tabRow })));
|
|
103
89
|
this.deps.mouseController.renderedRowTexts.set(tabRow, tabLayout.text);
|
|
104
|
-
appendFrameOutput(
|
|
90
|
+
appendFrameOutput(tabRow, this.renderFrameRow(tabRow, this.deps.tabLineRenderer.render(tabRow, tabLayout, columns)));
|
|
105
91
|
if (topReservedRows > 1) {
|
|
106
92
|
this.deps.mouseController.tabLineTargets.push(...tabLayout.targets
|
|
107
93
|
.filter((target) => target.kind === "new-tab")
|
|
108
94
|
.map((target) => ({ ...target, row: tabBottomRow })));
|
|
109
95
|
const bottomText = this.deps.tabLineRenderer.bottomText(tabLayout, columns);
|
|
110
96
|
this.deps.mouseController.renderedRowTexts.set(tabBottomRow, bottomText);
|
|
111
|
-
appendFrameOutput(
|
|
97
|
+
appendFrameOutput(tabBottomRow, this.renderFrameRow(tabBottomRow, this.deps.tabLineRenderer.renderBottom(tabBottomRow, tabLayout, columns)));
|
|
112
98
|
}
|
|
113
99
|
}
|
|
114
100
|
else {
|
|
@@ -125,12 +111,12 @@ export class AppRenderController {
|
|
|
125
111
|
this.deps.mouseController.renderedImageTargets.set(row, rendered.imageTargets);
|
|
126
112
|
this.deps.mouseController.renderedRowTexts.set(row, rendered?.text ?? "");
|
|
127
113
|
setRenderedBackground(row, rendered?.backgroundOverride);
|
|
128
|
-
appendFrameOutput(
|
|
114
|
+
appendFrameOutput(row, this.renderFrameRow(row, this.deps.screenStyler.styleBaseLine(row, rendered, conversationColumns)));
|
|
129
115
|
}
|
|
130
116
|
const loadingConversationOverlay = this.renderConversationLoadingOverlay(this.deps.loadingConversationOverlayText?.(), conversationColumns, topReservedRows, bodyHeight);
|
|
131
117
|
if (loadingConversationOverlay) {
|
|
132
118
|
this.deps.mouseController.renderedRowTexts.set(loadingConversationOverlay.row, loadingConversationOverlay.text);
|
|
133
|
-
appendFrameOutput(
|
|
119
|
+
appendFrameOutput(loadingConversationOverlay.row, this.renderFrameRow(loadingConversationOverlay.row, loadingConversationOverlay.output));
|
|
134
120
|
}
|
|
135
121
|
const aboveEditorStartRow = inputSeparatorRow + 1;
|
|
136
122
|
for (let index = 0; index < aboveEditorLines.length; index += 1) {
|
|
@@ -144,14 +130,14 @@ export class AppRenderController {
|
|
|
144
130
|
this.deps.mouseController.renderedImageTargets.set(row, rendered.line.imageTargets);
|
|
145
131
|
this.deps.mouseController.renderedRowTexts.set(row, rendered.text);
|
|
146
132
|
setRenderedBackground(row, rendered.line?.backgroundOverride);
|
|
147
|
-
appendFrameOutput(
|
|
133
|
+
appendFrameOutput(row, this.renderFrameRow(row, rendered.output(row)));
|
|
148
134
|
}
|
|
149
135
|
if (inputSeparatorRow > 1) {
|
|
150
136
|
const separatorText = inputFrameLine(columns, "top");
|
|
151
137
|
const row = toScreenRow(inputSeparatorRow);
|
|
152
138
|
if (row < statusRow) {
|
|
153
139
|
this.deps.mouseController.renderedRowTexts.set(row, separatorText);
|
|
154
|
-
appendFrameOutput(
|
|
140
|
+
appendFrameOutput(row, this.renderFrameRow(row, this.deps.screenStyler.styleLine(row, separatorText, columns, {
|
|
155
141
|
foreground: this.deps.theme.colors.tabBorder,
|
|
156
142
|
})));
|
|
157
143
|
}
|
|
@@ -164,7 +150,7 @@ export class AppRenderController {
|
|
|
164
150
|
this.deps.mouseController.renderedRowTexts.set(row, inputLine);
|
|
165
151
|
const tagColor = this.deps.theme.colors.accent;
|
|
166
152
|
const styledLine = this.deps.screenStyler.styleInputLine(row, inputLine, tagSpans, suggestionSpans, columns, tagColor, this.deps.theme.colors.muted);
|
|
167
|
-
appendFrameOutput(
|
|
153
|
+
appendFrameOutput(row, this.renderFrameRow(row, styledLine));
|
|
168
154
|
}
|
|
169
155
|
if (renderedInput.scrollBar && columns > 0) {
|
|
170
156
|
const scrollBar = renderedInput.scrollBar;
|
|
@@ -172,7 +158,7 @@ export class AppRenderController {
|
|
|
172
158
|
const row = toScreenRow(inputStartRow + renderedInput.editorStartRowOffset + offset);
|
|
173
159
|
const isThumb = offset >= scrollBar.top && offset < scrollBar.top + scrollBar.height;
|
|
174
160
|
const marker = isThumb ? " " : "│";
|
|
175
|
-
appendFrameOutput(
|
|
161
|
+
appendFrameOutput(row, `\x1b[${row};${columns}H${colorize(marker, {
|
|
176
162
|
foreground: this.deps.theme.colors.inputBorder,
|
|
177
163
|
...(isThumb ? { background: this.deps.theme.colors.inputBorder } : {}),
|
|
178
164
|
})}`);
|
|
@@ -182,7 +168,7 @@ export class AppRenderController {
|
|
|
182
168
|
const separatorText = inputFrameLine(columns, "bottom");
|
|
183
169
|
const row = toScreenRow(inputBottomSeparatorRow);
|
|
184
170
|
this.deps.mouseController.renderedRowTexts.set(row, separatorText);
|
|
185
|
-
appendFrameOutput(
|
|
171
|
+
appendFrameOutput(row, this.renderFrameRow(row, this.deps.screenStyler.styleLine(row, separatorText, columns, {
|
|
186
172
|
foreground: this.deps.theme.colors.tabBorder,
|
|
187
173
|
})));
|
|
188
174
|
}
|
|
@@ -198,16 +184,16 @@ export class AppRenderController {
|
|
|
198
184
|
this.deps.mouseController.renderedImageTargets.set(row, rendered.line.imageTargets);
|
|
199
185
|
this.deps.mouseController.renderedRowTexts.set(row, rendered.text);
|
|
200
186
|
setRenderedBackground(row, rendered.line?.backgroundOverride);
|
|
201
|
-
appendFrameOutput(
|
|
187
|
+
appendFrameOutput(row, this.renderFrameRow(row, rendered.output(row)));
|
|
202
188
|
}
|
|
203
189
|
const statusLayout = this.deps.statusLineRenderer.layout(columns);
|
|
204
190
|
this.updateStatusMouseState(statusLayout, statusRow);
|
|
205
|
-
appendFrameOutput(
|
|
191
|
+
appendFrameOutput(statusRow, this.renderFrameRow(statusRow, this.deps.statusLineRenderer.render(statusRow, statusLayout, columns)));
|
|
206
192
|
const voiceProgressOverlay = this.renderVoiceProgressOverlay(this.deps.voiceProgressOverlayText(), columns, statusRow);
|
|
207
193
|
if (voiceProgressOverlay) {
|
|
208
194
|
this.deps.mouseController.renderedRowTexts.set(voiceProgressOverlay.row, voiceProgressOverlay.text);
|
|
209
195
|
setRenderedBackground(voiceProgressOverlay.row, this.deps.theme.colors.info);
|
|
210
|
-
appendFrameOutput(
|
|
196
|
+
appendFrameOutput(voiceProgressOverlay.row, this.renderFrameRow(voiceProgressOverlay.row, voiceProgressOverlay.output));
|
|
211
197
|
}
|
|
212
198
|
if (defaultOverlayLines.length > 0 && popupMenuPlacement === "default") {
|
|
213
199
|
const overlayStartRow = Math.max(1, inputSeparatorRow - defaultOverlayLines.length);
|
|
@@ -219,7 +205,7 @@ export class AppRenderController {
|
|
|
219
205
|
this.deps.mouseController.renderedTargets.set(row, line?.target ?? fallbackTarget);
|
|
220
206
|
this.deps.mouseController.renderedRowTexts.set(row, this.deps.popupMenus.overlayPlainText(line ?? { text: "" }, columns));
|
|
221
207
|
setRenderedBackground(row, line?.backgroundOverride);
|
|
222
|
-
appendFrameOutput(
|
|
208
|
+
appendFrameOutput(row, this.renderFrameRow(row, this.deps.popupMenus.styleOverlayLine(row, line ?? { text: "" }, columns)));
|
|
223
209
|
}
|
|
224
210
|
}
|
|
225
211
|
if (underTabsOverlayLines.length > 0 && popupMenuPlacement === "under-tabs") {
|
|
@@ -231,7 +217,7 @@ export class AppRenderController {
|
|
|
231
217
|
this.deps.mouseController.renderedTargets.set(row, line?.target ?? fallbackTarget);
|
|
232
218
|
this.deps.mouseController.renderedRowTexts.set(row, this.deps.popupMenus.overlayPlainText(line ?? { text: "" }, columns));
|
|
233
219
|
setRenderedBackground(row, line?.backgroundOverride);
|
|
234
|
-
appendFrameOutput(
|
|
220
|
+
appendFrameOutput(row, this.renderFrameRow(row, this.deps.popupMenus.styleOverlayLine(row, line ?? { text: "" }, columns)));
|
|
235
221
|
}
|
|
236
222
|
}
|
|
237
223
|
for (const toastOverlay of renderToastOverlays(visibleToastStates(this.deps.toastController), columns, Math.max(0, statusRow - topReservedRows - 1), this.deps.theme)) {
|
|
@@ -240,7 +226,7 @@ export class AppRenderController {
|
|
|
240
226
|
if (toastOverlay.target)
|
|
241
227
|
this.deps.mouseController.renderedTargets.set(row, toastOverlay.target);
|
|
242
228
|
this.deps.mouseController.renderedRowTexts.set(row, overlayText(rowText, toastOverlay.column, toastOverlay.text));
|
|
243
|
-
appendFrameOutput(
|
|
229
|
+
appendFrameOutput(row, `\x1b[${row};${toastOverlay.column}H${toastOverlay.output}`);
|
|
244
230
|
}
|
|
245
231
|
if (topReservedRows === 0) {
|
|
246
232
|
const newTabTarget = tabLayout.targets.find((target) => target.kind === "new-tab");
|
|
@@ -248,7 +234,7 @@ export class AppRenderController {
|
|
|
248
234
|
const plusColumn = newTabTarget.endColumn - stringDisplayWidth(APP_ICONS.plus);
|
|
249
235
|
const rowText = this.deps.mouseController.renderedRowTexts.get(tabRow) ?? "";
|
|
250
236
|
this.deps.mouseController.renderedRowTexts.set(tabRow, overlayText(rowText, plusColumn, APP_ICONS.plus));
|
|
251
|
-
appendFrameOutput(
|
|
237
|
+
appendFrameOutput(tabRow, `\x1b[${tabRow};${plusColumn}H${colorize(APP_ICONS.plus, {
|
|
252
238
|
foreground: this.deps.theme.colors.info,
|
|
253
239
|
bold: true,
|
|
254
240
|
})}`);
|
|
@@ -258,11 +244,8 @@ export class AppRenderController {
|
|
|
258
244
|
const cursor = renderedInput.cursorVisible ? `\x1b[${cursorRow};${renderedInput.cursorColumn}H${SHOW_CURSOR}` : "";
|
|
259
245
|
if (this.deps.mouseController.consumeClickFlashDirty?.())
|
|
260
246
|
this.outputBuffer.reset();
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
conversation: frameLines.conversation.join(""),
|
|
264
|
-
inputStatus: frameLines.inputStatus.join(""),
|
|
265
|
-
});
|
|
247
|
+
const frame = [...frameRows.entries()].map(([row, output]) => ({ row, output }));
|
|
248
|
+
const output = this.outputBuffer.diffFrame(frame);
|
|
266
249
|
process.stdout.write(`${DISABLE_TERMINAL_WRAP}${HIDE_CURSOR}${output}${this.renderClickFlashOverlay(columns, rows)}${cursor}`);
|
|
267
250
|
}
|
|
268
251
|
renderFrameRow(row, output) {
|
|
@@ -71,6 +71,7 @@ export declare class StatusLineRenderer {
|
|
|
71
71
|
private pushVoiceWidgetSegment;
|
|
72
72
|
private pushWorkspaceSegments;
|
|
73
73
|
private pushModelUsageSegments;
|
|
74
|
+
private pushResetDurationSegments;
|
|
74
75
|
private modelUsageHasWarning;
|
|
75
76
|
private modelUsageResetLength;
|
|
76
77
|
private pushSegment;
|
|
@@ -501,8 +501,20 @@ export class StatusLineRenderer {
|
|
|
501
501
|
if (warningStart !== undefined) {
|
|
502
502
|
this.pushSegment(segments, warningStart, APP_ICONS.alert.length, this.host.theme.colors.warning);
|
|
503
503
|
}
|
|
504
|
-
const
|
|
505
|
-
this.
|
|
504
|
+
const localResetStart = resetStart - labelStart;
|
|
505
|
+
const resetLength = this.modelUsageResetLength(modelUsageLabel, localResetStart);
|
|
506
|
+
const resetText = modelUsageLabel.slice(localResetStart, localResetStart + resetLength);
|
|
507
|
+
this.pushResetDurationSegments(segments, resetStart, resetText);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
pushResetDurationSegments(segments, resetStart, resetText) {
|
|
511
|
+
const colors = this.host.theme.colors;
|
|
512
|
+
const muted = colors.muted;
|
|
513
|
+
const bright = colors.statusForeground;
|
|
514
|
+
for (const match of resetText.matchAll(/\d+|\D+/gu)) {
|
|
515
|
+
const text = match[0];
|
|
516
|
+
const localStart = match.index ?? 0;
|
|
517
|
+
this.pushSegment(segments, resetStart + localStart, text.length, /\d/.test(text) ? bright : muted);
|
|
506
518
|
}
|
|
507
519
|
}
|
|
508
520
|
modelUsageHasWarning(modelUsageLabel, localStart) {
|
package/dist/app/runtime.js
CHANGED
|
@@ -226,7 +226,12 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
|
|
|
226
226
|
const effectiveModelRef = resolvePixRuntimeModelRef(options, sessionManager, config);
|
|
227
227
|
const parsedModel = effectiveModelRef ? parseModelRef(effectiveModelRef) : undefined;
|
|
228
228
|
const initialThinkingLevel = resolvePixRuntimeInitialThinkingLevel(options, sessionManager, config);
|
|
229
|
-
|
|
229
|
+
// Only reuse services for the initial session. Session replacements
|
|
230
|
+
// (switchSession/newSession/fork) must get fresh services so extensions
|
|
231
|
+
// are re-loaded with a fresh pi — otherwise handlers capture the old,
|
|
232
|
+
// invalidated pi and throw stale-ctx errors on the next session_start.
|
|
233
|
+
const isInitialSession = !sessionStartEvent || sessionStartEvent.reason === "startup";
|
|
234
|
+
const services = isInitialSession && reusableServices && sameRuntimeServiceTarget(reusableServices, cwd, agentDir)
|
|
230
235
|
? reusableServices
|
|
231
236
|
: await createPixRuntimeServices({
|
|
232
237
|
cwd,
|
|
@@ -266,7 +271,12 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
|
|
|
266
271
|
return {
|
|
267
272
|
...created,
|
|
268
273
|
services,
|
|
269
|
-
diagnostics
|
|
274
|
+
// Snapshot diagnostics per-runtime. When services are reused across
|
|
275
|
+
// tabs (reuseServicesFrom), services.diagnostics is a shared array —
|
|
276
|
+
// giving each runtime its own copy prevents one tab's creation
|
|
277
|
+
// errors (model-not-found, extension flags, provider registration)
|
|
278
|
+
// from leaking into another tab's diagnostics surface.
|
|
279
|
+
diagnostics: [...(services.diagnostics ?? [])],
|
|
270
280
|
};
|
|
271
281
|
};
|
|
272
282
|
return await createAgentSessionRuntime(createRuntime, {
|
|
@@ -992,6 +992,8 @@ function selectedConversationLineText(rendered, text, startColumn, endColumn) {
|
|
|
992
992
|
const selectsWholeLine = startColumn <= 1 && endColumn >= text.length + 1;
|
|
993
993
|
if (selectsWholeLine && rendered?.copyText !== undefined)
|
|
994
994
|
return rendered.copyText;
|
|
995
|
+
if (rendered?.copyText !== undefined)
|
|
996
|
+
return sliceByDisplayColumns(rendered.copyText, startColumn, endColumn).trimEnd();
|
|
995
997
|
return sliceByDisplayColumns(text, startColumn, endColumn).trimEnd();
|
|
996
998
|
}
|
|
997
999
|
function orderedConversationSelection(anchor, current) {
|
|
@@ -41,6 +41,13 @@ export type AppSessionEventControllerHost = {
|
|
|
41
41
|
setSessionActivity(activity: SessionActivity): void;
|
|
42
42
|
updateQueuedMessageStatus(): void;
|
|
43
43
|
flushAutoUserMessages(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Emit a signal onto the active runtime's extension event bus. The SDK
|
|
46
|
+
* strips retry/streaming state from the events it forwards to extensions,
|
|
47
|
+
* so this is how the renderer informs extensions (terminal-bell, todo)
|
|
48
|
+
* about lifecycle state such as an auto-retry being in progress.
|
|
49
|
+
*/
|
|
50
|
+
emitExtensionEvent(channel: string, data: unknown): void;
|
|
44
51
|
prepareWorkspaceMutation(toolName: string, args: unknown): WorkspaceMutationPreparation | undefined;
|
|
45
52
|
recordWorkspaceMutationForUserEntry(entryId: string, mutation: WorkspaceMutation): void;
|
|
46
53
|
workspaceMutationFromToolExecution(input: {
|
|
@@ -4,8 +4,12 @@ import { customMessageEntry, loadSessionHistoryEntries, loadSessionHistoryEntrie
|
|
|
4
4
|
import { sessionHistoryDisplayMessages, sessionHistoryDisplayMessagesFromEntries, sessionHistoryFullBranchEntries } from "./pix-system-message.js";
|
|
5
5
|
import { THINKING_TOOL_NAME } from "../constants.js";
|
|
6
6
|
import { isRecord } from "../guards.js";
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Event bus channel the renderer emits to inform extensions that the session is
|
|
9
|
+
* in an auto-retry cycle. Payload: `{ active: boolean }`. Extensions cannot see
|
|
10
|
+
* retry state via the SDK's extension events, so the renderer relays it here.
|
|
11
|
+
*/
|
|
12
|
+
const RETRY_ACTIVE_EVENT = "pix:retry-active";
|
|
9
13
|
const MAX_HISTORY_WINDOW_ENTRIES = 360;
|
|
10
14
|
const HISTORY_WINDOW_TARGET_ENTRIES = 300;
|
|
11
15
|
const HISTORY_WINDOW_SHIFT_ENTRIES = 50;
|
|
@@ -265,11 +269,13 @@ export class AppSessionEventController {
|
|
|
265
269
|
case "auto_retry_start":
|
|
266
270
|
this.host.setSessionActivity("running");
|
|
267
271
|
this.host.setStatus(`retry ${event.attempt}/${event.maxAttempts}`);
|
|
272
|
+
this.host.emitExtensionEvent(RETRY_ACTIVE_EVENT, { active: true });
|
|
268
273
|
break;
|
|
269
274
|
case "auto_retry_end":
|
|
270
275
|
this.host.setSessionActivity(this.host.runtime()?.session.isStreaming ? "running" : "idle");
|
|
271
276
|
this.host.restoreSessionStatus();
|
|
272
277
|
this.host.flushAutoUserMessages();
|
|
278
|
+
this.host.emitExtensionEvent(RETRY_ACTIVE_EVENT, { active: false });
|
|
273
279
|
this.host.showToast(event.success ? "Retry succeeded" : `Retry failed: ${event.finalError}`, event.success ? "success" : "error");
|
|
274
280
|
break;
|
|
275
281
|
default:
|
|
@@ -1021,23 +1027,14 @@ function shouldDropAssistantStreamLine(line, hasVisibleText) {
|
|
|
1021
1027
|
function shouldHoldAssistantStreamTail(text, hasVisibleText) {
|
|
1022
1028
|
if (text.trim().length === 0)
|
|
1023
1029
|
return !hasVisibleText;
|
|
1024
|
-
return
|
|
1030
|
+
return false;
|
|
1025
1031
|
}
|
|
1026
1032
|
function shouldHoldAssistantStreamWhitespaceTail(text, hasVisibleText) {
|
|
1027
1033
|
return hasVisibleText && text.trim().length === 0;
|
|
1028
1034
|
}
|
|
1029
1035
|
function isHiddenMarkdownMetadataLine(line) {
|
|
1030
|
-
return isMarkdownReferenceDefinition(line)
|
|
1036
|
+
return isMarkdownReferenceDefinition(line);
|
|
1031
1037
|
}
|
|
1032
1038
|
function isMarkdownReferenceDefinition(line) {
|
|
1033
1039
|
return /^ {0,3}\[[^\]\n]+\]:[ \t]*\S.*$/u.test(line);
|
|
1034
1040
|
}
|
|
1035
|
-
function isPotentialDcpMetadataLine(line) {
|
|
1036
|
-
const content = line.replace(/^ {0,3}/u, "");
|
|
1037
|
-
if (content.length === 0)
|
|
1038
|
-
return false;
|
|
1039
|
-
return isPotentialDcpReference(content, DCP_MESSAGE_REFERENCE_PREFIX) || isPotentialDcpReference(content, DCP_BLOCK_REFERENCE_PREFIX);
|
|
1040
|
-
}
|
|
1041
|
-
function isPotentialDcpReference(content, markerPrefix) {
|
|
1042
|
-
return markerPrefix.startsWith(content) || (content.startsWith(markerPrefix) && /^\d*\)?$/u.test(content.slice(markerPrefix.length)));
|
|
1043
|
-
}
|
|
@@ -61,6 +61,7 @@ export declare class AppSessionLifecycleController {
|
|
|
61
61
|
constructor(host: AppSessionLifecycleHost);
|
|
62
62
|
start(): Promise<void>;
|
|
63
63
|
bindCurrentSession(options?: BindCurrentSessionOptions): Promise<void>;
|
|
64
|
+
awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime | undefined): Promise<void>;
|
|
64
65
|
unsubscribeSession(): void;
|
|
65
66
|
afterSessionReplacement(message?: string): void;
|
|
66
67
|
private loadReplacementHistory;
|
|
@@ -85,6 +85,13 @@ export class AppSessionLifecycleController {
|
|
|
85
85
|
}
|
|
86
86
|
await bindPromise;
|
|
87
87
|
}
|
|
88
|
+
async awaitCurrentSessionExtensions(runtime = this.host.runtime()) {
|
|
89
|
+
if (!runtime)
|
|
90
|
+
return;
|
|
91
|
+
if (this.extensionBindRuntime !== runtime || this.extensionBindSession !== runtime.session)
|
|
92
|
+
return;
|
|
93
|
+
await this.extensionBindPromise;
|
|
94
|
+
}
|
|
88
95
|
unsubscribeSession() {
|
|
89
96
|
this.unsubscribe?.();
|
|
90
97
|
}
|
|
@@ -18,6 +18,7 @@ export type AppTabsControllerHost = {
|
|
|
18
18
|
runtime(): AgentSessionRuntime | undefined;
|
|
19
19
|
createRuntimeForNewSession(): Promise<AgentSessionRuntime>;
|
|
20
20
|
createRuntimeForSession(sessionPath: string): Promise<AgentSessionRuntime>;
|
|
21
|
+
awaitCurrentSessionExtensions?(runtime?: AgentSessionRuntime): Promise<void>;
|
|
21
22
|
activateRuntime(runtime: AgentSessionRuntime, options?: BindCurrentSessionOptions): Promise<void>;
|
|
22
23
|
disposeRuntime(runtime: AgentSessionRuntime): Promise<void>;
|
|
23
24
|
isRunning(): boolean;
|
|
@@ -730,6 +730,7 @@ export class AppTabsController {
|
|
|
730
730
|
this.activeTabId = tab.id;
|
|
731
731
|
this.host.setStatus("starting new session");
|
|
732
732
|
this.host.render();
|
|
733
|
+
await this.host.awaitCurrentSessionExtensions?.(runtime);
|
|
733
734
|
const result = await runtime.newSession();
|
|
734
735
|
if (result.cancelled) {
|
|
735
736
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "New session cancelled." });
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
export declare const DISABLE_TERMINAL_OUTPUT_BUFFER_ENV = "PIX_DISABLE_TERMINAL_OUTPUT_BUFFER";
|
|
2
2
|
export declare const TERMINAL_OUTPUT_BUFFER_ENV = "PIX_TERMINAL_OUTPUT_BUFFER";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
export type TerminalOutputFrameRow = {
|
|
4
|
+
row: number;
|
|
5
|
+
output: string;
|
|
6
|
+
};
|
|
7
|
+
export type TerminalOutputFrame = readonly TerminalOutputFrameRow[];
|
|
8
|
+
export type TerminalOutputRegion = "statusLine";
|
|
7
9
|
export type TerminalOutputBufferOptions = {
|
|
8
10
|
enabled?: boolean;
|
|
9
11
|
env?: Record<string, string | undefined>;
|
|
10
12
|
};
|
|
11
13
|
export declare class TerminalOutputBuffer {
|
|
12
14
|
private readonly enabled;
|
|
13
|
-
private readonly
|
|
15
|
+
private readonly previousByRow;
|
|
16
|
+
private previousStatusLine;
|
|
14
17
|
constructor(options?: TerminalOutputBufferOptions);
|
|
15
18
|
diffFrame(frame: TerminalOutputFrame): string;
|
|
16
19
|
diff(region: TerminalOutputRegion, output: string): string;
|
|
17
20
|
reset(): void;
|
|
18
21
|
}
|
|
19
22
|
export declare function terminalOutputBufferDisabled(env?: Record<string, string | undefined>): boolean;
|
|
20
|
-
export {};
|
|
@@ -1,41 +1,49 @@
|
|
|
1
1
|
export const DISABLE_TERMINAL_OUTPUT_BUFFER_ENV = "PIX_DISABLE_TERMINAL_OUTPUT_BUFFER";
|
|
2
2
|
export const TERMINAL_OUTPUT_BUFFER_ENV = "PIX_TERMINAL_OUTPUT_BUFFER";
|
|
3
|
-
const
|
|
3
|
+
const ANSI_RESET = "\x1b[0m";
|
|
4
|
+
const CLEAR_LINE_PREFIX = (row) => `\x1b[${row};1H${ANSI_RESET}\x1b[2K`;
|
|
4
5
|
export class TerminalOutputBuffer {
|
|
5
6
|
enabled;
|
|
6
|
-
|
|
7
|
+
previousByRow = new Map();
|
|
8
|
+
previousStatusLine;
|
|
7
9
|
constructor(options = {}) {
|
|
8
10
|
this.enabled = options.enabled ?? !terminalOutputBufferDisabled(options.env ?? process.env);
|
|
9
11
|
}
|
|
10
12
|
diffFrame(frame) {
|
|
11
|
-
const outputByRegion = {
|
|
12
|
-
tabs: frame.tabs ?? "",
|
|
13
|
-
conversation: frame.conversation ?? "",
|
|
14
|
-
inputStatus: frame.inputStatus ?? "",
|
|
15
|
-
};
|
|
16
13
|
if (!this.enabled)
|
|
17
|
-
return
|
|
14
|
+
return frame.map((entry) => entry.output).join("");
|
|
18
15
|
const chunks = [];
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
const seenRows = new Set();
|
|
17
|
+
for (const { row, output } of frame) {
|
|
18
|
+
seenRows.add(row);
|
|
19
|
+
if (this.previousByRow.get(row) === output)
|
|
22
20
|
continue;
|
|
23
|
-
this.
|
|
21
|
+
this.previousByRow.set(row, output);
|
|
24
22
|
if (output.length > 0)
|
|
25
23
|
chunks.push(output);
|
|
26
24
|
}
|
|
25
|
+
for (const row of this.previousByRow.keys()) {
|
|
26
|
+
if (seenRows.has(row))
|
|
27
|
+
continue;
|
|
28
|
+
this.previousByRow.delete(row);
|
|
29
|
+
chunks.push(CLEAR_LINE_PREFIX(row));
|
|
30
|
+
}
|
|
27
31
|
return chunks.join("");
|
|
28
32
|
}
|
|
29
33
|
diff(region, output) {
|
|
30
34
|
if (!this.enabled)
|
|
31
35
|
return output;
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
if (region === "statusLine") {
|
|
37
|
+
if (this.previousStatusLine === output)
|
|
38
|
+
return "";
|
|
39
|
+
this.previousStatusLine = output;
|
|
40
|
+
return output;
|
|
41
|
+
}
|
|
35
42
|
return output;
|
|
36
43
|
}
|
|
37
44
|
reset() {
|
|
38
|
-
this.
|
|
45
|
+
this.previousByRow.clear();
|
|
46
|
+
this.previousStatusLine = undefined;
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
export function terminalOutputBufferDisabled(env = process.env) {
|
|
@@ -4,6 +4,7 @@ import { type WorkspaceMutation, type WorkspaceMutationFromToolInput, type Works
|
|
|
4
4
|
export type AppWorkspaceActionsControllerHost = {
|
|
5
5
|
readonly entries: Entry[];
|
|
6
6
|
runtime(): AgentSessionRuntime | undefined;
|
|
7
|
+
awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
|
|
7
8
|
findUserEntry(entryId: string): Extract<Entry, {
|
|
8
9
|
kind: "user";
|
|
9
10
|
}> | undefined;
|