bosun 0.36.2 → 0.36.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/analyze-agent-work-helpers.mjs +308 -0
- package/analyze-agent-work.mjs +926 -0
- package/autofix.mjs +2 -0
- package/codex-shell.mjs +85 -10
- package/git-editor-fix.mjs +273 -0
- package/mcp-registry.mjs +579 -0
- package/meeting-workflow-service.mjs +631 -0
- package/monitor.mjs +18 -103
- package/package.json +13 -2
- package/primary-agent.mjs +32 -12
- package/session-tracker.mjs +68 -0
- package/stream-resilience.mjs +17 -7
- package/ui/app.js +19 -4
- package/ui/components/chat-view.js +108 -5
- package/ui/components/session-list.js +1 -1
- package/ui/components/shared.js +188 -15
- package/ui/modules/icons.js +13 -0
- package/ui/modules/utils.js +44 -0
- package/ui/modules/voice.js +15 -6
- package/ui/styles/components.css +99 -3
- package/ui/styles/sessions.css +84 -12
- package/ui/tabs/chat.js +5 -1
- package/ui/tabs/control.js +16 -22
- package/ui/tabs/dashboard.js +85 -8
- package/ui/tabs/library.js +113 -17
- package/ui/tabs/settings.js +116 -2
- package/ui/tabs/tasks.js +388 -39
- package/ui/tabs/telemetry.js +0 -1
- package/ui/tabs/workflows.js +4 -0
- package/ui-server.mjs +193 -19
- package/update-check.mjs +41 -13
- package/voice-relay.mjs +816 -0
- package/voice-tools.mjs +679 -0
- package/workflow-templates/agents.mjs +6 -2
- package/workflow-templates/github.mjs +154 -12
- package/workflow-templates.mjs +3 -0
- package/github-reconciler.mjs +0 -506
- package/merge-strategy.mjs +0 -1210
- package/pr-cleanup-daemon.mjs +0 -992
- package/workspace-reaper.mjs +0 -405
package/ui/styles/sessions.css
CHANGED
|
@@ -103,8 +103,14 @@
|
|
|
103
103
|
left: 0;
|
|
104
104
|
bottom: calc(var(--nav-height) + var(--safe-bottom));
|
|
105
105
|
width: min(88vw, 360px);
|
|
106
|
-
transform: translateX(-100%);
|
|
107
|
-
|
|
106
|
+
transform: translateX(calc(-100% - 24px));
|
|
107
|
+
opacity: 0;
|
|
108
|
+
visibility: hidden;
|
|
109
|
+
pointer-events: none;
|
|
110
|
+
transition:
|
|
111
|
+
transform 0.28s cubic-bezier(0.32, 0.72, 0, 1),
|
|
112
|
+
opacity 0.2s ease,
|
|
113
|
+
visibility 0s linear 0.28s;
|
|
108
114
|
will-change: transform;
|
|
109
115
|
z-index: 160;
|
|
110
116
|
box-shadow: 12px 0 40px rgba(0, 0, 0, 0.35);
|
|
@@ -113,6 +119,10 @@
|
|
|
113
119
|
|
|
114
120
|
.session-split.drawer-open .session-pane {
|
|
115
121
|
transform: translateX(0);
|
|
122
|
+
opacity: 1;
|
|
123
|
+
visibility: visible;
|
|
124
|
+
pointer-events: auto;
|
|
125
|
+
transition-delay: 0s, 0s, 0s;
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
.session-split>.session-detail {
|
|
@@ -869,14 +879,56 @@
|
|
|
869
879
|
flex-direction: column;
|
|
870
880
|
align-items: center;
|
|
871
881
|
justify-content: center;
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
882
|
+
width: min(720px, 100%);
|
|
883
|
+
margin: 0 auto;
|
|
884
|
+
padding: clamp(18px, 2.8vw, 30px) clamp(18px, 3.2vw, 34px);
|
|
885
|
+
gap: 10px;
|
|
886
|
+
border: 1px dashed rgba(148, 163, 184, 0.32);
|
|
887
|
+
border-radius: 14px;
|
|
888
|
+
background: linear-gradient(180deg, rgba(17, 23, 34, 0.55), rgba(8, 12, 20, 0.35));
|
|
889
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
|
877
890
|
text-align: center;
|
|
878
891
|
}
|
|
879
892
|
|
|
893
|
+
.chat-empty-state-inline.chat-empty-state-inline--no-box {
|
|
894
|
+
width: auto;
|
|
895
|
+
max-width: 100%;
|
|
896
|
+
border: none !important;
|
|
897
|
+
border-radius: 0 !important;
|
|
898
|
+
background: transparent !important;
|
|
899
|
+
box-shadow: none !important;
|
|
900
|
+
padding: clamp(18px, 2.8vw, 30px) var(--space-lg);
|
|
901
|
+
margin-top: auto;
|
|
902
|
+
margin-bottom: auto;
|
|
903
|
+
transform: translateY(-18px);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.chat-empty-state-inline .session-empty-icon {
|
|
907
|
+
font-size: 24px;
|
|
908
|
+
opacity: 0.72;
|
|
909
|
+
color: var(--text-hint);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.chat-empty-state-inline .session-empty-text {
|
|
913
|
+
font-size: 15px;
|
|
914
|
+
font-weight: 600;
|
|
915
|
+
color: var(--text-primary);
|
|
916
|
+
line-height: 1.35;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.chat-empty-state-inline .session-empty-subtext {
|
|
920
|
+
margin-top: 6px;
|
|
921
|
+
font-size: 12px;
|
|
922
|
+
color: var(--text-secondary);
|
|
923
|
+
line-height: 1.45;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
@media (max-width: 768px) {
|
|
927
|
+
.chat-empty-state-inline--no-box {
|
|
928
|
+
transform: translateY(-10px);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
880
932
|
.chat-loading {
|
|
881
933
|
text-align: center;
|
|
882
934
|
color: var(--text-hint);
|
|
@@ -1977,15 +2029,15 @@ ul.md-list li::before {
|
|
|
1977
2029
|
|
|
1978
2030
|
.chat-input-wrapper {
|
|
1979
2031
|
display: flex;
|
|
1980
|
-
align-items:
|
|
1981
|
-
gap:
|
|
2032
|
+
align-items: center;
|
|
2033
|
+
gap: 10px;
|
|
1982
2034
|
background: var(--bg-card);
|
|
1983
2035
|
border: 1px solid var(--border);
|
|
1984
2036
|
border-radius: 24px;
|
|
1985
2037
|
padding: 8px 8px 8px 16px;
|
|
1986
2038
|
transition: border-color 0.15s, box-shadow 0.15s;
|
|
1987
2039
|
max-width: min(760px, 100%);
|
|
1988
|
-
margin:
|
|
2040
|
+
margin: 8px auto 0;
|
|
1989
2041
|
}
|
|
1990
2042
|
|
|
1991
2043
|
.chat-input-wrapper:focus-within {
|
|
@@ -1995,6 +2047,7 @@ ul.md-list li::before {
|
|
|
1995
2047
|
|
|
1996
2048
|
.chat-textarea {
|
|
1997
2049
|
flex: 1;
|
|
2050
|
+
align-self: center;
|
|
1998
2051
|
background: transparent;
|
|
1999
2052
|
border: none;
|
|
2000
2053
|
outline: none;
|
|
@@ -2005,10 +2058,25 @@ ul.md-list li::before {
|
|
|
2005
2058
|
resize: none;
|
|
2006
2059
|
min-height: 24px;
|
|
2007
2060
|
max-height: 120px;
|
|
2008
|
-
padding:
|
|
2061
|
+
padding: 0;
|
|
2009
2062
|
scrollbar-width: thin;
|
|
2010
2063
|
}
|
|
2011
2064
|
|
|
2065
|
+
.chat-input-wrapper .mic-btn,
|
|
2066
|
+
.chat-input-wrapper .chat-send-btn {
|
|
2067
|
+
align-self: center;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
.chat-input-wrapper .mic-btn {
|
|
2071
|
+
width: 34px;
|
|
2072
|
+
height: 34px;
|
|
2073
|
+
margin-right: 2px;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
.chat-input-wrapper .chat-send-btn {
|
|
2077
|
+
margin-left: 2px;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2012
2080
|
.chat-textarea::placeholder {
|
|
2013
2081
|
color: var(--text-muted, var(--text-hint));
|
|
2014
2082
|
}
|
|
@@ -2044,7 +2112,7 @@ ul.md-list li::before {
|
|
|
2044
2112
|
.chat-input-hint {
|
|
2045
2113
|
display: flex;
|
|
2046
2114
|
justify-content: space-between;
|
|
2047
|
-
padding:
|
|
2115
|
+
padding: 6px 8px 0;
|
|
2048
2116
|
font-size: 11px;
|
|
2049
2117
|
color: var(--text-hint);
|
|
2050
2118
|
max-width: min(760px, 100%);
|
|
@@ -2506,6 +2574,10 @@ ul.md-list li::before {
|
|
|
2506
2574
|
}
|
|
2507
2575
|
|
|
2508
2576
|
/* ─── Blend chat input seamlessly with bottom navbar ─── */
|
|
2577
|
+
.app-shell[data-tab="chat"] .main-content {
|
|
2578
|
+
background: var(--glass-bg);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2509
2581
|
.app-shell[data-tab="chat"] .chat-input-area {
|
|
2510
2582
|
border-top: none;
|
|
2511
2583
|
background: var(--glass-bg);
|
package/ui/tabs/chat.js
CHANGED
|
@@ -523,7 +523,11 @@ export function ChatTab() {
|
|
|
523
523
|
// Forward to agent SDK
|
|
524
524
|
const resp = await apiFetch("/api/agents/sdk-command", {
|
|
525
525
|
method: "POST",
|
|
526
|
-
body: JSON.stringify({
|
|
526
|
+
body: JSON.stringify({
|
|
527
|
+
command: cmdBase,
|
|
528
|
+
args: cmdArgs,
|
|
529
|
+
sessionId: sessionId || undefined,
|
|
530
|
+
}),
|
|
527
531
|
});
|
|
528
532
|
const resultText = resp?.result || resp?.data || `:check: SDK command executed: ${cmdBase}`;
|
|
529
533
|
if (sessionId) {
|
package/ui/tabs/control.js
CHANGED
|
@@ -865,29 +865,11 @@ export function ControlTab() {
|
|
|
865
865
|
|
|
866
866
|
<${Card} className="routing-card">
|
|
867
867
|
<${Collapsible} title="Routing" defaultOpen=${!isCompact}>
|
|
868
|
-
<div class="meta-text mb-sm">
|
|
868
|
+
<div class="meta-text mb-sm">Runtime region override only. SDK and kanban backend changes now live in Settings to avoid accidental disruptive switches.</div>
|
|
869
869
|
<div class="card-subtitle">SDK</div>
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
{ value: "copilot", label: "Copilot" },
|
|
874
|
-
{ value: "claude", label: "Claude" },
|
|
875
|
-
{ value: "auto", label: "Auto" },
|
|
876
|
-
]}
|
|
877
|
-
value=${config?.sdk || "auto"}
|
|
878
|
-
onChange=${(v) => updateConfig("sdk", v)}
|
|
879
|
-
/>
|
|
880
|
-
<div class="card-subtitle mt-sm">Kanban</div>
|
|
881
|
-
<${SegmentedControl}
|
|
882
|
-
options=${[
|
|
883
|
-
{ value: "internal", label: "Internal" },
|
|
884
|
-
{ value: "vk", label: "VK" },
|
|
885
|
-
{ value: "github", label: "GitHub" },
|
|
886
|
-
{ value: "jira", label: "Jira" },
|
|
887
|
-
]}
|
|
888
|
-
value=${config?.kanbanBackend || "internal"}
|
|
889
|
-
onChange=${(v) => updateConfig("kanban", v)}
|
|
890
|
-
/>
|
|
870
|
+
<div class="meta-text mb-sm">Current: ${String(config?.sdk || "auto").toUpperCase()}</div>
|
|
871
|
+
<div class="card-subtitle">Kanban</div>
|
|
872
|
+
<div class="meta-text mb-sm">Current: ${config?.kanbanBackend || "internal"}</div>
|
|
891
873
|
${regions.length > 1 && html`
|
|
892
874
|
<div class="card-subtitle mt-sm">Region</div>
|
|
893
875
|
<${SegmentedControl}
|
|
@@ -896,6 +878,18 @@ export function ControlTab() {
|
|
|
896
878
|
onChange=${(v) => updateConfig("region", v)}
|
|
897
879
|
/>
|
|
898
880
|
`}
|
|
881
|
+
${regions.length <= 1 && html`
|
|
882
|
+
<div class="card-subtitle mt-sm">Region</div>
|
|
883
|
+
<div class="meta-text mb-sm">Current: ${regions[0] || "auto"}</div>
|
|
884
|
+
`}
|
|
885
|
+
<button
|
|
886
|
+
class="btn btn-ghost btn-sm mt-sm"
|
|
887
|
+
onClick=${() => {
|
|
888
|
+
import("../modules/router.js").then(({ navigateTo }) => navigateTo("settings"));
|
|
889
|
+
}}
|
|
890
|
+
>
|
|
891
|
+
Open Settings
|
|
892
|
+
</button>
|
|
899
893
|
<//>
|
|
900
894
|
<//>
|
|
901
895
|
|
package/ui/tabs/dashboard.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
useEffect,
|
|
8
8
|
useCallback,
|
|
9
9
|
useRef,
|
|
10
|
+
useMemo,
|
|
10
11
|
} from "preact/hooks";
|
|
11
12
|
import htm from "htm";
|
|
12
13
|
|
|
@@ -27,10 +28,17 @@ import {
|
|
|
27
28
|
scheduleRefresh,
|
|
28
29
|
getTrend,
|
|
29
30
|
getDashboardHistory,
|
|
31
|
+
setPendingChange,
|
|
32
|
+
clearPendingChange,
|
|
30
33
|
} from "../modules/state.js";
|
|
31
34
|
import { navigateTo } from "../modules/router.js";
|
|
32
35
|
import { ICONS } from "../modules/icons.js";
|
|
33
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
cloneValue,
|
|
38
|
+
formatRelative,
|
|
39
|
+
truncate,
|
|
40
|
+
countChangedFields,
|
|
41
|
+
} from "../modules/utils.js";
|
|
34
42
|
import { iconText, resolveIcon } from "../modules/icon-utils.js";
|
|
35
43
|
import {
|
|
36
44
|
Card,
|
|
@@ -38,6 +46,7 @@ import {
|
|
|
38
46
|
SkeletonCard,
|
|
39
47
|
Modal,
|
|
40
48
|
EmptyState,
|
|
49
|
+
SaveDiscardBar,
|
|
41
50
|
} from "../components/shared.js";
|
|
42
51
|
import { DonutChart, ProgressBar, MiniSparkline } from "../components/charts.js";
|
|
43
52
|
import {
|
|
@@ -139,11 +148,44 @@ export function CreateTaskModal({ onClose }) {
|
|
|
139
148
|
const [description, setDescription] = useState("");
|
|
140
149
|
const [priority, setPriority] = useState("medium");
|
|
141
150
|
const [submitting, setSubmitting] = useState(false);
|
|
151
|
+
const initialSnapshotRef = useRef({
|
|
152
|
+
title: "",
|
|
153
|
+
description: "",
|
|
154
|
+
priority: "medium",
|
|
155
|
+
});
|
|
156
|
+
const pendingKey = "modal:create-task-dashboard";
|
|
157
|
+
|
|
158
|
+
const currentSnapshot = useMemo(
|
|
159
|
+
() => ({
|
|
160
|
+
title: title || "",
|
|
161
|
+
description: description || "",
|
|
162
|
+
priority: priority || "medium",
|
|
163
|
+
}),
|
|
164
|
+
[description, priority, title],
|
|
165
|
+
);
|
|
166
|
+
const changeCount = useMemo(
|
|
167
|
+
() => countChangedFields(initialSnapshotRef.current, currentSnapshot),
|
|
168
|
+
[currentSnapshot],
|
|
169
|
+
);
|
|
170
|
+
const hasUnsaved = changeCount > 0;
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
setPendingChange(pendingKey, hasUnsaved);
|
|
174
|
+
return () => clearPendingChange(pendingKey);
|
|
175
|
+
}, [hasUnsaved]);
|
|
142
176
|
|
|
143
|
-
const
|
|
177
|
+
const resetToInitial = useCallback(() => {
|
|
178
|
+
const base = initialSnapshotRef.current || {};
|
|
179
|
+
setTitle(base.title || "");
|
|
180
|
+
setDescription(base.description || "");
|
|
181
|
+
setPriority(base.priority || "medium");
|
|
182
|
+
showToast("Changes discarded", "info");
|
|
183
|
+
}, []);
|
|
184
|
+
|
|
185
|
+
const handleSubmit = useCallback(async ({ closeAfterSave = true } = {}) => {
|
|
144
186
|
if (!title.trim()) {
|
|
145
187
|
showToast("Title is required", "error");
|
|
146
|
-
return;
|
|
188
|
+
return false;
|
|
147
189
|
}
|
|
148
190
|
setSubmitting(true);
|
|
149
191
|
haptic("medium");
|
|
@@ -157,12 +199,22 @@ export function CreateTaskModal({ onClose }) {
|
|
|
157
199
|
}),
|
|
158
200
|
});
|
|
159
201
|
showToast("Task created", "success");
|
|
160
|
-
|
|
202
|
+
initialSnapshotRef.current = {
|
|
203
|
+
title: title.trim(),
|
|
204
|
+
description: description.trim(),
|
|
205
|
+
priority,
|
|
206
|
+
};
|
|
207
|
+
if (closeAfterSave) {
|
|
208
|
+
onClose?.();
|
|
209
|
+
}
|
|
161
210
|
await refreshTab("dashboard");
|
|
211
|
+
return closeAfterSave ? { closed: true } : true;
|
|
162
212
|
} catch {
|
|
163
213
|
/* toast shown by apiFetch */
|
|
214
|
+
return false;
|
|
215
|
+
} finally {
|
|
216
|
+
setSubmitting(false);
|
|
164
217
|
}
|
|
165
|
-
setSubmitting(false);
|
|
166
218
|
}, [title, description, priority, onClose]);
|
|
167
219
|
|
|
168
220
|
/* Telegram MainButton integration */
|
|
@@ -171,7 +223,9 @@ export function CreateTaskModal({ onClose }) {
|
|
|
171
223
|
if (tg?.MainButton) {
|
|
172
224
|
tg.MainButton.setText("Create Task");
|
|
173
225
|
tg.MainButton.show();
|
|
174
|
-
const handler = () =>
|
|
226
|
+
const handler = () => {
|
|
227
|
+
void handleSubmit({ closeAfterSave: true });
|
|
228
|
+
};
|
|
175
229
|
tg.MainButton.onClick(handler);
|
|
176
230
|
return () => {
|
|
177
231
|
tg.MainButton.hide();
|
|
@@ -181,7 +235,17 @@ export function CreateTaskModal({ onClose }) {
|
|
|
181
235
|
}, [handleSubmit]);
|
|
182
236
|
|
|
183
237
|
return html`
|
|
184
|
-
<${Modal}
|
|
238
|
+
<${Modal}
|
|
239
|
+
title="Create Task"
|
|
240
|
+
onClose=${onClose}
|
|
241
|
+
unsavedChanges=${changeCount}
|
|
242
|
+
onSaveBeforeClose=${() => handleSubmit({ closeAfterSave: true })}
|
|
243
|
+
onDiscardBeforeClose=${() => {
|
|
244
|
+
resetToInitial();
|
|
245
|
+
return true;
|
|
246
|
+
}}
|
|
247
|
+
activeOperationLabel=${submitting ? "Task creation is in progress" : ""}
|
|
248
|
+
>
|
|
185
249
|
<div class="flex-col gap-md">
|
|
186
250
|
<input
|
|
187
251
|
class="input"
|
|
@@ -212,11 +276,24 @@ export function CreateTaskModal({ onClose }) {
|
|
|
212
276
|
/>
|
|
213
277
|
<button
|
|
214
278
|
class="btn btn-primary"
|
|
215
|
-
onClick=${
|
|
279
|
+
onClick=${() => {
|
|
280
|
+
void handleSubmit({ closeAfterSave: true });
|
|
281
|
+
}}
|
|
216
282
|
disabled=${submitting}
|
|
217
283
|
>
|
|
218
284
|
${submitting ? "Creating…" : "Create Task"}
|
|
219
285
|
</button>
|
|
286
|
+
<${SaveDiscardBar}
|
|
287
|
+
dirty=${hasUnsaved}
|
|
288
|
+
message=${`You have unsaved changes (${changeCount})`}
|
|
289
|
+
saveLabel="Create Task"
|
|
290
|
+
discardLabel="Discard"
|
|
291
|
+
onSave=${() => {
|
|
292
|
+
void handleSubmit({ closeAfterSave: false });
|
|
293
|
+
}}
|
|
294
|
+
onDiscard=${resetToInitial}
|
|
295
|
+
saving=${submitting}
|
|
296
|
+
/>
|
|
220
297
|
</div>
|
|
221
298
|
<//>
|
|
222
299
|
`;
|
package/ui/tabs/library.js
CHANGED
|
@@ -11,11 +11,25 @@ const html = htm.bind(h);
|
|
|
11
11
|
|
|
12
12
|
import { haptic } from "../modules/telegram.js";
|
|
13
13
|
import { apiFetch } from "../modules/api.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
showToast,
|
|
16
|
+
refreshTab,
|
|
17
|
+
setPendingChange,
|
|
18
|
+
clearPendingChange,
|
|
19
|
+
} from "../modules/state.js";
|
|
15
20
|
import { ICONS } from "../modules/icons.js";
|
|
16
21
|
import { iconText, resolveIcon } from "../modules/icon-utils.js";
|
|
17
|
-
import { formatRelative } from "../modules/utils.js";
|
|
18
|
-
import {
|
|
22
|
+
import { formatRelative, countChangedFields } from "../modules/utils.js";
|
|
23
|
+
import {
|
|
24
|
+
Card,
|
|
25
|
+
Badge,
|
|
26
|
+
EmptyState,
|
|
27
|
+
Modal,
|
|
28
|
+
ConfirmDialog,
|
|
29
|
+
Spinner,
|
|
30
|
+
ListItem,
|
|
31
|
+
SaveDiscardBar,
|
|
32
|
+
} from "../components/shared.js";
|
|
19
33
|
import { SearchInput, SegmentedControl, Toggle } from "../components/forms.js";
|
|
20
34
|
|
|
21
35
|
/* ═══════════════════════════════════════════════════════════════
|
|
@@ -41,14 +55,17 @@ const LIBRARY_STYLES = `
|
|
|
41
55
|
|
|
42
56
|
.library-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); gap: 12px; }
|
|
43
57
|
.library-card { background: var(--bg-card, #1a1a2e); border: 1px solid var(--border, #333);
|
|
44
|
-
border-radius: 12px; padding: 16px; cursor: pointer; transition: all 0.15s; position: relative; }
|
|
58
|
+
border-radius: 12px; padding: 16px; padding-right: 96px; cursor: pointer; transition: all 0.15s; position: relative; }
|
|
45
59
|
.library-card:hover { border-color: var(--accent, #58a6ff); transform: translateY(-1px);
|
|
46
60
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
|
|
47
61
|
|
|
48
62
|
.library-card-header { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 8px; }
|
|
49
63
|
.library-card-icon { font-size: 1.4em; flex-shrink: 0; width: 32px; text-align: center; }
|
|
50
64
|
.library-card-icon svg { width: 20px; height: 20px; vertical-align: middle; }
|
|
51
|
-
.library-card-
|
|
65
|
+
.library-card-header > div { min-width: 0; flex: 1; }
|
|
66
|
+
.library-card-title { font-weight: 600; font-size: 0.95em; color: var(--text-primary, #eee);
|
|
67
|
+
overflow-wrap: anywhere; word-break: break-word;
|
|
68
|
+
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
52
69
|
.library-card-desc { font-size: 0.82em; color: var(--text-secondary, #aaa); margin-bottom: 8px;
|
|
53
70
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
54
71
|
.library-card-meta { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
|
@@ -123,7 +140,7 @@ const LIBRARY_STYLES = `
|
|
|
123
140
|
.library-toolbar { gap: 6px; }
|
|
124
141
|
.library-toolbar .search-wrap { flex: 1 1 100%; min-width: 0; }
|
|
125
142
|
.library-grid { grid-template-columns: 1fr; }
|
|
126
|
-
.library-card { padding: 12px; }
|
|
143
|
+
.library-card { padding: 12px; padding-right: 84px; }
|
|
127
144
|
.library-card-scope { margin-left: 0; }
|
|
128
145
|
.library-actions { flex-wrap: wrap; justify-content: stretch; }
|
|
129
146
|
.library-actions button { flex: 1 1 140px; padding: 10px 12px; }
|
|
@@ -307,7 +324,7 @@ function LibraryCard({ entry, onSelect }) {
|
|
|
307
324
|
|
|
308
325
|
function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
309
326
|
const isNew = !entry?.id;
|
|
310
|
-
const
|
|
327
|
+
const initialFormSnapshot = {
|
|
311
328
|
id: entry?.id || "",
|
|
312
329
|
type: entry?.type || "prompt",
|
|
313
330
|
name: entry?.name || "",
|
|
@@ -315,10 +332,31 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
315
332
|
tags: (entry?.tags || []).join(", "),
|
|
316
333
|
scope: entry?.scope || "global",
|
|
317
334
|
content: "",
|
|
318
|
-
}
|
|
335
|
+
};
|
|
336
|
+
const [form, setForm] = useState(initialFormSnapshot);
|
|
337
|
+
const [baseline, setBaseline] = useState(initialFormSnapshot);
|
|
319
338
|
const [loading, setLoading] = useState(false);
|
|
320
339
|
const [loadingContent, setLoadingContent] = useState(!isNew && !!entry?.id);
|
|
321
340
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
341
|
+
const pendingKey = useMemo(
|
|
342
|
+
() => `modal:library-entry:${entry?.id || "new"}`,
|
|
343
|
+
[entry?.id],
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
const next = {
|
|
348
|
+
id: entry?.id || "",
|
|
349
|
+
type: entry?.type || "prompt",
|
|
350
|
+
name: entry?.name || "",
|
|
351
|
+
description: entry?.description || "",
|
|
352
|
+
tags: (entry?.tags || []).join(", "),
|
|
353
|
+
scope: entry?.scope || "global",
|
|
354
|
+
content: "",
|
|
355
|
+
};
|
|
356
|
+
setForm(next);
|
|
357
|
+
setBaseline(next);
|
|
358
|
+
setLoadingContent(!isNew && !!entry?.id);
|
|
359
|
+
}, [entry?.id]);
|
|
322
360
|
|
|
323
361
|
// Load content for existing entries
|
|
324
362
|
useEffect(() => {
|
|
@@ -330,7 +368,11 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
330
368
|
if (cancelled) return;
|
|
331
369
|
let contentStr = detail?.content ?? "";
|
|
332
370
|
if (typeof contentStr === "object") contentStr = JSON.stringify(contentStr, null, 2);
|
|
333
|
-
setForm((f) =>
|
|
371
|
+
setForm((f) => {
|
|
372
|
+
const next = { ...f, content: contentStr };
|
|
373
|
+
setBaseline(next);
|
|
374
|
+
return next;
|
|
375
|
+
});
|
|
334
376
|
} catch { /* ignore */ }
|
|
335
377
|
setLoadingContent(false);
|
|
336
378
|
})();
|
|
@@ -338,9 +380,27 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
338
380
|
}, [entry?.id]);
|
|
339
381
|
|
|
340
382
|
const updateField = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value }));
|
|
383
|
+
const changeCount = useMemo(
|
|
384
|
+
() => countChangedFields(baseline, form),
|
|
385
|
+
[baseline, form],
|
|
386
|
+
);
|
|
387
|
+
const hasUnsaved = changeCount > 0;
|
|
341
388
|
|
|
342
|
-
|
|
343
|
-
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
setPendingChange(pendingKey, hasUnsaved);
|
|
391
|
+
return () => clearPendingChange(pendingKey);
|
|
392
|
+
}, [hasUnsaved, pendingKey]);
|
|
393
|
+
|
|
394
|
+
const resetToBaseline = useCallback(() => {
|
|
395
|
+
setForm(baseline);
|
|
396
|
+
showToast("Changes discarded", "info");
|
|
397
|
+
}, [baseline]);
|
|
398
|
+
|
|
399
|
+
const handleSave = useCallback(async ({ closeAfterSave = true } = {}) => {
|
|
400
|
+
if (!form.name.trim()) {
|
|
401
|
+
showToast("Name is required", "error");
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
344
404
|
setLoading(true);
|
|
345
405
|
try {
|
|
346
406
|
const tags = form.tags.split(/[,\s]+/).map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
@@ -359,14 +419,23 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
359
419
|
});
|
|
360
420
|
if (res?.ok) {
|
|
361
421
|
showToast(`${TYPE_LABELS[form.type] || "Entry"} saved`, "success");
|
|
362
|
-
|
|
422
|
+
const nextBaseline = { ...form };
|
|
423
|
+
setBaseline(nextBaseline);
|
|
424
|
+
if (closeAfterSave) {
|
|
425
|
+
onSaved?.();
|
|
426
|
+
return { closed: true };
|
|
427
|
+
}
|
|
428
|
+
return true;
|
|
363
429
|
} else {
|
|
364
430
|
showToast(res?.error || "Save failed", "error");
|
|
431
|
+
return false;
|
|
365
432
|
}
|
|
366
433
|
} catch (err) {
|
|
367
434
|
showToast(err.message, "error");
|
|
435
|
+
return false;
|
|
436
|
+
} finally {
|
|
437
|
+
setLoading(false);
|
|
368
438
|
}
|
|
369
|
-
setLoading(false);
|
|
370
439
|
}, [form, onSaved]);
|
|
371
440
|
|
|
372
441
|
const handleDelete = useCallback(async () => {
|
|
@@ -402,7 +471,17 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
402
471
|
: "# Skill Title\n\n## Purpose\nDescribe what this skill teaches agents.\n\n## Instructions\n...";
|
|
403
472
|
|
|
404
473
|
return html`
|
|
405
|
-
<${Modal}
|
|
474
|
+
<${Modal}
|
|
475
|
+
title=${isNew ? "New Resource" : `Edit: ${entry.name}`}
|
|
476
|
+
onClose=${onClose}
|
|
477
|
+
unsavedChanges=${changeCount}
|
|
478
|
+
onSaveBeforeClose=${() => handleSave({ closeAfterSave: true })}
|
|
479
|
+
onDiscardBeforeClose=${() => {
|
|
480
|
+
resetToBaseline();
|
|
481
|
+
return true;
|
|
482
|
+
}}
|
|
483
|
+
activeOperationLabel=${loading ? "Save/Delete request is still running" : ""}
|
|
484
|
+
>
|
|
406
485
|
<div class="library-editor">
|
|
407
486
|
${isNew && html`
|
|
408
487
|
<label>
|
|
@@ -459,10 +538,27 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
459
538
|
`}
|
|
460
539
|
<div style="flex:1" />
|
|
461
540
|
<button class="btn-ghost" onClick=${onClose}>Cancel</button>
|
|
462
|
-
<button
|
|
541
|
+
<button
|
|
542
|
+
class="btn-primary"
|
|
543
|
+
onClick=${() => {
|
|
544
|
+
void handleSave({ closeAfterSave: true });
|
|
545
|
+
}}
|
|
546
|
+
disabled=${loading}
|
|
547
|
+
>
|
|
463
548
|
${loading ? html`<${Spinner} size=${14} />` : (isNew ? "Create" : "Save")}
|
|
464
549
|
</button>
|
|
465
550
|
</div>
|
|
551
|
+
<${SaveDiscardBar}
|
|
552
|
+
dirty=${hasUnsaved}
|
|
553
|
+
message=${`You have unsaved changes (${changeCount})`}
|
|
554
|
+
saveLabel=${isNew ? "Create" : "Save Changes"}
|
|
555
|
+
discardLabel="Discard"
|
|
556
|
+
onSave=${() => {
|
|
557
|
+
void handleSave({ closeAfterSave: false });
|
|
558
|
+
}}
|
|
559
|
+
onDiscard=${resetToBaseline}
|
|
560
|
+
saving=${loading}
|
|
561
|
+
/>
|
|
466
562
|
</div>
|
|
467
563
|
${confirmDelete && html`
|
|
468
564
|
<${ConfirmDialog}
|
|
@@ -664,7 +760,7 @@ export function LibraryTab() {
|
|
|
664
760
|
${iconText(":refresh: Rebuild")}
|
|
665
761
|
</button>
|
|
666
762
|
<button class="library-type-pill active" onClick=${() => setEditing({})}>
|
|
667
|
-
|
|
763
|
+
${iconText("➕ New")}
|
|
668
764
|
</button>
|
|
669
765
|
</div>
|
|
670
766
|
|
|
@@ -704,7 +800,7 @@ export function LibraryTab() {
|
|
|
704
800
|
: "Create your first prompt, agent profile, or skill."}
|
|
705
801
|
action=${searchQuery.value
|
|
706
802
|
? { label: "Clear search", onClick: () => { searchQuery.value = ""; loadEntries(); } }
|
|
707
|
-
: { label: "
|
|
803
|
+
: { label: "➕ New Resource", onClick: () => setEditing({}) }} />
|
|
708
804
|
`}
|
|
709
805
|
|
|
710
806
|
${!loading && displayed.length > 0 && html`
|