gsd-pi 2.70.1-dev.bef631a → 2.70.1-dev.ec24142
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/resources/extensions/claude-code-cli/stream-adapter.js +127 -30
- package/dist/resources/extensions/get-secrets-from-user.js +17 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/env-writer.d.ts +39 -0
- package/packages/mcp-server/dist/env-writer.d.ts.map +1 -0
- package/packages/mcp-server/dist/env-writer.js +158 -0
- package/packages/mcp-server/dist/env-writer.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts +11 -2
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +102 -2
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/src/env-writer.test.ts +280 -0
- package/packages/mcp-server/src/env-writer.ts +183 -0
- package/packages/mcp-server/src/secure-env-collect.test.ts +265 -0
- package/packages/mcp-server/src/server.ts +137 -3
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +187 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +78 -21
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +220 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +102 -27
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +1 -1
- package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +1 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js +9 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
- package/packages/pi-tui/dist/components/input.d.ts +2 -0
- package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/input.js +7 -4
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/src/components/__tests__/input.test.ts +11 -0
- package/packages/pi-tui/src/components/input.ts +7 -4
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +164 -31
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +112 -0
- package/src/resources/extensions/get-secrets-from-user.ts +24 -1
- package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +45 -0
- /package/dist/web/standalone/.next/static/{UlX0WGGZ8aBPN0uSZ5Ki4 → 20e8bFnNjxQJflHNodEve}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{UlX0WGGZ8aBPN0uSZ5Ki4 → 20e8bFnNjxQJflHNodEve}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { handleAgentEvent } from "../modes/interactive/controllers/chat-controller.js";
|
|
5
|
+
|
|
6
|
+
function makeUsage() {
|
|
7
|
+
return {
|
|
8
|
+
input: 0,
|
|
9
|
+
output: 0,
|
|
10
|
+
cacheRead: 0,
|
|
11
|
+
cacheWrite: 0,
|
|
12
|
+
totalTokens: 0,
|
|
13
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makeAssistant(content: any[]) {
|
|
18
|
+
return {
|
|
19
|
+
role: "assistant",
|
|
20
|
+
content,
|
|
21
|
+
api: "anthropic-messages",
|
|
22
|
+
provider: "claude-code",
|
|
23
|
+
model: "claude-sonnet-4",
|
|
24
|
+
usage: makeUsage(),
|
|
25
|
+
stopReason: "stop",
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createHost() {
|
|
31
|
+
const chatContainer = {
|
|
32
|
+
children: [] as any[],
|
|
33
|
+
addChild(component: any) {
|
|
34
|
+
this.children.push(component);
|
|
35
|
+
},
|
|
36
|
+
removeChild(component: any) {
|
|
37
|
+
const idx = this.children.indexOf(component);
|
|
38
|
+
if (idx !== -1) this.children.splice(idx, 1);
|
|
39
|
+
},
|
|
40
|
+
clear() {
|
|
41
|
+
this.children = [];
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const host: any = {
|
|
46
|
+
isInitialized: true,
|
|
47
|
+
init: async () => {},
|
|
48
|
+
defaultEditor: { onEscape: undefined },
|
|
49
|
+
editor: {},
|
|
50
|
+
session: { retryAttempt: 0, abortCompaction: () => {}, abortRetry: () => {} },
|
|
51
|
+
ui: { requestRender: () => {} },
|
|
52
|
+
footer: { invalidate: () => {} },
|
|
53
|
+
keybindings: {},
|
|
54
|
+
statusContainer: { clear: () => {}, addChild: () => {} },
|
|
55
|
+
chatContainer,
|
|
56
|
+
settingsManager: { getTimestampFormat: () => "date-time-iso", getShowImages: () => false },
|
|
57
|
+
pendingTools: new Map(),
|
|
58
|
+
toolOutputExpanded: false,
|
|
59
|
+
hideThinkingBlock: false,
|
|
60
|
+
isBashMode: false,
|
|
61
|
+
defaultWorkingMessage: "Working...",
|
|
62
|
+
compactionQueuedMessages: [],
|
|
63
|
+
editorContainer: {},
|
|
64
|
+
pendingMessagesContainer: { clear: () => {} },
|
|
65
|
+
addMessageToChat: () => {},
|
|
66
|
+
getMarkdownThemeWithSettings: () => ({}),
|
|
67
|
+
formatWebSearchResult: () => "",
|
|
68
|
+
getRegisteredToolDefinition: () => undefined,
|
|
69
|
+
checkShutdownRequested: async () => {},
|
|
70
|
+
rebuildChatFromMessages: () => {},
|
|
71
|
+
flushCompactionQueue: async () => {},
|
|
72
|
+
showStatus: () => {},
|
|
73
|
+
showError: () => {},
|
|
74
|
+
updatePendingMessagesDisplay: () => {},
|
|
75
|
+
updateTerminalTitle: () => {},
|
|
76
|
+
updateEditorBorderColor: () => {},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return host;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
test("chat-controller keeps tool output ahead of delayed assistant text for external tool streams", async () => {
|
|
83
|
+
// ToolExecutionComponent uses the global theme singleton.
|
|
84
|
+
// Install a minimal no-op theme implementation for this unit test.
|
|
85
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
86
|
+
fg: (_key: string, text: string) => text,
|
|
87
|
+
bg: (_key: string, text: string) => text,
|
|
88
|
+
bold: (text: string) => text,
|
|
89
|
+
italic: (text: string) => text,
|
|
90
|
+
truncate: (text: string) => text,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const host = createHost();
|
|
94
|
+
const toolId = "mcp-tool-1";
|
|
95
|
+
const toolCall = {
|
|
96
|
+
type: "toolCall",
|
|
97
|
+
id: toolId,
|
|
98
|
+
name: "exec_command",
|
|
99
|
+
arguments: { cmd: "echo hi" },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
103
|
+
|
|
104
|
+
assert.equal(host.streamingComponent, undefined, "assistant component should be deferred at message_start");
|
|
105
|
+
assert.equal(host.chatContainer.children.length, 0, "nothing should render before content arrives");
|
|
106
|
+
|
|
107
|
+
await handleAgentEvent(
|
|
108
|
+
host,
|
|
109
|
+
{
|
|
110
|
+
type: "message_update",
|
|
111
|
+
message: makeAssistant([toolCall]),
|
|
112
|
+
assistantMessageEvent: {
|
|
113
|
+
type: "toolcall_end",
|
|
114
|
+
contentIndex: 0,
|
|
115
|
+
toolCall: {
|
|
116
|
+
...toolCall,
|
|
117
|
+
externalResult: {
|
|
118
|
+
content: [{ type: "text", text: "tool output" }],
|
|
119
|
+
details: {},
|
|
120
|
+
isError: false,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
partial: makeAssistant([toolCall]),
|
|
124
|
+
},
|
|
125
|
+
} as any,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
assert.equal(host.streamingComponent, undefined, "assistant text container should remain deferred for tool-only updates");
|
|
129
|
+
assert.equal(host.chatContainer.children.length, 1, "tool execution block should render immediately");
|
|
130
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
131
|
+
|
|
132
|
+
// Re-assert required host method before the text-bearing update path.
|
|
133
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
134
|
+
|
|
135
|
+
await handleAgentEvent(
|
|
136
|
+
host,
|
|
137
|
+
{
|
|
138
|
+
type: "message_update",
|
|
139
|
+
message: makeAssistant([toolCall, { type: "text", text: "done" }]),
|
|
140
|
+
assistantMessageEvent: {
|
|
141
|
+
type: "text_delta",
|
|
142
|
+
contentIndex: 1,
|
|
143
|
+
delta: "done",
|
|
144
|
+
partial: makeAssistant([toolCall, { type: "text", text: "done" }]),
|
|
145
|
+
},
|
|
146
|
+
} as any,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
assert.equal(host.chatContainer.children.length, 2, "assistant content should render after existing tool output");
|
|
150
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
151
|
+
assert.equal(host.chatContainer.children[1]?.constructor?.name, "AssistantMessageComponent");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("chat-controller keeps serverToolUse output ahead of assistant text when external results arrive", async () => {
|
|
155
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
156
|
+
fg: (_key: string, text: string) => text,
|
|
157
|
+
bg: (_key: string, text: string) => text,
|
|
158
|
+
bold: (text: string) => text,
|
|
159
|
+
italic: (text: string) => text,
|
|
160
|
+
truncate: (text: string) => text,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const host = createHost();
|
|
164
|
+
const toolId = "mcp-secure-1";
|
|
165
|
+
const serverToolUse = {
|
|
166
|
+
type: "serverToolUse",
|
|
167
|
+
id: toolId,
|
|
168
|
+
name: "mcp__gsd-workflow__secure_env_collect",
|
|
169
|
+
input: { projectDir: "/tmp/project", keys: [{ key: "SECURE_PASSWORD" }], destination: "dotenv" },
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
173
|
+
|
|
174
|
+
await handleAgentEvent(
|
|
175
|
+
host,
|
|
176
|
+
{
|
|
177
|
+
type: "message_update",
|
|
178
|
+
message: makeAssistant([serverToolUse]),
|
|
179
|
+
assistantMessageEvent: {
|
|
180
|
+
type: "server_tool_use",
|
|
181
|
+
contentIndex: 0,
|
|
182
|
+
partial: makeAssistant([serverToolUse]),
|
|
183
|
+
},
|
|
184
|
+
} as any,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
assert.equal(host.streamingComponent, undefined, "assistant content should stay deferred while only tool content streams");
|
|
188
|
+
assert.equal(host.chatContainer.children.length, 1, "server tool block should render immediately");
|
|
189
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
190
|
+
|
|
191
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
192
|
+
const resultMessage = makeAssistant([
|
|
193
|
+
{
|
|
194
|
+
...serverToolUse,
|
|
195
|
+
externalResult: {
|
|
196
|
+
content: [{ type: "text", text: "secure_env_collect was cancelled by user." }],
|
|
197
|
+
details: {},
|
|
198
|
+
isError: true,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{ type: "text", text: "The secure password collection was cancelled." },
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
await handleAgentEvent(
|
|
205
|
+
host,
|
|
206
|
+
{
|
|
207
|
+
type: "message_update",
|
|
208
|
+
message: resultMessage,
|
|
209
|
+
assistantMessageEvent: {
|
|
210
|
+
type: "server_tool_use",
|
|
211
|
+
contentIndex: 0,
|
|
212
|
+
partial: resultMessage,
|
|
213
|
+
},
|
|
214
|
+
} as any,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
assert.equal(host.chatContainer.children.length, 2, "assistant text should render after existing server tool output");
|
|
218
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
219
|
+
assert.equal(host.chatContainer.children[1]?.constructor?.name, "AssistantMessageComponent");
|
|
220
|
+
});
|
|
@@ -88,6 +88,8 @@ export interface ExtensionUIDialogOptions {
|
|
|
88
88
|
timeout?: number;
|
|
89
89
|
/** When true, the user can select multiple options. The return type becomes `string[]`. */
|
|
90
90
|
allowMultiple?: boolean;
|
|
91
|
+
/** When true, text input dialogs should hide typed characters if supported by the client surface. */
|
|
92
|
+
secure?: boolean;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
/** Placement for extension widgets. */
|
|
@@ -11,6 +11,7 @@ import { keyHint } from "./keybinding-hints.js";
|
|
|
11
11
|
export interface ExtensionInputOptions {
|
|
12
12
|
tui?: TUI;
|
|
13
13
|
timeout?: number;
|
|
14
|
+
secure?: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export class ExtensionInputComponent extends Container implements Focusable {
|
|
@@ -61,6 +62,7 @@ export class ExtensionInputComponent extends Container implements Focusable {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
this.input = new Input();
|
|
65
|
+
this.input.secure = opts?.secure === true;
|
|
64
66
|
if (placeholder) {
|
|
65
67
|
this.input.placeholder = placeholder;
|
|
66
68
|
}
|
|
@@ -9,6 +9,18 @@ import { appKey } from "../components/keybinding-hints.js";
|
|
|
9
9
|
// Tracks the last processed content index to avoid re-scanning all blocks on every message_update
|
|
10
10
|
let lastProcessedContentIndex = 0;
|
|
11
11
|
|
|
12
|
+
function hasVisibleAssistantContent(message: { content: Array<any> }): boolean {
|
|
13
|
+
return message.content.some(
|
|
14
|
+
(c) =>
|
|
15
|
+
(c.type === "text" && typeof c.text === "string" && c.text.trim().length > 0)
|
|
16
|
+
|| (c.type === "thinking" && typeof c.thinking === "string" && c.thinking.trim().length > 0),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hasAssistantToolBlocks(message: { content: Array<any> }): boolean {
|
|
21
|
+
return message.content.some((c) => c.type === "toolCall" || c.type === "serverToolUse");
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
13
25
|
init: () => Promise<void>;
|
|
14
26
|
getMarkdownThemeWithSettings: () => any;
|
|
@@ -104,45 +116,54 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
104
116
|
host.updatePendingMessagesDisplay();
|
|
105
117
|
host.ui.requestRender();
|
|
106
118
|
} else if (event.message.role === "assistant") {
|
|
107
|
-
host.streamingComponent = new AssistantMessageComponent(
|
|
108
|
-
undefined,
|
|
109
|
-
host.hideThinkingBlock,
|
|
110
|
-
host.getMarkdownThemeWithSettings(),
|
|
111
|
-
host.settingsManager.getTimestampFormat(),
|
|
112
|
-
);
|
|
113
119
|
host.streamingMessage = event.message;
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
// External-tool providers can stream multiple assistant turns through
|
|
121
|
+
// one response. Delay component creation until visible assistant text
|
|
122
|
+
// arrives so tool outputs keep chronological ordering.
|
|
116
123
|
host.ui.requestRender();
|
|
117
124
|
}
|
|
118
125
|
break;
|
|
119
126
|
|
|
120
127
|
case "message_update":
|
|
121
|
-
if (
|
|
128
|
+
if (event.message.role === "assistant") {
|
|
122
129
|
host.streamingMessage = event.message;
|
|
123
|
-
host.streamingComponent.updateContent(host.streamingMessage);
|
|
124
|
-
|
|
125
|
-
// When the stream adapter signals a completed tool call with an
|
|
126
|
-
// external result (from Claude Code SDK), update the pending
|
|
127
|
-
// ToolExecutionComponent immediately so output is visible in
|
|
128
|
-
// real-time instead of waiting for the session to end.
|
|
129
130
|
const innerEvent = event.assistantMessageEvent;
|
|
131
|
+
|
|
132
|
+
let externalToolResult:
|
|
133
|
+
| { toolCallId: string; content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; details: Record<string, unknown>; isError: boolean }
|
|
134
|
+
| undefined;
|
|
130
135
|
if (innerEvent.type === "toolcall_end" && innerEvent.toolCall) {
|
|
131
136
|
const tc = innerEvent.toolCall as any;
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
const ext = tc.externalResult;
|
|
138
|
+
if (ext) {
|
|
139
|
+
externalToolResult = {
|
|
140
|
+
toolCallId: tc.id,
|
|
141
|
+
content: ext.content ?? [{ type: "text", text: "" }],
|
|
142
|
+
details: ext.details ?? {},
|
|
143
|
+
isError: ext.isError ?? false,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
} else if (innerEvent.type === "server_tool_use") {
|
|
147
|
+
const idx = typeof innerEvent.contentIndex === "number" ? innerEvent.contentIndex : -1;
|
|
148
|
+
const block = idx >= 0 ? (host.streamingMessage.content[idx] as any) : undefined;
|
|
149
|
+
const ext = block?.externalResult;
|
|
150
|
+
if (block?.id && ext) {
|
|
151
|
+
externalToolResult = {
|
|
152
|
+
toolCallId: block.id,
|
|
153
|
+
content: ext.content ?? [{ type: "text", text: "" }],
|
|
154
|
+
details: ext.details ?? {},
|
|
155
|
+
isError: ext.isError ?? false,
|
|
156
|
+
};
|
|
142
157
|
}
|
|
143
158
|
}
|
|
144
159
|
|
|
145
160
|
const contentBlocks = host.streamingMessage.content;
|
|
161
|
+
// Some adapters reuse a single assistant lifecycle while internally
|
|
162
|
+
// spanning multiple provider turns. When a new turn starts, content
|
|
163
|
+
// length can shrink back to 0/1; reset scan index to avoid skipping.
|
|
164
|
+
if (lastProcessedContentIndex >= contentBlocks.length) {
|
|
165
|
+
lastProcessedContentIndex = 0;
|
|
166
|
+
}
|
|
146
167
|
for (let i = lastProcessedContentIndex; i < contentBlocks.length; i++) {
|
|
147
168
|
const content = contentBlocks[i];
|
|
148
169
|
if (content.type === "toolCall") {
|
|
@@ -192,6 +213,42 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
192
213
|
}
|
|
193
214
|
}
|
|
194
215
|
}
|
|
216
|
+
|
|
217
|
+
// When the stream adapter signals a completed tool call with an
|
|
218
|
+
// external result (from Claude Code SDK), update the pending
|
|
219
|
+
// ToolExecutionComponent immediately so output is visible in
|
|
220
|
+
// real-time instead of waiting for the session to end.
|
|
221
|
+
if (externalToolResult) {
|
|
222
|
+
const component = host.pendingTools.get(externalToolResult.toolCallId);
|
|
223
|
+
if (component) {
|
|
224
|
+
component.updateResult({
|
|
225
|
+
content: externalToolResult.content,
|
|
226
|
+
details: externalToolResult.details,
|
|
227
|
+
isError: externalToolResult.isError,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Render assistant text/thinking after tool components so mixed
|
|
233
|
+
// streams keep chronological ordering in the chat container.
|
|
234
|
+
const hasToolBlocks = hasAssistantToolBlocks(host.streamingMessage);
|
|
235
|
+
if (!host.streamingComponent && hasVisibleAssistantContent(host.streamingMessage)) {
|
|
236
|
+
host.streamingComponent = new AssistantMessageComponent(
|
|
237
|
+
undefined,
|
|
238
|
+
host.hideThinkingBlock,
|
|
239
|
+
host.getMarkdownThemeWithSettings(),
|
|
240
|
+
host.settingsManager.getTimestampFormat(),
|
|
241
|
+
);
|
|
242
|
+
host.chatContainer.addChild(host.streamingComponent);
|
|
243
|
+
}
|
|
244
|
+
if (host.streamingComponent) {
|
|
245
|
+
if (hasToolBlocks) {
|
|
246
|
+
host.chatContainer.removeChild(host.streamingComponent);
|
|
247
|
+
host.chatContainer.addChild(host.streamingComponent);
|
|
248
|
+
}
|
|
249
|
+
host.streamingComponent.updateContent(host.streamingMessage);
|
|
250
|
+
}
|
|
251
|
+
|
|
195
252
|
// Update index: fully processed blocks won't need re-scanning.
|
|
196
253
|
// Keep the last block's index (it may still be accumulating data),
|
|
197
254
|
// so we re-check it next time but skip all earlier ones.
|
|
@@ -204,7 +261,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
204
261
|
|
|
205
262
|
case "message_end":
|
|
206
263
|
if (event.message.role === "user") break;
|
|
207
|
-
if (
|
|
264
|
+
if (event.message.role === "assistant") {
|
|
208
265
|
host.streamingMessage = event.message;
|
|
209
266
|
let errorMessage: string | undefined;
|
|
210
267
|
if (host.streamingMessage.stopReason === "aborted") {
|
|
@@ -214,7 +271,25 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
214
271
|
: "Operation aborted";
|
|
215
272
|
host.streamingMessage.errorMessage = errorMessage;
|
|
216
273
|
}
|
|
217
|
-
|
|
274
|
+
|
|
275
|
+
const shouldRenderAssistant = hasVisibleAssistantContent(host.streamingMessage)
|
|
276
|
+
|| (
|
|
277
|
+
(host.streamingMessage.stopReason === "aborted" || host.streamingMessage.stopReason === "error")
|
|
278
|
+
&& !hasAssistantToolBlocks(host.streamingMessage)
|
|
279
|
+
);
|
|
280
|
+
if (!host.streamingComponent && shouldRenderAssistant) {
|
|
281
|
+
host.streamingComponent = new AssistantMessageComponent(
|
|
282
|
+
undefined,
|
|
283
|
+
host.hideThinkingBlock,
|
|
284
|
+
host.getMarkdownThemeWithSettings(),
|
|
285
|
+
host.settingsManager.getTimestampFormat(),
|
|
286
|
+
);
|
|
287
|
+
host.chatContainer.addChild(host.streamingComponent);
|
|
288
|
+
}
|
|
289
|
+
if (host.streamingComponent) {
|
|
290
|
+
host.streamingComponent.updateContent(host.streamingMessage);
|
|
291
|
+
}
|
|
292
|
+
|
|
218
293
|
if (host.streamingMessage.stopReason === "aborted" || host.streamingMessage.stopReason === "error") {
|
|
219
294
|
if (!errorMessage) {
|
|
220
295
|
errorMessage = host.streamingMessage.errorMessage || "Error";
|
|
@@ -1631,7 +1631,7 @@ export class InteractiveMode {
|
|
|
1631
1631
|
this.hideExtensionInput();
|
|
1632
1632
|
resolve(undefined);
|
|
1633
1633
|
},
|
|
1634
|
-
{ tui: this.ui, timeout: opts?.timeout },
|
|
1634
|
+
{ tui: this.ui, timeout: opts?.timeout, secure: opts?.secure },
|
|
1635
1635
|
);
|
|
1636
1636
|
|
|
1637
1637
|
this.editorContainer.clear();
|
|
@@ -224,7 +224,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
224
224
|
),
|
|
225
225
|
|
|
226
226
|
input: (title, placeholder, opts) =>
|
|
227
|
-
createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout }, (r) =>
|
|
227
|
+
createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout, secure: opts?.secure }, (r) =>
|
|
228
228
|
"cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined,
|
|
229
229
|
),
|
|
230
230
|
|
|
@@ -25,5 +25,14 @@ describe("Input", () => {
|
|
|
25
25
|
input.focused = false;
|
|
26
26
|
assert.equal(input.focused, false);
|
|
27
27
|
});
|
|
28
|
+
it("secure mode obscures typed characters in render output", () => {
|
|
29
|
+
const input = new Input();
|
|
30
|
+
input.secure = true;
|
|
31
|
+
input.focused = true;
|
|
32
|
+
input.handleInput("secret123");
|
|
33
|
+
const line = input.render(40)[0] ?? "";
|
|
34
|
+
assert.ok(!line.includes("secret123"), "rendered line must not expose raw secret text");
|
|
35
|
+
assert.ok(line.includes("*********"), "rendered line should include masked characters");
|
|
36
|
+
});
|
|
28
37
|
});
|
|
29
38
|
//# sourceMappingURL=input.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.test.js","sourceRoot":"","sources":["../../../src/components/__tests__/input.test.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,4DAA4D;AAE5D,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE;IACtB,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACrD,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QAErB,yDAAyD;QACzD,KAAK,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;QAEtC,2BAA2B;QAC3B,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;QAEtB,mDAAmD;QACnD,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QAErB,iEAAiE;QACjE,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC3B,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAChD,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACnC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QACrB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAClC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;QACtB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["// pi-tui Input component regression tests\n// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>\n\nimport { describe, it } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { Input } from \"../input.js\";\n\ndescribe(\"Input\", () => {\n\tit(\"paste buffer is cleared when focus is lost\", () => {\n\t\tconst input = new Input();\n\t\tinput.focused = true;\n\n\t\t// Simulate starting a paste (bracket paste start marker)\n\t\tinput.handleInput(\"\\x1b[200~partial\");\n\n\t\t// Now lose focus mid-paste\n\t\tinput.focused = false;\n\n\t\t// Regain focus — should not have stale paste state\n\t\tinput.focused = true;\n\n\t\t// Typing normal text should work without paste buffer corruption\n\t\tinput.handleInput(\"hello\");\n\t\tassert.equal(input.getValue(), \"hello\");\n\t});\n\n\tit(\"focused getter/setter works correctly\", () => {\n\t\tconst input = new Input();\n\t\tassert.equal(input.focused, false);\n\t\tinput.focused = true;\n\t\tassert.equal(input.focused, true);\n\t\tinput.focused = false;\n\t\tassert.equal(input.focused, false);\n\t});\n});\n"]}
|
|
1
|
+
{"version":3,"file":"input.test.js","sourceRoot":"","sources":["../../../src/components/__tests__/input.test.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,4DAA4D;AAE5D,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE;IACtB,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACrD,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QAErB,yDAAyD;QACzD,KAAK,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;QAEtC,2BAA2B;QAC3B,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;QAEtB,mDAAmD;QACnD,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QAErB,iEAAiE;QACjE,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC3B,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAChD,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACnC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QACrB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAClC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;QACtB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QACjE,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;QACpB,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QACrB,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;QAE/B,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACvC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,+CAA+C,CAAC,CAAC;QACxF,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,gDAAgD,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["// pi-tui Input component regression tests\n// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>\n\nimport { describe, it } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { Input } from \"../input.js\";\n\ndescribe(\"Input\", () => {\n\tit(\"paste buffer is cleared when focus is lost\", () => {\n\t\tconst input = new Input();\n\t\tinput.focused = true;\n\n\t\t// Simulate starting a paste (bracket paste start marker)\n\t\tinput.handleInput(\"\\x1b[200~partial\");\n\n\t\t// Now lose focus mid-paste\n\t\tinput.focused = false;\n\n\t\t// Regain focus — should not have stale paste state\n\t\tinput.focused = true;\n\n\t\t// Typing normal text should work without paste buffer corruption\n\t\tinput.handleInput(\"hello\");\n\t\tassert.equal(input.getValue(), \"hello\");\n\t});\n\n\tit(\"focused getter/setter works correctly\", () => {\n\t\tconst input = new Input();\n\t\tassert.equal(input.focused, false);\n\t\tinput.focused = true;\n\t\tassert.equal(input.focused, true);\n\t\tinput.focused = false;\n\t\tassert.equal(input.focused, false);\n\t});\n\n\tit(\"secure mode obscures typed characters in render output\", () => {\n\t\tconst input = new Input();\n\t\tinput.secure = true;\n\t\tinput.focused = true;\n\t\tinput.handleInput(\"secret123\");\n\n\t\tconst line = input.render(40)[0] ?? \"\";\n\t\tassert.ok(!line.includes(\"secret123\"), \"rendered line must not expose raw secret text\");\n\t\tassert.ok(line.includes(\"*********\"), \"rendered line should include masked characters\");\n\t});\n});\n"]}
|
|
@@ -8,6 +8,8 @@ export declare class Input implements Component, Focusable {
|
|
|
8
8
|
onSubmit?: (value: string) => void;
|
|
9
9
|
onEscape?: () => void;
|
|
10
10
|
placeholder: string;
|
|
11
|
+
/** When true, render obscured characters instead of the actual value. */
|
|
12
|
+
secure: boolean;
|
|
11
13
|
/** Focusable interface - set by TUI when focus changes */
|
|
12
14
|
private _focused;
|
|
13
15
|
get focused(): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/components/input.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,SAAS,EAAiB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAW1E;;GAEG;AACH,qBAAa,KAAM,YAAW,SAAS,EAAE,SAAS;IACjD,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAa;IACpB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,CAAM;
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/components/input.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,SAAS,EAAiB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAW1E;;GAEG;AACH,qBAAa,KAAM,YAAW,SAAS,EAAE,SAAS;IACjD,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAa;IACpB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,CAAM;IAChC,yEAAyE;IAClE,MAAM,EAAE,OAAO,CAAS;IAE/B,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,CAAkB;IAClC,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAMzB;IAGD,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,SAAS,CAAkB;IAGnC,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,UAAU,CAA8C;IAGhE,OAAO,CAAC,SAAS,CAA+B;IAEhD,QAAQ,IAAI,MAAM;IAIlB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK7B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAqK/B,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,mBAAmB;IAqB3B,OAAO,CAAC,iBAAiB;IAoBzB,OAAO,CAAC,IAAI;IAWZ,OAAO,CAAC,OAAO;IAkBf,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,IAAI;IAQZ,OAAO,CAAC,iBAAiB;IAkCzB,OAAO,CAAC,gBAAgB;IAmCxB,OAAO,CAAC,WAAW;IAYnB,UAAU,IAAI,IAAI;IAIlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;CAkG/B"}
|
|
@@ -13,6 +13,8 @@ export class Input {
|
|
|
13
13
|
this.value = "";
|
|
14
14
|
this.cursor = 0; // Cursor position in the value
|
|
15
15
|
this.placeholder = "";
|
|
16
|
+
/** When true, render obscured characters instead of the actual value. */
|
|
17
|
+
this.secure = false;
|
|
16
18
|
/** Focusable interface - set by TUI when focus changes */
|
|
17
19
|
this._focused = false;
|
|
18
20
|
// Bracketed paste mode buffering
|
|
@@ -376,6 +378,7 @@ export class Input {
|
|
|
376
378
|
// Calculate visible window
|
|
377
379
|
const prompt = "> ";
|
|
378
380
|
const availableWidth = width - prompt.length;
|
|
381
|
+
const renderValue = this.secure ? "*".repeat(this.value.length) : this.value;
|
|
379
382
|
if (availableWidth <= 0) {
|
|
380
383
|
return [prompt];
|
|
381
384
|
}
|
|
@@ -392,7 +395,7 @@ export class Input {
|
|
|
392
395
|
let cursorDisplay = this.cursor;
|
|
393
396
|
if (this.value.length < availableWidth) {
|
|
394
397
|
// Everything fits (leave room for cursor at end)
|
|
395
|
-
visibleText =
|
|
398
|
+
visibleText = renderValue;
|
|
396
399
|
}
|
|
397
400
|
else {
|
|
398
401
|
// Need horizontal scrolling
|
|
@@ -425,19 +428,19 @@ export class Input {
|
|
|
425
428
|
};
|
|
426
429
|
if (this.cursor < halfWidth) {
|
|
427
430
|
// Cursor near start
|
|
428
|
-
visibleText =
|
|
431
|
+
visibleText = renderValue.slice(0, findValidEnd(scrollWidth));
|
|
429
432
|
cursorDisplay = this.cursor;
|
|
430
433
|
}
|
|
431
434
|
else if (this.cursor > this.value.length - halfWidth) {
|
|
432
435
|
// Cursor near end
|
|
433
436
|
const start = findValidStart(this.value.length - scrollWidth);
|
|
434
|
-
visibleText =
|
|
437
|
+
visibleText = renderValue.slice(start);
|
|
435
438
|
cursorDisplay = this.cursor - start;
|
|
436
439
|
}
|
|
437
440
|
else {
|
|
438
441
|
// Cursor in middle
|
|
439
442
|
const start = findValidStart(this.cursor - halfWidth);
|
|
440
|
-
visibleText =
|
|
443
|
+
visibleText = renderValue.slice(start, findValidEnd(start + scrollWidth));
|
|
441
444
|
cursorDisplay = halfWidth;
|
|
442
445
|
}
|
|
443
446
|
}
|