wisp-agents 0.24.1 → 0.25.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/CHANGELOG.md +16 -1
- package/README.md +44 -4
- package/bin/wisp.js +55 -1
- package/package.json +2 -2
- package/server/adapters/codex.js +3 -0
- package/server/client.js +2 -2
- package/server/index.js +17 -2
- package/server/mcp-ask.js +44 -2
- package/server/openapi.js +38 -0
- package/server/protocol.js +32 -3
- package/server/render.js +16 -1
- package/server/routes/automations.js +8 -5
- package/server/routes/internal.js +61 -0
- package/server/routes/meta.js +5 -1
- package/server/routes/settings.js +17 -3
- package/server/routes/spells.js +147 -0
- package/server/runner.js +10 -3
- package/server/services/launch.js +7 -2
- package/server/services/spell-hooks.js +99 -0
- package/server/spell-guide.js +135 -0
- package/server/spells-data.js +60 -0
- package/server/spells.js +647 -0
- package/web/css/feed.css +1 -1
- package/web/css/spells.css +205 -0
- package/web/index.html +4 -0
- package/web/js/app.js +3 -0
- package/web/js/blocks.js +7 -1
- package/web/js/features/actions.js +3 -0
- package/web/js/features/commands.js +11 -0
- package/web/js/features/composer.js +12 -0
- package/web/js/features/feed.js +3 -0
- package/web/js/features/keys.js +15 -0
- package/web/js/features/launcher.js +56 -11
- package/web/js/features/live.js +9 -1
- package/web/js/features/settings.js +21 -0
- package/web/js/features/slash.js +2 -1
- package/web/js/features/spell-actions.js +64 -0
- package/web/js/features/spell-ui.js +398 -0
- package/web/js/features/spells.js +715 -0
- package/web/js/features/turns.js +2 -0
- package/web/js/spell-schema.js +246 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/* Spells: what spells add to wisp (slots, panels, blocks), and Settings → Spells. Plain 1px neutral
|
|
2
|
+
borders like the panels next to them. */
|
|
3
|
+
|
|
4
|
+
.sp-sandbox { position: absolute; width: 0; height: 0; border: 0; visibility: hidden; }
|
|
5
|
+
|
|
6
|
+
/* top bar */
|
|
7
|
+
.sp-top { display: contents; }
|
|
8
|
+
.sp-btn .sp-n, .sp-fab .sp-n, .sp-qr .sp-n { min-width: 17px; height: 17px; padding: 0 5px; border-radius: 99px; font-size: 10.5px; font-weight: 600; display: inline-grid; place-items: center; background: var(--accent-soft); color: var(--accent-ink); }
|
|
9
|
+
.sp-btn .i { width: 14px; height: 14px; }
|
|
10
|
+
|
|
11
|
+
/* floating buttons */
|
|
12
|
+
.sp-float {
|
|
13
|
+
position: fixed; right: 22px; bottom: 22px; z-index: 30; display: flex; flex-direction: column-reverse; align-items: flex-end; gap: 10px;
|
|
14
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
15
|
+
}
|
|
16
|
+
.sp-float.left { right: auto; left: 22px; align-items: flex-start; }
|
|
17
|
+
.sp-float[hidden] { display: none; }
|
|
18
|
+
.sp-fab {
|
|
19
|
+
display: inline-flex; align-items: center; gap: 8px; height: 42px; padding: 0 14px 0 13px; border-radius: 21px;
|
|
20
|
+
background: var(--surface); color: var(--ink-2); border: 1px solid var(--line-2); box-shadow: var(--shadow-2);
|
|
21
|
+
font-size: 13.5px; font-weight: 500; transition: translate 0.15s var(--ease), color 0.15s, border-color 0.15s;
|
|
22
|
+
animation: sp-in 0.25s var(--ease);
|
|
23
|
+
}
|
|
24
|
+
.sp-fab .i { width: 16px; height: 16px; }
|
|
25
|
+
.sp-fab:hover { color: var(--ink); border-color: var(--faint); translate: 0 -2px; }
|
|
26
|
+
.sp-fab.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
27
|
+
.sp-fab:focus-visible { outline: 0; box-shadow: 0 0 0 2px var(--accent-soft), 0 0 0 1px var(--accent); }
|
|
28
|
+
@keyframes sp-in { from { opacity: 0; transform: translateY(8px) scale(0.97); } }
|
|
29
|
+
.sp-fan { display: flex; flex-direction: column-reverse; align-items: inherit; gap: 8px; }
|
|
30
|
+
.sp-fan-items { display: none; flex-direction: column-reverse; align-items: inherit; gap: 8px; }
|
|
31
|
+
.sp-fan.open .sp-fan-items { display: flex; }
|
|
32
|
+
.sp-fan.open .sp-fan-btn { color: var(--ink); border-color: var(--faint); }
|
|
33
|
+
body:has(.launcher.docked:is(:focus-within, :has(.open))) .sp-float { bottom: 96px; }
|
|
34
|
+
|
|
35
|
+
/* card head chips and hover buttons */
|
|
36
|
+
.sp-chip {
|
|
37
|
+
display: inline-flex; align-items: center; gap: 5px; height: 20px; padding: 0 7px; border-radius: 6px; font-size: 11.5px; white-space: nowrap;
|
|
38
|
+
color: var(--muted); background: var(--bg-2); border: 1px solid var(--line);
|
|
39
|
+
}
|
|
40
|
+
.sp-chip .i { width: 12px; height: 12px; }
|
|
41
|
+
.sp-chip.tone-good { color: var(--good); }
|
|
42
|
+
.sp-chip.tone-bad { color: var(--bad); }
|
|
43
|
+
.sp-chip.tone-warn { color: var(--warn); }
|
|
44
|
+
.sp-chip.tone-info { color: var(--info); }
|
|
45
|
+
button.sp-chip:hover { border-color: var(--line-2); color: var(--ink); }
|
|
46
|
+
.sp-quick:hover { color: var(--accent); }
|
|
47
|
+
|
|
48
|
+
/* prompt box and follow-up box */
|
|
49
|
+
.sp-lbar, .sp-cbar { display: inline-flex; align-items: center; gap: 6px; }
|
|
50
|
+
.sp-lbar:empty, .sp-cbar:empty { display: none; }
|
|
51
|
+
.sp-lchips, .sp-cchips { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 2px 0; }
|
|
52
|
+
.sp-cchips { padding: 0 0 8px; }
|
|
53
|
+
.sp-cchips[hidden] { display: none; }
|
|
54
|
+
.sp-qr {
|
|
55
|
+
display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px; border-radius: 13px; font-size: 12.5px;
|
|
56
|
+
color: var(--ink-2); background: var(--surface-2); border: 1px solid var(--line); transition: border-color 0.15s, color 0.15s;
|
|
57
|
+
text-align: left; max-width: 100%;
|
|
58
|
+
}
|
|
59
|
+
.sp-qr .i { width: 13px; height: 13px; }
|
|
60
|
+
.sp-qr:hover { border-color: var(--line-2); color: var(--ink); }
|
|
61
|
+
|
|
62
|
+
/* panels: in cards, above the feed, and as dialogs */
|
|
63
|
+
.sp-card { display: grid; gap: 10px; padding: 12px 14px 0; }
|
|
64
|
+
.sp-card[hidden] { display: none; }
|
|
65
|
+
.sp-cpanel, .sp-feed { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
|
|
66
|
+
.sp-cpanel > header, .sp-feed > header { display: flex; align-items: center; gap: 8px; padding: 9px 12px; border-bottom: 1px solid var(--line); font-size: 12.5px; color: var(--ink-2); }
|
|
67
|
+
.sp-cpanel > header .i, .sp-feed > header .i { width: 14px; height: 14px; color: var(--muted); }
|
|
68
|
+
.sp-by { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--faint); }
|
|
69
|
+
.sp-by .i { width: 12px; height: 12px; }
|
|
70
|
+
.sp-cpanel-body, .sp-feed-body { padding: 12px; }
|
|
71
|
+
.sp-cpanel-body .presentation, .sp-feed-body .presentation, .sp-panel-body .presentation, .sp-pv-body .presentation, .sp-block .presentation { gap: 10px; }
|
|
72
|
+
.sp-feedtop { display: grid; gap: 12px; margin: 14px 0; }
|
|
73
|
+
.sp-feedtop[hidden] { display: none; }
|
|
74
|
+
.sp-panel { width: min(720px, 100%); }
|
|
75
|
+
.sp-panel header { gap: 10px; }
|
|
76
|
+
.sp-panel header h2 { display: flex; align-items: center; gap: 8px; margin-right: auto; }
|
|
77
|
+
.sp-panel header h2 .i { width: 18px; height: 18px; color: var(--muted); }
|
|
78
|
+
.sp-panel .sp-by { margin-left: 0; }
|
|
79
|
+
.sp-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(460px, 100%); max-height: none; border-radius: 0; border-width: 0 0 0 1px; }
|
|
80
|
+
.sp-out { max-height: 60vh; overflow: auto; white-space: pre-wrap; }
|
|
81
|
+
.sp-error { display: flex; gap: 8px; align-items: flex-start; padding: 10px 12px; border-radius: 9px; font-size: 13px; color: var(--bad); background: var(--del-bg); }
|
|
82
|
+
.sp-error .i { flex: none; width: 15px; height: 15px; margin-top: 1px; }
|
|
83
|
+
.sp-empty { margin: 0; color: var(--faint); font-size: 13px; }
|
|
84
|
+
|
|
85
|
+
/* blocks spells add to reports */
|
|
86
|
+
.sp-off { padding: 10px 12px; }
|
|
87
|
+
.sp-off-head { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); }
|
|
88
|
+
.sp-off-head .i { width: 14px; height: 14px; }
|
|
89
|
+
.sp-off-head .link { margin-left: auto; }
|
|
90
|
+
.sp-off pre { margin: 10px 0 0; }
|
|
91
|
+
|
|
92
|
+
/* the spell block in a Cast a spell report */
|
|
93
|
+
.b-spell { padding: 14px 16px; display: grid; gap: 10px; }
|
|
94
|
+
.sp-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 15px; }
|
|
95
|
+
.sp-head > .i { width: 17px; height: 17px; color: var(--accent); }
|
|
96
|
+
.sp-ver { font: 500 11.5px var(--mono); color: var(--faint); }
|
|
97
|
+
.sp-state { margin-left: auto; }
|
|
98
|
+
.sp-on { display: inline-flex; align-items: center; height: 20px; padding: 0 8px; border-radius: 10px; font-size: 11.5px; font-weight: 600; color: var(--good); background: var(--add-bg); }
|
|
99
|
+
.sp-desc { margin: 0; color: var(--ink-2); font-size: 13.5px; }
|
|
100
|
+
.sp-lines { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; font-size: 13px; color: var(--ink-2); }
|
|
101
|
+
.sp-lines li { display: flex; gap: 8px; align-items: flex-start; }
|
|
102
|
+
.sp-lines li .i { flex: none; width: 13px; height: 13px; margin-top: 3px; color: var(--good); }
|
|
103
|
+
.sp-lines li.bad, .sp-lines li.bad .i { color: var(--bad); }
|
|
104
|
+
.sp-lines li.warn .i { color: var(--warn); }
|
|
105
|
+
.sp-preview { display: grid; gap: 10px; }
|
|
106
|
+
.sp-preview:empty { display: none; }
|
|
107
|
+
.sp-pv-btns { display: flex; flex-wrap: wrap; gap: 12px; padding: 12px; border-radius: 10px; background: var(--bg-2); border: 1px solid var(--line); }
|
|
108
|
+
.sp-pv { display: grid; gap: 5px; justify-items: start; }
|
|
109
|
+
.sp-pv small, .sp-pv-panel small { font-size: 11px; color: var(--faint); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
110
|
+
.sp-pv .sp-fab { animation: none; box-shadow: none; }
|
|
111
|
+
.sp-pv .sp-quick { opacity: 1; margin: 0; }
|
|
112
|
+
.sp-pv > span:not(small) { pointer-events: none; }
|
|
113
|
+
.sp-pv-panel { display: grid; gap: 6px; }
|
|
114
|
+
.sp-pv-body { padding: 12px; border: 1px solid var(--line); border-radius: 10px; }
|
|
115
|
+
.sp-pv-note { display: flex; align-items: center; gap: 6px; margin: 0; font-size: 13px; color: var(--muted); }
|
|
116
|
+
.sp-pv-note .i { width: 13px; height: 13px; color: var(--accent); }
|
|
117
|
+
.sp-acts { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
118
|
+
.sp-acts .i { width: 14px; height: 14px; }
|
|
119
|
+
.sp-json { margin: 0; max-height: 360px; overflow: auto; }
|
|
120
|
+
|
|
121
|
+
/* how risky */
|
|
122
|
+
.sp-kind { display: inline-flex; align-items: center; height: 19px; padding: 0 7px; border-radius: 6px; font: 600 11px var(--sans); letter-spacing: 0.01em; border: 1px solid var(--line); color: var(--muted); background: var(--surface-2); vertical-align: 1px; }
|
|
123
|
+
.sp-kind.k-acts { color: var(--info); }
|
|
124
|
+
.sp-kind.k-reads { color: var(--ink-2); }
|
|
125
|
+
.sp-kind.k-runs { color: var(--warn); }
|
|
126
|
+
|
|
127
|
+
/* trying a spell */
|
|
128
|
+
.sp-try {
|
|
129
|
+
position: fixed; left: 50%; top: 70px; translate: -50% 0; z-index: 60; display: flex; align-items: center; gap: 10px;
|
|
130
|
+
padding: 8px 8px 8px 14px; border-radius: 14px; background: var(--surface); border: 1px solid var(--line-2); box-shadow: var(--shadow-2); font-size: 13.5px;
|
|
131
|
+
animation: sp-in 0.25s var(--ease);
|
|
132
|
+
}
|
|
133
|
+
.sp-try > .i { width: 15px; height: 15px; color: var(--accent); }
|
|
134
|
+
|
|
135
|
+
/* cast, review and Settings → Spells */
|
|
136
|
+
.sp-cast { display: grid; gap: 12px; }
|
|
137
|
+
.sp-cast header, .sp-review header { margin-bottom: 0; }
|
|
138
|
+
.sp-cast h2, .sp-review h2 { display: flex; align-items: center; gap: 8px; }
|
|
139
|
+
.sp-cast h2 .i, .sp-review h2 .i { width: 18px; height: 18px; color: var(--accent); }
|
|
140
|
+
.sp-cast footer, .sp-review footer, .sp-confirm footer { display: flex; align-items: center; gap: 10px; justify-content: flex-end; }
|
|
141
|
+
.sp-cast footer .hint { margin-right: auto; }
|
|
142
|
+
.sp-review { display: grid; gap: 12px; }
|
|
143
|
+
.sp-review p { margin: 0; }
|
|
144
|
+
.sp-review .grow { flex: 1; }
|
|
145
|
+
.sp-review-kind { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; background: var(--bg-2); border: 1px solid var(--line); font-size: 13px; color: var(--ink-2); }
|
|
146
|
+
.sp-review details pre { max-height: 300px; overflow: auto; }
|
|
147
|
+
.sp-confirm { display: grid; gap: 12px; }
|
|
148
|
+
.sp-confirm p { margin: 0; }
|
|
149
|
+
.sp-examples { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
150
|
+
/* a long example wraps inside its chip instead of spilling over the next one */
|
|
151
|
+
.sp-examples .sp-qr { height: auto; min-height: 26px; padding-block: 3px; line-height: 1.4; }
|
|
152
|
+
.sp-castbox { display: grid; gap: 10px; padding-bottom: 14px; }
|
|
153
|
+
.sp-castbox > .sp-examples { padding: 0 14px; }
|
|
154
|
+
.sp-castrow { display: flex; gap: 8px; align-items: center; padding: 0 14px 14px; }
|
|
155
|
+
.sp-castrow:has(.textarea) { align-items: flex-start; }
|
|
156
|
+
.sp-castrow .textarea { min-height: 56px; }
|
|
157
|
+
/* the cast box: the launcher's prompt in small, the send button in a bar under the text */
|
|
158
|
+
.sp-prompt {
|
|
159
|
+
margin: 0 14px; padding: 8px 10px 8px; border: 1px solid var(--line-2); border-radius: 12px; background: var(--surface);
|
|
160
|
+
box-shadow: var(--shadow-1); cursor: text; transition: box-shadow 0.15s, border-color 0.15s;
|
|
161
|
+
}
|
|
162
|
+
.sp-prompt:focus-within { border-color: color-mix(in srgb, var(--accent) 55%, var(--line-2)); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
163
|
+
.sp-prompt textarea {
|
|
164
|
+
display: block; width: 100%; resize: none; border: 0; outline: 0; background: transparent; padding: 6px 4px;
|
|
165
|
+
font-size: 15px; line-height: 1.55; color: var(--ink); min-height: 56px;
|
|
166
|
+
}
|
|
167
|
+
.sp-prompt textarea::placeholder { color: var(--faint); }
|
|
168
|
+
.sp-pbar { display: flex; align-items: center; gap: 8px; padding-top: 4px; }
|
|
169
|
+
.sp-pbar .grow { flex: 1; }
|
|
170
|
+
.sp-pin { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--faint); }
|
|
171
|
+
.sp-pin .i { width: 13px; height: 13px; color: var(--accent); }
|
|
172
|
+
.sp-castrow .primary, .sp-castrow .ghost { flex: none; }
|
|
173
|
+
.sp-row .sx-text details { margin-top: 4px; font-size: 12.5px; color: var(--muted); }
|
|
174
|
+
.sp-row .sx-text summary { cursor: pointer; width: max-content; }
|
|
175
|
+
.sp-row .sp-lines { margin-top: 6px; }
|
|
176
|
+
.sp-row .sx-ctl { display: flex; align-items: center; gap: 4px; }
|
|
177
|
+
.sp-row.broken b { color: var(--bad); }
|
|
178
|
+
.sp-none { display: flex; gap: 12px; align-items: center; padding: 16px 4px; color: var(--muted); }
|
|
179
|
+
.sp-none > .i { width: 20px; height: 20px; color: var(--accent); }
|
|
180
|
+
.sp-none p { margin: 2px 0 0; font-size: 13px; }
|
|
181
|
+
.sp-foot .link { vertical-align: baseline; }
|
|
182
|
+
.sp-lock { display: flex !important; align-items: center; gap: 5px; margin-top: 3px; color: var(--accent-ink) !important; }
|
|
183
|
+
.sp-lock .i { width: 12px; height: 12px; }
|
|
184
|
+
.menu .sp-menu { display: flex; align-items: center; gap: 9px; width: 100%; }
|
|
185
|
+
|
|
186
|
+
/* card density */
|
|
187
|
+
body.sp-compact #feed .it-head { padding-top: 7px; padding-bottom: 7px; }
|
|
188
|
+
body.sp-compact #feed .it-sub { display: none; }
|
|
189
|
+
body.sp-compact #feed .f-list { gap: 6px; }
|
|
190
|
+
body.sp-comfy #feed .it-head { padding-top: 15px; padding-bottom: 15px; }
|
|
191
|
+
|
|
192
|
+
@media (max-width: 640px) {
|
|
193
|
+
.sp-float { right: 14px; bottom: 14px; }
|
|
194
|
+
.sp-fab { height: 40px; padding: 0 12px; }
|
|
195
|
+
.sp-fab .sp-fab-l { display: none; }
|
|
196
|
+
.sp-fan.open .sp-fab-l { display: inline; }
|
|
197
|
+
.sp-try { left: 12px; right: 12px; translate: none; top: 62px; }
|
|
198
|
+
.sp-castrow { flex-direction: column; align-items: stretch; }
|
|
199
|
+
/* bar buttons fold to their icon (a button without one keeps its label) */
|
|
200
|
+
.sp-lbar .sp-btn:has(.i) > span, .sp-cbar .sp-btn:has(.i) > span { display: none; }
|
|
201
|
+
.sp-lbar .sp-btn, .sp-cbar .sp-btn { padding: 0 7px; }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* the floating stack moves up while the Collapse button is out */
|
|
205
|
+
body:has(.collapse-all.on) .sp-float:not(.left) { bottom: 76px; }
|
package/web/index.html
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
<link rel="stylesheet" href="/css/subagents.css">
|
|
24
24
|
<link rel="stylesheet" href="/css/auto.css">
|
|
25
25
|
<link rel="stylesheet" href="/css/transcript.css">
|
|
26
|
+
<link rel="stylesheet" href="/css/spells.css">
|
|
26
27
|
</head>
|
|
27
28
|
<body>
|
|
28
29
|
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
@@ -98,6 +99,7 @@
|
|
|
98
99
|
<button class="auto-btn" id="autoBtn" title="Automations: recipes and triggers">
|
|
99
100
|
<svg class="i"><use href="#i-bolt"/></svg><span>Automations</span><b class="ab-n" hidden></b>
|
|
100
101
|
</button>
|
|
102
|
+
<div class="sp-top" id="spTop"></div>
|
|
101
103
|
<button class="search-btn" id="searchBtn" title="Search">
|
|
102
104
|
<svg class="i"><use href="#i-search"/></svg><span>Search</span><kbd class="mod-k"></kbd>
|
|
103
105
|
</button>
|
|
@@ -117,6 +119,7 @@
|
|
|
117
119
|
</section>
|
|
118
120
|
</div>
|
|
119
121
|
<div class="feed-head" id="feedHead"></div>
|
|
122
|
+
<div class="sp-feedtop" id="spFeedTop" hidden></div>
|
|
120
123
|
<div class="feed" id="feed">
|
|
121
124
|
<section class="f-group needs" id="secNeeds" hidden><h4>Needs you<span class="n"></span></h4><div class="f-list"></div></section>
|
|
122
125
|
<section class="f-group working" id="secWorking" hidden><h4>Working<span class="n"></span></h4><div class="f-list"></div></section>
|
|
@@ -131,6 +134,7 @@
|
|
|
131
134
|
<svg class="i"><use href="#i-send"/></svg><kbd>U</kbd>
|
|
132
135
|
</button>
|
|
133
136
|
<div class="drag-strip"></div>
|
|
137
|
+
<div class="sp-float" id="spFloat" hidden></div>
|
|
134
138
|
<div class="overlay" id="overlay" hidden></div>
|
|
135
139
|
<div class="toasts" id="toasts"></div>
|
|
136
140
|
|
package/web/js/app.js
CHANGED
|
@@ -19,6 +19,8 @@ import { onKey } from './features/keys.js';
|
|
|
19
19
|
import { backToTop, onScroll, stage } from './features/scroll.js';
|
|
20
20
|
import { openSettings, telemetryNotice } from './features/settings.js';
|
|
21
21
|
import { installUpdate, loadUpdate } from './features/update.js';
|
|
22
|
+
import { routeSpell } from './features/spell-ui.js';
|
|
23
|
+
import './features/spells.js';
|
|
22
24
|
|
|
23
25
|
document.addEventListener('keydown', onKey);
|
|
24
26
|
stage.addEventListener('scroll', onScroll, { passive: true });
|
|
@@ -58,6 +60,7 @@ if (/Electron/.test(navigator.userAgent)) document.body.classList.add('electron'
|
|
|
58
60
|
|
|
59
61
|
// #/s/<id> still works (notifications, CLI): it reveals that item in the feed.
|
|
60
62
|
function route() {
|
|
63
|
+
if (routeSpell()) return; // #/spell?src=…: a shared spell to review
|
|
61
64
|
const m = location.hash.match(/^#\/s\/(\w+)/);
|
|
62
65
|
if (!m) return;
|
|
63
66
|
history.replaceState(null, '', '#/');
|
package/web/js/blocks.js
CHANGED
|
@@ -930,6 +930,12 @@ R.unknown = (b) => el(`<div class="b-code card"><div class="code-head"><span cla
|
|
|
930
930
|
|
|
931
931
|
// --- public -----------------------------------------------------------------
|
|
932
932
|
|
|
933
|
+
// Block types defined elsewhere: spells (features/spells.js) register the `spell` block and draw
|
|
934
|
+
// the types spells add through the fallback, before R.unknown.
|
|
935
|
+
export const registerBlock = (type, fn) => (R[type] = fn);
|
|
936
|
+
let fallback = () => null;
|
|
937
|
+
export const setBlockFallback = (fn) => (fallback = fn);
|
|
938
|
+
|
|
933
939
|
export function renderPresentation(presentation, ctx) {
|
|
934
940
|
const wrap = document.createElement('div');
|
|
935
941
|
wrap.className = 'presentation';
|
|
@@ -939,7 +945,7 @@ export function renderPresentation(presentation, ctx) {
|
|
|
939
945
|
const delay = reduced() ? 0 : Math.min(i, 10) * 110;
|
|
940
946
|
let node;
|
|
941
947
|
try {
|
|
942
|
-
node =
|
|
948
|
+
node = R[b.type] ? R[b.type](b, { ...ctx, delay }) : fallback(b, { ...ctx, delay }) || R.unknown(b);
|
|
943
949
|
} catch (err) {
|
|
944
950
|
console.error('block failed', b, err);
|
|
945
951
|
node = R.unknown(b);
|
|
@@ -23,6 +23,7 @@ import { machineById, pathAt, placeName, projectById } from './places.js';
|
|
|
23
23
|
import { agentById, agentName, DEFAULT_MODE, effortsFor, harnessesAt, modeMenu, MODES, paintRunnerBtn, savedEffort, savedModel } from './runners.js';
|
|
24
24
|
import { feedAnchor, keepInPlace } from './scroll.js';
|
|
25
25
|
import { mergePr, shipDialog, shipLabel, shippable } from './ship.js';
|
|
26
|
+
import { slotHtml } from './spells.js';
|
|
26
27
|
|
|
27
28
|
export async function toggleDone(id) {
|
|
28
29
|
const s = S.sessions.get(id);
|
|
@@ -192,9 +193,11 @@ export function taskMenu(id, e) {
|
|
|
192
193
|
<button data-a="recipe">${icon('spark')}Save as recipe…</button>
|
|
193
194
|
${busy || s.archived ? '' : `<button data-a="done">${icon('done')}${s.state === 'done' ? 'Reopen' : 'Mark done'}${keyHint('E')}</button>`}
|
|
194
195
|
${s.archived ? `<button data-a="unarchive">${icon('archive')}Unarchive${keyHint('A')}</button>` : busy ? '' : `<button data-a="archive">${icon('archive')}Archive${keyHint('A')}</button>`}
|
|
196
|
+
${slotHtml('card.menu', s).replace(/class="ghost sm sp-btn"/g, 'class="sp-menu"').replace(/<button /g, `<button data-task="${s.id}" `)}
|
|
195
197
|
<button data-a="delete" class="danger">${icon('trash')}Delete session${keyHint('⌫')}</button>
|
|
196
198
|
</div>`);
|
|
197
199
|
menu.onclick = async (ev) => {
|
|
200
|
+
if (ev.target.closest('[data-sp]')) return close();
|
|
198
201
|
const a = ev.target.closest('button')?.dataset.a;
|
|
199
202
|
close();
|
|
200
203
|
if (a === 'open') expand(id);
|
|
@@ -23,6 +23,8 @@ import { mergePr, shipDialog, shipLabel, shippable } from './ship.js';
|
|
|
23
23
|
import { transcriptOn } from './transcript.js';
|
|
24
24
|
import { replay, toggleTranscript } from './turns.js';
|
|
25
25
|
import { openUsage } from './usage.js';
|
|
26
|
+
import { activeSpells, runAction } from './spells.js';
|
|
27
|
+
import { openCast } from './spell-ui.js';
|
|
26
28
|
|
|
27
29
|
const STATE_LABEL = { running: 'Working', waiting: 'Asking', review: 'Review', done: 'Done' };
|
|
28
30
|
|
|
@@ -96,6 +98,15 @@ function paletteItems() {
|
|
|
96
98
|
{ group: a, label: 'Settings', icon: 'settings', keywords: 'preferences lessons learn retro editor terminal notifications', run: openSettings },
|
|
97
99
|
{ group: a, label: 'Keyboard shortcuts', icon: 'info', kbd: '?', keywords: 'keys help hotkeys', run: shortcuts },
|
|
98
100
|
);
|
|
101
|
+
items.push(
|
|
102
|
+
{ group: a, label: 'Cast a spell…', icon: 'spark', keywords: 'spell customize change wisp add button panel block theme plugin extension mod', run: () => openCast() },
|
|
103
|
+
{ group: a, label: 'Spells', icon: 'spark', keywords: 'spells installed manage import export share customize', run: () => openSettings({ pane: 'spells' }) },
|
|
104
|
+
);
|
|
105
|
+
// What spells add to ⌘K: their commands, and their buttons for the palette slot.
|
|
106
|
+
for (const spell of activeSpells()) {
|
|
107
|
+
const cmds = [...(spell.commands || []), ...(spell.buttons || []).filter((b) => b.slot === 'palette')];
|
|
108
|
+
for (const c of cmds) items.push({ group: 'Spells', label: c.label, sub: spell.name, icon: c.icon || 'spark', kbd: c.key ? c.key.replace(/^shift\+/i, '⇧').toUpperCase() : undefined, keywords: `spell ${c.keywords || ''}`, run: () => runAction(spell, c.do, { session: S.focus }) });
|
|
109
|
+
}
|
|
99
110
|
if (feedbackOn()) items.push({ group: a, label: 'Send feedback…', icon: 'thumb-up', keywords: 'feedback bug report idea suggestion thumbs contact team', run: () => openFeedback() });
|
|
100
111
|
|
|
101
112
|
for (const r of S.config.recipes || []) items.push({ group: 'Recipes', label: `Run: ${r.name}`, sub: r.prompt.replace(/\s+/g, ' ').slice(0, 80), icon: 'spark', keywords: `recipe ${r.prompt}`, run: () => runRecipe({ ...r, params: recipeParams(r.prompt) }) });
|
|
@@ -24,6 +24,8 @@ import { feedAnchor, keepInPlace, watching } from './scroll.js';
|
|
|
24
24
|
import { shipDialog } from './ship.js';
|
|
25
25
|
import { slashMenu } from './slash.js';
|
|
26
26
|
import { renderBody } from './turns.js';
|
|
27
|
+
import { paintCard, runAction, spellProp } from './spells.js';
|
|
28
|
+
import { spellSlash } from './launcher.js';
|
|
27
29
|
|
|
28
30
|
export function draftInto(id, text) {
|
|
29
31
|
const ta = $('.reply', S.nodes.get(id)?.card);
|
|
@@ -91,6 +93,7 @@ export function renderDock(id) {
|
|
|
91
93
|
}
|
|
92
94
|
});
|
|
93
95
|
$('.send', comp).onclick = () => sendReply(id, ta.value);
|
|
96
|
+
paintCard(id); // what spells add to the box and the card
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
// The mode the next follow-up runs in: the session's own until the user picks another here.
|
|
@@ -243,6 +246,8 @@ export function updateDock(id) {
|
|
|
243
246
|
? `Follow up with ${next} to reopen…`
|
|
244
247
|
: 'Follow up to reopen…'
|
|
245
248
|
: `Follow up with ${next}…`;
|
|
249
|
+
const spellPh = spellProp('composer.input', 'placeholder');
|
|
250
|
+
if (spellPh && !tour && !s.ask) ta.placeholder = spellPh;
|
|
246
251
|
paintSide(id);
|
|
247
252
|
paintReplyMode(id, $('.c-mode', n.card));
|
|
248
253
|
paintReplyRunner(id, $('.c-runner', n.card));
|
|
@@ -335,6 +340,13 @@ export async function sendReply(id, text) {
|
|
|
335
340
|
text = (text || '').trim();
|
|
336
341
|
if (!s || !text) return;
|
|
337
342
|
const ta = n && $('.reply', n.card);
|
|
343
|
+
// A spell's slash command: it does its thing on this task instead of being sent.
|
|
344
|
+
const slash = !isBtw(text) && spellSlash(text);
|
|
345
|
+
if (slash) {
|
|
346
|
+
if (ta) ((ta.value = ''), ta.dispatchEvent(new Event('input')));
|
|
347
|
+
S.drafts.delete(id);
|
|
348
|
+
return runAction(slash.spell, slash.cmd.do, { session: id, args: slash.args, input: ta });
|
|
349
|
+
}
|
|
338
350
|
// A side question: answered next to the run, so nothing is interrupted, queued or switched.
|
|
339
351
|
if (isBtw(text)) {
|
|
340
352
|
const question = btwQuestion(text);
|
package/web/js/features/feed.js
CHANGED
|
@@ -16,6 +16,7 @@ import { scrollByPx, stage, topInset } from './scroll.js';
|
|
|
16
16
|
import { prOf, prPill, shipPill } from './ship.js';
|
|
17
17
|
import { teamStack } from './team.js';
|
|
18
18
|
import { renderBody } from './turns.js';
|
|
19
|
+
import { slotHtml } from './spells.js';
|
|
19
20
|
|
|
20
21
|
function visible(list) {
|
|
21
22
|
return S.filter === 'all' ? list : list.filter((s) => inProject(s, S.filter));
|
|
@@ -247,7 +248,9 @@ function renderHead(n, s) {
|
|
|
247
248
|
${s.team?.length ? teamStack(s.agent, s.team) : modelBadge(s.agent, s.model, s.effort, s.route?.switched ? null : s.route)}
|
|
248
249
|
${s.machine ? machinePill(s.machine) : ''}
|
|
249
250
|
${projectLabel(s)}
|
|
251
|
+
${s.pending ? '' : slotHtml('card.head', s)}
|
|
250
252
|
<span class="q-time">${ago(s.updatedAt)}</span>
|
|
253
|
+
${s.pending ? '' : slotHtml('card.hover', s)}
|
|
251
254
|
${s.state === 'review' && !open && !s.archived ? `<button class="it-quick" data-a="quick-done" title="Mark done (E)">${icon('done')}</button>` : ''}
|
|
252
255
|
${icon('chevron', 'chev')}
|
|
253
256
|
</span>`;
|
package/web/js/features/keys.js
CHANGED
|
@@ -11,6 +11,10 @@ import { focusLauncher } from './launcher.js';
|
|
|
11
11
|
import { backToTop } from './scroll.js';
|
|
12
12
|
import { shipDialog, shippable } from './ship.js';
|
|
13
13
|
import { replay, toggleTranscript } from './turns.js';
|
|
14
|
+
import { activeSpells, runAction } from './spells.js';
|
|
15
|
+
|
|
16
|
+
// Keys wisp uses itself (with or without Shift), which spells can't claim.
|
|
17
|
+
const BUILT_IN = new Set(['n', 'j', 'k', 'o', 'enter', 'e', 'a', 'c', 'u', 'home', 'm', 'delete', 'backspace', 't', 'shift+r', 's', 'r', 'v', '/', '?', 'shift+?', 'escape', ...'123456789'.split('')]);
|
|
14
18
|
|
|
15
19
|
export function onKey(e) {
|
|
16
20
|
// ⌘K / Ctrl+K works from anywhere, even mid-sentence in the launcher.
|
|
@@ -37,6 +41,17 @@ export function onKey(e) {
|
|
|
37
41
|
if (typing || e.metaKey || e.ctrlKey || e.altKey || !$('#overlay').hidden) return;
|
|
38
42
|
const k = e.key.toLowerCase();
|
|
39
43
|
const id = S.focus && S.nodes.has(S.focus) ? S.focus : null;
|
|
44
|
+
// A spell's key (only keys wisp doesn't use itself: spells can't take over built-in ones).
|
|
45
|
+
const combo = `${e.shiftKey ? 'shift+' : ''}${k}`;
|
|
46
|
+
if (!BUILT_IN.has(combo)) {
|
|
47
|
+
for (const spell of activeSpells()) {
|
|
48
|
+
const c = [...(spell.commands || []), ...(spell.buttons || [])].find((x) => x.key && x.key.toLowerCase() === combo);
|
|
49
|
+
if (c) {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
return runAction(spell, c.do, { session: id });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
40
55
|
if (k === 'n') {
|
|
41
56
|
e.preventDefault();
|
|
42
57
|
focusLauncher();
|
|
@@ -27,6 +27,8 @@ import { needsSetup, nudgeSetup, renderSetup } from './setup.js';
|
|
|
27
27
|
import { agentName, agentsAt, DEFAULT_MODE, effortLabel, effortsFor, harnessesAt, harnessLogo, MAX_RUNNERS, modeMenu, MODES, paintRunnerBtn, runnerTip, savedEffort, savedModel } from './runners.js';
|
|
28
28
|
import { feedAnchor, keepInPlace, stage, topInset } from './scroll.js';
|
|
29
29
|
import { canLead, paintTeamBtn, teamEditor } from './team.js';
|
|
30
|
+
import { activeSpells, launcherParts, runAction, spellProp } from './spells.js';
|
|
31
|
+
import { cast } from './spell-ui.js';
|
|
30
32
|
|
|
31
33
|
// "No folder": a general ask with no project. The server gives it an empty scratch directory.
|
|
32
34
|
const NO_FOLDER = 'none';
|
|
@@ -72,6 +74,19 @@ function syncWhere() {
|
|
|
72
74
|
launch.project = storedPick() || null;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
// A spell's slash command at the start of `text`: { spell, cmd, args } or null.
|
|
78
|
+
export function spellSlash(text) {
|
|
79
|
+
const m = /^\/([a-z][a-z0-9-]*)(?:\s+([\s\S]*))?$/i.exec(String(text || '').trim());
|
|
80
|
+
if (!m) return null;
|
|
81
|
+
for (const spell of activeSpells()) {
|
|
82
|
+
const cmd = (spell.slash || []).find((c) => c.name === m[1].toLowerCase());
|
|
83
|
+
if (cmd) return { spell, cmd, args: (m[2] || '').trim() };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
bus.on('spells:launcher', () => $('#launcher:not(.skel)') && renderLauncher());
|
|
89
|
+
|
|
75
90
|
export function renderLauncher() {
|
|
76
91
|
const box = $('#launcher');
|
|
77
92
|
const mode = S.config.defaults.mode || DEFAULT_MODE.id;
|
|
@@ -104,18 +119,19 @@ export function renderLauncher() {
|
|
|
104
119
|
<div class="l-recipes"></div>`;
|
|
105
120
|
renderLaunchChips();
|
|
106
121
|
renderSetup();
|
|
122
|
+
launcherParts(box); // what spells add to the prompt box
|
|
107
123
|
|
|
108
124
|
const ta = mentionBox($('#prompt', box));
|
|
109
125
|
const tray = attachTray();
|
|
110
126
|
ta.parentNode.before(tray.node); // before the mention box's wrapper
|
|
111
127
|
acceptFiles(box, ta, tray);
|
|
112
|
-
autosize(ta);
|
|
128
|
+
autosize(ta, (spellProp('launcher.input', 'maxRows') || 0) * 24 || undefined);
|
|
113
129
|
ta.value = S.drafts.get('home') || '';
|
|
114
130
|
ta.addEventListener('input', () => (S.drafts.set('home', ta.value), paintSaveRecipe()));
|
|
115
131
|
// While focus is elsewhere, the placeholder advertises the shortcut that brings you back.
|
|
116
|
-
const hint = () => (ta.placeholder = document.activeElement === ta
|
|
132
|
+
const hint = () => (ta.placeholder = spellProp('launcher.input', 'placeholder') || (document.activeElement === ta
|
|
117
133
|
? 'What should I go do? Paste screenshots, drop logs or videos…'
|
|
118
|
-
: `What should I go do? Press ${combo('L')} to start typing`);
|
|
134
|
+
: `What should I go do? Press ${combo('L')} to start typing`));
|
|
119
135
|
ta.addEventListener('focus', hint);
|
|
120
136
|
ta.addEventListener('blur', hint);
|
|
121
137
|
hint();
|
|
@@ -247,8 +263,7 @@ export function renderLauncher() {
|
|
|
247
263
|
});
|
|
248
264
|
});
|
|
249
265
|
paintCompare();
|
|
250
|
-
|
|
251
|
-
runnerPicker(runnerBtn, { agent: agent?.id, model: curModel, effort: curEffort, savedModel, savedEffort, places: launch.where, auto: true }, (id, model, effort) => {
|
|
266
|
+
const pickRun = (id, model, effort) => {
|
|
252
267
|
agent = harnessesAt(launch.where, { auto: true }).find((a) => a.id === id);
|
|
253
268
|
curModel = model;
|
|
254
269
|
curEffort = effort;
|
|
@@ -260,11 +275,38 @@ export function renderLauncher() {
|
|
|
260
275
|
const efforts = effort || effortsFor(id, model).length ? { ...d.efforts, [id]: effort } : d.efforts;
|
|
261
276
|
Object.assign(d, { agent: id, model, models, effort, efforts });
|
|
262
277
|
api('/config', { method: 'PATCH', body: { defaults: { agent: id, model, models, effort, efforts } } }).catch(() => {});
|
|
263
|
-
|
|
278
|
+
};
|
|
279
|
+
runnerBtn.onclick = () => runnerPicker(runnerBtn, { agent: agent?.id, model: curModel, effort: curEffort, savedModel, savedEffort, places: launch.where, auto: true }, pickRun);
|
|
280
|
+
// Spells: set the pickers (setLaunch), or launch once with other settings (launchWith).
|
|
281
|
+
box.spellSet = (v) => {
|
|
282
|
+
if (v.agent && harnessesAt(launch.where, { auto: true }).some((a) => a.id === v.agent)) pickRun(v.agent, v.model ?? savedModel(v.agent), v.effort ?? savedEffort(v.agent, v.model ?? savedModel(v.agent)));
|
|
283
|
+
else if (v.agent) toast(`${esc(agentName(v.agent))} isn’t installed here`, { tone: 'bad' });
|
|
284
|
+
if (v.mode) setMode(v.mode);
|
|
285
|
+
};
|
|
286
|
+
box.spellLaunch = (v) => go(v);
|
|
264
287
|
|
|
265
|
-
|
|
288
|
+
// over: a spell's launchWith ({ agent, model, effort, mode, text }) for this one launch.
|
|
289
|
+
const go = async (over = null) => {
|
|
290
|
+
if (over && !(over instanceof Event) && over.text && !ta.value.trim().startsWith(over.text)) ta.value = `${over.text}${ta.value.trim() ? `\n\n${ta.value.trim()}` : ''}`;
|
|
291
|
+
if (over instanceof Event) over = null;
|
|
266
292
|
const prompt = ta.value.trim();
|
|
267
293
|
if (!prompt) return ta.focus();
|
|
294
|
+
// /spell <what to change>: cast a spell instead of starting a task.
|
|
295
|
+
const spellAsk = /^\/spell(?:\s+|$)([\s\S]*)$/i.exec(prompt);
|
|
296
|
+
if (spellAsk) {
|
|
297
|
+
if (!spellAsk[1].trim()) return toast('Say what to change after /spell, like “/spell a floating button that opens my PRs”');
|
|
298
|
+
ta.value = '';
|
|
299
|
+
S.drafts.delete('home');
|
|
300
|
+
ta.dispatchEvent(new Event('input'));
|
|
301
|
+
return cast(spellAsk[1]).catch((err) => (toastError(err), (ta.value = prompt), ta.dispatchEvent(new Event('input'))));
|
|
302
|
+
}
|
|
303
|
+
// A spell's slash command, typed in the prompt box.
|
|
304
|
+
const slash = spellSlash(prompt);
|
|
305
|
+
if (slash) {
|
|
306
|
+
ta.value = '';
|
|
307
|
+
ta.dispatchEvent(new Event('input'));
|
|
308
|
+
return runAction(slash.spell, slash.cmd.do, { args: slash.args, input: ta });
|
|
309
|
+
}
|
|
268
310
|
if (isFeedback(prompt)) {
|
|
269
311
|
ta.value = '';
|
|
270
312
|
ta.dispatchEvent(new Event('input'));
|
|
@@ -277,7 +319,7 @@ export function renderLauncher() {
|
|
|
277
319
|
const off = targets.find((t) => t.m && t.m.state !== 'online');
|
|
278
320
|
if (off) return offlineToast(off.k);
|
|
279
321
|
if (needsSetup()) return nudgeSetup();
|
|
280
|
-
const wanted = [agent?.id, ...compare.map((c) => c.agent)].filter((id) => id !== 'auto'); // Auto picks from what's there
|
|
322
|
+
const wanted = [over?.agent || agent?.id, ...compare.map((c) => c.agent)].filter((id) => id !== 'auto'); // Auto picks from what's there
|
|
281
323
|
for (const t of targets) {
|
|
282
324
|
const list = agentsAt(t.k);
|
|
283
325
|
const missing = list && wanted.find((id) => !list.some((x) => x.id === id && x.available));
|
|
@@ -290,7 +332,8 @@ export function renderLauncher() {
|
|
|
290
332
|
const attachments = await tray.ids();
|
|
291
333
|
const files = tray.take();
|
|
292
334
|
const cmp = compare;
|
|
293
|
-
const runner = { agent: agent?.id || 'claude', model: curModel, effort: curEffort };
|
|
335
|
+
const runner = over?.agent ? { agent: over.agent, model: over.model ?? savedModel(over.agent), effort: over.effort ?? '' } : { agent: agent?.id || 'claude', model: curModel, effort: curEffort };
|
|
336
|
+
const launchMode = over?.mode || curMode;
|
|
294
337
|
// A repo still cloning gets a stand-in project; its card says so until the clone lands.
|
|
295
338
|
const fetching = (t) => t.k && [t.project, ...t.also].some((p) => p && !p.scratch && !pathAt(p, t.k)); // not on that machine yet
|
|
296
339
|
const pending = targets.map((t) => addPending(prompt, t.project ? t : { ...t, project: { id: t.clone.id, name: t.clone.name, path: '', color: 'slate' } }, runner, t.clone ? `Cloning ${t.clone.label}` : fetching(t) ? `Cloning ${t.project.name} on ${placeName(t.k)}` : undefined));
|
|
@@ -320,7 +363,7 @@ export function renderLauncher() {
|
|
|
320
363
|
targets.map((t) =>
|
|
321
364
|
api('/sessions', {
|
|
322
365
|
method: 'POST',
|
|
323
|
-
body: { prompt, cwd: t.project.scratch ? undefined : t.project.path, also: t.also.length ? t.also.map((p) => p.path) : undefined, scratch: t.project.scratch || undefined, machine: t.k, groupId, agent: runner.agent, mode:
|
|
366
|
+
body: { prompt, cwd: t.project.scratch ? undefined : t.project.path, also: t.also.length ? t.also.map((p) => p.path) : undefined, scratch: t.project.scratch || undefined, machine: t.k, groupId, agent: runner.agent, mode: launchMode, model: runner.model, effort: runner.effort, attachments, worktree: S.config.defaults.worktree === 'off' && !cmp.length ? false : undefined, compare: cmp.length ? cmp : undefined, subagents: team.length && canLead(runner.agent) ? team : undefined },
|
|
324
367
|
}),
|
|
325
368
|
),
|
|
326
369
|
);
|
|
@@ -347,7 +390,9 @@ export function renderLauncher() {
|
|
|
347
390
|
tray.restore(files);
|
|
348
391
|
}
|
|
349
392
|
};
|
|
350
|
-
$('#launch', box).onclick =
|
|
393
|
+
$('#launch', box).onclick = () => {
|
|
394
|
+
go();
|
|
395
|
+
};
|
|
351
396
|
// Picking other places (or a machine coming online) changes which harnesses exist: keep the
|
|
352
397
|
// pick if one of them still has it, else move to one that's installed there.
|
|
353
398
|
box.placesChanged = () => {
|
package/web/js/features/live.js
CHANGED
|
@@ -17,6 +17,7 @@ import { openSettings } from './settings.js';
|
|
|
17
17
|
import { renderPrStrip } from './ship.js';
|
|
18
18
|
import { onChildSummary } from './team.js';
|
|
19
19
|
import { onActivity } from './turns.js';
|
|
20
|
+
import { applySpells, spellEvent } from './spells.js';
|
|
20
21
|
import { loadUpdate, renderUpdate } from './update.js';
|
|
21
22
|
|
|
22
23
|
export function connect() {
|
|
@@ -37,10 +38,16 @@ export function connect() {
|
|
|
37
38
|
keepInPlace(follow ? S.nodes.get(s.id).card : feedAnchor(s.id), renderFeed);
|
|
38
39
|
if (prev?.state !== s.state || prev?.queued !== s.queued) scheduleRefetch(s.id);
|
|
39
40
|
if (JSON.stringify([prev?.pr, prev?.toShip, prev?.state, prev?.shipping]) !== JSON.stringify([s.pr, s.toShip, s.state, s.shipping])) renderPrStrip(s.id);
|
|
41
|
+
if (prev?.state !== s.state || prev?.pr?.state !== s.pr?.state) spellEvent('summary');
|
|
40
42
|
});
|
|
41
43
|
es.addEventListener('activity', (e) => onActivity(JSON.parse(e.data)));
|
|
42
44
|
es.addEventListener('turn', (e) => scheduleRefetch(JSON.parse(e.data).sessionId));
|
|
43
|
-
es.addEventListener('finished', (e) => notifyFinished(JSON.parse(e.data).session));
|
|
45
|
+
es.addEventListener('finished', (e) => (notifyFinished(JSON.parse(e.data).session), spellEvent('finished')));
|
|
46
|
+
// A spell hook's notify, from the server.
|
|
47
|
+
es.addEventListener('spell', (e) => {
|
|
48
|
+
const m = JSON.parse(e.data);
|
|
49
|
+
if (m.toast) toast(`<b>${esc(m.spell)}</b> · ${esc(m.toast)}`);
|
|
50
|
+
});
|
|
44
51
|
es.addEventListener('removed', (e) => {
|
|
45
52
|
const { id } = JSON.parse(e.data);
|
|
46
53
|
S.sessions.delete(id);
|
|
@@ -80,6 +87,7 @@ export async function loadSessions() {
|
|
|
80
87
|
|
|
81
88
|
export async function loadConfig() {
|
|
82
89
|
S.config = await api('/config');
|
|
90
|
+
applySpells();
|
|
83
91
|
renderProjects();
|
|
84
92
|
renderLaunchChips();
|
|
85
93
|
paintAutoBtn();
|
|
@@ -23,6 +23,7 @@ import { copySetupCommand, INSTALLABLE, installHarness, isInstalling, setupComma
|
|
|
23
23
|
import { clearPicks } from './transcript.js';
|
|
24
24
|
import { repaintViews } from './turns.js';
|
|
25
25
|
import { openUsage } from './usage.js';
|
|
26
|
+
import { spellsPane } from './spell-ui.js';
|
|
26
27
|
|
|
27
28
|
// A sidebar of sections and one pane at a time, like every other app's settings. Big lists
|
|
28
29
|
// (memory, automations, usage history) keep their own views; a pane only summarises and links.
|
|
@@ -34,6 +35,7 @@ const SETTINGS_PANES = [
|
|
|
34
35
|
{ id: 'machines', label: 'Machines', icon: 'server', lede: 'Other computers that run agents for you, over SSH.' },
|
|
35
36
|
{ id: 'memory', label: 'Memory', icon: 'spark', lede: "Learned from your sessions and added to every agent's prompt. What you ask always wins." },
|
|
36
37
|
{ id: 'automations', label: 'Automations', icon: 'play', lede: 'Saved prompts, and the triggers that launch them for you.' },
|
|
38
|
+
{ id: 'spells', label: 'Spells', icon: 'spark', lede: 'Changes to wisp itself: buttons, panels, blocks, looks and behaviour. Cast one, or add one a colleague sent.' },
|
|
37
39
|
{ id: 'usage', label: 'Usage', icon: 'stats', lede: 'Spend and outcomes over the last 7 days.' },
|
|
38
40
|
{ id: 'privacy', label: 'Privacy', icon: 'info', lede: 'What wisp sends about itself. Never prompts, paths or code.' },
|
|
39
41
|
];
|
|
@@ -135,6 +137,7 @@ export function openSettings(opts) {
|
|
|
135
137
|
${row('Triggers', '<span class="trigger-hint"></span>', '<button class="ghost sm" data-auto="triggers">Manage</button>')}
|
|
136
138
|
</div>
|
|
137
139
|
</section>
|
|
140
|
+
<section class="sx-pane sp-pane" data-pane="spells"></section>
|
|
138
141
|
<section class="sx-pane" data-pane="usage">
|
|
139
142
|
<div class="us-tiles sx-usage"><div class="it-loading"><span class="orb tiny"></span>Adding it up…</div></div>
|
|
140
143
|
<div class="sx-group">${row('Charts and breakdowns', 'Per day, per project and per harness · model', '<button class="ghost sm" id="openUsage">Open usage</button>')}</div>
|
|
@@ -149,6 +152,7 @@ export function openSettings(opts) {
|
|
|
149
152
|
</div>`);
|
|
150
153
|
const close = overlay(node);
|
|
151
154
|
$('.close', node).onclick = close;
|
|
155
|
+
spellsPane($('[data-pane="spells"].sx-pane', node));
|
|
152
156
|
const paintAgents = () => {
|
|
153
157
|
$('.sx-agents', node).innerHTML = S.agents.length
|
|
154
158
|
? S.agents.map((a) => {
|
|
@@ -441,6 +445,23 @@ export function openSettings(opts) {
|
|
|
441
445
|
$('#openRouter', node).onclick = () => openRouter({ host: host('auto') });
|
|
442
446
|
$('#openMemory', node).onclick = () => openMemory({ host: host('memory') });
|
|
443
447
|
$$('[data-auto]', node).forEach((b) => (b.onclick = () => openAutomations({ tab: b.dataset.auto, host: host('automations') })));
|
|
448
|
+
// Settings a spell sets are locked while it's on: the row says which spell, and links to Spells.
|
|
449
|
+
const LOCKABLE = { transcript: '#txDefault', worktree: '#wtDefault', fixConflicts: '#fixConflicts', learn: '#learn', editor: 'select[name="editor"]', notify: '#notif' };
|
|
450
|
+
const paintLocks = () => {
|
|
451
|
+
for (const n of $$('.sp-lock', node)) n.remove();
|
|
452
|
+
for (const [key, sel] of Object.entries(LOCKABLE)) {
|
|
453
|
+
const ctl = $(sel, node);
|
|
454
|
+
if (!ctl) continue;
|
|
455
|
+
const by = S.config.lockedBy?.[key];
|
|
456
|
+
ctl.disabled = !!by;
|
|
457
|
+
if (by) $('.sx-text', ctl.closest('.sx-row'))?.insertAdjacentHTML('beforeend', `<small class="sp-lock">${icon('spark')}Set by the spell <b>${esc(by.name)}</b> · <button class="link xs" data-pane-go="spells">Spells</button></small>`);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
paintLocks();
|
|
461
|
+
bus.on('config', paintLocks, { node });
|
|
462
|
+
node.addEventListener('click', (e) => {
|
|
463
|
+
if (e.target.closest('[data-pane-go]')) show(e.target.closest('[data-pane-go]').dataset.paneGo);
|
|
464
|
+
});
|
|
444
465
|
$('#openUsage', node).onclick = () => openUsage({ days: 7, host: host('usage') });
|
|
445
466
|
$('#openPhone', node).onclick = () => openPhone({ host: host('notifications') });
|
|
446
467
|
$('#openMachines', node).onclick = () => openMachines({ host: host('machines') });
|