@g3nomaly/g3n-mods 0.1.7 โ 0.1.8
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/.claude-plugin/marketplace.json +7 -1
- package/README.md +12 -5
- package/mods/agent-adventure/.claude-plugin/plugin.json +23 -0
- package/mods/agent-adventure/hooks/content.ts +263 -0
- package/mods/agent-adventure/hooks/engine.ts +1125 -0
- package/mods/agent-adventure/hooks/hooks.json +1 -0
- package/mods/agent-adventure/hooks/register.tsx +465 -0
- package/mods/agent-adventure/hooks/sprite.ts +424 -0
- package/mods/agent-adventure/types/index.d.ts +248 -0
- package/mods/agent-adventure/view/panes.json +8 -0
- package/mods/agent-adventure/view/view.css +230 -0
- package/mods/agent-adventure/view/view.ts +308 -0
- package/mods/agents-panel/.claude-plugin/plugin.json +9 -1
- package/mods/agents-panel/hooks/register.tsx +18 -4
- package/mods/agents-panel/view/panes.json +8 -0
- package/mods/context-bar/.claude-plugin/plugin.json +9 -1
- package/mods/context-bar/hooks/register.tsx +4 -3
- package/mods/context-bar/view/panes.json +8 -0
- package/mods/custom-crab-agents/hooks/register.tsx +15 -1
- package/mods/delete-guard/.claude-plugin/plugin.json +9 -1
- package/mods/delete-guard/hooks/register.tsx +15 -5
- package/mods/delete-guard/view/panes.json +8 -0
- package/mods/focus-mode/.claude-plugin/plugin.json +6 -0
- package/mods/focus-mode/hooks/register.tsx +7 -4
- package/mods/focus-mode/view/panes.json +8 -0
- package/mods/mini-game/.claude-plugin/plugin.json +9 -1
- package/mods/mini-game/hooks/register.tsx +21 -7
- package/mods/mini-game/view/panes.json +8 -0
- package/mods/tool-timeline/.claude-plugin/plugin.json +9 -1
- package/mods/tool-timeline/hooks/register.tsx +18 -4
- package/mods/tool-timeline/view/panes.json +8 -0
- package/mods/you-should-know/.claude-plugin/plugin.json +3 -3
- package/mods/you-should-know/hooks/register.tsx +173 -45
- package/mods/you-should-know/types/index.d.ts +13 -4
- package/package.json +2 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/* packages/g3n-mods/mods/agent-adventure/view/view.css */
|
|
2
|
+
.aa {
|
|
3
|
+
--dim: var(--vscode-descriptionForeground, #9d9d9d);
|
|
4
|
+
--line: color-mix(in srgb, var(--vscode-foreground, #ccc) 12%, transparent);
|
|
5
|
+
--mono: var(--vscode-editor-font-family, ui-monospace, monospace);
|
|
6
|
+
--ok: #3fa66b;
|
|
7
|
+
--warn: #e0a030;
|
|
8
|
+
--low: #e05a4f;
|
|
9
|
+
--gold: #f2c14e;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: 8px;
|
|
13
|
+
}
|
|
14
|
+
/* The view hides parts with the hidden attribute; a display rule below must not show them again. */
|
|
15
|
+
.aa [hidden] { display: none !important; }
|
|
16
|
+
|
|
17
|
+
/* Header: what the party is doing, where, and the calm switch. */
|
|
18
|
+
.aa-head { display: flex; align-items: baseline; gap: 10px; }
|
|
19
|
+
.aa-caption { font-weight: 600; font-size: 14px; }
|
|
20
|
+
.aa-meta { flex: 1; min-width: 0; color: var(--dim); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
21
|
+
.aa-calm {
|
|
22
|
+
flex-shrink: 0; padding: 1px 8px; border: 1px solid var(--line); border-radius: 10px;
|
|
23
|
+
background: transparent; color: var(--dim); font: inherit; font-size: 11px; cursor: pointer;
|
|
24
|
+
}
|
|
25
|
+
.aa-calm:hover { color: var(--vscode-foreground); border-color: var(--dim); }
|
|
26
|
+
.aa-calm.is-on { color: var(--vscode-foreground); background: color-mix(in srgb, var(--vscode-foreground) 10%, transparent); }
|
|
27
|
+
.aa-calm:focus-visible { outline: 1px solid var(--vscode-focusBorder); }
|
|
28
|
+
|
|
29
|
+
/* The run's path: one icon per stop. */
|
|
30
|
+
.aa-path { display: flex; flex-wrap: wrap; gap: 4px; font-size: 13px; }
|
|
31
|
+
.aa-stop { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 6px; background: var(--line); color: var(--dim); }
|
|
32
|
+
.aa-stop[data-state="done"] { opacity: 0.45; }
|
|
33
|
+
.aa-stop[data-state="here"] { background: color-mix(in srgb, var(--gold) 30%, transparent); color: var(--vscode-foreground); box-shadow: 0 0 0 1px var(--gold); }
|
|
34
|
+
.aa-stop[data-kind="boss"] { color: var(--low); }
|
|
35
|
+
.aa-stop[data-kind="elite"] { color: var(--gold); }
|
|
36
|
+
|
|
37
|
+
/* The stage: scenery behind, heroes on the left, enemies on the right. */
|
|
38
|
+
.aa-stage {
|
|
39
|
+
position: relative;
|
|
40
|
+
display: flex;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
align-items: flex-end;
|
|
43
|
+
gap: 12px;
|
|
44
|
+
min-height: 190px;
|
|
45
|
+
padding: 44px 14px 12px;
|
|
46
|
+
border-radius: 10px;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
isolation: isolate;
|
|
49
|
+
}
|
|
50
|
+
.aa-scenery { position: absolute; inset: 0; z-index: -1; }
|
|
51
|
+
|
|
52
|
+
.aa-stage[data-region="tavern"] .aa-scenery {
|
|
53
|
+
background:
|
|
54
|
+
linear-gradient(to bottom, transparent 72%, #4a2f1c 72%, #3b2415 100%),
|
|
55
|
+
repeating-linear-gradient(90deg, #6b4428 0 46px, #5e3a22 46px 48px),
|
|
56
|
+
#6b4428;
|
|
57
|
+
}
|
|
58
|
+
/* The fireplace: a stone arch with a fire in it. */
|
|
59
|
+
.aa-stage[data-region="tavern"] .aa-scenery::before {
|
|
60
|
+
content: ''; position: absolute; right: 18px; bottom: 28%; width: 74px; height: 66px;
|
|
61
|
+
border-radius: 36px 36px 0 0;
|
|
62
|
+
background: radial-gradient(ellipse at 50% 100%, #1b120c 55%, transparent 56%), #8a8f98;
|
|
63
|
+
box-shadow: inset 0 0 0 6px #6f747c;
|
|
64
|
+
}
|
|
65
|
+
.aa-stage[data-region="tavern"] .aa-scenery::after {
|
|
66
|
+
content: ''; position: absolute; right: 40px; bottom: 28%; width: 30px; height: 30px;
|
|
67
|
+
border-radius: 50% 50% 40% 40%;
|
|
68
|
+
background: radial-gradient(ellipse at 50% 80%, #fff3b0 0 20%, #f2c14e 35%, #e0773a 60%, transparent 70%);
|
|
69
|
+
animation: aa-flicker 0.9s ease-in-out infinite alternate;
|
|
70
|
+
transform-origin: 50% 100%;
|
|
71
|
+
}
|
|
72
|
+
@keyframes aa-flicker { from { transform: scale(1, 1); opacity: 0.9; } to { transform: scale(0.9, 1.15); opacity: 1; } }
|
|
73
|
+
|
|
74
|
+
.aa-stage[data-region="woods"] .aa-scenery {
|
|
75
|
+
background:
|
|
76
|
+
radial-gradient(circle at 12% 62%, #2f6b44 0 26px, transparent 27px),
|
|
77
|
+
radial-gradient(circle at 30% 58%, #3b7d52 0 34px, transparent 35px),
|
|
78
|
+
radial-gradient(circle at 72% 60%, #2f6b44 0 30px, transparent 31px),
|
|
79
|
+
radial-gradient(circle at 90% 56%, #3b7d52 0 38px, transparent 39px),
|
|
80
|
+
linear-gradient(to bottom, #9fd3e0 0%, #cfe9d8 55%, #5c9a5a 70%, #3f7a42 100%);
|
|
81
|
+
}
|
|
82
|
+
.aa-stage[data-region="caves"] .aa-scenery {
|
|
83
|
+
background:
|
|
84
|
+
radial-gradient(circle at 18% 30%, #6fd3e0 0 2px, transparent 3px),
|
|
85
|
+
radial-gradient(circle at 46% 18%, #b07ad8 0 2px, transparent 3px),
|
|
86
|
+
radial-gradient(circle at 80% 26%, #6fd3e0 0 3px, transparent 4px),
|
|
87
|
+
radial-gradient(circle at 64% 40%, #b07ad8 0 2px, transparent 3px),
|
|
88
|
+
linear-gradient(to bottom, #2b2540 0%, #221d35 70%, #18142a 70%, #120f20 100%);
|
|
89
|
+
}
|
|
90
|
+
.aa-stage[data-region="crypt"] .aa-scenery {
|
|
91
|
+
background:
|
|
92
|
+
linear-gradient(to bottom, transparent 70%, #22252d 70%),
|
|
93
|
+
repeating-linear-gradient(0deg, #3f444f 0 22px, #353a44 22px 24px),
|
|
94
|
+
#3f444f;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.aa-side { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 6px 10px; min-width: 0; }
|
|
98
|
+
.aa-side.is-heroes { flex: 1; justify-content: flex-start; }
|
|
99
|
+
.aa-side.is-enemies { flex: 1; justify-content: flex-end; }
|
|
100
|
+
.aa-stage[data-scene="tavern"] .aa-side.is-heroes { justify-content: flex-start; padding-right: 90px; }
|
|
101
|
+
|
|
102
|
+
/* One fighter: speech bubble, sprite, name plate. */
|
|
103
|
+
.aa-unit { position: relative; display: flex; flex-direction: column; align-items: center; width: 92px; transition: transform 160ms ease; }
|
|
104
|
+
.aa-unit[data-rank="boss"] { width: 120px; }
|
|
105
|
+
.aa-sprite { display: flex; align-items: flex-end; justify-content: center; min-height: 56px; }
|
|
106
|
+
.aa-sprite svg { display: block; image-rendering: pixelated; animation: aa-bob 1.1s steps(2, jump-none) infinite; }
|
|
107
|
+
.aa-unit:nth-child(2n) .aa-sprite svg { animation-delay: -0.4s; }
|
|
108
|
+
.aa-unit:nth-child(3n) .aa-sprite svg { animation-delay: -0.8s; }
|
|
109
|
+
.aa-stage[data-scene="tavern"] .aa-sprite svg { animation-duration: 2.4s; }
|
|
110
|
+
@keyframes aa-bob { from { transform: translateY(0); } to { transform: translateY(-2px); } }
|
|
111
|
+
|
|
112
|
+
.aa-unit.is-acting[data-side="hero"] { transform: translateX(12px); }
|
|
113
|
+
.aa-unit.is-acting[data-side="enemy"] { transform: translateX(-12px); }
|
|
114
|
+
.aa-unit.is-hit .aa-sprite { animation: aa-hit 300ms ease; }
|
|
115
|
+
@keyframes aa-hit {
|
|
116
|
+
0% { filter: brightness(2.2); transform: translateX(0); }
|
|
117
|
+
30% { transform: translateX(-3px); }
|
|
118
|
+
60% { transform: translateX(3px); }
|
|
119
|
+
100% { filter: none; transform: translateX(0); }
|
|
120
|
+
}
|
|
121
|
+
.aa-unit.is-down .aa-sprite svg { animation: none; transform: rotate(90deg) translateX(18%); }
|
|
122
|
+
.aa-unit.is-down .aa-plate { opacity: 0.55; }
|
|
123
|
+
.aa-unit.is-recruit .aa-sprite svg { opacity: 0.75; }
|
|
124
|
+
|
|
125
|
+
.aa-plate {
|
|
126
|
+
display: flex; flex-direction: column; gap: 2px; width: 100%; margin-top: 4px; padding: 3px 5px;
|
|
127
|
+
border-radius: 6px; background: rgba(15, 17, 22, 0.72); color: #e8e8e8;
|
|
128
|
+
}
|
|
129
|
+
.aa-label { display: flex; justify-content: space-between; gap: 4px; font-size: 11px; line-height: 1.2; }
|
|
130
|
+
.aa-name { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-weight: 600; }
|
|
131
|
+
.aa-level { flex-shrink: 0; color: #b8bec8; font-family: var(--mono); font-size: 10px; }
|
|
132
|
+
.aa-unit[data-rank="elite"] .aa-name { color: var(--gold); }
|
|
133
|
+
.aa-unit[data-rank="boss"] .aa-name { color: #ff8a7a; }
|
|
134
|
+
.aa-hp, .aa-xp { position: relative; height: 4px; border-radius: 2px; background: #2b3240; overflow: hidden; }
|
|
135
|
+
.aa-xp { height: 2px; }
|
|
136
|
+
.aa-hp-fill, .aa-xp-fill { position: absolute; inset: 0 auto 0 0; border-radius: 2px; transition: width 300ms ease; }
|
|
137
|
+
.aa-hp-fill[data-tone="ok"] { background: var(--ok); }
|
|
138
|
+
.aa-hp-fill[data-tone="warn"] { background: var(--warn); }
|
|
139
|
+
.aa-hp-fill[data-tone="low"] { background: var(--low); }
|
|
140
|
+
.aa-xp-fill { background: #8e7ad8; }
|
|
141
|
+
.aa-tags { min-height: 0; color: #9fb8d8; font-size: 9px; line-height: 1.1; }
|
|
142
|
+
.aa-tags:empty { display: none; }
|
|
143
|
+
.aa-stage[data-scene="tavern"] .aa-hp { display: none; }
|
|
144
|
+
|
|
145
|
+
/* Numbers and words that float up from a fighter. */
|
|
146
|
+
.aa-float {
|
|
147
|
+
position: absolute; left: 50%; top: 18px; z-index: 2; pointer-events: none;
|
|
148
|
+
font-family: var(--mono); font-size: 12px; font-weight: 700; white-space: nowrap;
|
|
149
|
+
text-shadow: 0 1px 0 #000, 0 0 3px #000;
|
|
150
|
+
animation: aa-float 1.3s ease-out forwards;
|
|
151
|
+
}
|
|
152
|
+
@keyframes aa-float { from { opacity: 1; transform: translate(-50%, 0); } to { opacity: 0; transform: translate(-50%, -30px); } }
|
|
153
|
+
.aa-float.is-hit { color: #ffffff; }
|
|
154
|
+
.aa-float.is-crit { color: #ffb347; font-size: 14px; }
|
|
155
|
+
.aa-float.is-miss { color: #9aa3b0; font-style: italic; }
|
|
156
|
+
.aa-float.is-heal { color: #7cf09a; }
|
|
157
|
+
.aa-float.is-buff { color: #9fd3ff; font-size: 10px; }
|
|
158
|
+
.aa-float.is-skill { top: -2px; color: var(--gold); font-size: 10px; }
|
|
159
|
+
.aa-float.is-level { top: -6px; color: var(--gold); font-size: 13px; }
|
|
160
|
+
|
|
161
|
+
/* A line of speech over a fighter or a hero in the tavern. */
|
|
162
|
+
.aa-bubble {
|
|
163
|
+
position: absolute; bottom: calc(100% - 14px); left: 50%; z-index: 3; max-width: 170px; width: max-content;
|
|
164
|
+
padding: 4px 8px; border-radius: 8px; background: #fbf7ee; color: #2a2620;
|
|
165
|
+
font-size: 11px; line-height: 1.3; text-align: center;
|
|
166
|
+
opacity: 0; transform: translate(-50%, 4px); transition: opacity 200ms ease, transform 200ms ease; pointer-events: none;
|
|
167
|
+
}
|
|
168
|
+
.aa-bubble::after { content: ''; position: absolute; top: 100%; left: 50%; margin-left: -4px; border: 4px solid transparent; border-top-color: #fbf7ee; }
|
|
169
|
+
.aa-bubble.is-open { opacity: 1; transform: translate(-50%, 0); }
|
|
170
|
+
|
|
171
|
+
.aa-banner {
|
|
172
|
+
position: absolute; left: 50%; top: 40%; z-index: 4; pointer-events: none;
|
|
173
|
+
font-size: 22px; font-weight: 800; letter-spacing: 0.04em; color: var(--gold);
|
|
174
|
+
text-shadow: 0 2px 0 #000, 0 0 8px rgba(0, 0, 0, 0.7);
|
|
175
|
+
opacity: 0; transform: translate(-50%, -50%) scale(0.9);
|
|
176
|
+
}
|
|
177
|
+
.aa-banner[data-tone="wipe"] { color: #ff8a7a; }
|
|
178
|
+
.aa-banner.is-shown { animation: aa-banner 2.6s ease forwards; }
|
|
179
|
+
@keyframes aa-banner {
|
|
180
|
+
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.85); }
|
|
181
|
+
15%, 75% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
|
182
|
+
100% { opacity: 0; transform: translate(-50%, -60%) scale(1); }
|
|
183
|
+
}
|
|
184
|
+
.aa-stage.is-quake { animation: aa-quake 600ms ease; }
|
|
185
|
+
@keyframes aa-quake { 20%, 60% { transform: translateX(-3px); } 40%, 80% { transform: translateX(3px); } }
|
|
186
|
+
|
|
187
|
+
.aa-notices { position: absolute; top: 8px; right: 8px; z-index: 5; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; pointer-events: none; }
|
|
188
|
+
.aa-notice { padding: 3px 8px; border-radius: 6px; background: rgba(15, 17, 22, 0.82); color: #e8e8e8; font-size: 11px; animation: aa-notice 3.5s ease forwards; }
|
|
189
|
+
.aa-notice.is-level { box-shadow: inset 2px 0 0 var(--gold); }
|
|
190
|
+
.aa-notice.is-learn { box-shadow: inset 2px 0 0 #8e7ad8; }
|
|
191
|
+
@keyframes aa-notice { 0% { opacity: 0; transform: translateY(-4px); } 10%, 85% { opacity: 1; transform: none; } 100% { opacity: 0; } }
|
|
192
|
+
|
|
193
|
+
/* The run's loot and the log. */
|
|
194
|
+
.aa-loot { color: var(--dim); font-size: 12px; }
|
|
195
|
+
.aa-log { display: flex; flex-direction: column; gap: 1px; margin: 0; padding: 0; list-style: none; font-size: 12px; }
|
|
196
|
+
.aa-line { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--dim); }
|
|
197
|
+
.aa-line:last-child { color: var(--vscode-foreground); }
|
|
198
|
+
.aa-line[data-tone="good"] { color: #7cc47f; }
|
|
199
|
+
.aa-line[data-tone="bad"] { color: #e06c6c; }
|
|
200
|
+
.aa-line[data-tone="loot"] { color: #e5b567; }
|
|
201
|
+
.aa-line[data-tone="level"] { color: #b48ead; }
|
|
202
|
+
|
|
203
|
+
/* The guild: every hero, its level, skills and record. */
|
|
204
|
+
.aa-roster-label { cursor: pointer; color: var(--dim); font-size: 12px; padding: 2px; list-style: none; }
|
|
205
|
+
.aa-roster-label::before { content: 'โธ'; display: inline-block; width: 1.2em; transition: transform 120ms ease; }
|
|
206
|
+
.aa-roster[open] > .aa-roster-label::before { transform: rotate(90deg); }
|
|
207
|
+
.aa-roster-label::-webkit-details-marker { display: none; }
|
|
208
|
+
.aa-roster-list { display: flex; flex-direction: column; gap: 6px; padding-top: 6px; }
|
|
209
|
+
.aa-hero { display: grid; grid-template-columns: 40px minmax(0, 1fr); align-items: center; gap: 10px; padding: 4px 6px; border-radius: 8px; }
|
|
210
|
+
.aa-hero.is-active { background: color-mix(in srgb, var(--vscode-foreground) 6%, transparent); }
|
|
211
|
+
.aa-hero-sprite svg { display: block; image-rendering: pixelated; }
|
|
212
|
+
.aa-hero-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
213
|
+
.aa-hero-top { display: flex; gap: 8px; align-items: baseline; font-size: 12px; }
|
|
214
|
+
.aa-hero-name { font-weight: 600; }
|
|
215
|
+
.aa-hero-level { color: var(--dim); font-size: 11px; }
|
|
216
|
+
.aa-hero-skills, .aa-hero-record { color: var(--dim); font-size: 11px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
217
|
+
.aa-hero .aa-xp { max-width: 180px; }
|
|
218
|
+
|
|
219
|
+
/* Calm: nothing bobs, lunges or shakes; numbers still fade. */
|
|
220
|
+
.aa.is-calm .aa-sprite svg,
|
|
221
|
+
.aa.is-calm .aa-unit.is-hit .aa-sprite,
|
|
222
|
+
.aa.is-calm .aa-stage.is-quake,
|
|
223
|
+
.aa.is-calm .aa-stage .aa-scenery::after { animation: none; }
|
|
224
|
+
.aa.is-calm .aa-unit.is-acting { transform: none; }
|
|
225
|
+
@media (prefers-reduced-motion: reduce) {
|
|
226
|
+
.aa-sprite svg, .aa-unit.is-hit .aa-sprite, .aa-stage.is-quake, .aa-stage .aa-scenery::after { animation: none; }
|
|
227
|
+
.aa-unit.is-acting { transform: none; }
|
|
228
|
+
.aa-float { animation-name: aa-fade; }
|
|
229
|
+
@keyframes aa-fade { from { opacity: 1; transform: translate(-50%, 0); } to { opacity: 0; transform: translate(-50%, 0); } }
|
|
230
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// packages/g3n-mods/mods/agent-adventure/view/view.ts
|
|
2
|
+
import { RANK_SCALE, eliteTint, spriteSvg } from '../hooks/sprite'
|
|
3
|
+
import type { Fx, RosterEntry, StopKind, UnitView, ViewData } from '../types'
|
|
4
|
+
|
|
5
|
+
/** What the viewer hands a view: a way to send the mod an action and to open a link. */
|
|
6
|
+
type ViewApi = { act: (name: string, arg?: unknown) => void; openLink: (href: string) => void }
|
|
7
|
+
|
|
8
|
+
/** Size of a sprite pixel on the stage, in CSS pixels, before a rank's scale. */
|
|
9
|
+
const STAGE_PIXEL = 4
|
|
10
|
+
/** Most milliseconds between the visual events of one update, so a tick's hits play one after another within it. */
|
|
11
|
+
const FX_STAGGER = 220
|
|
12
|
+
/** How long a speech bubble stays up. */
|
|
13
|
+
const BUBBLE_MS = 3800
|
|
14
|
+
/** How long a notice stays in the corner. */
|
|
15
|
+
const NOTICE_MS = 3500
|
|
16
|
+
|
|
17
|
+
const STOP_ICONS: Readonly<Record<StopKind, string>> = { fight: 'โ', elite: 'โ
', fork: 'โ', camp: 'โบ', boss: 'โ ' }
|
|
18
|
+
const STOP_NAMES: Readonly<Record<StopKind, string>> = { fight: 'Fight', elite: 'Elite fight', fork: 'Fork in the path', camp: 'Camp', boss: 'Boss' }
|
|
19
|
+
|
|
20
|
+
/** Makes an element with a class, and text when given. */
|
|
21
|
+
function el<K extends keyof HTMLElementTagNameMap>(tag: K, className: string, text?: string): HTMLElementTagNameMap[K] {
|
|
22
|
+
const node = document.createElement(tag)
|
|
23
|
+
node.className = className
|
|
24
|
+
if (text !== undefined) node.textContent = text
|
|
25
|
+
return node
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Sets an element's text only when it changed. */
|
|
29
|
+
function setText(node: Element, text: string): void {
|
|
30
|
+
if (node.textContent !== text) node.textContent = text
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Moves `children` into `parent` in this order, touching only those out of place, and drops the rest. */
|
|
34
|
+
function arrange(parent: HTMLElement, children: HTMLElement[]): void {
|
|
35
|
+
children.forEach((child, index) => {
|
|
36
|
+
if (parent.children[index] !== child) parent.insertBefore(child, parent.children[index] ?? null)
|
|
37
|
+
})
|
|
38
|
+
while (parent.children.length > children.length) parent.lastElementChild?.remove()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Puts a sprite in its slot, only when its look changed, so a redraw does not restart its animations. */
|
|
42
|
+
function setSprite(slot: HTMLElement, look: string, pixel: number, isDown: boolean, tint: string | undefined): void {
|
|
43
|
+
const key = `${look}:${pixel}:${isDown}:${tint ?? ''}`
|
|
44
|
+
if (slot.dataset.look === key) return
|
|
45
|
+
slot.dataset.look = key
|
|
46
|
+
slot.innerHTML = spriteSvg(look, { pixel, isDown, ...(tint === undefined ? {} : { tint }) })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Adds a class for `ms`, restarting its animation when it is already on. */
|
|
50
|
+
function flash(node: HTMLElement, className: string, ms: number): void {
|
|
51
|
+
node.classList.remove(className)
|
|
52
|
+
void node.offsetWidth
|
|
53
|
+
node.classList.add(className)
|
|
54
|
+
window.setTimeout(() => node.classList.remove(className), ms)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** HP bar tone by how full it is. */
|
|
58
|
+
function hpTone(share: number): string {
|
|
59
|
+
return share > 0.5 ? 'ok' : share > 0.25 ? 'warn' : 'low'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One hero or enemy on the stage. */
|
|
63
|
+
type UnitNode = { root: HTMLElement; sprite: HTMLElement; name: HTMLElement; level: HTMLElement; fill: HTMLElement; xp: HTMLElement; tags: HTMLElement; bubble: HTMLElement; bubbleTimer?: number }
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @brief Mounts the adventure: a header with the scene and the run's path, a stage that is the tavern when nobody
|
|
67
|
+
* works and the dungeon while agents do, the run's loot, the log, and the guild's roster. Every hit, heal, level-up
|
|
68
|
+
* and line of speech plays once, by its id, so redraws never replay them.
|
|
69
|
+
*
|
|
70
|
+
* @param root - The shadow root the viewer gives this view.
|
|
71
|
+
* @param api - Sends `calm` to the mod.
|
|
72
|
+
* @returns The instance the viewer hands each update to.
|
|
73
|
+
*/
|
|
74
|
+
export function mount(root: ShadowRoot, api: ViewApi): { update: (data: unknown) => void } {
|
|
75
|
+
const frame = el('div', 'aa')
|
|
76
|
+
const head = el('header', 'aa-head')
|
|
77
|
+
const caption = el('span', 'aa-caption')
|
|
78
|
+
const meta = el('span', 'aa-meta')
|
|
79
|
+
const calm = el('button', 'aa-calm', 'calm')
|
|
80
|
+
calm.type = 'button'
|
|
81
|
+
calm.title = 'Calmer animations: no bobbing, lunging or shaking'
|
|
82
|
+
calm.addEventListener('click', () => api.act('calm'))
|
|
83
|
+
head.append(caption, meta, calm)
|
|
84
|
+
|
|
85
|
+
const path = el('div', 'aa-path')
|
|
86
|
+
const stage = el('div', 'aa-stage')
|
|
87
|
+
const scenery = el('div', 'aa-scenery')
|
|
88
|
+
const heroesSide = el('div', 'aa-side is-heroes')
|
|
89
|
+
const enemiesSide = el('div', 'aa-side is-enemies')
|
|
90
|
+
const banner = el('div', 'aa-banner')
|
|
91
|
+
const notices = el('div', 'aa-notices')
|
|
92
|
+
stage.append(scenery, heroesSide, enemiesSide, banner, notices)
|
|
93
|
+
|
|
94
|
+
const loot = el('div', 'aa-loot')
|
|
95
|
+
const log = el('ol', 'aa-log')
|
|
96
|
+
const roster = el('details', 'aa-roster')
|
|
97
|
+
const rosterLabel = el('summary', 'aa-roster-label')
|
|
98
|
+
const rosterList = el('div', 'aa-roster-list')
|
|
99
|
+
roster.append(rosterLabel, rosterList)
|
|
100
|
+
frame.append(head, path, stage, loot, log, roster)
|
|
101
|
+
root.append(frame)
|
|
102
|
+
|
|
103
|
+
const units = new Map<string, UnitNode>()
|
|
104
|
+
const rosterRows = new Map<string, HTMLElement>()
|
|
105
|
+
const seen = new Set<number>()
|
|
106
|
+
let isFirst = true
|
|
107
|
+
let lastBanner = ''
|
|
108
|
+
|
|
109
|
+
/** Builds a unit's nodes once. */
|
|
110
|
+
const makeUnit = (id: string): UnitNode => {
|
|
111
|
+
const node: UnitNode = {
|
|
112
|
+
root: el('div', 'aa-unit'),
|
|
113
|
+
sprite: el('div', 'aa-sprite'),
|
|
114
|
+
name: el('span', 'aa-name'),
|
|
115
|
+
level: el('span', 'aa-level'),
|
|
116
|
+
fill: el('span', 'aa-hp-fill'),
|
|
117
|
+
xp: el('span', 'aa-xp-fill'),
|
|
118
|
+
tags: el('span', 'aa-tags'),
|
|
119
|
+
bubble: el('div', 'aa-bubble'),
|
|
120
|
+
}
|
|
121
|
+
node.root.dataset.id = id
|
|
122
|
+
const plate = el('div', 'aa-plate')
|
|
123
|
+
const label = el('div', 'aa-label')
|
|
124
|
+
label.append(node.name, node.level)
|
|
125
|
+
const hp = el('span', 'aa-hp')
|
|
126
|
+
hp.append(node.fill)
|
|
127
|
+
const xp = el('span', 'aa-xp')
|
|
128
|
+
xp.append(node.xp)
|
|
129
|
+
plate.append(label, hp, xp, node.tags)
|
|
130
|
+
node.root.append(node.bubble, node.sprite, plate)
|
|
131
|
+
return node
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Brings a unit up to date. */
|
|
135
|
+
const fillUnit = (unit: UnitView): HTMLElement => {
|
|
136
|
+
let node = units.get(unit.id)
|
|
137
|
+
if (node === undefined) {
|
|
138
|
+
node = makeUnit(unit.id)
|
|
139
|
+
units.set(unit.id, node)
|
|
140
|
+
}
|
|
141
|
+
const share = unit.maxHp > 0 ? unit.hp / unit.maxHp : 0
|
|
142
|
+
node.root.dataset.side = unit.side
|
|
143
|
+
node.root.dataset.rank = unit.rank
|
|
144
|
+
node.root.classList.toggle('is-down', unit.isDown)
|
|
145
|
+
node.root.classList.toggle('is-recruit', unit.look === 'recruit')
|
|
146
|
+
node.root.title = [unit.title, unit.motto === undefined ? '' : `โ${unit.motto}โ`, unit.skills?.length ? `Skills: ${unit.skills.join(', ')}` : ''].filter(Boolean).join('\n')
|
|
147
|
+
setSprite(node.sprite, unit.look, Math.round(STAGE_PIXEL * RANK_SCALE[unit.rank]), unit.isDown, eliteTint(unit.rank))
|
|
148
|
+
setText(node.name, unit.name)
|
|
149
|
+
setText(node.level, unit.look === 'recruit' ? unit.title : `Lv ${unit.level}`)
|
|
150
|
+
node.fill.style.width = `${(Math.max(0, Math.min(1, share)) * 100).toFixed(1)}%`
|
|
151
|
+
node.fill.dataset.tone = hpTone(share)
|
|
152
|
+
node.xp.style.width = `${(Math.max(0, Math.min(1, unit.xpShare ?? 0)) * 100).toFixed(1)}%`
|
|
153
|
+
node.xp.parentElement?.toggleAttribute('hidden', unit.xpShare === undefined)
|
|
154
|
+
setText(node.tags, unit.tags.join(' ยท '))
|
|
155
|
+
return node.root
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Floats a number or word up from a unit. */
|
|
159
|
+
const float = (at: string, text: string, tone: string): void => {
|
|
160
|
+
const node = units.get(at)
|
|
161
|
+
if (node === undefined) return
|
|
162
|
+
const pop = el('span', `aa-float is-${tone}`, text)
|
|
163
|
+
node.root.append(pop)
|
|
164
|
+
window.setTimeout(() => pop.remove(), 1400)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Shows a line of speech over a unit. */
|
|
168
|
+
const say = (at: string, text: string): void => {
|
|
169
|
+
const node = units.get(at)
|
|
170
|
+
if (node === undefined) return
|
|
171
|
+
setText(node.bubble, text)
|
|
172
|
+
node.bubble.classList.add('is-open')
|
|
173
|
+
if (node.bubbleTimer !== undefined) window.clearTimeout(node.bubbleTimer)
|
|
174
|
+
node.bubbleTimer = window.setTimeout(() => node.bubble.classList.remove('is-open'), BUBBLE_MS)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Shows a short notice in the stage's corner. */
|
|
178
|
+
const notice = (text: string, tone: string): void => {
|
|
179
|
+
const item = el('div', `aa-notice is-${tone}`, text)
|
|
180
|
+
notices.append(item)
|
|
181
|
+
window.setTimeout(() => item.remove(), NOTICE_MS)
|
|
182
|
+
while (notices.children.length > 3) notices.firstElementChild?.remove()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Plays one visual event. */
|
|
186
|
+
const play = (event: Fx): void => {
|
|
187
|
+
const node = units.get(event.at)
|
|
188
|
+
switch (event.kind) {
|
|
189
|
+
case 'act':
|
|
190
|
+
if (node !== undefined) flash(node.root, 'is-acting', 380)
|
|
191
|
+
if (event.text !== undefined) float(event.at, event.text, 'skill')
|
|
192
|
+
break
|
|
193
|
+
case 'hit':
|
|
194
|
+
if (node !== undefined) flash(node.root, 'is-hit', 320)
|
|
195
|
+
float(event.at, event.isCrit === true ? `${event.amount} crit!` : String(event.amount ?? ''), event.isCrit === true ? 'crit' : 'hit')
|
|
196
|
+
break
|
|
197
|
+
case 'miss': float(event.at, 'miss', 'miss'); break
|
|
198
|
+
case 'heal': float(event.at, `+${event.amount ?? ''}`, 'heal'); break
|
|
199
|
+
case 'buff': float(event.at, event.text ?? '', 'buff'); break
|
|
200
|
+
case 'down': break
|
|
201
|
+
case 'level':
|
|
202
|
+
float(event.at, event.text ?? 'Level up!', 'level')
|
|
203
|
+
notice(`${units.get(event.at)?.name.textContent ?? 'A hero'} reached ${event.text?.replace('!', '') ?? 'a new level'}`, 'level')
|
|
204
|
+
break
|
|
205
|
+
case 'learn': notice(`${units.get(event.at)?.name.textContent ?? 'A hero'} learned ${event.text ?? 'a skill'}`, 'learn'); break
|
|
206
|
+
case 'phase': if (node !== undefined) flash(stage, 'is-quake', 600); break
|
|
207
|
+
case 'say': say(event.at, event.text ?? ''); break
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Builds or updates a hero's row in the roster. */
|
|
212
|
+
const rosterRow = (entry: RosterEntry): HTMLElement => {
|
|
213
|
+
let row = rosterRows.get(entry.id)
|
|
214
|
+
if (row === undefined) {
|
|
215
|
+
row = el('div', 'aa-hero')
|
|
216
|
+
const sprite = el('span', 'aa-hero-sprite')
|
|
217
|
+
const body = el('div', 'aa-hero-body')
|
|
218
|
+
const top = el('div', 'aa-hero-top')
|
|
219
|
+
top.append(el('span', 'aa-hero-name'), el('span', 'aa-hero-level'))
|
|
220
|
+
const xp = el('span', 'aa-xp')
|
|
221
|
+
xp.append(el('span', 'aa-xp-fill'))
|
|
222
|
+
body.append(top, el('div', 'aa-hero-skills'), xp, el('div', 'aa-hero-record'))
|
|
223
|
+
row.append(sprite, body)
|
|
224
|
+
rosterRows.set(entry.id, row)
|
|
225
|
+
}
|
|
226
|
+
row.classList.toggle('is-active', entry.isActive)
|
|
227
|
+
setSprite(row.querySelector('.aa-hero-sprite') as HTMLElement, entry.look, 3, false, undefined)
|
|
228
|
+
setText(row.querySelector('.aa-hero-name') as HTMLElement, entry.name)
|
|
229
|
+
setText(row.querySelector('.aa-hero-level') as HTMLElement, `Lv ${entry.level} ${entry.title}${entry.isActive ? ' ยท adventuring' : ''}`)
|
|
230
|
+
setText(row.querySelector('.aa-hero-skills') as HTMLElement, [entry.skills.length > 0 ? entry.skills.join(' ยท ') : 'No skills yet (one every 10 levels)', entry.motto === undefined ? '' : `โ${entry.motto}โ`].filter(Boolean).join(' '))
|
|
231
|
+
;(row.querySelector('.aa-xp-fill') as HTMLElement).style.width = `${(Math.max(0, Math.min(1, entry.xpShare)) * 100).toFixed(1)}%`
|
|
232
|
+
setText(row.querySelector('.aa-hero-record') as HTMLElement, `${entry.record}${entry.trophies > 0 ? ` ยท ๐ ${entry.trophies}` : ''}`)
|
|
233
|
+
return row
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
update: (raw: unknown) => {
|
|
238
|
+
const data = raw as ViewData | null
|
|
239
|
+
if (data === null || typeof data !== 'object' || !Array.isArray(data.heroes)) return
|
|
240
|
+
frame.classList.toggle('is-calm', data.isCalm)
|
|
241
|
+
calm.classList.toggle('is-on', data.isCalm)
|
|
242
|
+
calm.setAttribute('aria-pressed', String(data.isCalm))
|
|
243
|
+
|
|
244
|
+
const isTavern = data.scene === 'tavern'
|
|
245
|
+
stage.dataset.scene = data.scene
|
|
246
|
+
stage.dataset.region = isTavern ? 'tavern' : data.region?.id ?? 'tavern'
|
|
247
|
+
setText(caption, data.caption)
|
|
248
|
+
setText(meta, isTavern
|
|
249
|
+
? `${data.roster.length} hero${data.roster.length === 1 ? '' : 'es'} ยท ๐ ${data.trophies}`
|
|
250
|
+
: `${data.region?.name ?? ''} ยท run ${data.runNumber} ยท threat ${data.threat}%`)
|
|
251
|
+
|
|
252
|
+
// The run's path: done stops dim, the current one lit.
|
|
253
|
+
path.hidden = data.stops.length === 0 || isTavern
|
|
254
|
+
arrange(path, data.stops.map((stop, i) => {
|
|
255
|
+
const dot = (path.children[i] as HTMLElement | undefined) ?? el('span', 'aa-stop')
|
|
256
|
+
dot.dataset.state = stop.state
|
|
257
|
+
dot.dataset.kind = stop.kind
|
|
258
|
+
dot.title = STOP_NAMES[stop.kind]
|
|
259
|
+
setText(dot, STOP_ICONS[stop.kind])
|
|
260
|
+
return dot
|
|
261
|
+
}))
|
|
262
|
+
|
|
263
|
+
for (const [id, node] of units) {
|
|
264
|
+
if (!data.heroes.some(u => u.id === id) && !data.enemies.some(u => u.id === id)) {
|
|
265
|
+
node.root.remove()
|
|
266
|
+
units.delete(id)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
arrange(heroesSide, data.heroes.map(fillUnit))
|
|
270
|
+
arrange(enemiesSide, data.enemies.map(fillUnit))
|
|
271
|
+
enemiesSide.hidden = data.enemies.length === 0
|
|
272
|
+
|
|
273
|
+
const bannerText = data.scene === 'victory' ? 'Victory!' : data.scene === 'wipe' ? 'Defeatedโฆ' : data.scene === 'cleared' ? 'Run cleared!' : ''
|
|
274
|
+
if (bannerText !== lastBanner) {
|
|
275
|
+
lastBanner = bannerText
|
|
276
|
+
setText(banner, bannerText)
|
|
277
|
+
banner.dataset.tone = data.scene
|
|
278
|
+
if (bannerText !== '') flash(banner, 'is-shown', 2600)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
setText(loot, [data.potions > 0 ? `๐งช ${data.potions} potion${data.potions === 1 ? '' : 's'}` : '', ...data.relics.map(relic => `${relic.icon} ${relic.name}`)].filter(Boolean).join(' '))
|
|
282
|
+
loot.title = data.relics.map(relic => `${relic.name}: ${relic.blurb}`).join('\n')
|
|
283
|
+
loot.hidden = isTavern || loot.textContent === ''
|
|
284
|
+
|
|
285
|
+
arrange(log, data.log.map(line => {
|
|
286
|
+
const item = (log.querySelector(`[data-id="${line.id}"]`) as HTMLElement | null) ?? el('li', 'aa-line', line.text)
|
|
287
|
+
item.dataset.id = String(line.id)
|
|
288
|
+
item.dataset.tone = line.tone
|
|
289
|
+
return item
|
|
290
|
+
}))
|
|
291
|
+
|
|
292
|
+
setText(rosterLabel, `Guild ยท ${data.roster.length} hero${data.roster.length === 1 ? '' : 'es'}`)
|
|
293
|
+
for (const id of [...rosterRows.keys()]) if (!data.roster.some(entry => entry.id === id)) rosterRows.delete(id)
|
|
294
|
+
arrange(rosterList, data.roster.map(rosterRow))
|
|
295
|
+
|
|
296
|
+
// Visual events play once each; on the first update they are history, not news.
|
|
297
|
+
const fresh = data.fx.filter(event => !seen.has(event.id))
|
|
298
|
+
for (const event of data.fx) seen.add(event.id)
|
|
299
|
+
if (seen.size > 400) for (const id of [...seen].slice(0, seen.size - 200)) seen.delete(id)
|
|
300
|
+
if (isFirst) {
|
|
301
|
+
isFirst = false
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
const gap = Math.min(FX_STAGGER, 900 / Math.max(1, fresh.length))
|
|
305
|
+
fresh.forEach((event, i) => window.setTimeout(() => play(event), i * gap))
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -5,5 +5,13 @@
|
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "g3ntrovert"
|
|
7
7
|
},
|
|
8
|
-
"types": "./types/index.d.ts"
|
|
8
|
+
"types": "./types/index.d.ts",
|
|
9
|
+
"userConfig": {
|
|
10
|
+
"showInClaudeCode": {
|
|
11
|
+
"type": "boolean",
|
|
12
|
+
"title": "Show in Claude Code",
|
|
13
|
+
"default": false,
|
|
14
|
+
"description": "Open this mod's pane in the Claude Code chat when a session starts. The mods viewer shows it either way."
|
|
15
|
+
}
|
|
16
|
+
}
|
|
9
17
|
}
|
|
@@ -17,6 +17,19 @@ const NAMED_COLORS: Readonly<Record<string, string>> = {
|
|
|
17
17
|
purple: '#b48ead', orange: '#e0775a', pink: '#f09bb5', cyan: '#5fb3c4',
|
|
18
18
|
}
|
|
19
19
|
const SPINNER = ['โ', 'โ', 'โ', 'โ']
|
|
20
|
+
|
|
21
|
+
/** Prop the mods viewer adds to every pane it draws; the Claude Code extension draws on the `vscode` surface too, without it. */
|
|
22
|
+
const VIEWER_PROP = 'g3nViewer'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Tells whether the mods viewer asked for this drawing, rather than the Claude Code extension on the same surface.
|
|
26
|
+
*
|
|
27
|
+
* @param props - The `Pane` props of the `ui.render` argument.
|
|
28
|
+
* @returns `true` when the viewer's prop is set.
|
|
29
|
+
*/
|
|
30
|
+
function isViewer(props: object): boolean {
|
|
31
|
+
return (props as Readonly<Record<string, unknown>>)[VIEWER_PROP] === true
|
|
32
|
+
}
|
|
20
33
|
const DONE_STATUSES = new Set(['completed', 'failed', 'killed', 'idle'])
|
|
21
34
|
|
|
22
35
|
/** Key prefix of the panel's own elements; an add-on looks for it to find the panel's tree. */
|
|
@@ -191,12 +204,13 @@ async function act($: EngineInterface, value: string): Promise<void> {
|
|
|
191
204
|
* Registers this mod's `agents-panel` command, its session and turn hooks, and its pane.
|
|
192
205
|
*
|
|
193
206
|
* @param on - Hook registration function the engine passes in.
|
|
207
|
+
* @param options - Plugin options; `showInClaudeCode` opens the pane in the Claude Code chat at session start.
|
|
194
208
|
*/
|
|
195
|
-
export const register: Register = on => {
|
|
209
|
+
export const register: Register = (on, options) => {
|
|
196
210
|
on('session.start', async ($, e, next) => {
|
|
197
|
-
await $.command.register({ name: 'agents-panel', description: 'Show or hide the agents panel' })
|
|
211
|
+
await $.command.register({ name: 'agents-panel', description: 'Show or hide the agents panel in the Claude Code chat' })
|
|
198
212
|
await loadAgents($)
|
|
199
|
-
void $.ui.open({ id: PANE, title: TITLE })
|
|
213
|
+
if (options.showInClaudeCode === true) void $.ui.open({ id: PANE, title: TITLE })
|
|
200
214
|
$.clock.every(1000, () => void pollRunning($))
|
|
201
215
|
return next(e)
|
|
202
216
|
})
|
|
@@ -236,7 +250,7 @@ export const register: Register = on => {
|
|
|
236
250
|
const frame = SPINNER[Math.floor(now / 250) % SPINNER.length] ?? 'โ'
|
|
237
251
|
|
|
238
252
|
// The viewer draws the panel with the mod's own view: hand it the agents as data, inside the key an add-on looks for.
|
|
239
|
-
if (e.surface === 'vscode') {
|
|
253
|
+
if (e.surface === 'vscode' && isViewer(e.props)) {
|
|
240
254
|
const { Input } = $.ui.resolve(e)
|
|
241
255
|
const data: AgentsViewData = {
|
|
242
256
|
agents: list.map((agent, index) => {
|
|
@@ -5,5 +5,13 @@
|
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "g3ntrovert"
|
|
7
7
|
},
|
|
8
|
-
"types": "./types/index.d.ts"
|
|
8
|
+
"types": "./types/index.d.ts",
|
|
9
|
+
"userConfig": {
|
|
10
|
+
"showInClaudeCode": {
|
|
11
|
+
"type": "boolean",
|
|
12
|
+
"title": "Show in Claude Code",
|
|
13
|
+
"default": true,
|
|
14
|
+
"description": "Open this mod's pane in the Claude Code chat when a session starts. The mods viewer shows it either way."
|
|
15
|
+
}
|
|
16
|
+
}
|
|
9
17
|
}
|
|
@@ -134,11 +134,12 @@ async function refresh($: EngineInterface): Promise<void> {
|
|
|
134
134
|
* Registers this mod's `context-bar` command, its session, measure and turn hooks, and its pane.
|
|
135
135
|
*
|
|
136
136
|
* @param on - Hook registration function the engine passes in.
|
|
137
|
+
* @param options - Plugin options; `showInClaudeCode` opens the pane in the Claude Code chat at session start.
|
|
137
138
|
*/
|
|
138
|
-
export const register: Register = on => {
|
|
139
|
+
export const register: Register = (on, options) => {
|
|
139
140
|
on('session.start', async ($, e, next) => {
|
|
140
|
-
await $.command.register({ name: 'context-bar', description: 'Show or hide the context window bar' })
|
|
141
|
-
void $.ui.open({ id: PANE, title: TITLE })
|
|
141
|
+
await $.command.register({ name: 'context-bar', description: 'Show or hide the context window bar in the Claude Code chat' })
|
|
142
|
+
if (options.showInClaudeCode === true) void $.ui.open({ id: PANE, title: TITLE })
|
|
142
143
|
void refresh($)
|
|
143
144
|
return next(e)
|
|
144
145
|
})
|