shadok-ai 0.1.199 → 0.1.201
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/package.json +1 -1
- package/public/index.html +333 -147
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -426,20 +426,6 @@
|
|
|
426
426
|
dépasse (quand une session tourne, le transcript remplit exactement → inerte). */
|
|
427
427
|
.chat-pane { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow-y: auto; }
|
|
428
428
|
|
|
429
|
-
/* Start screen */
|
|
430
|
-
#setup {
|
|
431
|
-
margin: auto;
|
|
432
|
-
width: min(520px, 92vw);
|
|
433
|
-
border: 1px solid var(--line);
|
|
434
|
-
border-radius: 8px;
|
|
435
|
-
background: var(--bg-raised);
|
|
436
|
-
padding: 28px;
|
|
437
|
-
display: flex;
|
|
438
|
-
flex-direction: column;
|
|
439
|
-
gap: 18px;
|
|
440
|
-
}
|
|
441
|
-
#setup h1 { font-family: var(--mono); font-size: 16px; color: var(--amber); font-weight: 500; }
|
|
442
|
-
#setup p.hint { color: var(--text-dim); font-size: 13px; line-height: 1.5; }
|
|
443
429
|
.field { display: flex; flex-direction: column; gap: 6px; }
|
|
444
430
|
input[type="text"] {
|
|
445
431
|
font-family: var(--mono);
|
|
@@ -852,7 +838,6 @@
|
|
|
852
838
|
/* Interactive terminal (xterm.js), per channel, filling the main area. Toggled
|
|
853
839
|
by ⌨️; when on, .chat-pane gets .terminal-mode → chat hidden, terminal shown. */
|
|
854
840
|
#terminalView { flex: 1; min-height: 0; display: none; flex-direction: column; }
|
|
855
|
-
.chat-pane.terminal-mode #setup,
|
|
856
841
|
.chat-pane.terminal-mode #transcripts,
|
|
857
842
|
.chat-pane.terminal-mode #thinking,
|
|
858
843
|
.chat-pane.terminal-mode #composer { display: none !important; }
|
|
@@ -870,8 +855,26 @@
|
|
|
870
855
|
#chanBar button.icon { padding: 4px 8px; font-size: 14px; line-height: 1; letter-spacing: 0; }
|
|
871
856
|
#chanBar button.icon.stop:hover { border-color: var(--err); color: var(--err); }
|
|
872
857
|
#chanBar button.icon.on { border-color: var(--amber); color: var(--amber); background: rgba(240, 168, 72, 0.15); }
|
|
858
|
+
/* Status / Directory / Session / Branch : inline & compact, so the channel
|
|
859
|
+
header stays a thin single line. */
|
|
860
|
+
#chanBar .gauge { flex-direction: row; align-items: baseline; gap: 4px; }
|
|
861
|
+
#chanBar .gauge .label { font-size: 10px; }
|
|
862
|
+
#chanBar #cwdView { display: inline-block; max-width: 200px; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
|
|
873
863
|
#xtermHost { flex: 1; min-height: 0; overflow: auto; padding: 8px 10px; }
|
|
874
864
|
#xtermHost .xterm { height: 100%; }
|
|
865
|
+
/* Aucun agent ouvert. Le panneau central n'affiche plus un formulaire à demi
|
|
866
|
+
rempli d'avance : il invite à agir, et rien d'autre. */
|
|
867
|
+
#emptyState {
|
|
868
|
+
flex: 1; min-height: 0; display: none;
|
|
869
|
+
flex-direction: column; align-items: center; justify-content: center;
|
|
870
|
+
gap: 12px; padding: 32px; text-align: center;
|
|
871
|
+
}
|
|
872
|
+
#emptyState.visible { display: flex; }
|
|
873
|
+
#emptyState .es-title {
|
|
874
|
+
font-family: var(--mono); font-size: 12px;
|
|
875
|
+
letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-dim);
|
|
876
|
+
}
|
|
877
|
+
#emptyState .es-hint { color: var(--text-dim); font-size: 13px; line-height: 1.5; max-width: 44ch; }
|
|
875
878
|
.keys {
|
|
876
879
|
display: flex;
|
|
877
880
|
gap: 6px;
|
|
@@ -978,8 +981,78 @@
|
|
|
978
981
|
@media (max-width: 900px) {
|
|
979
982
|
#machine, #diffpanel { position: fixed; inset: 0; width: auto; max-width: none; z-index: 10; border-left: none; }
|
|
980
983
|
}
|
|
981
|
-
|
|
982
|
-
|
|
984
|
+
/* ── Popins ───────────────────────────────────────────────
|
|
985
|
+
UNE CLASSE, pas une liste d'ids. La règle nommait
|
|
986
|
+
#secretsOverlay/#profilesOverlay/#telegramOverlay ; le panneau des crons,
|
|
987
|
+
ajouté après, n'y figurait pas — il s'affichait donc dans le flux de la page
|
|
988
|
+
au lieu de se centrer. Toute popin hérite maintenant du comportement en
|
|
989
|
+
portant `.overlay`, il n'y a plus de liste à ne pas oublier. */
|
|
990
|
+
.overlay {
|
|
991
|
+
position: fixed; inset: 0; z-index: 50;
|
|
992
|
+
display: flex; align-items: center; justify-content: center;
|
|
993
|
+
padding: 24px;
|
|
994
|
+
background: rgba(8, 9, 13, 0.62);
|
|
995
|
+
backdrop-filter: blur(2px);
|
|
996
|
+
}
|
|
997
|
+
.overlay[hidden] { display: none; }
|
|
998
|
+
/* Le panneau était hors palette : #1b1d22 au lieu de --bg-raised, #e8e8ea au
|
|
999
|
+
lieu de --text, des bordures rgba(255,255,255,.15) et un rayon de 10px quand
|
|
1000
|
+
tout le cockpit est à 6 — soit un gris et un blanc qui n'existaient nulle
|
|
1001
|
+
part ailleurs. Le filet ambre en haut reprend la grammaire des instruments
|
|
1002
|
+
(bord de l'onglet actif, ⌨️ allumé) : ce panneau est au premier plan. */
|
|
1003
|
+
#secretsPanel, .sec-panel {
|
|
1004
|
+
background: var(--bg-raised); color: var(--text);
|
|
1005
|
+
width: min(560px, 100%); max-height: min(80vh, 760px); overflow: auto;
|
|
1006
|
+
border: 1px solid var(--line); border-top: 1px solid var(--amber);
|
|
1007
|
+
border-radius: 6px; padding: 14px 16px;
|
|
1008
|
+
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
|
|
1009
|
+
}
|
|
1010
|
+
/* Squelette entête / corps / pied. Seul le CORPS défile, donc l'action
|
|
1011
|
+
principale reste atteignable : le formulaire de création dépasse l'écran sur
|
|
1012
|
+
un portable, et son bouton « Start » se trouvait en bas d'une page à
|
|
1013
|
+
dérouler. Réservé aux popins qui ont un pied — les autres gardent leur
|
|
1014
|
+
rembourrage simple. */
|
|
1015
|
+
.sec-panel.modal { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
|
|
1016
|
+
.modal > .sec-head {
|
|
1017
|
+
flex: 0 0 auto; margin: 0; padding: 11px 16px;
|
|
1018
|
+
border-bottom: 1px solid var(--line);
|
|
1019
|
+
}
|
|
1020
|
+
/* Même traitement que les libellés du cockpit (.label, barre du canal) : la
|
|
1021
|
+
popin appartient visiblement à la même famille. */
|
|
1022
|
+
.modal > .sec-head strong {
|
|
1023
|
+
font-family: var(--mono); font-size: 11px; font-weight: normal;
|
|
1024
|
+
letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-dim);
|
|
1025
|
+
}
|
|
1026
|
+
.modal > .sec-head strong .obj { color: var(--amber); }
|
|
1027
|
+
.modal-body { flex: 1 1 auto; overflow: auto; padding: 14px 16px; }
|
|
1028
|
+
.modal-foot {
|
|
1029
|
+
flex: 0 0 auto; display: flex; align-items: center; justify-content: flex-end;
|
|
1030
|
+
gap: 8px; padding: 11px 16px;
|
|
1031
|
+
border-top: 1px solid var(--line); background: rgba(0, 0, 0, 0.18);
|
|
1032
|
+
}
|
|
1033
|
+
/* Pousse ce qui suit à droite : un pied peut porter une note à gauche. */
|
|
1034
|
+
.modal-foot .foot-note { margin-right: auto; font-size: 11px; color: var(--text-dim); }
|
|
1035
|
+
.sec-panel :focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }
|
|
1036
|
+
/* Les <select> et les champs numériques n'étaient couverts par aucune règle :
|
|
1037
|
+
ils tombaient sur le rendu par défaut du navigateur — clair — au milieu d'un
|
|
1038
|
+
panneau sombre. */
|
|
1039
|
+
.sec-panel select, .sec-panel input[type=number], .sec-panel input[type=time] {
|
|
1040
|
+
padding: 6px 8px; border-radius: 6px; border: 1px solid var(--line);
|
|
1041
|
+
background: rgba(0, 0, 0, 0.25); color: inherit; font-family: inherit; font-size: inherit;
|
|
1042
|
+
}
|
|
1043
|
+
/* Une précision dans un libellé ne SE CRIE PAS : `.label` met en majuscules et
|
|
1044
|
+
espace les lettres, ce qui rendait une phrase entière illisible. */
|
|
1045
|
+
.label .check-hint { text-transform: none; letter-spacing: normal; font-weight: normal; }
|
|
1046
|
+
#cronList { list-style: none; padding: 0; margin: 0 0 14px; }
|
|
1047
|
+
#cronList li { padding: 6px 8px; border-radius: 6px; }
|
|
1048
|
+
#cronList li:nth-child(odd) { background: rgba(255, 255, 255, 0.04); }
|
|
1049
|
+
.cron-form { border-top: 1px solid var(--line); padding-top: 12px; }
|
|
1050
|
+
.cron-form .field { margin: 12px 0; }
|
|
1051
|
+
.cron-form .field:last-child { margin-bottom: 0; }
|
|
1052
|
+
/* `.field` est en colonne : sans redresser la direction ici, les trois champs
|
|
1053
|
+
s'empilaient — et `align-items: center` les centrait au lieu de les aligner. */
|
|
1054
|
+
.cron-when { flex-direction: row; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
1055
|
+
.cron-when input[type=number] { width: 76px; }
|
|
983
1056
|
.sec-panel .field { margin: 8px 0; }
|
|
984
1057
|
.sec-panel textarea, .sec-panel input[type=text], .sec-panel input:not([type]) { width: 100%; box-sizing: border-box; padding: 6px 8px; border-radius: 6px; border: 1px solid rgba(255,255,255,.15); background: rgba(0,0,0,.25); color: inherit; font-family: inherit; }
|
|
985
1058
|
#profilesList { list-style: none; padding: 0; margin: 0 0 8px; }
|
|
@@ -1009,22 +1082,6 @@
|
|
|
1009
1082
|
|
|
1010
1083
|
<header>
|
|
1011
1084
|
<span class="brand">shadok-ai <span class="ver" id="verView" title="running version"></span></span>
|
|
1012
|
-
<div class="gauge">
|
|
1013
|
-
<span class="label">Status</span>
|
|
1014
|
-
<span class="value"><span id="led" class="led"></span><span id="state">—</span></span>
|
|
1015
|
-
</div>
|
|
1016
|
-
<div class="gauge">
|
|
1017
|
-
<span class="label">Directory</span>
|
|
1018
|
-
<span class="value" id="cwdView">—</span>
|
|
1019
|
-
</div>
|
|
1020
|
-
<div class="gauge">
|
|
1021
|
-
<span class="label">Session</span>
|
|
1022
|
-
<span class="value" id="sessionView">—</span>
|
|
1023
|
-
</div>
|
|
1024
|
-
<div class="gauge" id="branchGauge" hidden>
|
|
1025
|
-
<span class="label">Branch</span>
|
|
1026
|
-
<span class="value" id="branchView">—</span>
|
|
1027
|
-
</div>
|
|
1028
1085
|
<span class="spacer"></span>
|
|
1029
1086
|
<div class="gauge quota" id="quota5h" title="5-hour rolling limit">
|
|
1030
1087
|
<span class="label">5h</span>
|
|
@@ -1059,7 +1116,7 @@
|
|
|
1059
1116
|
<div class="vm-hint" id="vmModeHint"></div>
|
|
1060
1117
|
</div>
|
|
1061
1118
|
|
|
1062
|
-
<div id="secretsOverlay" hidden>
|
|
1119
|
+
<div id="secretsOverlay" class="overlay" hidden>
|
|
1063
1120
|
<div id="secretsPanel">
|
|
1064
1121
|
<div class="sec-head">
|
|
1065
1122
|
<strong>Secret vault</strong>
|
|
@@ -1075,36 +1132,106 @@
|
|
|
1075
1132
|
</div>
|
|
1076
1133
|
</div>
|
|
1077
1134
|
|
|
1078
|
-
<div id="cronOverlay" hidden>
|
|
1079
|
-
|
|
1135
|
+
<div id="cronOverlay" class="overlay" hidden>
|
|
1136
|
+
<!-- Le panneau portait id="secretsPanel" — un id en DOUBLON de la vraie popin
|
|
1137
|
+
des secrets, donc $("secretsPanel") pouvait tomber sur le mauvais. -->
|
|
1138
|
+
<div id="cronPanel" class="sec-panel modal">
|
|
1080
1139
|
<div class="sec-head">
|
|
1081
|
-
<strong>⏰ Schedule — <span id="cronChanName">this agent</span></strong>
|
|
1140
|
+
<strong>⏰ Schedule — <span class="obj" id="cronChanName">this agent</span></strong>
|
|
1082
1141
|
<button id="cronClose" title="Close">✕</button>
|
|
1083
1142
|
</div>
|
|
1084
|
-
<
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
<
|
|
1088
|
-
<
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
<
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
<input id="
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1143
|
+
<div class="modal-body">
|
|
1144
|
+
<p class="sec-note">Prompts sent to this agent on a timer — monitoring, reporting. The server fires them even when nobody's watching; answers land here and in Telegram.</p>
|
|
1145
|
+
<p class="sec-note"><b>Simplest way:</b> just ask the agent — « surveille X chaque matin et alerte-moi si Y ». It writes the check and schedules it itself.</p>
|
|
1146
|
+
<ul id="cronList"></ul>
|
|
1147
|
+
<form id="cronAdd" class="cron-form">
|
|
1148
|
+
<div class="field">
|
|
1149
|
+
<span class="label">Prompt to run</span>
|
|
1150
|
+
<textarea id="cronPrompt" rows="2" placeholder="e.g. « Rédige une alerte à partir de l'anomalie détectée »"></textarea>
|
|
1151
|
+
</div>
|
|
1152
|
+
<div class="field">
|
|
1153
|
+
<span class="label">Check command <span class="check-hint">— optional; prints nothing = nothing to report, and the agent stays asleep (0 tokens)</span></span>
|
|
1154
|
+
<input id="cronCheck" placeholder="e.g. ~/scripts/check-budget.sh" autocomplete="off" spellcheck="false" />
|
|
1155
|
+
</div>
|
|
1156
|
+
<div class="field cron-when">
|
|
1157
|
+
<select id="cronKind" aria-label="How often">
|
|
1158
|
+
<option value="interval">Every…</option>
|
|
1159
|
+
<option value="daily">Daily at…</option>
|
|
1160
|
+
</select>
|
|
1161
|
+
<span id="cronIntervalFields">
|
|
1162
|
+
<input id="cronEvery" type="number" min="1" value="60" aria-label="Interval">
|
|
1163
|
+
<select id="cronUnit" aria-label="Unit"><option value="1">minutes</option><option value="60">hours</option></select>
|
|
1164
|
+
</span>
|
|
1165
|
+
<span id="cronDailyFields" hidden>
|
|
1166
|
+
<input id="cronTime" type="time" value="09:00" aria-label="Time of day">
|
|
1167
|
+
</span>
|
|
1168
|
+
</div>
|
|
1169
|
+
</form>
|
|
1170
|
+
</div>
|
|
1171
|
+
<div class="modal-foot">
|
|
1172
|
+
<span class="foot-note" id="cronTzNote"></span>
|
|
1173
|
+
<button type="submit" form="cronAdd" class="primary">Add schedule</button>
|
|
1174
|
+
</div>
|
|
1104
1175
|
</div>
|
|
1105
1176
|
</div>
|
|
1106
1177
|
|
|
1107
|
-
<div id="
|
|
1178
|
+
<div id="setupOverlay" class="overlay" hidden>
|
|
1179
|
+
<div id="setupPanel" class="sec-panel modal">
|
|
1180
|
+
<div class="sec-head">
|
|
1181
|
+
<strong>New <span class="obj">agent</span></strong>
|
|
1182
|
+
<button id="setupClose" title="Close">✕</button>
|
|
1183
|
+
</div>
|
|
1184
|
+
<div class="modal-body">
|
|
1185
|
+
<p class="sec-note">A real Claude Code session, driven inside a pseudo-terminal. Open as many as you want Claudes working in parallel.</p>
|
|
1186
|
+
<div class="field" id="profileField">
|
|
1187
|
+
<div class="field-head">
|
|
1188
|
+
<span class="label">Which agent?</span>
|
|
1189
|
+
<button type="button" class="linkish" id="editProfilesLink" title="Create or edit agent profiles">✎ edit profiles</button>
|
|
1190
|
+
</div>
|
|
1191
|
+
<div id="profileGrid" role="radiogroup" aria-label="Agent profile"></div>
|
|
1192
|
+
<span class="check-hint" id="profileNaNote" hidden>Applies to new sessions only.</span>
|
|
1193
|
+
</div>
|
|
1194
|
+
<div class="field">
|
|
1195
|
+
<span class="label">Working directory</span>
|
|
1196
|
+
<input type="text" id="cwdInput" placeholder="/path/to/project">
|
|
1197
|
+
</div>
|
|
1198
|
+
<label class="check" id="worktreeField">
|
|
1199
|
+
<input type="checkbox" id="worktreeInput" checked>
|
|
1200
|
+
<span>Isolate in a git worktree <span class="check-hint">— the agent edits a fresh branch; review & merge from the Diff panel. Auto-removed on close if unused, kept if it has changes. (new sessions only)</span></span>
|
|
1201
|
+
</label>
|
|
1202
|
+
<details class="fold" id="advancedField">
|
|
1203
|
+
<summary>Advanced — resume an existing session</summary>
|
|
1204
|
+
<div class="field">
|
|
1205
|
+
<div class="radio-row">
|
|
1206
|
+
<label><input type="radio" name="mode" value="new" checked> new session</label>
|
|
1207
|
+
<label><input type="radio" name="mode" value="continue"> latest in directory</label>
|
|
1208
|
+
<label><input type="radio" name="mode" value="resume"> by id</label>
|
|
1209
|
+
</div>
|
|
1210
|
+
</div>
|
|
1211
|
+
<div class="field" id="resumeField" hidden>
|
|
1212
|
+
<span class="label">Session id</span>
|
|
1213
|
+
<input type="text" id="resumeInput" placeholder="5fe046dd-…">
|
|
1214
|
+
<span class="label">Sessions in this directory</span>
|
|
1215
|
+
<div class="session-list" id="sessionList"></div>
|
|
1216
|
+
</div>
|
|
1217
|
+
</details>
|
|
1218
|
+
<details class="fold" id="recoverField" hidden>
|
|
1219
|
+
<summary>Reopen a past session <span class="fold-count" id="recoverCount"></span></summary>
|
|
1220
|
+
<div class="recover-list" id="recoverList"></div>
|
|
1221
|
+
</details>
|
|
1222
|
+
<details class="fold" id="liveField" hidden>
|
|
1223
|
+
<summary>Agents running now <span class="fold-count" id="liveCount"></span></summary>
|
|
1224
|
+
<div class="recover-list" id="liveList"></div>
|
|
1225
|
+
</details>
|
|
1226
|
+
</div>
|
|
1227
|
+
<div class="modal-foot">
|
|
1228
|
+
<button id="setupCancel">Cancel</button>
|
|
1229
|
+
<button class="primary" id="startBtn">Start agent</button>
|
|
1230
|
+
</div>
|
|
1231
|
+
</div>
|
|
1232
|
+
</div>
|
|
1233
|
+
|
|
1234
|
+
<div id="profilesOverlay" class="overlay" hidden>
|
|
1108
1235
|
<div id="profilesPanel" class="sec-panel">
|
|
1109
1236
|
<div class="sec-head">
|
|
1110
1237
|
<strong>Agent profiles</strong>
|
|
@@ -1127,7 +1254,7 @@
|
|
|
1127
1254
|
</div>
|
|
1128
1255
|
</div>
|
|
1129
1256
|
|
|
1130
|
-
<div id="telegramOverlay" hidden>
|
|
1257
|
+
<div id="telegramOverlay" class="overlay" hidden>
|
|
1131
1258
|
<div id="telegramPanel" class="sec-panel">
|
|
1132
1259
|
<div class="sec-head">
|
|
1133
1260
|
<strong>Telegram bot</strong>
|
|
@@ -1172,6 +1299,22 @@
|
|
|
1172
1299
|
séparé, donc ses actions n'ont pas de raison d'apparaître avec lui. -->
|
|
1173
1300
|
<div id="chanBar" hidden>
|
|
1174
1301
|
<span class="label" id="chanBarName"></span>
|
|
1302
|
+
<div class="gauge">
|
|
1303
|
+
<span class="label">Status</span>
|
|
1304
|
+
<span class="value"><span id="led" class="led"></span><span id="state">—</span></span>
|
|
1305
|
+
</div>
|
|
1306
|
+
<div class="gauge">
|
|
1307
|
+
<span class="label">Directory</span>
|
|
1308
|
+
<span class="value" id="cwdView">—</span>
|
|
1309
|
+
</div>
|
|
1310
|
+
<div class="gauge">
|
|
1311
|
+
<span class="label">Session</span>
|
|
1312
|
+
<span class="value" id="sessionView">—</span>
|
|
1313
|
+
</div>
|
|
1314
|
+
<div class="gauge" id="branchGauge" hidden>
|
|
1315
|
+
<span class="label">Branch</span>
|
|
1316
|
+
<span class="value" id="branchView">—</span>
|
|
1317
|
+
</div>
|
|
1175
1318
|
<span class="chan-actions">
|
|
1176
1319
|
<button id="chanCron" class="icon" title="Scheduled prompts for this agent (monitoring / reporting)">⏰</button>
|
|
1177
1320
|
<button id="chanStop" class="icon stop" title="End session">⏹️</button>
|
|
@@ -1179,52 +1322,13 @@
|
|
|
1179
1322
|
</span>
|
|
1180
1323
|
</div>
|
|
1181
1324
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
<
|
|
1188
|
-
|
|
1189
|
-
<span class="label">Which agent?</span>
|
|
1190
|
-
<button type="button" class="linkish" id="editProfilesLink" title="Create or edit agent profiles">✎ edit profiles</button>
|
|
1191
|
-
</div>
|
|
1192
|
-
<div id="profileGrid" role="radiogroup" aria-label="Agent profile"></div>
|
|
1193
|
-
<span class="check-hint" id="profileNaNote" hidden>Applies to new sessions only.</span>
|
|
1194
|
-
</div>
|
|
1195
|
-
<div class="field">
|
|
1196
|
-
<span class="label">Working directory</span>
|
|
1197
|
-
<input type="text" id="cwdInput" placeholder="/path/to/project">
|
|
1198
|
-
</div>
|
|
1199
|
-
<label class="check" id="worktreeField">
|
|
1200
|
-
<input type="checkbox" id="worktreeInput" checked>
|
|
1201
|
-
<span>Isolate in a git worktree <span class="check-hint">— the agent edits a fresh branch; review & merge from the Diff panel. Auto-removed on close if unused, kept if it has changes. (new sessions only)</span></span>
|
|
1202
|
-
</label>
|
|
1203
|
-
<details class="fold" id="advancedField">
|
|
1204
|
-
<summary>Advanced — resume an existing session</summary>
|
|
1205
|
-
<div class="field">
|
|
1206
|
-
<div class="radio-row">
|
|
1207
|
-
<label><input type="radio" name="mode" value="new" checked> new session</label>
|
|
1208
|
-
<label><input type="radio" name="mode" value="continue"> latest in directory</label>
|
|
1209
|
-
<label><input type="radio" name="mode" value="resume"> by id</label>
|
|
1210
|
-
</div>
|
|
1211
|
-
</div>
|
|
1212
|
-
<div class="field" id="resumeField" hidden>
|
|
1213
|
-
<span class="label">Session id</span>
|
|
1214
|
-
<input type="text" id="resumeInput" placeholder="5fe046dd-…">
|
|
1215
|
-
<span class="label">Sessions in this directory</span>
|
|
1216
|
-
<div class="session-list" id="sessionList"></div>
|
|
1217
|
-
</div>
|
|
1218
|
-
</details>
|
|
1219
|
-
<details class="fold" id="recoverField" hidden>
|
|
1220
|
-
<summary>Reopen a past session <span class="fold-count" id="recoverCount"></span></summary>
|
|
1221
|
-
<div class="recover-list" id="recoverList"></div>
|
|
1222
|
-
</details>
|
|
1223
|
-
<details class="fold" id="liveField" hidden>
|
|
1224
|
-
<summary>Agents running now <span class="fold-count" id="liveCount"></span></summary>
|
|
1225
|
-
<div class="recover-list" id="liveList"></div>
|
|
1226
|
-
</details>
|
|
1227
|
-
<button class="primary" id="startBtn">Start agent</button>
|
|
1325
|
+
<!-- Aucun agent : une invitation, pas un formulaire pré-rempli. La création
|
|
1326
|
+
se fait dans une popin (#setupOverlay) et ne crée le canal qu'à la
|
|
1327
|
+
validation — ouvrir puis renoncer ne laisse plus d'onglet mort-né. -->
|
|
1328
|
+
<div id="emptyState">
|
|
1329
|
+
<span class="es-title">No agent open</span>
|
|
1330
|
+
<p class="es-hint">Each agent is its own Claude Code session, working on its own directory. Start one and it takes the controls.</p>
|
|
1331
|
+
<button class="primary" id="emptyNewBtn">+ new agent</button>
|
|
1228
1332
|
</div>
|
|
1229
1333
|
|
|
1230
1334
|
<div id="transcripts"></div>
|
|
@@ -1299,7 +1403,7 @@
|
|
|
1299
1403
|
const $ = (id) => document.getElementById(id);
|
|
1300
1404
|
const tabbar = $("tabbar"), newTabBtn = $("newTab"), newGroupBtn = $("newGroup");
|
|
1301
1405
|
const ungroupedEl = $("ungrouped"), groupsEl = $("groups");
|
|
1302
|
-
const
|
|
1406
|
+
const transcripts = $("transcripts"), thinking = $("thinking");
|
|
1303
1407
|
const composer = $("composer"), promptInput = $("promptInput"), sendBtn = $("sendBtn");
|
|
1304
1408
|
const machine = $("machine"), screenEl = $("screen");
|
|
1305
1409
|
|
|
@@ -1315,7 +1419,7 @@
|
|
|
1315
1419
|
.then((r) => r.json())
|
|
1316
1420
|
.then((d) => {
|
|
1317
1421
|
serverCwd = d.cwd || "";
|
|
1318
|
-
if (
|
|
1422
|
+
if (!$("setupOverlay").hidden && !$("cwdInput").value) $("cwdInput").value = serverCwd || "";
|
|
1319
1423
|
})
|
|
1320
1424
|
.catch(() => {});
|
|
1321
1425
|
|
|
@@ -1915,7 +2019,8 @@
|
|
|
1915
2019
|
refreshTitle();
|
|
1916
2020
|
if (active === tab) {
|
|
1917
2021
|
if (tabs.length) activate(tabs[tabs.length - 1]);
|
|
1918
|
-
|
|
2022
|
+
// Plus aucun agent : l'invitation, pas un onglet « setup » fantôme.
|
|
2023
|
+
else { active = null; refreshChrome(); }
|
|
1919
2024
|
}
|
|
1920
2025
|
}
|
|
1921
2026
|
|
|
@@ -2126,9 +2231,38 @@
|
|
|
2126
2231
|
refreshBadge();
|
|
2127
2232
|
}
|
|
2128
2233
|
|
|
2129
|
-
/**
|
|
2234
|
+
/**
|
|
2235
|
+
* Keeps header, composer, channel bar and engine room in sync with the active
|
|
2236
|
+
* channel — ou avec l'ABSENCE de canal.
|
|
2237
|
+
*
|
|
2238
|
+
* `active` peut désormais être nul : la création vit dans une popin et ne crée
|
|
2239
|
+
* plus d'onglet « setup » d'avance, donc fermer le dernier agent ne laisse plus
|
|
2240
|
+
* un formulaire à la place. Tout ce qui décrit un canal retombe alors sur « — »
|
|
2241
|
+
* et le panneau central montre l'invitation.
|
|
2242
|
+
*/
|
|
2130
2243
|
function refreshChrome() {
|
|
2131
2244
|
const t = active;
|
|
2245
|
+
if (!t) {
|
|
2246
|
+
$("led").className = "led ";
|
|
2247
|
+
$("state").textContent = "no agent";
|
|
2248
|
+
$("cwdView").textContent = "—";
|
|
2249
|
+
$("sessionView").textContent = "—";
|
|
2250
|
+
$("sessionView").title = "";
|
|
2251
|
+
$("branchGauge").hidden = true;
|
|
2252
|
+
composer.hidden = true;
|
|
2253
|
+
$("chanBar").hidden = true;
|
|
2254
|
+
thinking.classList.remove("visible");
|
|
2255
|
+
screenEl.textContent = "";
|
|
2256
|
+
// Programmer ou arrêter n'a pas de cible : mieux vaut des boutons éteints
|
|
2257
|
+
// qu'une popin qui parle d'un agent inexistant.
|
|
2258
|
+
$("cronBtn").disabled = true;
|
|
2259
|
+
$("stopBtn").disabled = true;
|
|
2260
|
+
$("emptyState").classList.add("visible");
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
$("emptyState").classList.remove("visible");
|
|
2264
|
+
$("cronBtn").disabled = !t.sessionId;
|
|
2265
|
+
$("stopBtn").disabled = false;
|
|
2132
2266
|
$("led").className = "led " +
|
|
2133
2267
|
({ setup: "", connecting: "busy", ready: "on", busy: "busy", dead: "off" }[t.status]);
|
|
2134
2268
|
$("state").textContent = t.stateText;
|
|
@@ -2139,33 +2273,14 @@
|
|
|
2139
2273
|
$("sessionView").title = t.sessionId || "";
|
|
2140
2274
|
$("branchGauge").hidden = !t.branch;
|
|
2141
2275
|
$("branchView").textContent = t.branch || "—";
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
// agent » : il n'y a pas encore de canal à programmer, arrêter ou ouvrir en
|
|
2146
|
-
// terminal. Le nom vit ici (et non dans syncTerm) pour qu'un renommage ou un
|
|
2147
|
-
// changement de canal le mette à jour même terminal fermé.
|
|
2148
|
-
$("chanBar").hidden = t.status === "setup";
|
|
2276
|
+
composer.hidden = false;
|
|
2277
|
+
// La barre du canal accompagne les deux vues du canal.
|
|
2278
|
+
$("chanBar").hidden = false;
|
|
2149
2279
|
$("chanBarName").textContent = t.nameEl ? t.nameEl.textContent : "";
|
|
2150
2280
|
$("chanBarName").title = t.cwd || "";
|
|
2151
2281
|
sendBtn.disabled = t.status !== "ready" || !!t.pendingChoices;
|
|
2152
2282
|
thinking.classList.toggle("visible", t.status === "busy");
|
|
2153
2283
|
screenEl.textContent = t.screenText;
|
|
2154
|
-
if (t.status === "setup") {
|
|
2155
|
-
// Only pre-fill when empty — never clobber what the user is typing.
|
|
2156
|
-
if (!$("cwdInput").value) {
|
|
2157
|
-
$("cwdInput").value =
|
|
2158
|
-
t.cwd || localStorage.getItem("cp.cwd") || serverCwd || "";
|
|
2159
|
-
}
|
|
2160
|
-
const last = localStorage.getItem("cp.session");
|
|
2161
|
-
if (last && !$("resumeInput").value) $("resumeInput").value = last;
|
|
2162
|
-
$("startBtn").disabled = false;
|
|
2163
|
-
// refreshChrome tourne souvent : on ne rappelle la mémoire qu'au premier
|
|
2164
|
-
// passage sur CET onglet, sinon on écraserait le choix de l'utilisateur.
|
|
2165
|
-
if (profileTab !== t) { profileTab = t; profileTouched = false; applyRememberedProfile(); }
|
|
2166
|
-
refreshRecoverList();
|
|
2167
|
-
refreshLiveList();
|
|
2168
|
-
}
|
|
2169
2284
|
}
|
|
2170
2285
|
|
|
2171
2286
|
/** Paints a channel's context-window usage bar + tiny number in its tab. */
|
|
@@ -2205,12 +2320,32 @@
|
|
|
2205
2320
|
return el;
|
|
2206
2321
|
}
|
|
2207
2322
|
|
|
2323
|
+
/** Temps relatif compact : "now" / "3m ago" / "2h ago" / "5d ago". */
|
|
2324
|
+
function relTime(ts) {
|
|
2325
|
+
const s = Math.max(0, (Date.now() - ts) / 1000);
|
|
2326
|
+
if (s < 45) return "now";
|
|
2327
|
+
const m = Math.round(s / 60);
|
|
2328
|
+
if (m < 60) return m + "m ago";
|
|
2329
|
+
const h = Math.round(m / 60);
|
|
2330
|
+
if (h < 24) return h + "h ago";
|
|
2331
|
+
return Math.round(h / 24) + "d ago";
|
|
2332
|
+
}
|
|
2333
|
+
/** (Re)pose l'heure relative du dernier message sur l'onglet. */
|
|
2334
|
+
function renderLastAt(tab) {
|
|
2335
|
+
if (!tab.lastAtEl) return;
|
|
2336
|
+
if (!Number.isFinite(tab.lastMsgAt)) { tab.lastAtEl.textContent = ""; return; }
|
|
2337
|
+
tab.lastAtEl.textContent = relTime(tab.lastMsgAt);
|
|
2338
|
+
tab.lastAtEl.title = new Date(tab.lastMsgAt).toLocaleString();
|
|
2339
|
+
}
|
|
2340
|
+
// Le relatif vieillit tout seul : on rafraîchit tous les onglets périodiquement.
|
|
2341
|
+
setInterval(() => { for (const t of tabs) renderLastAt(t); }, 30000);
|
|
2342
|
+
|
|
2208
2343
|
function addTurn(tab, role, who, text, extraClass, at) {
|
|
2209
|
-
// Heure du dernier message dans la liste des canaux (
|
|
2210
|
-
// message (historique rejoué ou live) porte `at` ; le dernier gagne.
|
|
2211
|
-
if (Number.isFinite(at)
|
|
2344
|
+
// Heure du dernier message dans la liste des canaux, en relatif (« 3m ago »).
|
|
2345
|
+
// Chaque message (historique rejoué ou live) porte `at` ; le dernier gagne.
|
|
2346
|
+
if (Number.isFinite(at)) {
|
|
2212
2347
|
tab.lastMsgAt = at;
|
|
2213
|
-
tab
|
|
2348
|
+
renderLastAt(tab);
|
|
2214
2349
|
}
|
|
2215
2350
|
const turn = document.createElement("div");
|
|
2216
2351
|
turn.className = "turn " + role + (extraClass ? " " + extraClass : "");
|
|
@@ -2778,9 +2913,33 @@
|
|
|
2778
2913
|
}
|
|
2779
2914
|
}
|
|
2780
2915
|
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2916
|
+
/**
|
|
2917
|
+
* Ouvre la popin de création. Elle ne crée RIEN : l'onglet naît au « Start
|
|
2918
|
+
* agent » (cf. startNewAgent). Ouvrir puis renoncer ne laissait auparavant pas
|
|
2919
|
+
* seulement un onglet mort-né — il devenait le canal actif, avec son formulaire
|
|
2920
|
+
* à la place de la conversation qu'on regardait.
|
|
2921
|
+
*/
|
|
2922
|
+
function openSetup() {
|
|
2923
|
+
// Ne jamais écraser ce qui est déjà tapé (la popin peut se rouvrir).
|
|
2924
|
+
if (!$("cwdInput").value) {
|
|
2925
|
+
$("cwdInput").value = (active && active.cwd) || localStorage.getItem("cp.cwd") || serverCwd || "";
|
|
2926
|
+
}
|
|
2927
|
+
const last = localStorage.getItem("cp.session");
|
|
2928
|
+
if (last && !$("resumeInput").value) $("resumeInput").value = last;
|
|
2929
|
+
$("startBtn").disabled = false;
|
|
2930
|
+
// Le profil retenu n'est rappelé qu'à l'OUVERTURE : rappelé en continu, il
|
|
2931
|
+
// écraserait le choix en cours de saisie.
|
|
2932
|
+
profileTouched = false;
|
|
2933
|
+
applyRememberedProfile();
|
|
2934
|
+
refreshRecoverList();
|
|
2935
|
+
refreshLiveList();
|
|
2936
|
+
$("setupOverlay").hidden = false;
|
|
2937
|
+
$("cwdInput").focus();
|
|
2938
|
+
}
|
|
2939
|
+
function closeSetup() { $("setupOverlay").hidden = true; }
|
|
2940
|
+
|
|
2941
|
+
/** Valide le formulaire → C'EST ICI que le canal est créé, et pas avant. */
|
|
2942
|
+
function startNewAgent() {
|
|
2784
2943
|
const cwd = $("cwdInput").value.trim();
|
|
2785
2944
|
const mode = document.querySelector('input[name="mode"]:checked').value;
|
|
2786
2945
|
const msg = { cwd };
|
|
@@ -2797,7 +2956,9 @@
|
|
|
2797
2956
|
if (cwd) localStorage.setItem("cp.profile:" + cwd, selectedProfile);
|
|
2798
2957
|
}
|
|
2799
2958
|
$("startBtn").disabled = true;
|
|
2800
|
-
|
|
2959
|
+
closeSetup();
|
|
2960
|
+
// createTab active l'onglet créé ; launchTab ouvre le lien.
|
|
2961
|
+
launchTab(createTab(), msg);
|
|
2801
2962
|
}
|
|
2802
2963
|
|
|
2803
2964
|
// ── Version display + auto-reload ─────────────────────────────────────────
|
|
@@ -3189,13 +3350,17 @@
|
|
|
3189
3350
|
}
|
|
3190
3351
|
|
|
3191
3352
|
// ── Controls ────────────────────────────────────────────
|
|
3192
|
-
$("startBtn").addEventListener("click",
|
|
3353
|
+
$("startBtn").addEventListener("click", startNewAgent);
|
|
3354
|
+
$("setupClose").addEventListener("click", closeSetup);
|
|
3355
|
+
$("setupCancel").addEventListener("click", closeSetup);
|
|
3356
|
+
$("setupOverlay").addEventListener("click", (e) => { if (e.target === $("setupOverlay")) closeSetup(); });
|
|
3357
|
+
$("emptyNewBtn").addEventListener("click", openSetup);
|
|
3193
3358
|
document.querySelectorAll('input[name="mode"]').forEach((r) =>
|
|
3194
3359
|
r.addEventListener("change", () => {
|
|
3195
3360
|
const mode = document.querySelector('input[name="mode"]:checked').value;
|
|
3196
3361
|
const isNew = mode === "new";
|
|
3197
3362
|
$("resumeField").hidden = mode !== "resume";
|
|
3198
|
-
// Profil et worktree ne valent que pour une session neuve :
|
|
3363
|
+
// Profil et worktree ne valent que pour une session neuve : startNewAgent
|
|
3199
3364
|
// les ignore déjà en resume/continue, l'UI cesse simplement de le cacher.
|
|
3200
3365
|
$("profileField").classList.toggle("na", !isNew);
|
|
3201
3366
|
$("profileNaNote").hidden = isNew;
|
|
@@ -3213,7 +3378,7 @@
|
|
|
3213
3378
|
// Agents can appear while the setup screen sits open (pilotctl spawns one,
|
|
3214
3379
|
// another tab starts one) — keep the list current without a reload.
|
|
3215
3380
|
setInterval(() => {
|
|
3216
|
-
if (
|
|
3381
|
+
if (!$("setupOverlay").hidden) refreshLiveList();
|
|
3217
3382
|
}, 5000);
|
|
3218
3383
|
|
|
3219
3384
|
function submitPrompt(t, text, force) {
|
|
@@ -3323,6 +3488,7 @@
|
|
|
3323
3488
|
}
|
|
3324
3489
|
promptInput.addEventListener("input", () => {
|
|
3325
3490
|
autosizePrompt();
|
|
3491
|
+
if (!active) return; // aucun agent : le composer est masqué, mais pas de crash gratuit
|
|
3326
3492
|
active.draft = promptInput.value;
|
|
3327
3493
|
persistDraft(active);
|
|
3328
3494
|
});
|
|
@@ -3452,7 +3618,7 @@
|
|
|
3452
3618
|
$("refreshDiff").addEventListener("click", loadDiff);
|
|
3453
3619
|
document.querySelectorAll(".keys button[data-key]").forEach((b) =>
|
|
3454
3620
|
b.addEventListener("click", () => {
|
|
3455
|
-
if (active.ws) active.ws.send(JSON.stringify({ type: "key", key: b.dataset.key }));
|
|
3621
|
+
if (active && active.ws) active.ws.send(JSON.stringify({ type: "key", key: b.dataset.key }));
|
|
3456
3622
|
})
|
|
3457
3623
|
);
|
|
3458
3624
|
// La touche Échap envoie esc à la session active (comme le bouton Esc de
|
|
@@ -3469,6 +3635,9 @@
|
|
|
3469
3635
|
// 1. Overlays / panneaux ouverts → fermer, pas d'esc.
|
|
3470
3636
|
if (!$("secretsOverlay").hidden) { $("secretsOverlay").hidden = true; e.preventDefault(); return; }
|
|
3471
3637
|
if (!$("profilesOverlay").hidden) { closeProfilesPanel(); e.preventDefault(); return; }
|
|
3638
|
+
if (!$("setupOverlay").hidden) { closeSetup(); e.preventDefault(); return; }
|
|
3639
|
+
if (!$("cronOverlay").hidden) { $("cronOverlay").hidden = true; e.preventDefault(); return; }
|
|
3640
|
+
if (!$("telegramOverlay").hidden) { $("telegramOverlay").hidden = true; e.preventDefault(); return; }
|
|
3472
3641
|
if ($("diffpanel").classList.contains("open")) { $("diffpanel").classList.remove("open"); e.preventDefault(); return; }
|
|
3473
3642
|
// 2. Dialogue clickable en attente de réponse → ne rien faire.
|
|
3474
3643
|
if (active && active.dialogBubble) return;
|
|
@@ -3488,11 +3657,11 @@
|
|
|
3488
3657
|
}
|
|
3489
3658
|
});
|
|
3490
3659
|
$("settleBtn").addEventListener("click", () => {
|
|
3491
|
-
if (active.ws && active.status === "ready")
|
|
3660
|
+
if (active && active.ws && active.status === "ready")
|
|
3492
3661
|
active.ws.send(JSON.stringify({ type: "settle" }));
|
|
3493
3662
|
});
|
|
3494
3663
|
$("stopBtn").addEventListener("click", () => {
|
|
3495
|
-
if (active.ws) active.ws.send(JSON.stringify({ type: "stop" }));
|
|
3664
|
+
if (active && active.ws) active.ws.send(JSON.stringify({ type: "stop" }));
|
|
3496
3665
|
});
|
|
3497
3666
|
|
|
3498
3667
|
// ── Secret vault panel (central, global; profiles pick which to inject) ──
|
|
@@ -3577,8 +3746,23 @@
|
|
|
3577
3746
|
if (s.kind === "interval") return s.everyMin % 60 === 0 ? "every " + (s.everyMin / 60) + "h" : "every " + s.everyMin + "m";
|
|
3578
3747
|
return "daily at " + String(s.hour).padStart(2, "0") + ":" + String(s.minute).padStart(2, "0");
|
|
3579
3748
|
}
|
|
3749
|
+
/**
|
|
3750
|
+
* Fuseau réellement appliqué aux horaires quotidiens, dans le pied de la popin.
|
|
3751
|
+
* Le panneau annonçait « Times are server-local », ce qui ne dit pas 9h OÙ —
|
|
3752
|
+
* or le serveur peut tourner dans un autre fuseau que le navigateur.
|
|
3753
|
+
*/
|
|
3754
|
+
async function showCronTz() {
|
|
3755
|
+
const el = $("cronTzNote");
|
|
3756
|
+
el.textContent = "";
|
|
3757
|
+
try {
|
|
3758
|
+
const tz = await (await fetch("/timezone")).json();
|
|
3759
|
+
const zone = tz.timezone || tz.system;
|
|
3760
|
+
if (zone) el.textContent = "Daily times run in " + zone;
|
|
3761
|
+
} catch { /* la popin marche très bien sans cette note */ }
|
|
3762
|
+
}
|
|
3580
3763
|
async function renderCrons() {
|
|
3581
3764
|
const ul = $("cronList"); ul.innerHTML = "";
|
|
3765
|
+
showCronTz();
|
|
3582
3766
|
$("cronChanName").textContent = (active && (active.name || (active.sessionId || "").slice(0, 8))) || "no agent";
|
|
3583
3767
|
if (!active || !active.sessionId) { ul.innerHTML = '<li><span style="opacity:.6">Open an agent to schedule prompts.</span></li>'; return; }
|
|
3584
3768
|
let all = [];
|
|
@@ -3937,7 +4121,7 @@
|
|
|
3937
4121
|
el.value = "";
|
|
3938
4122
|
});
|
|
3939
4123
|
|
|
3940
|
-
newTabBtn.addEventListener("click",
|
|
4124
|
+
newTabBtn.addEventListener("click", openSetup);
|
|
3941
4125
|
newGroupBtn.addEventListener("click", () => {
|
|
3942
4126
|
createGroup();
|
|
3943
4127
|
persistGroups();
|
|
@@ -4086,7 +4270,9 @@
|
|
|
4086
4270
|
if (i !== targetIdx) launchTab(pending[i].t, pending[i].msg);
|
|
4087
4271
|
}
|
|
4088
4272
|
} else {
|
|
4089
|
-
|
|
4273
|
+
// Rien à restaurer : l'invitation. Le formulaire ne s'ouvre pas de force —
|
|
4274
|
+
// on peut aussi arriver là pour regarder, ou lancer depuis Telegram.
|
|
4275
|
+
refreshChrome();
|
|
4090
4276
|
// Nothing to restore: enable persistence for user actions, but do NOT
|
|
4091
4277
|
// push a blank list (a failed /channels fetch must never wipe the store).
|
|
4092
4278
|
restoring = false;
|