pi-open-tui 0.4.0 → 0.4.2
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/CHANGELOG.md +29 -0
- package/README.md +238 -197
- package/README.zh-CN.md +238 -197
- package/assets/preview_dashboard_1.png +0 -0
- package/extensions/open-tui/config.ts +9 -2
- package/extensions/open-tui/copy-buttons.ts +391 -0
- package/extensions/open-tui/editor.ts +311 -119
- package/extensions/open-tui/footer.ts +13 -11
- package/extensions/open-tui/git.ts +10 -32
- package/extensions/open-tui/header.ts +133 -244
- package/extensions/open-tui/index.ts +34 -33
- package/extensions/open-tui/runtime.ts +10 -11
- package/extensions/open-tui/session-lifecycle.ts +23 -43
- package/extensions/open-tui/settings-command.ts +54 -45
- package/extensions/open-tui/state.ts +11 -14
- package/extensions/open-tui/tool-preview.ts +167 -0
- package/extensions/open-tui/tool-view.ts +121 -36
- package/extensions/open-tui/utils.ts +3 -3
- package/package.json +8 -11
|
@@ -1,244 +1,133 @@
|
|
|
1
|
-
import { VERSION, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
4
|
-
center,
|
|
5
|
-
collectPiCommandNames,
|
|
6
|
-
formatCwd,
|
|
7
|
-
formatModelLabel,
|
|
8
|
-
formatThinkingLabel,
|
|
9
|
-
headerColumnWidths,
|
|
10
|
-
padRight,
|
|
11
|
-
pickSlashCommandTips,
|
|
12
|
-
truncateToWidth,
|
|
13
|
-
visibleWidth,
|
|
14
|
-
} from "./utils.ts";
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
paint: (text: string) => string,
|
|
135
|
-
): string {
|
|
136
|
-
if (width <= 1) return "";
|
|
137
|
-
if (width < 8 || label.length === 0) {
|
|
138
|
-
return paint(truncateToWidth(left + "─".repeat(Math.max(0, width - 2)) + right, width, ""));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const before = "─── ";
|
|
142
|
-
const after = " ─────";
|
|
143
|
-
const fixedWidth = visibleWidth(before) + visibleWidth(label) + visibleWidth(after);
|
|
144
|
-
const fill = Math.max(0, width - 2 - fixedWidth);
|
|
145
|
-
return `${paint(left)}${paint(before)}${label}${paint(after)}${paint("─".repeat(fill))}${paint(right)}`;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function boxedLine(content: string, width: number, paint: (text: string) => string): string {
|
|
149
|
-
if (width <= 2) return truncateToWidth(content, width, "");
|
|
150
|
-
return `${paint("│")}${padRight(content, width - 2)}${paint("│")}`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function twoColumn(
|
|
154
|
-
left: string,
|
|
155
|
-
right: string,
|
|
156
|
-
leftWidth: number,
|
|
157
|
-
rightWidth: number,
|
|
158
|
-
paint: (text: string) => string,
|
|
159
|
-
): string {
|
|
160
|
-
return `${padRight(left, leftWidth)} ${paint("│")} ${padRight(right, rightWidth, "…")}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export class OpenTuiHeader implements Component {
|
|
164
|
-
private readonly pi: ExtensionAPI;
|
|
165
|
-
private readonly ctx: ExtensionContext;
|
|
166
|
-
private readonly frame = LOGO_FRAMES.length - 1;
|
|
167
|
-
private readonly tipCommands: string[];
|
|
168
|
-
|
|
169
|
-
constructor(pi: ExtensionAPI, ctx: ExtensionContext, _tui: TUI) {
|
|
170
|
-
this.pi = pi;
|
|
171
|
-
this.ctx = ctx;
|
|
172
|
-
const pool = collectPiCommandNames(pi.getCommands());
|
|
173
|
-
this.tipCommands = pickSlashCommandTips(pool, {
|
|
174
|
-
fixed: ["open-tui"],
|
|
175
|
-
count: 3,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
render(width: number): string[] {
|
|
180
|
-
const theme = this.ctx.ui.theme;
|
|
181
|
-
const paint = (s: string) => theme.fg("accent", s);
|
|
182
|
-
const muted = (s: string) => theme.fg("muted", s);
|
|
183
|
-
const dim = (s: string) => theme.fg("dim", s);
|
|
184
|
-
const bold = (s: string) => theme.bold(s);
|
|
185
|
-
|
|
186
|
-
if (width < 24) return [paint(`Pi v${VERSION}`)];
|
|
187
|
-
|
|
188
|
-
const innerWidth = width - 2;
|
|
189
|
-
const { leftWidth, rightWidth, useTips } = headerColumnWidths(innerWidth);
|
|
190
|
-
const model = formatModelLabel(this.ctx.model);
|
|
191
|
-
const effort = formatThinkingLabel(this.pi.getThinkingLevel());
|
|
192
|
-
const cwd = formatCwd(this.ctx.cwd);
|
|
193
|
-
|
|
194
|
-
const leftLines = [
|
|
195
|
-
...renderLogo(this.frame, paint).map((line) => center(line, leftWidth)),
|
|
196
|
-
center(bold("Let's build something great"), leftWidth),
|
|
197
|
-
center(muted(`${model} · ${effort}`), leftWidth),
|
|
198
|
-
center(dim(cwd), leftWidth),
|
|
199
|
-
];
|
|
200
|
-
|
|
201
|
-
const tipDivider = paint("─".repeat(Math.max(8, Math.min(rightWidth, 22))));
|
|
202
|
-
const [cmd0 = "", cmd1 = "", cmd2 = "", cmd3 = ""] = this.tipCommands;
|
|
203
|
-
const tipLines = [
|
|
204
|
-
"",
|
|
205
|
-
paint(bold("Welcome")),
|
|
206
|
-
muted("Ask Pi anything"),
|
|
207
|
-
tipDivider,
|
|
208
|
-
paint(bold("Commands")),
|
|
209
|
-
muted(cmd0),
|
|
210
|
-
muted(cmd1),
|
|
211
|
-
muted(cmd2),
|
|
212
|
-
muted(cmd3),
|
|
213
|
-
"",
|
|
214
|
-
];
|
|
215
|
-
|
|
216
|
-
const lines = [borderLine("╭", `${paint("Pi")} v${VERSION}`, "╮", width, paint)];
|
|
217
|
-
for (let i = 0; i < leftLines.length; i++) {
|
|
218
|
-
const content = useTips
|
|
219
|
-
? twoColumn(leftLines[i] ?? "", tipLines[i] ?? "", leftWidth, rightWidth, paint)
|
|
220
|
-
: padRight(leftLines[i] ?? "", leftWidth);
|
|
221
|
-
lines.push(boxedLine(content, width, paint));
|
|
222
|
-
}
|
|
223
|
-
lines.push(borderLine("╰", "", "╯", width, paint));
|
|
224
|
-
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
invalidate(): void {}
|
|
228
|
-
|
|
229
|
-
dispose(): void {}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function installHeader(pi: ExtensionAPI, ctx: ExtensionContext): () => void {
|
|
233
|
-
let header: OpenTuiHeader | undefined;
|
|
234
|
-
ctx.ui.setHeader((tui) => {
|
|
235
|
-
header?.dispose();
|
|
236
|
-
header = new OpenTuiHeader(pi, ctx, tui);
|
|
237
|
-
return header;
|
|
238
|
-
});
|
|
239
|
-
return () => {
|
|
240
|
-
header?.dispose();
|
|
241
|
-
header = undefined;
|
|
242
|
-
ctx.ui.setHeader(undefined);
|
|
243
|
-
};
|
|
244
|
-
}
|
|
1
|
+
import { VERSION, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
center,
|
|
5
|
+
collectPiCommandNames,
|
|
6
|
+
formatCwd,
|
|
7
|
+
formatModelLabel,
|
|
8
|
+
formatThinkingLabel,
|
|
9
|
+
headerColumnWidths,
|
|
10
|
+
padRight,
|
|
11
|
+
pickSlashCommandTips,
|
|
12
|
+
truncateToWidth,
|
|
13
|
+
visibleWidth,
|
|
14
|
+
} from "./utils.ts";
|
|
15
|
+
|
|
16
|
+
const LOGO = [
|
|
17
|
+
" ",
|
|
18
|
+
" ",
|
|
19
|
+
"█████████ ",
|
|
20
|
+
"███ ███ ",
|
|
21
|
+
"██████ ███",
|
|
22
|
+
"███ ███",
|
|
23
|
+
" ",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function renderLogo(paintBrand: (text: string) => string): string[] {
|
|
27
|
+
return LOGO.map((line) => line.replace(/███/g, paintBrand));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function borderLine(
|
|
31
|
+
left: string,
|
|
32
|
+
label: string,
|
|
33
|
+
right: string,
|
|
34
|
+
width: number,
|
|
35
|
+
paint: (text: string) => string,
|
|
36
|
+
): string {
|
|
37
|
+
if (width <= 1) return "";
|
|
38
|
+
if (width < 8 || label.length === 0) {
|
|
39
|
+
return paint(truncateToWidth(left + "─".repeat(Math.max(0, width - 2)) + right, width, ""));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const before = "─── ";
|
|
43
|
+
const after = " ─────";
|
|
44
|
+
const fixedWidth = visibleWidth(before) + visibleWidth(label) + visibleWidth(after);
|
|
45
|
+
const fill = Math.max(0, width - 2 - fixedWidth);
|
|
46
|
+
return `${paint(left)}${paint(before)}${label}${paint(after)}${paint("─".repeat(fill))}${paint(right)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function boxedLine(content: string, width: number, paint: (text: string) => string): string {
|
|
50
|
+
if (width <= 2) return truncateToWidth(content, width, "");
|
|
51
|
+
return `${paint("│")}${padRight(content, width - 2)}${paint("│")}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function twoColumn(
|
|
55
|
+
left: string,
|
|
56
|
+
right: string,
|
|
57
|
+
leftWidth: number,
|
|
58
|
+
rightWidth: number,
|
|
59
|
+
paint: (text: string) => string,
|
|
60
|
+
): string {
|
|
61
|
+
return `${padRight(left, leftWidth)} ${paint("│")} ${padRight(right, rightWidth, "…")}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class OpenTuiHeader implements Component {
|
|
65
|
+
private readonly pi: ExtensionAPI;
|
|
66
|
+
private readonly ctx: ExtensionContext;
|
|
67
|
+
private readonly tipCommands: string[];
|
|
68
|
+
|
|
69
|
+
constructor(pi: ExtensionAPI, ctx: ExtensionContext, _tui: TUI) {
|
|
70
|
+
this.pi = pi;
|
|
71
|
+
this.ctx = ctx;
|
|
72
|
+
const pool = collectPiCommandNames(pi.getCommands());
|
|
73
|
+
this.tipCommands = pickSlashCommandTips(pool, {
|
|
74
|
+
fixed: ["open-tui"],
|
|
75
|
+
count: 3,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
render(width: number): string[] {
|
|
80
|
+
const theme = this.ctx.ui.theme;
|
|
81
|
+
const paint = (s: string) => theme.fg("accent", s);
|
|
82
|
+
const muted = (s: string) => theme.fg("muted", s);
|
|
83
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
84
|
+
const bold = (s: string) => theme.bold(s);
|
|
85
|
+
|
|
86
|
+
if (width < 24) return [truncateToWidth(paint(`Pi v${VERSION}`), width, "")];
|
|
87
|
+
|
|
88
|
+
const innerWidth = width - 2;
|
|
89
|
+
const { leftWidth, rightWidth, useTips } = headerColumnWidths(innerWidth);
|
|
90
|
+
const model = formatModelLabel(this.ctx.model);
|
|
91
|
+
const effort = formatThinkingLabel(this.pi.getThinkingLevel());
|
|
92
|
+
const cwd = formatCwd(this.ctx.cwd);
|
|
93
|
+
|
|
94
|
+
const leftLines = [
|
|
95
|
+
...renderLogo(paint).map((line) => center(line, leftWidth)),
|
|
96
|
+
center(bold("Let's build something great"), leftWidth),
|
|
97
|
+
center(muted(`${model} · ${effort}`), leftWidth),
|
|
98
|
+
center(dim(cwd), leftWidth),
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const tipDivider = paint("─".repeat(Math.max(8, Math.min(rightWidth, 22))));
|
|
102
|
+
const [cmd0 = "", cmd1 = "", cmd2 = "", cmd3 = ""] = this.tipCommands;
|
|
103
|
+
const tipLines = [
|
|
104
|
+
"",
|
|
105
|
+
paint(bold("Welcome")),
|
|
106
|
+
muted("Ask Pi anything"),
|
|
107
|
+
tipDivider,
|
|
108
|
+
paint(bold("Commands")),
|
|
109
|
+
muted(cmd0),
|
|
110
|
+
muted(cmd1),
|
|
111
|
+
muted(cmd2),
|
|
112
|
+
muted(cmd3),
|
|
113
|
+
"",
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const lines = [borderLine("╭", `${paint("Pi")} v${VERSION}`, "╮", width, paint)];
|
|
117
|
+
for (let i = 0; i < leftLines.length; i++) {
|
|
118
|
+
const content = useTips
|
|
119
|
+
? twoColumn(leftLines[i] ?? "", tipLines[i] ?? "", leftWidth, rightWidth, paint)
|
|
120
|
+
: padRight(leftLines[i] ?? "", leftWidth);
|
|
121
|
+
lines.push(boxedLine(content, width, paint));
|
|
122
|
+
}
|
|
123
|
+
lines.push(borderLine("╰", "", "╯", width, paint));
|
|
124
|
+
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
invalidate(): void {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function installHeader(pi: ExtensionAPI, ctx: ExtensionContext): () => void {
|
|
131
|
+
ctx.ui.setHeader((tui) => new OpenTuiHeader(pi, ctx, tui));
|
|
132
|
+
return () => ctx.ui.setHeader(undefined);
|
|
133
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type OpenTuiConfig, DEFAULT_CONFIG, ensureConfigExists, loadConfig, saveConfig } from "./config.ts";
|
|
3
|
+
import { installCopyButtons, markCopyScope } from "./copy-buttons.ts";
|
|
3
4
|
import { installEditor } from "./editor.ts";
|
|
4
5
|
import { installFooter } from "./footer.ts";
|
|
5
6
|
import { installHeader } from "./header.ts";
|
|
@@ -32,12 +33,7 @@ export function getPendingUiChange(enabled: boolean, active: boolean): PendingUi
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function isTuiContext(ctx: ExtensionContext): boolean {
|
|
35
|
-
|
|
36
|
-
const mode = (ctx as ExtensionContext & { mode?: string }).mode;
|
|
37
|
-
return ctx.hasUI && (mode === undefined || mode === "tui");
|
|
38
|
-
} catch {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
36
|
+
return ctx.hasUI && ctx.mode === "tui";
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
export default function (pi: ExtensionAPI) {
|
|
@@ -54,6 +50,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
54
50
|
let cleanupHeader: (() => void) | undefined;
|
|
55
51
|
let cleanupFooter: (() => void) | undefined;
|
|
56
52
|
let editor: ReturnType<typeof installEditor> | undefined;
|
|
53
|
+
let copyButtons: ReturnType<typeof installCopyButtons> | undefined;
|
|
57
54
|
let pendingUiChange: PendingUiChange | undefined;
|
|
58
55
|
|
|
59
56
|
// thinking-peek state (rendered by Pi inside its hidden-thinking block)
|
|
@@ -65,15 +62,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
65
62
|
|
|
66
63
|
const getThinkingLevel = () => (sessionLifecycle.isCurrent() ? pi.getThinkingLevel() : "off");
|
|
67
64
|
|
|
68
|
-
/** True while the agent is running (false during session-restore replay). */
|
|
69
|
-
const isAgentIdle = (ctx: ExtensionContext): boolean => {
|
|
70
|
-
try {
|
|
71
|
-
return (ctx as ExtensionContext & { isIdle?: () => boolean }).isIdle?.() === true;
|
|
72
|
-
} catch {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
65
|
/** Pi decides whether this label is visible; our toggle only controls updates. */
|
|
78
66
|
const isPeekEnabled = () => sessionLifecycle.isCurrent() && config.enabled && config.thinkingPeek.lines > 0 && active;
|
|
79
67
|
|
|
@@ -131,6 +119,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
131
119
|
void scheduleGitRefresh(ctx);
|
|
132
120
|
},
|
|
133
121
|
getLiveTps: () => turnTelemetry.getLiveTps(),
|
|
122
|
+
canEmbedInEditor: () => editor?.isFramed() ?? false,
|
|
134
123
|
},
|
|
135
124
|
);
|
|
136
125
|
cleanupFooter = footer.cleanup;
|
|
@@ -143,6 +132,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
143
132
|
render: footer.renderInline,
|
|
144
133
|
},
|
|
145
134
|
);
|
|
135
|
+
copyButtons = installCopyButtons({
|
|
136
|
+
getTui: editor.getTui,
|
|
137
|
+
getTheme: () => ctx.ui.theme,
|
|
138
|
+
isEnabled: () => config.copyButtons,
|
|
139
|
+
});
|
|
146
140
|
active = true;
|
|
147
141
|
}
|
|
148
142
|
};
|
|
@@ -152,9 +146,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
152
146
|
if (active) {
|
|
153
147
|
cleanupHeader?.();
|
|
154
148
|
cleanupFooter?.();
|
|
149
|
+
copyButtons?.cleanup();
|
|
155
150
|
editor?.cleanup();
|
|
156
151
|
cleanupHeader = undefined;
|
|
157
152
|
cleanupFooter = undefined;
|
|
153
|
+
copyButtons = undefined;
|
|
158
154
|
editor = undefined;
|
|
159
155
|
requestFooterRender = undefined;
|
|
160
156
|
resetPeek();
|
|
@@ -224,7 +220,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
224
220
|
pi.on("session_start", async (_event, ctx) => {
|
|
225
221
|
sessionLifecycle.start();
|
|
226
222
|
lastCtx = ctx;
|
|
227
|
-
state.sessionStartEpoch = Date.now();
|
|
228
223
|
state.workingSince = undefined;
|
|
229
224
|
state.lastDoneIn = undefined;
|
|
230
225
|
invalidateUsageCache();
|
|
@@ -275,7 +270,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
275
270
|
|
|
276
271
|
pi.on("message_start", (event, ctx) => {
|
|
277
272
|
turnTelemetry.handle(event);
|
|
278
|
-
if (!sessionLifecycle.isCurrent() ||
|
|
273
|
+
if (!sessionLifecycle.isCurrent() || ctx.isIdle()) return;
|
|
279
274
|
const role = event.message?.role;
|
|
280
275
|
if (role === "user") {
|
|
281
276
|
// Restore Pi's native label until real thinking arrives. The task epoch is
|
|
@@ -290,7 +285,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
290
285
|
|
|
291
286
|
pi.on("message_update", (event, ctx) => {
|
|
292
287
|
turnTelemetry.handle(event);
|
|
293
|
-
if (!sessionLifecycle.isCurrent() ||
|
|
288
|
+
if (!sessionLifecycle.isCurrent() || ctx.isIdle()) return;
|
|
294
289
|
if (!isPeekEnabled()) return;
|
|
295
290
|
const message = event.message;
|
|
296
291
|
if (!message || message.role !== "assistant") return;
|
|
@@ -370,28 +365,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
370
365
|
refreshInteractiveState(ctx);
|
|
371
366
|
});
|
|
372
367
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
368
|
+
registerToolView(
|
|
369
|
+
pi,
|
|
370
|
+
() => {
|
|
371
|
+
// Resumed sessions rebuild their rows before session_start fires, so read the file once here.
|
|
372
|
+
if (!configLoaded) {
|
|
373
|
+
config = loadConfig();
|
|
374
|
+
configLoaded = true;
|
|
375
|
+
}
|
|
376
|
+
return config.enabled ? config.toolView.mode : "native";
|
|
377
|
+
},
|
|
378
|
+
() => resolveGlyphs(config.icons.mode),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
pi.registerMarkdownTransformer(markCopyScope);
|
|
387
382
|
|
|
388
383
|
registerSettingsCommand(pi, {
|
|
389
384
|
getConfig: () => config,
|
|
390
385
|
onConfigChanged: (newConfig) => {
|
|
391
386
|
const cursorStyleChanged = config.cursorStyle !== newConfig.cursorStyle;
|
|
392
387
|
const headerChanged = config.header !== newConfig.header;
|
|
388
|
+
const copyButtonsChanged = config.copyButtons !== newConfig.copyButtons;
|
|
393
389
|
const thinkingPeekLinesChanged = config.thinkingPeek.lines !== newConfig.thinkingPeek.lines;
|
|
394
|
-
saveConfig(newConfig)
|
|
390
|
+
if (!saveConfig(newConfig)) {
|
|
391
|
+
lastCtx?.ui.notify(newConfig.settingsLanguage === "zh"
|
|
392
|
+
? "open-tui 设置本次生效,但未保存到磁盘。请检查配置目录权限和可用空间。"
|
|
393
|
+
: "open-tui settings apply for this session but could not be saved. Check configuration directory permissions and free space.", "warning");
|
|
394
|
+
}
|
|
395
395
|
config = newConfig;
|
|
396
396
|
if (newConfig.thinkingPeek.lines === 0 || !newConfig.enabled) {
|
|
397
397
|
resetPeek();
|
|
@@ -406,6 +406,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
406
406
|
cleanupHeader?.();
|
|
407
407
|
cleanupHeader = newConfig.header ? installHeader(pi, lastCtx) : undefined;
|
|
408
408
|
}
|
|
409
|
+
if (copyButtonsChanged) copyButtons?.refresh();
|
|
409
410
|
if (lastCtx) {
|
|
410
411
|
pendingUiChange = getPendingUiChange(newConfig.enabled, active);
|
|
411
412
|
}
|
|
@@ -31,10 +31,9 @@ const RUNTIMES: readonly RuntimeDef[] = [
|
|
|
31
31
|
{ name: "swift", files: ["Package.swift"], versionCommand: { cmd: "swift", args: ["--version"], pattern: /Swift\s+(\d+\.\d+)/ } },
|
|
32
32
|
{ name: "kotlin", files: ["build.gradle.kts", "settings.gradle.kts"] },
|
|
33
33
|
{ name: "cpp", files: ["CMakeLists.txt", "Makefile"] },
|
|
34
|
-
{ name: "c", files: ["Makefile", "CMakeLists.txt"] },
|
|
35
34
|
{ name: "deno", files: ["deno.json", "deno.jsonc", "deno.lock"], versionCommand: { cmd: "deno", args: ["--version"], pattern: /deno\s+(\d+\.\d+\.\d+)/ } },
|
|
36
35
|
{ name: "php", files: ["composer.json"], versionCommand: { cmd: "php", args: ["--version"], pattern: /PHP\s+(\d+\.\d+\.\d+)/ } },
|
|
37
|
-
{ name: "haskell", files: ["stack.yaml", "cabal.project", ".cabal"], versionCommand: { cmd: "ghc", args: ["--version"], pattern: /(\d+\.\d+\.\d+)/ } },
|
|
36
|
+
{ name: "haskell", files: ["stack.yaml", "cabal.project"], extensions: [".cabal"], versionCommand: { cmd: "ghc", args: ["--version"], pattern: /(\d+\.\d+\.\d+)/ } },
|
|
38
37
|
{ name: "julia", files: ["Project.toml", "Manifest.toml"], versionCommand: { cmd: "julia", args: ["--version"], pattern: /julia\s+(\d+\.\d+\.\d+)/ } },
|
|
39
38
|
{ name: "lua", files: ["stylua.toml", ".luarc.json"], versionCommand: { cmd: "lua", args: ["-v"], pattern: /Lua\s+(\d+\.\d+)/ } },
|
|
40
39
|
{ name: "elixir", files: ["mix.exs"], versionCommand: { cmd: "elixir", args: ["--version"], pattern: /Elixir\s+(\d+\.\d+\.\d+)/ } },
|
|
@@ -42,13 +41,13 @@ const RUNTIMES: readonly RuntimeDef[] = [
|
|
|
42
41
|
{ name: "gleam", files: ["gleam.toml"], versionCommand: { cmd: "gleam", args: ["--version"], pattern: /gleam\s+(\d+\.\d+\.\d+)/ } },
|
|
43
42
|
{ name: "crystal", files: ["shard.yml"], versionCommand: { cmd: "crystal", args: ["--version"], pattern: /Crystal\s+(\d+\.\d+\.\d+)/ } },
|
|
44
43
|
{ name: "dart", files: ["pubspec.yaml"], versionCommand: { cmd: "dart", args: ["--version"], pattern: /Dart\s+SDK\s+version:\s+(\d+\.\d+\.\d+)/ } },
|
|
45
|
-
{ name: "nim", files: ["nim.cfg", ".nimble"] },
|
|
44
|
+
{ name: "nim", files: ["nim.cfg"], extensions: [".nimble"] },
|
|
46
45
|
{ name: "zig", files: ["build.zig"], versionCommand: { cmd: "zig", args: ["version"], pattern: /(\d+\.\d+\.\d+)/ } },
|
|
47
|
-
{ name: "ocaml", files: ["
|
|
46
|
+
{ name: "ocaml", files: ["dune", "dune-project"], extensions: [".opam"] },
|
|
48
47
|
{ name: "clojure", files: ["project.clj", "deps.edn"] },
|
|
49
|
-
{ name: "scala", files: ["build.sbt", ".
|
|
48
|
+
{ name: "scala", files: ["build.sbt", ".metals"], extensions: [".scala"] },
|
|
50
49
|
{ name: "perl", files: ["Makefile.PL", "cpanfile"] },
|
|
51
|
-
{ name: "r", files: ["
|
|
50
|
+
{ name: "r", files: ["DESCRIPTION"], extensions: [".Rproj"] },
|
|
52
51
|
{ name: "elm", files: ["elm.json"] },
|
|
53
52
|
{ name: "haxe", files: ["haxelib.json", ".haxerc"] },
|
|
54
53
|
{ name: "vagrant", files: ["Vagrantfile"] },
|
|
@@ -65,7 +64,6 @@ const RUNTIMES: readonly RuntimeDef[] = [
|
|
|
65
64
|
{ name: "v", files: ["v.mod", "vpkg.json"], extensions: [".v"] },
|
|
66
65
|
{ name: "xmake", files: ["xmake.lua"] },
|
|
67
66
|
{ name: "gradle", files: ["build.gradle", "build.gradle.kts"], folders: ["gradle"] },
|
|
68
|
-
{ name: "maven", files: ["pom.xml"] },
|
|
69
67
|
{ name: "cmake", files: ["CMakeLists.txt", "CMakeCache.txt"] },
|
|
70
68
|
{ name: "meson", files: ["meson.build"], env: "MESON_DEVENV" },
|
|
71
69
|
{ name: "nix", files: ["flake.nix", "shell.nix"], env: "IN_NIX_SHELL" },
|
|
@@ -76,7 +74,7 @@ const RUNTIMES: readonly RuntimeDef[] = [
|
|
|
76
74
|
{ name: "pulumi", files: ["Pulumi.yaml", "Pulumi.yml"] },
|
|
77
75
|
{ name: "typst", files: ["template.typ"], extensions: [".typ"] },
|
|
78
76
|
{ name: "buf", files: ["buf.yaml", "buf.gen.yaml", "buf.work.yaml"] },
|
|
79
|
-
{ name: "dotnet", files: [".
|
|
77
|
+
{ name: "dotnet", files: ["global.json", "Directory.Build.props"], extensions: [".csproj", ".fsproj"] },
|
|
80
78
|
{ name: "cobol", files: [], extensions: [".cbl", ".cob"] },
|
|
81
79
|
];
|
|
82
80
|
|
|
@@ -124,16 +122,17 @@ function matchesDef(cwd: string, def: RuntimeDef): boolean {
|
|
|
124
122
|
async function fetchVersion(def: RuntimeDef, cwd: string): Promise<string | undefined> {
|
|
125
123
|
if (!def.versionCommand) return undefined;
|
|
126
124
|
try {
|
|
127
|
-
const { stdout } = await execFileAsync(def.versionCommand.cmd, def.versionCommand.args ?? [], {
|
|
125
|
+
const { stdout, stderr } = await execFileAsync(def.versionCommand.cmd, def.versionCommand.args ?? [], {
|
|
128
126
|
cwd,
|
|
129
127
|
timeout: VERSION_TIMEOUT_MS,
|
|
130
128
|
maxBuffer: 64 * 1024,
|
|
131
129
|
});
|
|
130
|
+
const output = `${stdout}\n${stderr}`;
|
|
132
131
|
if (def.versionCommand.pattern) {
|
|
133
|
-
const match =
|
|
132
|
+
const match = output.match(def.versionCommand.pattern);
|
|
134
133
|
return match?.[1];
|
|
135
134
|
}
|
|
136
|
-
return
|
|
135
|
+
return output.trim() || undefined;
|
|
137
136
|
} catch {
|
|
138
137
|
return undefined;
|
|
139
138
|
}
|