pi-one-ui 0.7.0 → 0.7.1
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 +25 -1
- package/README.en.md +19 -6
- package/README.md +19 -6
- package/extensions/app/config/context.ts +42 -0
- package/extensions/app/config/editor.ts +80 -0
- package/extensions/app/config/footer.ts +291 -0
- package/extensions/app/config/panel.ts +1 -5
- package/extensions/app/config/renderer.ts +1 -6
- package/extensions/app/config/shell.ts +358 -1435
- package/extensions/app/config/store.ts +22 -1
- package/extensions/app/config/values.ts +33 -0
- package/extensions/app/config/working-line.ts +100 -0
- package/extensions/app/overlay/input-router.ts +56 -32
- package/extensions/app/overlay/overlay-manager.ts +183 -21
- package/extensions/app/runtime/event-coordinator.ts +90 -42
- package/extensions/{features → app/runtime}/flush-docked-bash.ts +12 -2
- package/extensions/app/runtime/render-scheduler.ts +35 -31
- package/extensions/app/runtime/tui-runtime.ts +315 -312
- package/extensions/app/settings/controller.ts +202 -0
- package/extensions/app/settings/panel.ts +81 -0
- package/extensions/app/settings/presets.ts +43 -0
- package/extensions/features/context-inspector/index.ts +83 -662
- package/extensions/layouts/context/index.ts +58 -161
- package/extensions/layouts/context/message/user-message.ts +1 -1
- package/extensions/layouts/context/renderer/index.ts +31 -13
- package/extensions/layouts/context/renderer/mouse/interaction.ts +29 -14
- package/extensions/layouts/editor/controller.ts +9 -18
- package/extensions/layouts/editor/factory.ts +3 -3
- package/extensions/layouts/footer/controller.ts +30 -19
- package/extensions/layouts/footer/footer.ts +2 -2
- package/extensions/layouts/overlay/context-inspector.ts +497 -0
- package/extensions/{app → layouts}/overlay/selector-border.ts +5 -5
- package/extensions/{app → layouts}/overlay/selector-controller.ts +1 -1
- package/extensions/layouts/overlay/settings-panel.ts +359 -0
- package/extensions/{app/commands → layouts/overlay}/settings-previews.ts +4 -4
- package/extensions/layouts/working-line/controller.ts +3 -29
- package/extensions/layouts/working-line/working-line.ts +17 -114
- package/extensions/shared/working-line-text.ts +108 -0
- package/package.json +1 -1
- package/extensions/app/host/pi-extension-port.ts +0 -25
- package/extensions/app/host/pi-ui-port.ts +0 -52
- package/extensions/app/host/tui-capabilities.ts +0 -27
- package/extensions/app/ownership/layout-registry.ts +0 -65
- package/extensions/app/ownership.ts +0 -32
- package/extensions/app/panel.ts +0 -770
- package/extensions/app/presets.ts +0 -35
- package/extensions/app/runtime/runtime-state.ts +0 -83
- /package/extensions/{layouts/working-line → app/config}/working-line-messages.ts +0 -0
- /package/extensions/app/{ownership → runtime}/prototype-patch-registry.ts +0 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizePanelOverlay,
|
|
3
|
+
type PanelOverlayConfig,
|
|
4
|
+
} from "../config/panel.ts";
|
|
5
|
+
import { type Config, normalizeConfig } from "../config/renderer.ts";
|
|
6
|
+
import {
|
|
7
|
+
mergeConfig,
|
|
8
|
+
mutateComponentsRecord,
|
|
9
|
+
type ZentuiConfig,
|
|
10
|
+
} from "../config/shell.ts";
|
|
11
|
+
import {
|
|
12
|
+
type ConfigRecord,
|
|
13
|
+
type ConfigStore,
|
|
14
|
+
configStore,
|
|
15
|
+
} from "../config/store.ts";
|
|
16
|
+
import { recordValue } from "../config/values.ts";
|
|
17
|
+
import { applyPresetRecord, isPreset } from "./presets.ts";
|
|
18
|
+
|
|
19
|
+
export type SettingsChange = Readonly<{ id: string; value: string }>;
|
|
20
|
+
|
|
21
|
+
export type SettingsSnapshot = Readonly<{
|
|
22
|
+
shell: ZentuiConfig;
|
|
23
|
+
renderer: Config;
|
|
24
|
+
panel: PanelOverlayConfig;
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
export function settingsSnapshot(record: ConfigRecord): SettingsSnapshot {
|
|
28
|
+
return {
|
|
29
|
+
shell: mergeConfig(record),
|
|
30
|
+
renderer: normalizeConfig(recordValue(record.renderer)),
|
|
31
|
+
panel: normalizePanelOverlay(record.panel),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const booleanSettings = [
|
|
36
|
+
"enableSessionReference",
|
|
37
|
+
"enableSubagentAutocomplete",
|
|
38
|
+
"enableContextCommand",
|
|
39
|
+
"enableAgentSummary",
|
|
40
|
+
"enableAliases",
|
|
41
|
+
"enableEffortCommand",
|
|
42
|
+
"showStartupHeader",
|
|
43
|
+
"diffWordWrap",
|
|
44
|
+
"dimThinkingText",
|
|
45
|
+
] as const;
|
|
46
|
+
|
|
47
|
+
/** 设置命令只修改配置;运行时订阅负责应用,视图只提交用户选择。 */
|
|
48
|
+
export class SettingsController {
|
|
49
|
+
private change: SettingsChange | undefined;
|
|
50
|
+
|
|
51
|
+
constructor(readonly store: ConfigStore = configStore) {}
|
|
52
|
+
|
|
53
|
+
snapshot(): SettingsSnapshot {
|
|
54
|
+
return settingsSnapshot(this.store.read());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
subscribe(
|
|
58
|
+
listener: (snapshot: SettingsSnapshot, change?: SettingsChange) => void,
|
|
59
|
+
): () => void {
|
|
60
|
+
return this.store.subscribe((record) =>
|
|
61
|
+
listener(settingsSnapshot(record), this.change),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
update(id: string, value: string): SettingsSnapshot {
|
|
66
|
+
const previous = this.change;
|
|
67
|
+
this.change = { id, value };
|
|
68
|
+
try {
|
|
69
|
+
return this.commit(id, value);
|
|
70
|
+
} finally {
|
|
71
|
+
this.change = previous;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private commit(id: string, value: string): SettingsSnapshot {
|
|
76
|
+
const record = this.store.update((record) => {
|
|
77
|
+
record.version = 1;
|
|
78
|
+
if (id.startsWith("preset:")) {
|
|
79
|
+
const preset = id.slice("preset:".length);
|
|
80
|
+
if (!isPreset(preset)) {
|
|
81
|
+
throw new Error(`Unknown preset: ${preset}`);
|
|
82
|
+
}
|
|
83
|
+
applyPresetRecord(record, preset);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (booleanSettings.some((key) => key === id)) {
|
|
87
|
+
this.updateRenderer(record, { [id]: this.boolean(value) });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
switch (id) {
|
|
91
|
+
case "editorStyle":
|
|
92
|
+
this.enum(value, ["on", "off"]);
|
|
93
|
+
mutateComponentsRecord(
|
|
94
|
+
record,
|
|
95
|
+
(components) => {
|
|
96
|
+
components.editor.style = value;
|
|
97
|
+
},
|
|
98
|
+
["editor"],
|
|
99
|
+
);
|
|
100
|
+
break;
|
|
101
|
+
case "editorBorderColorMode":
|
|
102
|
+
this.enum(value, ["static", "adaptive"]);
|
|
103
|
+
mutateComponentsRecord(record, (components) => {
|
|
104
|
+
components.editor.borderColorMode = value;
|
|
105
|
+
});
|
|
106
|
+
break;
|
|
107
|
+
case "userMessagesEnabled":
|
|
108
|
+
mutateComponentsRecord(record, (components) => {
|
|
109
|
+
components.userMessages.enabled = this.boolean(value);
|
|
110
|
+
});
|
|
111
|
+
break;
|
|
112
|
+
case "userMessagesStyle":
|
|
113
|
+
this.enum(value, [
|
|
114
|
+
"framed",
|
|
115
|
+
"framed-copy-friendly",
|
|
116
|
+
"compact",
|
|
117
|
+
"labeled",
|
|
118
|
+
]);
|
|
119
|
+
mutateComponentsRecord(
|
|
120
|
+
record,
|
|
121
|
+
(components) => {
|
|
122
|
+
components.userMessages.style = value;
|
|
123
|
+
},
|
|
124
|
+
["userMessages"],
|
|
125
|
+
);
|
|
126
|
+
break;
|
|
127
|
+
case "workingLineEnabled":
|
|
128
|
+
mutateComponentsRecord(record, (components) => {
|
|
129
|
+
components.workingLine.enabled = this.boolean(value);
|
|
130
|
+
});
|
|
131
|
+
break;
|
|
132
|
+
case "workingLineSpinner":
|
|
133
|
+
this.enum(value, [
|
|
134
|
+
"braille",
|
|
135
|
+
"star-bloom",
|
|
136
|
+
"pinwheel",
|
|
137
|
+
"claude-inspired",
|
|
138
|
+
"pulse",
|
|
139
|
+
]);
|
|
140
|
+
mutateComponentsRecord(record, (components) => {
|
|
141
|
+
components.workingLine.spinner = value;
|
|
142
|
+
});
|
|
143
|
+
break;
|
|
144
|
+
case "footerStyle":
|
|
145
|
+
this.enum(value, ["starship", "native", "hidden"]);
|
|
146
|
+
mutateComponentsRecord(
|
|
147
|
+
record,
|
|
148
|
+
(components) => {
|
|
149
|
+
components.footer.style = value;
|
|
150
|
+
},
|
|
151
|
+
["footer"],
|
|
152
|
+
);
|
|
153
|
+
break;
|
|
154
|
+
case "contextMode":
|
|
155
|
+
this.enum(value, ["on", "compact", "off"]);
|
|
156
|
+
this.updateRenderer(record, { mode: value });
|
|
157
|
+
break;
|
|
158
|
+
case "diffViewMode":
|
|
159
|
+
this.enum(value, ["auto", "split", "unified"]);
|
|
160
|
+
this.updateRenderer(record, { diffViewMode: value });
|
|
161
|
+
break;
|
|
162
|
+
case "diffIndicatorMode":
|
|
163
|
+
this.enum(value, ["bars", "classic", "none"]);
|
|
164
|
+
this.updateRenderer(record, { diffIndicatorMode: value });
|
|
165
|
+
break;
|
|
166
|
+
case "thinkingPreviewLines": {
|
|
167
|
+
const count = Number(value);
|
|
168
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
169
|
+
throw new Error(`Invalid preview line count: ${value}`);
|
|
170
|
+
}
|
|
171
|
+
this.updateRenderer(record, { previewLines: count });
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
default:
|
|
175
|
+
throw new Error(`Unknown setting: ${id}`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
return settingsSnapshot(record);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private updateRenderer(record: ConfigRecord, patch: Partial<Config>): void {
|
|
182
|
+
const current = recordValue(record.renderer);
|
|
183
|
+
record.renderer = {
|
|
184
|
+
...current,
|
|
185
|
+
...normalizeConfig({ ...current, ...patch }),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private boolean(value: string): boolean {
|
|
190
|
+
this.enum(value, ["on", "off"]);
|
|
191
|
+
return value === "on";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private enum<T extends string>(
|
|
195
|
+
value: string,
|
|
196
|
+
values: readonly T[],
|
|
197
|
+
): asserts value is T {
|
|
198
|
+
if (!(values as readonly string[]).includes(value)) {
|
|
199
|
+
throw new Error(`Invalid setting value: ${value}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { createSettingsPanel } from "../../layouts/overlay/settings-panel.ts";
|
|
3
|
+
import { ConfigApplicationError } from "../config/store.ts";
|
|
4
|
+
import {
|
|
5
|
+
type OverlayManager,
|
|
6
|
+
overlayManager,
|
|
7
|
+
} from "../overlay/overlay-manager.ts";
|
|
8
|
+
import { SettingsController, settingsSnapshot } from "./controller.ts";
|
|
9
|
+
|
|
10
|
+
export type SettingsPanelController = Pick<
|
|
11
|
+
SettingsController,
|
|
12
|
+
"snapshot" | "update"
|
|
13
|
+
>;
|
|
14
|
+
type PanelDependencies = {
|
|
15
|
+
settings?: SettingsPanelController;
|
|
16
|
+
overlays?: OverlayManager;
|
|
17
|
+
};
|
|
18
|
+
const openPanels = new WeakMap<OverlayManager, object>();
|
|
19
|
+
|
|
20
|
+
/** 命令负责配置提交和通知,OverlayManager 负责窗口与 Editor 焦点。 */
|
|
21
|
+
export async function showOneUiPanel(
|
|
22
|
+
ctx: ExtensionCommandContext,
|
|
23
|
+
dependencies: PanelDependencies = {},
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
if (ctx.mode !== "tui" || !ctx.hasUI || typeof ctx.ui.custom !== "function") {
|
|
26
|
+
ctx.ui.notify("/oneui requires TUI mode", "warning");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const overlays = dependencies.overlays ?? overlayManager;
|
|
30
|
+
if (openPanels.has(overlays) && overlays.hasActive()) {
|
|
31
|
+
ctx.ui.notify("/oneui panel is already open", "info");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const settings = dependencies.settings ?? new SettingsController();
|
|
35
|
+
let snapshot = settings.snapshot();
|
|
36
|
+
const token = {};
|
|
37
|
+
openPanels.set(overlays, token);
|
|
38
|
+
try {
|
|
39
|
+
await overlays.open(
|
|
40
|
+
ctx,
|
|
41
|
+
(tui, theme, _keybindings, done) =>
|
|
42
|
+
createSettingsPanel(tui, theme, () => done(undefined), {
|
|
43
|
+
snapshot: () => snapshot,
|
|
44
|
+
change(id, value) {
|
|
45
|
+
const apply = () => {
|
|
46
|
+
snapshot = settings.update(id, value);
|
|
47
|
+
ctx.ui.notify(
|
|
48
|
+
`${id}: ${value} saved; live preview updated when supported.`,
|
|
49
|
+
"info",
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
if (
|
|
53
|
+
id === "editorStyle" ||
|
|
54
|
+
id === "editorBorderColorMode" ||
|
|
55
|
+
id.startsWith("preset:")
|
|
56
|
+
) {
|
|
57
|
+
overlays.updateEditor(apply);
|
|
58
|
+
} else {
|
|
59
|
+
apply();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
onError(error) {
|
|
63
|
+
if (error instanceof ConfigApplicationError) {
|
|
64
|
+
snapshot = settingsSnapshot(error.record);
|
|
65
|
+
ctx.ui.notify(error.message, "error");
|
|
66
|
+
} else {
|
|
67
|
+
ctx.ui.notify(
|
|
68
|
+
`Could not update /oneui setting: ${error instanceof Error ? error.message : String(error)}`,
|
|
69
|
+
"error",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
{ overlayOptions: snapshot.panel, preserveEditorFocus: true },
|
|
75
|
+
);
|
|
76
|
+
} finally {
|
|
77
|
+
if (openPanels.get(overlays) === token) {
|
|
78
|
+
openPanels.delete(overlays);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { normalizeConfig } from "../config/renderer.ts";
|
|
2
|
+
import { mutateComponentsRecord } from "../config/shell.ts";
|
|
3
|
+
import {
|
|
4
|
+
type ConfigRecord,
|
|
5
|
+
type ConfigStore,
|
|
6
|
+
configStore,
|
|
7
|
+
} from "../config/store.ts";
|
|
8
|
+
import { recordValue } from "../config/values.ts";
|
|
9
|
+
|
|
10
|
+
export type Preset = "balanced" | "compact" | "native";
|
|
11
|
+
export const PRESET_VALUES: Preset[] = ["balanced", "compact", "native"];
|
|
12
|
+
export function isPreset(value: string): value is Preset {
|
|
13
|
+
return PRESET_VALUES.includes(value as Preset);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Preset 在同一个配置记录中更新所有组件。 */
|
|
17
|
+
export function applyPresetRecord(record: ConfigRecord, preset: Preset): void {
|
|
18
|
+
const enabled = preset !== "native";
|
|
19
|
+
mutateComponentsRecord(
|
|
20
|
+
record,
|
|
21
|
+
(components) => {
|
|
22
|
+
components.editor.style = enabled ? "on" : "off";
|
|
23
|
+
components.userMessages.enabled = enabled;
|
|
24
|
+
components.workingLine.enabled = enabled;
|
|
25
|
+
components.footer.style = enabled ? "starship" : "native";
|
|
26
|
+
},
|
|
27
|
+
["editor", "footer"],
|
|
28
|
+
);
|
|
29
|
+
record.renderer = {
|
|
30
|
+
...recordValue(record.renderer),
|
|
31
|
+
...normalizeConfig({
|
|
32
|
+
...recordValue(record.renderer),
|
|
33
|
+
mode: preset === "compact" ? "compact" : enabled ? "on" : "off",
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function applyPreset(
|
|
39
|
+
preset: Preset,
|
|
40
|
+
store: ConfigStore = configStore,
|
|
41
|
+
): void {
|
|
42
|
+
store.update((record) => applyPresetRecord(record, preset));
|
|
43
|
+
}
|