@narumitw/pi-sync 0.35.0 → 0.38.0
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 +14 -2
- package/package.json +2 -1
- package/src/file-selection.ts +88 -73
- package/src/manager-state.ts +155 -0
- package/src/manager-ui.ts +230 -289
- package/src/settings-ui.ts +103 -145
- package/src/storage-connections-ui.ts +157 -83
- package/src/sync-setups-ui.ts +186 -99
package/src/settings-ui.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
getSettingsListTheme,
|
|
4
|
-
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
6
3
|
import { loadConfig, localConfigPath } from "./config.js";
|
|
7
4
|
import { updateSyncSetup } from "./settings-management.js";
|
|
8
5
|
import {
|
|
@@ -30,149 +27,110 @@ export async function showSyncSettings(
|
|
|
30
27
|
ctx.ui.notify(`Edit pi-sync settings manually: ${safeTerminalText(localConfigPath())}`, "info");
|
|
31
28
|
return;
|
|
32
29
|
}
|
|
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
|
-
const onAbort = () => {
|
|
104
|
-
if (!closing && !disposed) done(undefined);
|
|
105
|
-
};
|
|
106
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
107
|
-
settingsList = new SettingsList(
|
|
108
|
-
items,
|
|
109
|
-
Math.min(items.length + 2, 15),
|
|
110
|
-
getSettingsListTheme(),
|
|
111
|
-
(id, newValue) => {
|
|
112
|
-
if (id === "include") {
|
|
113
|
-
closeAfterSaves("files");
|
|
114
|
-
return;
|
|
30
|
+
const initial = await loadConfig();
|
|
31
|
+
if (signal?.aborted) return;
|
|
32
|
+
const setupName = initial.setupName;
|
|
33
|
+
type Action = "automatic" | "on-switch" | "include";
|
|
34
|
+
const menu = defineMenu<
|
|
35
|
+
Awaited<ReturnType<typeof loadConfig>>,
|
|
36
|
+
"settings",
|
|
37
|
+
Action,
|
|
38
|
+
ExtensionCommandContext
|
|
39
|
+
>({
|
|
40
|
+
start: "settings",
|
|
41
|
+
screens: {
|
|
42
|
+
settings: ({ state }) => ({
|
|
43
|
+
kind: "settings",
|
|
44
|
+
title: "Pi Sync Settings",
|
|
45
|
+
lines: [
|
|
46
|
+
`Sync setup: ${safeTerminalText(state.setupName)} · Storage connection: ${safeTerminalText(state.connectionName)}`,
|
|
47
|
+
],
|
|
48
|
+
items: [
|
|
49
|
+
{
|
|
50
|
+
id: "automatic",
|
|
51
|
+
label: "Automatic sync",
|
|
52
|
+
description: "Run conservative synchronization at session startup and shutdown.",
|
|
53
|
+
currentValue: state.automatic ? "On" : "Off",
|
|
54
|
+
values: ["On", "Off"],
|
|
55
|
+
action: "automatic",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "onSwitch",
|
|
59
|
+
label: "After switching setup",
|
|
60
|
+
description:
|
|
61
|
+
"Ask before a reviewed pull, start a reviewed pull, or switch without checking remote files.",
|
|
62
|
+
currentValue: setupSwitchActionLabel(state.onSwitch),
|
|
63
|
+
values: SETUP_SWITCH_ACTION_OPTIONS.map(({ label }) => label),
|
|
64
|
+
action: "on-switch",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "include",
|
|
68
|
+
label: "Included content",
|
|
69
|
+
description: `${state.include.length} selected path${state.include.length === 1 ? "" : "s"}. Opens the reviewed content-selection draft.`,
|
|
70
|
+
currentValue: "Open editor",
|
|
71
|
+
action: "include",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
actions: {
|
|
77
|
+
automatic: async ({ value, signal: actionSignal }) => {
|
|
78
|
+
const automatic = value === "On";
|
|
79
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
80
|
+
try {
|
|
81
|
+
const latest = await loadConfig(setupName);
|
|
82
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
83
|
+
if (latest.automatic === automatic) return { kind: "stay" };
|
|
84
|
+
await updateSyncSetup(
|
|
85
|
+
setupName,
|
|
86
|
+
(setup) => ({ ...setup, sync: { ...setup.sync, automatic } }),
|
|
87
|
+
{ signal: mutationSignal },
|
|
88
|
+
);
|
|
89
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
90
|
+
ctx.ui.notify(
|
|
91
|
+
`Automatic sync ${automatic ? "enabled" : "disabled"} for “${safeTerminalText(setupName)}”.`,
|
|
92
|
+
"info",
|
|
93
|
+
);
|
|
94
|
+
return { kind: "stay" };
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
97
|
+
return { kind: "rejected" };
|
|
115
98
|
}
|
|
116
|
-
latestRequested.set(id, newValue);
|
|
117
|
-
const fallback = id === "automatic" ? automaticValue : switchValue;
|
|
118
|
-
const operation = saveQueue.then(async () => {
|
|
119
|
-
if (disposed || signal?.aborted) return;
|
|
120
|
-
let previousValue = fallback;
|
|
121
|
-
try {
|
|
122
|
-
const latest = await loadConfig(config.setupName);
|
|
123
|
-
if (disposed || signal?.aborted) return;
|
|
124
|
-
if (id === "automatic") {
|
|
125
|
-
previousValue = latest.automatic ? "On" : "Off";
|
|
126
|
-
const automatic = newValue === "On";
|
|
127
|
-
await updateSyncSetup(
|
|
128
|
-
config.setupName,
|
|
129
|
-
(setup) => ({
|
|
130
|
-
...setup,
|
|
131
|
-
sync: { ...setup.sync, automatic },
|
|
132
|
-
}),
|
|
133
|
-
{ signal: mutationSignal },
|
|
134
|
-
);
|
|
135
|
-
if (disposed || signal?.aborted) return;
|
|
136
|
-
ctx.ui.notify(
|
|
137
|
-
`Automatic sync ${automatic ? "enabled" : "disabled"} for “${safeTerminalText(config.setupName)}”.`,
|
|
138
|
-
"info",
|
|
139
|
-
);
|
|
140
|
-
} else {
|
|
141
|
-
previousValue = setupSwitchActionLabel(latest.onSwitch);
|
|
142
|
-
const action = setupSwitchActionFromLabel(newValue);
|
|
143
|
-
if (!action) throw new Error(`Invalid setup-switch action: ${newValue}`);
|
|
144
|
-
await saveOnSwitch(action, mutationSignal);
|
|
145
|
-
if (disposed || signal?.aborted) return;
|
|
146
|
-
ctx.ui.notify(`After switching setup: ${newValue}.`, "info");
|
|
147
|
-
}
|
|
148
|
-
} catch (error) {
|
|
149
|
-
if (disposed || signal?.aborted) return;
|
|
150
|
-
if (latestRequested.get(id) === newValue) settingsList.updateValue(id, previousValue);
|
|
151
|
-
ctx.ui.notify(
|
|
152
|
-
`Pi Sync settings save failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
153
|
-
"error",
|
|
154
|
-
);
|
|
155
|
-
tui.requestRender();
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
saveQueue = operation.catch(() => undefined);
|
|
159
99
|
},
|
|
160
|
-
() =>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
100
|
+
"on-switch": async ({ value, signal: actionSignal }) => {
|
|
101
|
+
const action = value ? setupSwitchActionFromLabel(value) : undefined;
|
|
102
|
+
if (!action) return { kind: "rejected" };
|
|
103
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
104
|
+
try {
|
|
105
|
+
const latest = await loadConfig(setupName);
|
|
106
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
107
|
+
if (latest.onSwitch === action) return { kind: "stay" };
|
|
108
|
+
await saveOnSwitch(action, mutationSignal);
|
|
109
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
110
|
+
ctx.ui.notify(`After switching setup: ${value}.`, "info");
|
|
111
|
+
return { kind: "stay" };
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
114
|
+
return { kind: "rejected" };
|
|
115
|
+
}
|
|
170
116
|
},
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
117
|
+
include: async ({ signal: actionSignal }) => {
|
|
118
|
+
const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
119
|
+
await runRoute("files", editorSignal, undefined, setupName);
|
|
120
|
+
return editorSignal.aborted ? { kind: "rejected" } : { kind: "stay" };
|
|
175
121
|
},
|
|
176
|
-
}
|
|
122
|
+
},
|
|
177
123
|
});
|
|
124
|
+
await runMenu(ctx, menu, {
|
|
125
|
+
getState: () => loadConfig(setupName),
|
|
126
|
+
signal,
|
|
127
|
+
isCurrent: () => !signal?.aborted,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function notifySaveFailure(ctx: ExtensionCommandContext, error: unknown) {
|
|
132
|
+
ctx.ui.notify(
|
|
133
|
+
`Pi Sync settings save failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
134
|
+
"error",
|
|
135
|
+
);
|
|
178
136
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
2
3
|
import { isCloudflareR2Endpoint, readLocalConfigObject } from "./config.js";
|
|
3
4
|
import { showAddGitStorageProfile, showEditGitStorageProfile } from "./git-ui.js";
|
|
4
5
|
import { errorMessage, ownRecord, requiredInput, safeTerminalText } from "./manager-helpers.js";
|
|
@@ -14,64 +15,151 @@ import {
|
|
|
14
15
|
} from "./settings-management.js";
|
|
15
16
|
import { showAddWebDavStorageProfile, showEditWebDavStorageProfile } from "./webdav-ui.js";
|
|
16
17
|
|
|
17
|
-
const BACK = "Back";
|
|
18
|
-
|
|
19
18
|
export async function showStorageConnections(ctx: ExtensionCommandContext, signal?: AbortSignal) {
|
|
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
|
-
|
|
19
|
+
let selectedName: string | undefined;
|
|
20
|
+
const nameById = new Map<string, string>();
|
|
21
|
+
type Screen = "list" | "detail";
|
|
22
|
+
type Action = "add" | "select" | "edit" | "remove" | "back";
|
|
23
|
+
const menu = defineMenu<
|
|
24
|
+
Awaited<ReturnType<typeof loadStorageMenuState>>,
|
|
25
|
+
Screen,
|
|
26
|
+
Action,
|
|
27
|
+
ExtensionCommandContext
|
|
28
|
+
>({
|
|
29
|
+
start: "list",
|
|
30
|
+
screens: {
|
|
31
|
+
list: ({ state }) => {
|
|
32
|
+
nameById.clear();
|
|
33
|
+
const names = Object.keys(state.profiles).sort((left, right) => left.localeCompare(right));
|
|
34
|
+
return {
|
|
35
|
+
kind: "actions",
|
|
36
|
+
title: "Storage connections",
|
|
37
|
+
lines: state.version3
|
|
38
|
+
? []
|
|
39
|
+
: ["Create version 3 settings before managing storage connections."],
|
|
40
|
+
items: state.version3
|
|
41
|
+
? [
|
|
42
|
+
{ id: "add", label: "Add storage connection", action: "add" as const },
|
|
43
|
+
...names.map((name, index) => {
|
|
44
|
+
const id = `connection:${index}`;
|
|
45
|
+
nameById.set(id, name);
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
label: safeTerminalText(name),
|
|
49
|
+
action: "select" as const,
|
|
50
|
+
};
|
|
51
|
+
}),
|
|
52
|
+
]
|
|
53
|
+
: [],
|
|
54
|
+
hint: "back",
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
detail: ({ state }) => ({
|
|
58
|
+
kind: "actions",
|
|
59
|
+
title: state.selected
|
|
60
|
+
? `Storage connection “${safeTerminalText(state.selected.name)}”`
|
|
61
|
+
: "Storage connection",
|
|
62
|
+
lines: state.selected?.lines ?? ["This storage connection no longer exists."],
|
|
63
|
+
items: state.selected
|
|
64
|
+
? [
|
|
65
|
+
{ id: "edit", label: "Edit storage connection…", action: "edit" },
|
|
66
|
+
...(state.selected.usedBy.length === 0
|
|
67
|
+
? [
|
|
68
|
+
{
|
|
69
|
+
id: "remove",
|
|
70
|
+
label: "Remove storage connection…",
|
|
71
|
+
action: "remove" as const,
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
: []),
|
|
75
|
+
{ id: "back", label: "Back", action: "back" },
|
|
76
|
+
]
|
|
77
|
+
: [{ id: "back", label: "Back", action: "back" }],
|
|
78
|
+
hint: "back",
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
actions: {
|
|
82
|
+
add: async () => {
|
|
83
|
+
try {
|
|
84
|
+
await showAddStorageConnection(ctx, signal);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (!signal?.aborted) {
|
|
87
|
+
ctx.ui.notify(
|
|
88
|
+
`Storage connection was not added: ${safeTerminalText(errorMessage(error))} Retry from Add storage connection.`,
|
|
89
|
+
"error",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { kind: "stay" };
|
|
94
|
+
},
|
|
95
|
+
select: async ({ itemId }) => {
|
|
96
|
+
selectedName = nameById.get(itemId);
|
|
97
|
+
return selectedName ? { kind: "to", screen: "detail" } : { kind: "rejected" };
|
|
98
|
+
},
|
|
99
|
+
edit: async ({ state }) => {
|
|
100
|
+
if (!state.selected || state.selected.name !== selectedName) return { kind: "rejected" };
|
|
101
|
+
try {
|
|
102
|
+
await editStorageConnection(
|
|
103
|
+
ctx,
|
|
104
|
+
state.selected.name,
|
|
105
|
+
state.selected.profile,
|
|
106
|
+
state.selected.usedBy,
|
|
107
|
+
signal,
|
|
108
|
+
);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
notifyConnectionError(ctx, state.selected.name, error, signal);
|
|
111
|
+
}
|
|
112
|
+
return { kind: "stay" };
|
|
113
|
+
},
|
|
114
|
+
remove: async ({ state }) => {
|
|
115
|
+
if (!state.selected || state.selected.name !== selectedName) return { kind: "rejected" };
|
|
116
|
+
const name = state.selected.name;
|
|
117
|
+
const confirmed = await ctx.ui.confirm(
|
|
118
|
+
"Remove storage connection?",
|
|
119
|
+
`Remove local storage connection “${safeTerminalText(name)}”? Remote data and history are not deleted.`,
|
|
120
|
+
{ signal },
|
|
47
121
|
);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
122
|
+
if (!confirmed || signal?.aborted) return { kind: "rejected" };
|
|
123
|
+
try {
|
|
124
|
+
await removeStorageConnection(name, signal);
|
|
125
|
+
ctx.ui.notify(`Removed storage connection “${safeTerminalText(name)}”.`, "info");
|
|
126
|
+
selectedName = undefined;
|
|
127
|
+
return { kind: "back" };
|
|
128
|
+
} catch (error) {
|
|
129
|
+
notifyConnectionError(ctx, name, error, signal);
|
|
130
|
+
return { kind: "stay" };
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
back: async () => {
|
|
134
|
+
selectedName = undefined;
|
|
135
|
+
return { kind: "back" };
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
await runMenu(ctx, menu, {
|
|
140
|
+
getState: () => loadStorageMenuState(selectedName, signal),
|
|
141
|
+
signal,
|
|
142
|
+
isCurrent: () => !signal?.aborted,
|
|
143
|
+
});
|
|
54
144
|
}
|
|
55
145
|
|
|
56
|
-
async function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
[
|
|
73
|
-
`Storage connection “${safeTerminalText(name)}”`,
|
|
74
|
-
"",
|
|
146
|
+
async function loadStorageMenuState(selectedName: string | undefined, signal?: AbortSignal) {
|
|
147
|
+
const raw = await readLocalConfigObject();
|
|
148
|
+
if (signal?.aborted) throw signal.reason;
|
|
149
|
+
const profiles = ownRecord(raw?.storageConnections) ?? {};
|
|
150
|
+
const profile = selectedName ? ownRecord(profiles[selectedName]) : undefined;
|
|
151
|
+
if (!selectedName || !profile) {
|
|
152
|
+
return { version3: raw?.version === 3, profiles, selected: undefined };
|
|
153
|
+
}
|
|
154
|
+
const usedBy = referencingSetups(raw, selectedName);
|
|
155
|
+
return {
|
|
156
|
+
version3: raw?.version === 3,
|
|
157
|
+
profiles,
|
|
158
|
+
selected: {
|
|
159
|
+
name: selectedName,
|
|
160
|
+
profile,
|
|
161
|
+
usedBy,
|
|
162
|
+
lines: [
|
|
75
163
|
`Type: ${connectionType(profile)}`,
|
|
76
164
|
`Endpoint: ${connectionEndpoint(profile)}`,
|
|
77
165
|
`Credentials: ${credentialSource(profile)}`,
|
|
@@ -79,36 +167,22 @@ async function showStorageConnectionDetail(
|
|
|
79
167
|
...(usedBy.length > 0
|
|
80
168
|
? ["Remove unavailable: edit or remove the listed sync setups first."]
|
|
81
169
|
: []),
|
|
82
|
-
].join("\n"),
|
|
83
|
-
[
|
|
84
|
-
"Edit storage connection…",
|
|
85
|
-
...(usedBy.length === 0 ? ["Remove storage connection…"] : []),
|
|
86
|
-
BACK,
|
|
87
170
|
],
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
await editStorageConnection(ctx, name, profile, usedBy, signal);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
if (signal?.aborted) return;
|
|
106
|
-
ctx.ui.notify(
|
|
107
|
-
`Storage connection “${safeTerminalText(name)}” was not changed: ${safeTerminalText(errorMessage(error))} Reopen it and retry.`,
|
|
108
|
-
"error",
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function notifyConnectionError(
|
|
176
|
+
ctx: ExtensionCommandContext,
|
|
177
|
+
name: string,
|
|
178
|
+
error: unknown,
|
|
179
|
+
signal?: AbortSignal,
|
|
180
|
+
) {
|
|
181
|
+
if (signal?.aborted) return;
|
|
182
|
+
ctx.ui.notify(
|
|
183
|
+
`Storage connection “${safeTerminalText(name)}” was not changed: ${safeTerminalText(errorMessage(error))} Reopen it and retry.`,
|
|
184
|
+
"error",
|
|
185
|
+
);
|
|
112
186
|
}
|
|
113
187
|
|
|
114
188
|
async function editStorageConnection(
|