pi-ui-extend 0.1.21 → 0.1.25
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/README.md +1 -10
- package/bin/pix.mjs +11 -154
- package/dist/app/app.d.ts +1 -0
- package/dist/app/app.js +34 -9
- package/dist/app/cli/startup-info.d.ts +0 -1
- package/dist/app/cli/startup-info.js +0 -3
- package/dist/app/commands/command-session-actions.js +3 -0
- package/dist/app/input/autocomplete-controller.js +0 -1
- package/dist/app/popup/popup-menu-controller.js +7 -1
- package/dist/app/rendering/conversation-entry-renderer.js +29 -40
- package/dist/app/rendering/render-text.d.ts +6 -0
- package/dist/app/rendering/render-text.js +9 -0
- package/dist/app/rendering/tab-line-renderer.js +1 -5
- package/dist/app/rendering/tool-block-renderer.js +7 -1
- package/dist/app/screen/mouse-controller.js +14 -6
- package/dist/app/session/session-event-controller.js +5 -4
- package/dist/app/session/session-lifecycle-controller.js +0 -4
- package/dist/app/session/tabs-controller.d.ts +5 -1
- package/dist/app/session/tabs-controller.js +111 -23
- package/dist/app/types.d.ts +5 -0
- package/dist/app/workspace/workspace-actions-controller.d.ts +3 -0
- package/dist/app/workspace/workspace-actions-controller.js +71 -16
- package/dist/app/workspace/workspace-undo.js +41 -6
- package/dist/markdown-format.d.ts +4 -0
- package/dist/markdown-format.js +6 -1
- package/dist/schemas/pi-tools-suite-schema.d.ts +0 -1
- package/dist/schemas/pi-tools-suite-schema.js +0 -1
- package/dist/theme.js +18 -18
- package/extensions/session-title/config.ts +0 -5
- package/extensions/session-title/index.ts +0 -1
- package/external/pi-tools-suite/README.md +1 -1
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -1
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -5
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +0 -1
- package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +0 -1
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +1 -1
- package/external/pi-tools-suite/src/telegram-mirror/README.md +81 -46
- package/external/pi-tools-suite/src/telegram-mirror/bot.ts +81 -10
- package/external/pi-tools-suite/src/telegram-mirror/events.ts +6 -38
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +246 -40
- package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +20 -0
- package/external/pi-tools-suite/src/telegram-mirror/multiplexer.ts +247 -17
- package/external/pi-tools-suite/src/telegram-mirror/renderer.ts +75 -78
- package/external/pi-tools-suite/src/todo/index.ts +7 -6
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +1 -1
- package/external/pi-tools-suite/src/web-search/index.ts +139 -2
- package/package.json +7 -7
- package/schemas/pi-tools-suite.json +0 -6
package/README.md
CHANGED
|
@@ -87,7 +87,6 @@ Useful flags:
|
|
|
87
87
|
- `--cwd <path>`: workspace used for Pi tools, settings, resources, and sessions.
|
|
88
88
|
- `--no-session`: run with an in-memory SDK session.
|
|
89
89
|
- `--model <provider/model[:thinking]>`: request a specific model, for example `anthropic/claude-sonnet-4-20250514:medium`.
|
|
90
|
-
- `--reload-on-build`: restart the running Pix process after a successful watcher build.
|
|
91
90
|
|
|
92
91
|
## Updating Pix
|
|
93
92
|
|
|
@@ -143,20 +142,12 @@ Run Pix against a workspace:
|
|
|
143
142
|
pix --cwd /path/to/workspace
|
|
144
143
|
```
|
|
145
144
|
|
|
146
|
-
During UI development, run the watcher in another terminal:
|
|
145
|
+
During UI development, run the watcher in another terminal, then restart Pix after rebuilds when needed:
|
|
147
146
|
|
|
148
147
|
```bash
|
|
149
148
|
npm run watch:pix
|
|
150
149
|
```
|
|
151
150
|
|
|
152
|
-
Each running instance can reload after successful builds:
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
PIX_RELOAD_ON_BUILD=1 pix --cwd /path/to/workspace
|
|
156
|
-
# or
|
|
157
|
-
pix --reload-on-build --cwd /path/to/workspace
|
|
158
|
-
```
|
|
159
|
-
|
|
160
151
|
For a one-shot dev launch that rebuilds and refreshes the global link first:
|
|
161
152
|
|
|
162
153
|
```bash
|
package/bin/pix.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
4
3
|
import { delimiter, dirname, join } from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
5
|
|
|
@@ -11,10 +10,7 @@ const packageRoot = dirname(dirname(launcherPath));
|
|
|
11
10
|
const mainPath = fileURLToPath(new URL("../dist/main.js", import.meta.url));
|
|
12
11
|
const updatePath = fileURLToPath(new URL("../dist/app/cli/update.js", import.meta.url));
|
|
13
12
|
const installPath = fileURLToPath(new URL("../dist/app/cli/install.js", import.meta.url));
|
|
14
|
-
const
|
|
15
|
-
const rawArgs = process.argv.slice(2);
|
|
16
|
-
const childArgs = [];
|
|
17
|
-
let reloadOnBuild = truthyEnv(process.env.PIX_RELOAD_ON_BUILD);
|
|
13
|
+
const cliArgs = process.argv.slice(2);
|
|
18
14
|
|
|
19
15
|
if (!isCurrentNodeSupported()) {
|
|
20
16
|
console.error(`[pix] Node ${minimumNodeVersionLabel}+ is required; current Node is ${process.versions.node}.`);
|
|
@@ -22,63 +18,31 @@ if (!isCurrentNodeSupported()) {
|
|
|
22
18
|
process.exit(1);
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
if (arg === "--reload-on-build") {
|
|
27
|
-
reloadOnBuild = true;
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (arg === "--no-reload-on-build") {
|
|
31
|
-
reloadOnBuild = false;
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
childArgs.push(arg);
|
|
35
|
-
}
|
|
21
|
+
applyPixRuntimeEnv();
|
|
36
22
|
|
|
37
|
-
if (
|
|
23
|
+
if (cliArgs[0] === "update") {
|
|
38
24
|
if (!existsSync(updatePath)) {
|
|
39
25
|
console.error("pix update is not built yet. Run `npm run build:pix` or update from a published package.");
|
|
40
26
|
process.exit(1);
|
|
41
27
|
}
|
|
42
28
|
const { runPixUpdateCli } = await import(new URL("../dist/app/cli/update.js", import.meta.url));
|
|
43
|
-
process.exit(await runPixUpdateCli(
|
|
29
|
+
process.exit(await runPixUpdateCli(cliArgs.slice(1)));
|
|
44
30
|
}
|
|
45
31
|
|
|
46
|
-
if (
|
|
32
|
+
if (cliArgs[0] === "install" || cliArgs[0] === "setup") {
|
|
47
33
|
if (!existsSync(installPath)) {
|
|
48
34
|
console.error("pix install is not built yet. Run `npm run build:pix` or update from a published package.");
|
|
49
35
|
process.exit(1);
|
|
50
36
|
}
|
|
51
37
|
const { runPixInstallCli } = await import(new URL("../dist/app/cli/install.js", import.meta.url));
|
|
52
|
-
process.exit(await runPixInstallCli(
|
|
38
|
+
process.exit(await runPixInstallCli(cliArgs.slice(1), { env: process.env }));
|
|
53
39
|
}
|
|
54
40
|
|
|
55
41
|
if (!existsSync(mainPath)) {
|
|
56
42
|
console.error("pix is not built yet. Run `npm run build:pix` or `npm run watch:pix`.");
|
|
57
43
|
process.exit(1);
|
|
58
44
|
}
|
|
59
|
-
|
|
60
|
-
let child = undefined;
|
|
61
|
-
let reloadTimer = undefined;
|
|
62
|
-
let distPollTimer = undefined;
|
|
63
|
-
let distSnapshot = snapshotDist();
|
|
64
|
-
let restarting = false;
|
|
65
|
-
let shuttingDown = false;
|
|
66
|
-
|
|
67
|
-
startChild();
|
|
68
|
-
if (reloadOnBuild) startDistPolling();
|
|
69
|
-
|
|
70
|
-
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
71
|
-
process.on(signal, () => {
|
|
72
|
-
shuttingDown = true;
|
|
73
|
-
stopDistPolling();
|
|
74
|
-
child?.kill(signal);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function truthyEnv(value) {
|
|
79
|
-
if (!value) return false;
|
|
80
|
-
return !["0", "false", "no", "off"].includes(value.toLowerCase());
|
|
81
|
-
}
|
|
45
|
+
await import(new URL("../dist/main.js", import.meta.url));
|
|
82
46
|
|
|
83
47
|
function isCurrentNodeSupported() {
|
|
84
48
|
const parts = process.versions.node.split(".").map((part) => Number.parseInt(part, 10));
|
|
@@ -91,117 +55,10 @@ function isCurrentNodeSupported() {
|
|
|
91
55
|
return true;
|
|
92
56
|
}
|
|
93
57
|
|
|
94
|
-
function
|
|
95
|
-
child = spawn(process.execPath, [mainPath, ...childArgs], {
|
|
96
|
-
stdio: "inherit",
|
|
97
|
-
env: pixChildEnv(),
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
child.on("error", (error) => {
|
|
101
|
-
console.error(error.message);
|
|
102
|
-
process.exitCode = 1;
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
child.on("exit", (code, signal) => {
|
|
106
|
-
child = undefined;
|
|
107
|
-
if (restarting) return;
|
|
108
|
-
|
|
109
|
-
shuttingDown = true;
|
|
110
|
-
stopDistPolling();
|
|
111
|
-
if (signal) {
|
|
112
|
-
process.exitCode = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
process.exitCode = code ?? 1;
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function pixChildEnv() {
|
|
120
|
-
const env = { ...process.env };
|
|
58
|
+
function applyPixRuntimeEnv() {
|
|
121
59
|
const bundledBinPath = join(packageRoot, "node_modules", ".bin");
|
|
122
60
|
if (existsSync(bundledBinPath)) {
|
|
123
|
-
env.PATH = [bundledBinPath, env.PATH ?? ""].filter(Boolean).join(delimiter);
|
|
124
|
-
env.PIX_BUNDLED_PI_BIN = bundledBinPath;
|
|
61
|
+
process.env.PATH = [bundledBinPath, process.env.PATH ?? ""].filter(Boolean).join(delimiter);
|
|
62
|
+
process.env.PIX_BUNDLED_PI_BIN = bundledBinPath;
|
|
125
63
|
}
|
|
126
|
-
return env;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function startDistPolling() {
|
|
130
|
-
const pollInterval = Number(process.env.PIX_RELOAD_POLL_MS ?? 1000);
|
|
131
|
-
distPollTimer = setInterval(() => {
|
|
132
|
-
const nextSnapshot = snapshotDist();
|
|
133
|
-
if (nextSnapshot === distSnapshot) return;
|
|
134
|
-
|
|
135
|
-
distSnapshot = nextSnapshot;
|
|
136
|
-
queueReload();
|
|
137
|
-
}, Number.isFinite(pollInterval) && pollInterval > 0 ? pollInterval : 1000);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function stopDistPolling() {
|
|
141
|
-
if (reloadTimer) clearTimeout(reloadTimer);
|
|
142
|
-
if (distPollTimer) clearInterval(distPollTimer);
|
|
143
|
-
reloadTimer = undefined;
|
|
144
|
-
distPollTimer = undefined;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function queueReload() {
|
|
148
|
-
if (shuttingDown) return;
|
|
149
|
-
if (reloadTimer) clearTimeout(reloadTimer);
|
|
150
|
-
reloadTimer = setTimeout(() => {
|
|
151
|
-
reloadTimer = undefined;
|
|
152
|
-
restartChild();
|
|
153
|
-
}, 250);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function restartChild() {
|
|
157
|
-
if (shuttingDown) return;
|
|
158
|
-
if (!child) {
|
|
159
|
-
startChild();
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
restarting = true;
|
|
164
|
-
const currentChild = child;
|
|
165
|
-
currentChild.once("exit", () => {
|
|
166
|
-
restarting = false;
|
|
167
|
-
if (!shuttingDown) startChild();
|
|
168
|
-
});
|
|
169
|
-
console.error("[pix] dist changed; restarting renderer");
|
|
170
|
-
currentChild.kill("SIGTERM");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function snapshotDist() {
|
|
174
|
-
let newestMtime = 0;
|
|
175
|
-
let runtimeFileCount = 0;
|
|
176
|
-
const pendingDirs = [distPath];
|
|
177
|
-
|
|
178
|
-
while (pendingDirs.length > 0) {
|
|
179
|
-
const currentDir = pendingDirs.pop();
|
|
180
|
-
if (!currentDir) continue;
|
|
181
|
-
|
|
182
|
-
let entries;
|
|
183
|
-
try {
|
|
184
|
-
entries = readdirSync(currentDir, { withFileTypes: true });
|
|
185
|
-
} catch {
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
for (const entry of entries) {
|
|
190
|
-
const entryPath = join(currentDir, entry.name);
|
|
191
|
-
if (entry.isDirectory()) {
|
|
192
|
-
pendingDirs.push(entryPath);
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
if (!entry.isFile() || !entry.name.endsWith(".js")) continue;
|
|
196
|
-
|
|
197
|
-
runtimeFileCount += 1;
|
|
198
|
-
try {
|
|
199
|
-
newestMtime = Math.max(newestMtime, statSync(entryPath).mtimeMs);
|
|
200
|
-
} catch {
|
|
201
|
-
// If tsc is replacing a file while we scan, the next poll will see the final state.
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return `${runtimeFileCount}:${newestMtime}`;
|
|
207
64
|
}
|
package/dist/app/app.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ export declare class PiUiExtendApp {
|
|
|
85
85
|
private openSearchResultInNewTab;
|
|
86
86
|
private scrollToUserMessageJumpTarget;
|
|
87
87
|
private findUserEntryBySessionEntryId;
|
|
88
|
+
private findUserEntryByJumpText;
|
|
88
89
|
private loadSessionHistoryAsync;
|
|
89
90
|
private handleSessionEvent;
|
|
90
91
|
private findEntry;
|
package/dist/app/app.js
CHANGED
|
@@ -46,6 +46,9 @@ import { AppVoiceController } from "./input/voice-controller.js";
|
|
|
46
46
|
import { createIsolatedExtensionEventBus } from "./extensions/extension-event-bus.js";
|
|
47
47
|
import { setAppIconTheme } from "./icons.js";
|
|
48
48
|
const TERMINAL_BELL_ATTENTION_EVENT = "pix:terminal-bell:attention";
|
|
49
|
+
function normalizeJumpTargetText(text) {
|
|
50
|
+
return text.replace(/\s+/gu, " ").trim();
|
|
51
|
+
}
|
|
49
52
|
const SUBAGENTS_LIVE_STATE_EVENT = "pi-tools-suite:async-subagents:live-state";
|
|
50
53
|
const TODO_STATE_EVENT = "pi-tools-suite:todo:state";
|
|
51
54
|
const COALESCED_RENDER_DELAY_MS = 16;
|
|
@@ -914,20 +917,42 @@ export class PiUiExtendApp {
|
|
|
914
917
|
async scrollToUserMessageJumpTarget(target) {
|
|
915
918
|
if (target.entryId && this.scrollController.scrollToConversationEntry(target.entryId))
|
|
916
919
|
return true;
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
920
|
+
this.workspaceActions.syncUserSessionEntryMetadata();
|
|
921
|
+
if (target.sessionEntryId) {
|
|
922
|
+
let entry = this.findUserEntryBySessionEntryId(target.sessionEntryId);
|
|
923
|
+
while (!entry && this.sessionEvents.hasOlderSessionHistory() && !this.sessionEvents.isLoadingOlderSessionHistory()) {
|
|
924
|
+
const loaded = await this.sessionEvents.loadOlderSessionHistory({ render: false });
|
|
925
|
+
if (!loaded)
|
|
926
|
+
break;
|
|
927
|
+
entry = this.findUserEntryBySessionEntryId(target.sessionEntryId);
|
|
928
|
+
}
|
|
929
|
+
if (entry && this.scrollController.scrollToConversationEntry(entry.id))
|
|
930
|
+
return true;
|
|
925
931
|
}
|
|
926
|
-
|
|
932
|
+
const fallbackEntry = this.findUserEntryByJumpText(target);
|
|
933
|
+
return fallbackEntry ? this.scrollController.scrollToConversationEntry(fallbackEntry.id) : false;
|
|
927
934
|
}
|
|
928
935
|
findUserEntryBySessionEntryId(sessionEntryId) {
|
|
929
936
|
return this.entries.find((entry) => entry.kind === "user" && entry.sessionEntryId === sessionEntryId);
|
|
930
937
|
}
|
|
938
|
+
findUserEntryByJumpText(target) {
|
|
939
|
+
if (!target.text)
|
|
940
|
+
return undefined;
|
|
941
|
+
const userEntries = this.entries.filter((entry) => entry.kind === "user");
|
|
942
|
+
if (target.userIndex !== undefined && target.userCount !== undefined) {
|
|
943
|
+
const visibleIndex = target.userIndex - (target.userCount - userEntries.length);
|
|
944
|
+
const entry = userEntries[visibleIndex];
|
|
945
|
+
if (entry && normalizeJumpTargetText(entry.text) === normalizeJumpTargetText(target.text))
|
|
946
|
+
return entry;
|
|
947
|
+
}
|
|
948
|
+
const normalizedTargetText = normalizeJumpTargetText(target.text);
|
|
949
|
+
for (let index = userEntries.length - 1; index >= 0; index -= 1) {
|
|
950
|
+
const entry = userEntries[index];
|
|
951
|
+
if (entry && normalizeJumpTargetText(entry.text) === normalizedTargetText)
|
|
952
|
+
return entry;
|
|
953
|
+
}
|
|
954
|
+
return undefined;
|
|
955
|
+
}
|
|
931
956
|
async loadSessionHistoryAsync(options) {
|
|
932
957
|
return this.sessionEvents.loadSessionHistoryAsync(options);
|
|
933
958
|
}
|
|
@@ -12,9 +12,6 @@ export function createStartupInfoMessage(runtime) {
|
|
|
12
12
|
...sections.flatMap(formatSection),
|
|
13
13
|
].join("\n").trimEnd();
|
|
14
14
|
}
|
|
15
|
-
export function isEmptyStartupSession(runtime) {
|
|
16
|
-
return Array.isArray(runtime.session.messages) && runtime.session.messages.length === 0;
|
|
17
|
-
}
|
|
18
15
|
function startupSections(runtime) {
|
|
19
16
|
const loader = runtime.session.resourceLoader;
|
|
20
17
|
const context = loader.getAgentsFiles();
|
|
@@ -9,6 +9,7 @@ import { runProcess } from "../process.js";
|
|
|
9
9
|
import { copyTextToClipboard } from "../screen/clipboard.js";
|
|
10
10
|
import { formatAccountUsageReport, queryAccountUsageReport } from "../model/model-usage-status.js";
|
|
11
11
|
import { checkPixUpdate, formatPixUpdateCheck, parsePixUpdateArgs, pixUpdateUsage } from "../cli/update.js";
|
|
12
|
+
import { createStartupInfoMessage } from "../cli/startup-info.js";
|
|
12
13
|
export class SessionCommandActions {
|
|
13
14
|
host;
|
|
14
15
|
constructor(host) {
|
|
@@ -114,6 +115,8 @@ export class SessionCommandActions {
|
|
|
114
115
|
return;
|
|
115
116
|
const stats = runtime.session.getSessionStats();
|
|
116
117
|
const lines = [
|
|
118
|
+
createStartupInfoMessage(runtime),
|
|
119
|
+
"",
|
|
117
120
|
"Session info",
|
|
118
121
|
...(runtime.session.sessionName ? [`name: ${runtime.session.sessionName}`] : []),
|
|
119
122
|
`file: ${stats.sessionFile ?? "in-memory"}`,
|
|
@@ -177,7 +177,6 @@ export async function completeInputWithPi(runtime, draft, config, signal) {
|
|
|
177
177
|
maxTokens: requestModel.maxTokens,
|
|
178
178
|
...(parsedModel.thinkingLevel && parsedModel.thinkingLevel !== "off" ? { reasoning: parsedModel.thinkingLevel } : {}),
|
|
179
179
|
signal: requestSignal.signal,
|
|
180
|
-
temperature: 0.1,
|
|
181
180
|
timeoutMs,
|
|
182
181
|
});
|
|
183
182
|
for await (const event of stream) {
|
|
@@ -810,7 +810,13 @@ export function buildUserMessageJumpItems(entries) {
|
|
|
810
810
|
const preview = sanitizeText(entry.text).replace(/\s+/g, " ").trim();
|
|
811
811
|
const label = `${index + 1}. ${preview || "(empty message)"}`;
|
|
812
812
|
return {
|
|
813
|
-
value: {
|
|
813
|
+
value: {
|
|
814
|
+
...(entry.entryId === undefined ? {} : { entryId: entry.entryId }),
|
|
815
|
+
...(entry.sessionEntryId === undefined ? {} : { sessionEntryId: entry.sessionEntryId }),
|
|
816
|
+
text: entry.text,
|
|
817
|
+
userIndex: index,
|
|
818
|
+
userCount: userEntries.length,
|
|
819
|
+
},
|
|
814
820
|
label,
|
|
815
821
|
...(entry.entryId ? {} : { description: "load older history and jump" }),
|
|
816
822
|
aliases: [entry.sessionEntryId ?? "", entry.entryId ?? ""],
|
|
@@ -1,43 +1,20 @@
|
|
|
1
1
|
import { applyOutputFilters } from "../../config.js";
|
|
2
2
|
import { renderMarkdownTextLines } from "../../markdown-format.js";
|
|
3
|
-
import { stringDisplayWidth } from "../../terminal-width.js";
|
|
4
3
|
import { attachImageClickTargets } from "../screen/image-click-targets.js";
|
|
5
4
|
import { APP_ICONS } from "../icons.js";
|
|
6
|
-
import { horizontalPaddingLayout, padHorizontalText,
|
|
5
|
+
import { horizontalPaddingLayout, padHorizontalText, wrapTextLines } from "./render-text.js";
|
|
7
6
|
import { renderConversationShellEntry } from "./conversation-shell-renderer.js";
|
|
8
7
|
import { renderConversationToolEntry, renderThinkingEntry } from "./conversation-tool-renderer.js";
|
|
9
8
|
export function renderConversationEntry(entry, width, options) {
|
|
10
9
|
const { left: userContentLeft, contentWidth: userContentWidth } = horizontalPaddingLayout(width);
|
|
11
|
-
const userLine = (text, entryId, syntaxHighlight, segments) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
backgroundOverride: options.colors.userMessageBackground,
|
|
20
|
-
...(segments && segments.length > 0
|
|
21
|
-
? {
|
|
22
|
-
segments: segments.map((segment) => ({
|
|
23
|
-
...segment,
|
|
24
|
-
start: segment.start + offset,
|
|
25
|
-
end: segment.end + offset,
|
|
26
|
-
foreground: options.colors.userForeground,
|
|
27
|
-
})),
|
|
28
|
-
}
|
|
29
|
-
: {}),
|
|
30
|
-
...(syntaxHighlight === undefined
|
|
31
|
-
? {}
|
|
32
|
-
: {
|
|
33
|
-
syntaxHighlight: {
|
|
34
|
-
...syntaxHighlight,
|
|
35
|
-
start: syntaxHighlight.start + offset,
|
|
36
|
-
},
|
|
37
|
-
}),
|
|
38
|
-
...(entryId === undefined ? {} : { target: { kind: "user-message", id: entryId } }),
|
|
39
|
-
};
|
|
40
|
-
};
|
|
10
|
+
const userLine = (text, entryId, syntaxHighlight, segments) => ({
|
|
11
|
+
text: padHorizontalText(text, width),
|
|
12
|
+
colorOverride: options.colors.userForeground,
|
|
13
|
+
backgroundOverride: options.colors.userMessageBackground,
|
|
14
|
+
...(segments && segments.length > 0 ? { segments: segments.map((segment) => ({ ...segment, start: segment.start + userContentLeft, end: segment.end + userContentLeft })) } : {}),
|
|
15
|
+
...(syntaxHighlight === undefined ? {} : { syntaxHighlight }),
|
|
16
|
+
...(entryId === undefined ? {} : { target: { kind: "user-message", id: entryId } }),
|
|
17
|
+
});
|
|
41
18
|
const queuedLine = (text, entryId, segments) => ({
|
|
42
19
|
text,
|
|
43
20
|
colorOverride: options.colors.userForeground,
|
|
@@ -45,17 +22,25 @@ export function renderConversationEntry(entry, width, options) {
|
|
|
45
22
|
target: { kind: "queue-message", id: entryId },
|
|
46
23
|
});
|
|
47
24
|
const userMessageLines = (userEntry) => {
|
|
48
|
-
const lines = renderMarkdownTextLines(userEntry.text, userContentWidth, userContentLeft).map((line) =>
|
|
25
|
+
const lines = renderMarkdownTextLines(userEntry.text, userContentWidth, userContentLeft).map((line) => ({
|
|
26
|
+
...userLine(line.text, userEntry.id, line.syntaxHighlight, line.segments),
|
|
27
|
+
...(line.copyText === undefined ? {} : { copyText: line.copyText }),
|
|
28
|
+
...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
|
|
29
|
+
}));
|
|
49
30
|
return attachImageClickTargets(lines, userEntry.id, userEntry.images, { foreground: options.colors.info, underline: true });
|
|
50
31
|
};
|
|
51
32
|
const queuedMessageLines = (queuedEntry) => {
|
|
52
33
|
const icon = queuedEntry.queueSource === "deferred" ? APP_ICONS.pause : APP_ICONS.timerSand;
|
|
53
|
-
const contentLines =
|
|
54
|
-
return contentLines.map((
|
|
34
|
+
const contentLines = wrapTextLines(`${icon} ${queuedEntry.text}`, width);
|
|
35
|
+
return contentLines.map((line, index) => ({
|
|
36
|
+
...queuedLine(line.text, queuedEntry.id, index === 0 ? [{ start: 0, end: icon.length, foreground: options.colors.info }] : undefined),
|
|
37
|
+
copyText: line.copyText,
|
|
38
|
+
...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
|
|
39
|
+
}));
|
|
55
40
|
};
|
|
56
41
|
switch (entry.kind) {
|
|
57
42
|
case "system":
|
|
58
|
-
return
|
|
43
|
+
return wrapTextLines(`system: ${entry.text}`, width).map((line) => ({ text: line.text, copyText: line.copyText, ...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}), variant: "muted" }));
|
|
59
44
|
case "user":
|
|
60
45
|
return userMessageLines(entry);
|
|
61
46
|
case "queued":
|
|
@@ -65,21 +50,23 @@ export function renderConversationEntry(entry, width, options) {
|
|
|
65
50
|
case "custom":
|
|
66
51
|
return renderCustomEntry(entry, width);
|
|
67
52
|
case "session-aborted":
|
|
68
|
-
return
|
|
53
|
+
return wrapTextLines(entry.text, width).map((line) => ({ text: line.text, copyText: line.copyText, ...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}), variant: "error" }));
|
|
69
54
|
case "shell":
|
|
70
55
|
return renderConversationShellEntry(entry, width, options);
|
|
71
56
|
case "thinking":
|
|
72
57
|
return renderThinkingEntry(entry, width, options);
|
|
73
58
|
case "error":
|
|
74
|
-
return
|
|
59
|
+
return wrapTextLines(`error: ${entry.text}`, width).map((line) => ({ text: line.text, copyText: line.copyText, ...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}), variant: "error" }));
|
|
75
60
|
case "tool":
|
|
76
61
|
return renderConversationToolEntry(entry, width, options);
|
|
77
62
|
}
|
|
78
63
|
}
|
|
79
64
|
function renderCustomEntry(entry, width) {
|
|
80
65
|
const label = `[${entry.customType}]`;
|
|
81
|
-
return
|
|
82
|
-
text,
|
|
66
|
+
return wrapTextLines(`${label}\n${entry.text}`, width).map((line, index) => ({
|
|
67
|
+
text: line.text,
|
|
68
|
+
copyText: line.copyText,
|
|
69
|
+
...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
|
|
83
70
|
variant: index === 0 ? "accent" : "normal",
|
|
84
71
|
}));
|
|
85
72
|
}
|
|
@@ -100,6 +87,8 @@ function renderAssistantLines(text, width, options) {
|
|
|
100
87
|
const allSegments = headingSegment ? [headingSegment, ...existingSegments] : existingSegments;
|
|
101
88
|
lines.push({
|
|
102
89
|
text: padHorizontalText(line.text, width),
|
|
90
|
+
...(line.copyText === undefined ? {} : { copyText: line.copyText }),
|
|
91
|
+
...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
|
|
103
92
|
colorOverride: options.colors.assistantForeground,
|
|
104
93
|
backgroundOverride: options.colors.assistantMessageBackground,
|
|
105
94
|
...(allSegments.length > 0 ? { segments: allSegments } : {}),
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Theme } from "../../theme.js";
|
|
2
2
|
import type { ToolStatusEntry } from "../types.js";
|
|
3
|
+
export type WrappedTextLine = {
|
|
4
|
+
text: string;
|
|
5
|
+
copyText: string;
|
|
6
|
+
continuesOnNextLine?: boolean;
|
|
7
|
+
};
|
|
3
8
|
export declare function sanitizeText(text: string): string;
|
|
4
9
|
export declare function alertIconPrefixLength(text: string): number | undefined;
|
|
5
10
|
export declare function normalizePastedTextForDuplicateKey(text: string): string;
|
|
@@ -12,6 +17,7 @@ export declare function toolStatusIcon(entry: ToolStatusEntry): string;
|
|
|
12
17
|
export declare function toolStatusIconColor(entry: ToolStatusEntry, colors: Theme["colors"]): string;
|
|
13
18
|
export declare function wrapLine(text: string, width: number): string[];
|
|
14
19
|
export declare function wrapText(text: string, width: number): string[];
|
|
20
|
+
export declare function wrapTextLines(text: string, width: number): WrappedTextLine[];
|
|
15
21
|
export declare function padOrTrimPlain(text: string, width: number): string;
|
|
16
22
|
export declare function horizontalPaddingLayout(width: number): {
|
|
17
23
|
left: number;
|
|
@@ -93,6 +93,15 @@ export function wrapText(text, width) {
|
|
|
93
93
|
const lines = sanitizeText(text).split("\n");
|
|
94
94
|
return lines.flatMap((line) => wrapLine(line, width));
|
|
95
95
|
}
|
|
96
|
+
export function wrapTextLines(text, width) {
|
|
97
|
+
return sanitizeText(text)
|
|
98
|
+
.split("\n")
|
|
99
|
+
.flatMap((line) => wrapDisplayLine(line, width).map((wrapped, index, wrappedLines) => ({
|
|
100
|
+
text: wrapped,
|
|
101
|
+
copyText: wrapped,
|
|
102
|
+
...(index < wrappedLines.length - 1 ? { continuesOnNextLine: true } : {}),
|
|
103
|
+
})));
|
|
104
|
+
}
|
|
96
105
|
export function padOrTrimPlain(text, width) {
|
|
97
106
|
return padOrTrimDisplay(text, width);
|
|
98
107
|
}
|
|
@@ -3,7 +3,6 @@ import { APP_ICONS } from "../icons.js";
|
|
|
3
3
|
import { ellipsizeDisplay } from "./render-text.js";
|
|
4
4
|
const TAB_SEPARATOR = " │ ";
|
|
5
5
|
const EMPTY_NEW_TAB_PREFIX = "│ ";
|
|
6
|
-
const DEFAULT_SESSION_TITLE_PATTERN = /^session [0-9a-f]{8}$/iu;
|
|
7
6
|
export const TAB_PANEL_ROWS = 2;
|
|
8
7
|
export function tabPanelRows(tabLineVisible, terminalRows, tabCount = TAB_PANEL_ROWS) {
|
|
9
8
|
if (!tabLineVisible)
|
|
@@ -158,10 +157,7 @@ export class TabLineRenderer {
|
|
|
158
157
|
return this.buttonLayoutFromText(`${prefix}${ellipsizeDisplay(title, titleWidth)}${suffix}`, 0, statusText.length);
|
|
159
158
|
}
|
|
160
159
|
displayTitle(tab) {
|
|
161
|
-
|
|
162
|
-
if (!DEFAULT_SESSION_TITLE_PATTERN.test(title))
|
|
163
|
-
return tab.title;
|
|
164
|
-
return tab.titlePlaceholder === "loading" ? "Loading…" : "New";
|
|
160
|
+
return tab.title;
|
|
165
161
|
}
|
|
166
162
|
buttonLayoutFromText(text, statusStart, statusLength) {
|
|
167
163
|
const closeStart = Math.max(0, text.lastIndexOf(APP_ICONS.close));
|
|
@@ -117,7 +117,13 @@ function renderToolBodyLines(text, width, target, color, style, colors, syntaxHi
|
|
|
117
117
|
? wrapAnsiStyledDisplayLine(ansiLine, bodyWidth)
|
|
118
118
|
: wrapBodyLine(displayLine, bodyWidth).map((wrapped) => ({ text: wrapped, segments: [] }));
|
|
119
119
|
for (const [wrapIndex, wrapped] of wrappedLines.entries()) {
|
|
120
|
-
const line = {
|
|
120
|
+
const line = {
|
|
121
|
+
text: ` ${wrapped.text}`,
|
|
122
|
+
copyText: ` ${wrapped.text}`,
|
|
123
|
+
...(wrapIndex < wrappedLines.length - 1 ? { continuesOnNextLine: true } : {}),
|
|
124
|
+
target,
|
|
125
|
+
colorOverride: color,
|
|
126
|
+
};
|
|
121
127
|
if (diffStyle) {
|
|
122
128
|
const segment = { start: 2, end: line.text.length, foreground: diffStyle.foreground };
|
|
123
129
|
if (diffStyle.bold != null)
|
|
@@ -799,17 +799,19 @@ export class AppMouseController {
|
|
|
799
799
|
const width = this.conversationArea()?.viewportColumns ?? this.host.terminalColumns();
|
|
800
800
|
const count = range.end.line - range.start.line + 1;
|
|
801
801
|
const renderedLines = this.host.conversationViewport().slice(width, range.start.line, count);
|
|
802
|
-
|
|
802
|
+
let copiedText = "";
|
|
803
803
|
for (let index = 0; index < count; index += 1) {
|
|
804
804
|
const rendered = renderedLines[index];
|
|
805
|
-
const
|
|
805
|
+
const lineText = rendered?.text ?? "";
|
|
806
806
|
const line = range.start.line + index;
|
|
807
807
|
const startColumn = line === range.start.line ? range.start.x : 1;
|
|
808
|
-
const endColumn = line === range.end.line ? range.end.x :
|
|
809
|
-
const
|
|
810
|
-
|
|
808
|
+
const endColumn = line === range.end.line ? range.end.x : lineText.length + 1;
|
|
809
|
+
const selectedLine = selectedConversationLineText(rendered, lineText, startColumn, endColumn);
|
|
810
|
+
copiedText += selectedLine;
|
|
811
|
+
if (!(rendered?.continuesOnNextLine))
|
|
812
|
+
copiedText += "\n";
|
|
811
813
|
}
|
|
812
|
-
return
|
|
814
|
+
return copiedText.replace(/\s+$/u, "");
|
|
813
815
|
}
|
|
814
816
|
conversationPointFromMouse(event, clampToViewport) {
|
|
815
817
|
const area = this.conversationArea();
|
|
@@ -930,6 +932,12 @@ export class AppMouseController {
|
|
|
930
932
|
selection.moved = true;
|
|
931
933
|
}
|
|
932
934
|
}
|
|
935
|
+
function selectedConversationLineText(rendered, text, startColumn, endColumn) {
|
|
936
|
+
const selectsWholeLine = startColumn <= 1 && endColumn >= text.length + 1;
|
|
937
|
+
if (selectsWholeLine && rendered?.copyText !== undefined)
|
|
938
|
+
return rendered.copyText;
|
|
939
|
+
return sliceByDisplayColumns(text, startColumn, endColumn).trimEnd();
|
|
940
|
+
}
|
|
933
941
|
function orderedConversationSelection(anchor, current) {
|
|
934
942
|
if (anchor.line < current.line)
|
|
935
943
|
return { start: anchor, end: current };
|
|
@@ -133,6 +133,8 @@ export class AppSessionEventController {
|
|
|
133
133
|
case "tool_execution_end":
|
|
134
134
|
this.host.setSessionActivity(this.host.runtime()?.session.isStreaming ? "running" : "idle");
|
|
135
135
|
this.recordToolWorkspaceMutation(event.toolCallId, event.toolName, event.result.details, event.isError);
|
|
136
|
+
if (this.currentUserEntryId)
|
|
137
|
+
this.host.scheduleUserSessionEntryMetadataSync();
|
|
136
138
|
this.host.observeSubagentsToolResult(event.toolName, isRecord(event.result) ? event.result.details : undefined);
|
|
137
139
|
this.host.observeTodoToolResult(event.toolName, isRecord(event.result) ? event.result.details : undefined, event.isError);
|
|
138
140
|
this.upsertToolEntry(event.toolCallId, {
|
|
@@ -274,7 +276,6 @@ export class AppSessionEventController {
|
|
|
274
276
|
this.finishCurrentThinkingEntry();
|
|
275
277
|
this.flushAssistantTextBuffer(true);
|
|
276
278
|
this.clearCurrentAssistantState();
|
|
277
|
-
this.currentUserEntryId = undefined;
|
|
278
279
|
}
|
|
279
280
|
}
|
|
280
281
|
prepareToolWorkspaceMutation(toolCallId, toolName, args) {
|
|
@@ -362,7 +363,7 @@ export class AppSessionEventController {
|
|
|
362
363
|
}
|
|
363
364
|
if (!this.assistantTextBuffer)
|
|
364
365
|
return visibleText;
|
|
365
|
-
if (shouldHoldAssistantStreamTail(this.assistantTextBuffer)) {
|
|
366
|
+
if (shouldHoldAssistantStreamTail(this.assistantTextBuffer, this.hasVisibleAssistantText(visibleText))) {
|
|
366
367
|
if (final)
|
|
367
368
|
this.assistantTextBuffer = "";
|
|
368
369
|
return visibleText;
|
|
@@ -439,9 +440,9 @@ function shouldDropAssistantStreamLine(line, hasVisibleText) {
|
|
|
439
440
|
return true;
|
|
440
441
|
return isHiddenMarkdownMetadataLine(line);
|
|
441
442
|
}
|
|
442
|
-
function shouldHoldAssistantStreamTail(text) {
|
|
443
|
+
function shouldHoldAssistantStreamTail(text, hasVisibleText) {
|
|
443
444
|
if (text.trim().length === 0)
|
|
444
|
-
return
|
|
445
|
+
return !hasVisibleText;
|
|
445
446
|
return isPotentialDcpMetadataLine(text);
|
|
446
447
|
}
|
|
447
448
|
function isHiddenMarkdownMetadataLine(line) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createId } from "../id.js";
|
|
2
2
|
import { stringifyUnknown } from "../rendering/message-content.js";
|
|
3
3
|
import { collectStartupAvailabilityIssues } from "../cli/startup-checks.js";
|
|
4
|
-
import { createStartupInfoMessage, isEmptyStartupSession } from "../cli/startup-info.js";
|
|
5
4
|
export class AppSessionLifecycleController {
|
|
6
5
|
host;
|
|
7
6
|
unsubscribe;
|
|
@@ -37,9 +36,6 @@ export class AppSessionLifecycleController {
|
|
|
37
36
|
await this.bindCurrentSession({ awaitExtensions: false });
|
|
38
37
|
});
|
|
39
38
|
await this.bindCurrentSession({ awaitExtensions: false });
|
|
40
|
-
if (isEmptyStartupSession(runtime)) {
|
|
41
|
-
this.host.addEntry({ id: createId("system"), kind: "system", text: createStartupInfoMessage(runtime) });
|
|
42
|
-
}
|
|
43
39
|
if (runtime.modelFallbackMessage) {
|
|
44
40
|
this.host.addEntry({ id: createId("system"), kind: "system", text: runtime.modelFallbackMessage });
|
|
45
41
|
}
|