clawtool 0.1.1 → 0.1.3
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 -1
- package/dist/index.js +1 -1
- package/dist/loop.js +0 -11
- package/dist/planner.js +1 -1
- package/package.json +1 -1
- package/web/index.html +205 -163
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -59,7 +59,7 @@ async function main() {
|
|
|
59
59
|
const requestedPort = portArgIndex > -1 ? Number(process.argv[portArgIndex + 1]) : Number.NaN;
|
|
60
60
|
const port = Number.isFinite(requestedPort) && requestedPort > 0
|
|
61
61
|
? requestedPort
|
|
62
|
-
: await (0, get_port_1.default)({ port: (0, get_port_1.portNumbers)(
|
|
62
|
+
: await (0, get_port_1.default)({ port: (0, get_port_1.portNumbers)(9527, 9600) });
|
|
63
63
|
await (0, server_1.createServer)(port);
|
|
64
64
|
const url = `http://127.0.0.1:${port}`;
|
|
65
65
|
console.log(`ClawTool running at ${url}`);
|
package/dist/loop.js
CHANGED
|
@@ -45,17 +45,6 @@ class DoctorLoop {
|
|
|
45
45
|
try {
|
|
46
46
|
const sessionId = Date.now().toString();
|
|
47
47
|
this.emit('session_start', { sessionId });
|
|
48
|
-
this.setState('waiting_user_description');
|
|
49
|
-
this.emit('request_input', {
|
|
50
|
-
field: 'userDescription',
|
|
51
|
-
instructions: 'Describe your issue or skip.',
|
|
52
|
-
allowSkip: true,
|
|
53
|
-
});
|
|
54
|
-
this.userDescription = await new Promise((resolve) => {
|
|
55
|
-
this.pendingDescription = resolve;
|
|
56
|
-
});
|
|
57
|
-
if (this.stopped)
|
|
58
|
-
return;
|
|
59
48
|
this.setState('observing');
|
|
60
49
|
this.emit('progress', { message: 'Scanning your local OpenClaw setup...' });
|
|
61
50
|
const observation = await this.deps.collectObservation();
|
package/dist/planner.js
CHANGED
package/package.json
CHANGED
package/web/index.html
CHANGED
|
@@ -2,265 +2,307 @@
|
|
|
2
2
|
<html lang="en" class="notranslate" translate="no">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<meta name="google" content="notranslate" />
|
|
7
7
|
<title>ClawTool</title>
|
|
8
8
|
<style>
|
|
9
9
|
:root {
|
|
10
|
-
--bg: #
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
10
|
+
--bg: #f5f4ef;
|
|
11
|
+
--surface: #fffdf7;
|
|
12
|
+
--surface-alt: #f3efe5;
|
|
13
|
+
--line: #dfd9c8;
|
|
14
|
+
--text: #213237;
|
|
15
|
+
--muted: #5f7479;
|
|
16
|
+
--accent: #0d6c74;
|
|
17
|
+
--accent-soft: #d9eef0;
|
|
18
|
+
--ok: #1f8a63;
|
|
19
|
+
--warn: #b26b18;
|
|
20
|
+
--danger: #b84040;
|
|
21
|
+
--radius: 16px;
|
|
22
|
+
--radius-sm: 12px;
|
|
20
23
|
}
|
|
24
|
+
|
|
21
25
|
* { box-sizing: border-box; }
|
|
26
|
+
|
|
22
27
|
body {
|
|
23
28
|
margin: 0;
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
min-height: 100vh;
|
|
30
|
+
padding: 24px 12px 80px;
|
|
31
|
+
font-family: 'Avenir Next', 'Trebuchet MS', 'PingFang SC', 'Noto Sans SC', sans-serif;
|
|
26
32
|
color: var(--text);
|
|
27
|
-
|
|
33
|
+
background:
|
|
34
|
+
radial-gradient(1000px 560px at 105% -10%, #d8ecec, transparent 60%),
|
|
35
|
+
radial-gradient(860px 500px at -15% 12%, #efe8d8, transparent 56%),
|
|
36
|
+
var(--bg);
|
|
28
37
|
}
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
border: 1px solid var(--border);
|
|
38
|
+
|
|
39
|
+
.app { max-width: 640px; margin: 0 auto; }
|
|
40
|
+
|
|
41
|
+
.brand {
|
|
42
|
+
background: var(--surface);
|
|
43
|
+
border: 1px solid var(--line);
|
|
36
44
|
border-radius: var(--radius);
|
|
37
|
-
padding:
|
|
38
|
-
box-shadow: 0
|
|
45
|
+
padding: 20px 22px;
|
|
46
|
+
box-shadow: 0 8px 24px rgba(25, 39, 42, 0.06);
|
|
39
47
|
margin-bottom: 12px;
|
|
40
48
|
}
|
|
41
|
-
|
|
49
|
+
|
|
50
|
+
.brand-tag {
|
|
51
|
+
font-size: 11px;
|
|
52
|
+
letter-spacing: .12em;
|
|
53
|
+
text-transform: uppercase;
|
|
54
|
+
color: var(--muted);
|
|
55
|
+
margin-bottom: 8px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.brand h1 {
|
|
59
|
+
margin: 0;
|
|
60
|
+
font-size: 28px;
|
|
61
|
+
letter-spacing: -0.02em;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.brand p {
|
|
65
|
+
margin: 8px 0 0;
|
|
66
|
+
color: var(--muted);
|
|
67
|
+
font-size: 14px;
|
|
68
|
+
line-height: 1.5;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.card {
|
|
72
|
+
background: var(--surface);
|
|
73
|
+
border: 1px solid var(--line);
|
|
74
|
+
border-radius: var(--radius);
|
|
75
|
+
box-shadow: 0 8px 24px rgba(25, 39, 42, 0.05);
|
|
76
|
+
padding: 16px;
|
|
77
|
+
margin-bottom: 10px;
|
|
78
|
+
animation: rise .22s ease both;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.hidden { display: none !important; }
|
|
82
|
+
|
|
42
83
|
.btn {
|
|
43
84
|
border: 0;
|
|
44
|
-
border-radius:
|
|
45
|
-
padding:
|
|
46
|
-
|
|
85
|
+
border-radius: 11px;
|
|
86
|
+
padding: 11px 14px;
|
|
87
|
+
font-size: 14px;
|
|
47
88
|
font-weight: 600;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
transition: transform .15s ease, opacity .15s ease;
|
|
48
91
|
}
|
|
49
|
-
|
|
50
|
-
.btn
|
|
51
|
-
.btn
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
92
|
+
|
|
93
|
+
.btn:hover { transform: translateY(-1px); }
|
|
94
|
+
.btn:active { transform: translateY(0); }
|
|
95
|
+
|
|
96
|
+
.btn-main { width: 100%; background: var(--accent); color: #fff; }
|
|
97
|
+
.btn-allow { background: var(--ok); color: #fff; }
|
|
98
|
+
.btn-skip { background: #fff5e8; color: var(--warn); border: 1px solid #efd5b1; }
|
|
99
|
+
|
|
100
|
+
.row { display: flex; gap: 8px; margin-top: 10px; }
|
|
101
|
+
.row .btn { flex: 1; }
|
|
102
|
+
|
|
103
|
+
.feed { margin-top: 10px; }
|
|
104
|
+
|
|
105
|
+
.step-title {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 8px;
|
|
62
109
|
font-size: 14px;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
margin-bottom: 8px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.badge {
|
|
115
|
+
display: inline-block;
|
|
116
|
+
padding: 2px 8px;
|
|
117
|
+
border-radius: 999px;
|
|
118
|
+
font-size: 11px;
|
|
119
|
+
font-weight: 700;
|
|
120
|
+
text-transform: uppercase;
|
|
121
|
+
letter-spacing: .04em;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.b-read { color: #29527a; background: #e7f1fb; }
|
|
125
|
+
.b-fix { color: #8a5316; background: #f9ecd9; }
|
|
126
|
+
.b-done { color: #1f6a4b; background: #dff5eb; }
|
|
127
|
+
|
|
128
|
+
.mono {
|
|
129
|
+
font-family: Menlo, Consolas, 'Liberation Mono', monospace;
|
|
130
|
+
font-size: 12px;
|
|
131
|
+
line-height: 1.55;
|
|
132
|
+
white-space: pre-wrap;
|
|
133
|
+
background: var(--surface-alt);
|
|
134
|
+
border: 1px solid var(--line);
|
|
135
|
+
border-radius: var(--radius-sm);
|
|
136
|
+
padding: 10px 11px;
|
|
137
|
+
color: #2f474d;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.hint { font-size: 13px; color: var(--muted); }
|
|
141
|
+
.ok { color: var(--ok); }
|
|
142
|
+
.err { color: var(--danger); }
|
|
143
|
+
|
|
144
|
+
@keyframes rise {
|
|
145
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
146
|
+
to { opacity: 1; transform: translateY(0); }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@media (max-width: 520px) {
|
|
150
|
+
.brand h1 { font-size: 24px; }
|
|
151
|
+
.card { padding: 14px; }
|
|
63
152
|
}
|
|
64
|
-
.row { display: flex; gap: 8px; }
|
|
65
|
-
.row .btn { flex: 1; }
|
|
66
|
-
.feed-item { font-size: 14px; line-height: 1.6; }
|
|
67
|
-
.mono { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; color: #334155; white-space: pre-wrap; }
|
|
68
|
-
.badge { display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 999px; margin-left: 6px; }
|
|
69
|
-
.b-read { background: #e0e7ff; color: #3730a3; }
|
|
70
|
-
.b-fix { background: #fef3c7; color: #92400e; }
|
|
71
|
-
.b-done { background: #dcfce7; color: #166534; }
|
|
72
|
-
.status { color: var(--muted); font-size: 13px; margin-top: 8px; }
|
|
73
153
|
</style>
|
|
74
154
|
</head>
|
|
75
155
|
<body>
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
<
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<div class="row">
|
|
91
|
-
<button id="btn-submit-input" class="btn btn-ok">Continue</button>
|
|
92
|
-
<button id="btn-skip-input" class="btn btn-ghost">Skip</button>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
<div id="feed" class="hidden"></div>
|
|
97
|
-
</div>
|
|
156
|
+
<main class="app notranslate">
|
|
157
|
+
<section class="brand">
|
|
158
|
+
<div class="brand-tag" id="brand-tag">Local-First Diagnose Engine</div>
|
|
159
|
+
<h1>ClawTool</h1>
|
|
160
|
+
<p id="brand-sub">Detect and repair OpenClaw issues locally with explicit fix confirmation.</p>
|
|
161
|
+
</section>
|
|
162
|
+
|
|
163
|
+
<section id="start" class="card">
|
|
164
|
+
<div class="hint" id="start-text">No cloud call. No token required. Local execution only.</div>
|
|
165
|
+
<button id="btn-start" class="btn btn-main">Start Scan</button>
|
|
166
|
+
</section>
|
|
167
|
+
|
|
168
|
+
<section id="feed" class="feed hidden"></section>
|
|
169
|
+
</main>
|
|
98
170
|
|
|
99
171
|
<script>
|
|
100
172
|
(function () {
|
|
101
173
|
const isZh = /^zh/i.test(navigator.language || '');
|
|
102
174
|
const T = isZh ? {
|
|
103
|
-
|
|
104
|
-
|
|
175
|
+
tag: '本地优先诊断引擎',
|
|
176
|
+
sub: '本地检测并修复 OpenClaw 问题,所有修复步骤都需你确认。',
|
|
177
|
+
startText: '不走云端,不需要 token,仅本地执行。',
|
|
105
178
|
start: '开始扫描',
|
|
106
|
-
|
|
107
|
-
inputPlaceholder: '例如:升级后 gateway 无法启动',
|
|
108
|
-
continue: '继续',
|
|
109
|
-
skip: '跳过',
|
|
110
|
-
waiting: '等待输入...',
|
|
111
|
-
confirm: '确认执行修复步骤?',
|
|
179
|
+
confirm: '确认执行这个修复步骤?',
|
|
112
180
|
allow: '执行',
|
|
113
|
-
|
|
114
|
-
done: '
|
|
115
|
-
|
|
181
|
+
skip: '跳过',
|
|
182
|
+
done: '完成',
|
|
183
|
+
error: '发生错误'
|
|
116
184
|
} : {
|
|
117
|
-
|
|
118
|
-
|
|
185
|
+
tag: 'Local-First Diagnose Engine',
|
|
186
|
+
sub: 'Detect and repair OpenClaw issues locally with explicit fix confirmation.',
|
|
187
|
+
startText: 'No cloud call. No token required. Local execution only.',
|
|
119
188
|
start: 'Start Scan',
|
|
120
|
-
inputTitle: 'Describe your issue (optional)',
|
|
121
|
-
inputPlaceholder: 'Example: gateway not running after upgrade',
|
|
122
|
-
continue: 'Continue',
|
|
123
|
-
skip: 'Skip',
|
|
124
|
-
waiting: 'Waiting for input...',
|
|
125
189
|
confirm: 'Confirm this fix step?',
|
|
126
190
|
allow: 'Allow',
|
|
127
|
-
|
|
191
|
+
skip: 'Skip',
|
|
128
192
|
done: 'Completed',
|
|
129
|
-
|
|
193
|
+
error: 'Error occurred'
|
|
130
194
|
};
|
|
131
195
|
|
|
132
196
|
const el = {
|
|
133
|
-
|
|
134
|
-
|
|
197
|
+
tag: document.getElementById('brand-tag'),
|
|
198
|
+
sub: document.getElementById('brand-sub'),
|
|
199
|
+
startText: document.getElementById('start-text'),
|
|
200
|
+
startWrap: document.getElementById('start'),
|
|
135
201
|
btnStart: document.getElementById('btn-start'),
|
|
136
|
-
start: document.getElementById('start'),
|
|
137
|
-
inputCard: document.getElementById('input-card'),
|
|
138
|
-
inputTitle: document.getElementById('input-title'),
|
|
139
|
-
inputDescription: document.getElementById('input-description'),
|
|
140
|
-
btnSubmitInput: document.getElementById('btn-submit-input'),
|
|
141
|
-
btnSkipInput: document.getElementById('btn-skip-input'),
|
|
142
202
|
feed: document.getElementById('feed')
|
|
143
203
|
};
|
|
144
204
|
|
|
145
|
-
el.
|
|
146
|
-
el.
|
|
205
|
+
el.tag.textContent = T.tag;
|
|
206
|
+
el.sub.textContent = T.sub;
|
|
207
|
+
el.startText.textContent = T.startText;
|
|
147
208
|
el.btnStart.textContent = T.start;
|
|
148
|
-
el.inputTitle.textContent = T.inputTitle;
|
|
149
|
-
el.inputDescription.placeholder = T.inputPlaceholder;
|
|
150
|
-
el.btnSubmitInput.textContent = T.continue;
|
|
151
|
-
el.btnSkipInput.textContent = T.skip;
|
|
152
209
|
|
|
153
|
-
let sessionId = null;
|
|
154
210
|
let eventSource = null;
|
|
155
|
-
let
|
|
156
|
-
|
|
157
|
-
function addCard(
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
el.feed.appendChild(
|
|
162
|
-
return
|
|
211
|
+
let sessionId = null;
|
|
212
|
+
|
|
213
|
+
function addCard(innerHtml) {
|
|
214
|
+
const card = document.createElement('article');
|
|
215
|
+
card.className = 'card';
|
|
216
|
+
card.innerHTML = innerHtml;
|
|
217
|
+
el.feed.appendChild(card);
|
|
218
|
+
return card;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function typeBadge(type) {
|
|
222
|
+
if (type === 'fix') return 'b-fix';
|
|
223
|
+
if (type === 'done') return 'b-done';
|
|
224
|
+
return 'b-read';
|
|
163
225
|
}
|
|
164
226
|
|
|
165
|
-
async function
|
|
166
|
-
|
|
227
|
+
async function postConfirm(confirmed) {
|
|
228
|
+
if (!sessionId) return;
|
|
229
|
+
await fetch('/api/confirm', {
|
|
167
230
|
method: 'POST',
|
|
168
231
|
headers: { 'Content-Type': 'application/json' },
|
|
169
|
-
body: JSON.stringify(
|
|
232
|
+
body: JSON.stringify({ sessionId, confirmed })
|
|
170
233
|
});
|
|
171
234
|
}
|
|
172
235
|
|
|
173
236
|
function connect() {
|
|
174
237
|
eventSource = new EventSource('/api/diagnose?lang=' + (isZh ? 'zh' : 'en'));
|
|
175
|
-
eventSource.onmessage =
|
|
238
|
+
eventSource.onmessage = function (evt) {
|
|
176
239
|
const payload = JSON.parse(evt.data);
|
|
177
240
|
const type = payload.type;
|
|
178
241
|
const data = payload.data || {};
|
|
179
242
|
|
|
180
243
|
if (type === 'session_start') {
|
|
181
244
|
sessionId = payload.sessionId;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (type === 'request_input') {
|
|
185
|
-
el.inputCard.classList.remove('hidden');
|
|
186
|
-
addCard('<div class="status">' + T.waiting + '</div>');
|
|
245
|
+
return;
|
|
187
246
|
}
|
|
188
247
|
|
|
189
248
|
if (type === 'progress') {
|
|
190
|
-
addCard('<div>' + (data.message || '') + '</div>');
|
|
249
|
+
addCard('<div class="hint">' + (data.message || '') + '</div>');
|
|
250
|
+
return;
|
|
191
251
|
}
|
|
192
252
|
|
|
193
253
|
if (type === 'step_start') {
|
|
194
254
|
const step = data.step || {};
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
255
|
+
const card = addCard(
|
|
256
|
+
'<div class="step-title">'
|
|
257
|
+
+ '<span>' + (step.description || 'step') + '</span>'
|
|
258
|
+
+ '<span class="badge ' + typeBadge(step.type) + '">' + (step.type || 'read') + '</span>'
|
|
259
|
+
+ '</div>'
|
|
260
|
+
+ (step.type !== 'read' && step.command ? '<div class="mono" translate="no">' + step.command + '</div>' : '')
|
|
200
261
|
);
|
|
201
262
|
|
|
202
263
|
if (step.type === 'fix') {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
'<div class="
|
|
206
|
-
|
|
207
|
-
|
|
264
|
+
card.insertAdjacentHTML('beforeend',
|
|
265
|
+
'<div class="hint" style="margin-top:10px">' + T.confirm + '</div>'
|
|
266
|
+
+ '<div class="row">'
|
|
267
|
+
+ '<button class="btn btn-allow" data-confirm="yes">' + T.allow + '</button>'
|
|
268
|
+
+ '<button class="btn btn-skip" data-confirm="no">' + T.skip + '</button>'
|
|
269
|
+
+ '</div>'
|
|
208
270
|
);
|
|
209
271
|
}
|
|
272
|
+
return;
|
|
210
273
|
}
|
|
211
274
|
|
|
212
275
|
if (type === 'step_done') {
|
|
213
276
|
addCard('<div class="mono" translate="no">' + (data.output || '') + '</div>');
|
|
277
|
+
return;
|
|
214
278
|
}
|
|
215
279
|
|
|
216
280
|
if (type === 'complete') {
|
|
217
|
-
addCard('<h3>' + T.done + '</h3><p>' + (data.summary || '') + '</p>');
|
|
281
|
+
addCard('<h3 class="ok">' + T.done + '</h3><p>' + (data.summary || '') + '</p>');
|
|
218
282
|
if (eventSource) eventSource.close();
|
|
283
|
+
return;
|
|
219
284
|
}
|
|
220
285
|
|
|
221
286
|
if (type === 'error') {
|
|
222
|
-
addCard('<h3
|
|
287
|
+
addCard('<h3 class="err">' + T.error + '</h3><p>' + (data.message || '') + '</p>');
|
|
223
288
|
if (eventSource) eventSource.close();
|
|
224
289
|
}
|
|
225
290
|
};
|
|
226
291
|
}
|
|
227
292
|
|
|
228
293
|
el.btnStart.addEventListener('click', function () {
|
|
229
|
-
el.
|
|
294
|
+
el.startWrap.classList.add('hidden');
|
|
230
295
|
el.feed.classList.remove('hidden');
|
|
231
296
|
connect();
|
|
232
297
|
});
|
|
233
298
|
|
|
234
|
-
el.btnSubmitInput.addEventListener('click', async function () {
|
|
235
|
-
if (!sessionId) return;
|
|
236
|
-
await post('/api/input', {
|
|
237
|
-
sessionId,
|
|
238
|
-
field: 'userDescription',
|
|
239
|
-
value: el.inputDescription.value
|
|
240
|
-
});
|
|
241
|
-
el.inputCard.classList.add('hidden');
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
el.btnSkipInput.addEventListener('click', async function () {
|
|
245
|
-
if (!sessionId) return;
|
|
246
|
-
await post('/api/input', {
|
|
247
|
-
sessionId,
|
|
248
|
-
field: 'userDescription',
|
|
249
|
-
value: ''
|
|
250
|
-
});
|
|
251
|
-
el.inputCard.classList.add('hidden');
|
|
252
|
-
});
|
|
253
|
-
|
|
254
299
|
document.addEventListener('click', async function (event) {
|
|
255
300
|
const target = event.target;
|
|
256
301
|
if (!(target instanceof HTMLElement)) return;
|
|
257
302
|
const choice = target.getAttribute('data-confirm');
|
|
258
|
-
if (!choice
|
|
259
|
-
await
|
|
260
|
-
|
|
261
|
-
confirmed: choice === 'yes'
|
|
262
|
-
});
|
|
263
|
-
pendingConfirm = null;
|
|
303
|
+
if (!choice) return;
|
|
304
|
+
await postConfirm(choice === 'yes');
|
|
305
|
+
target.closest('.row')?.remove();
|
|
264
306
|
});
|
|
265
307
|
})();
|
|
266
308
|
</script>
|