shadok-ai 0.1.199 → 0.1.200

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/index.html +288 -127
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadok-ai",
3
- "version": "0.1.199",
3
+ "version": "0.1.200",
4
4
  "main": "dist/session.js",
5
5
  "scripts": {
6
6
  "test": "node --import tsx --test test/*.ts .claude/skills/shadok-ai-agents/test/*.test.mjs",
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; }
@@ -872,6 +857,19 @@
872
857
  #chanBar button.icon.on { border-color: var(--amber); color: var(--amber); background: rgba(240, 168, 72, 0.15); }
873
858
  #xtermHost { flex: 1; min-height: 0; overflow: auto; padding: 8px 10px; }
874
859
  #xtermHost .xterm { height: 100%; }
860
+ /* Aucun agent ouvert. Le panneau central n'affiche plus un formulaire à demi
861
+ rempli d'avance : il invite à agir, et rien d'autre. */
862
+ #emptyState {
863
+ flex: 1; min-height: 0; display: none;
864
+ flex-direction: column; align-items: center; justify-content: center;
865
+ gap: 12px; padding: 32px; text-align: center;
866
+ }
867
+ #emptyState.visible { display: flex; }
868
+ #emptyState .es-title {
869
+ font-family: var(--mono); font-size: 12px;
870
+ letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-dim);
871
+ }
872
+ #emptyState .es-hint { color: var(--text-dim); font-size: 13px; line-height: 1.5; max-width: 44ch; }
875
873
  .keys {
876
874
  display: flex;
877
875
  gap: 6px;
@@ -978,8 +976,78 @@
978
976
  @media (max-width: 900px) {
979
977
  #machine, #diffpanel { position: fixed; inset: 0; width: auto; max-width: none; z-index: 10; border-left: none; }
980
978
  }
981
- #secretsOverlay, #profilesOverlay, #telegramOverlay { position: fixed; inset: 0; background: rgba(0,0,0,.45); display: flex; align-items: center; justify-content: center; z-index: 50; }
982
- #secretsPanel, .sec-panel { background: var(--panel, #1b1d22); color: var(--fg, #e8e8ea); width: min(520px, 92vw); max-height: 80vh; overflow: auto; border-radius: 10px; padding: 16px 18px; box-shadow: 0 10px 40px rgba(0,0,0,.5); }
979
+ /* ── Popins ───────────────────────────────────────────────
980
+ UNE CLASSE, pas une liste d'ids. La règle nommait
981
+ #secretsOverlay/#profilesOverlay/#telegramOverlay ; le panneau des crons,
982
+ ajouté après, n'y figurait pas — il s'affichait donc dans le flux de la page
983
+ au lieu de se centrer. Toute popin hérite maintenant du comportement en
984
+ portant `.overlay`, il n'y a plus de liste à ne pas oublier. */
985
+ .overlay {
986
+ position: fixed; inset: 0; z-index: 50;
987
+ display: flex; align-items: center; justify-content: center;
988
+ padding: 24px;
989
+ background: rgba(8, 9, 13, 0.62);
990
+ backdrop-filter: blur(2px);
991
+ }
992
+ .overlay[hidden] { display: none; }
993
+ /* Le panneau était hors palette : #1b1d22 au lieu de --bg-raised, #e8e8ea au
994
+ lieu de --text, des bordures rgba(255,255,255,.15) et un rayon de 10px quand
995
+ tout le cockpit est à 6 — soit un gris et un blanc qui n'existaient nulle
996
+ part ailleurs. Le filet ambre en haut reprend la grammaire des instruments
997
+ (bord de l'onglet actif, ⌨️ allumé) : ce panneau est au premier plan. */
998
+ #secretsPanel, .sec-panel {
999
+ background: var(--bg-raised); color: var(--text);
1000
+ width: min(560px, 100%); max-height: min(80vh, 760px); overflow: auto;
1001
+ border: 1px solid var(--line); border-top: 1px solid var(--amber);
1002
+ border-radius: 6px; padding: 14px 16px;
1003
+ box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
1004
+ }
1005
+ /* Squelette entête / corps / pied. Seul le CORPS défile, donc l'action
1006
+ principale reste atteignable : le formulaire de création dépasse l'écran sur
1007
+ un portable, et son bouton « Start » se trouvait en bas d'une page à
1008
+ dérouler. Réservé aux popins qui ont un pied — les autres gardent leur
1009
+ rembourrage simple. */
1010
+ .sec-panel.modal { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
1011
+ .modal > .sec-head {
1012
+ flex: 0 0 auto; margin: 0; padding: 11px 16px;
1013
+ border-bottom: 1px solid var(--line);
1014
+ }
1015
+ /* Même traitement que les libellés du cockpit (.label, barre du canal) : la
1016
+ popin appartient visiblement à la même famille. */
1017
+ .modal > .sec-head strong {
1018
+ font-family: var(--mono); font-size: 11px; font-weight: normal;
1019
+ letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-dim);
1020
+ }
1021
+ .modal > .sec-head strong .obj { color: var(--amber); }
1022
+ .modal-body { flex: 1 1 auto; overflow: auto; padding: 14px 16px; }
1023
+ .modal-foot {
1024
+ flex: 0 0 auto; display: flex; align-items: center; justify-content: flex-end;
1025
+ gap: 8px; padding: 11px 16px;
1026
+ border-top: 1px solid var(--line); background: rgba(0, 0, 0, 0.18);
1027
+ }
1028
+ /* Pousse ce qui suit à droite : un pied peut porter une note à gauche. */
1029
+ .modal-foot .foot-note { margin-right: auto; font-size: 11px; color: var(--text-dim); }
1030
+ .sec-panel :focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }
1031
+ /* Les <select> et les champs numériques n'étaient couverts par aucune règle :
1032
+ ils tombaient sur le rendu par défaut du navigateur — clair — au milieu d'un
1033
+ panneau sombre. */
1034
+ .sec-panel select, .sec-panel input[type=number], .sec-panel input[type=time] {
1035
+ padding: 6px 8px; border-radius: 6px; border: 1px solid var(--line);
1036
+ background: rgba(0, 0, 0, 0.25); color: inherit; font-family: inherit; font-size: inherit;
1037
+ }
1038
+ /* Une précision dans un libellé ne SE CRIE PAS : `.label` met en majuscules et
1039
+ espace les lettres, ce qui rendait une phrase entière illisible. */
1040
+ .label .check-hint { text-transform: none; letter-spacing: normal; font-weight: normal; }
1041
+ #cronList { list-style: none; padding: 0; margin: 0 0 14px; }
1042
+ #cronList li { padding: 6px 8px; border-radius: 6px; }
1043
+ #cronList li:nth-child(odd) { background: rgba(255, 255, 255, 0.04); }
1044
+ .cron-form { border-top: 1px solid var(--line); padding-top: 12px; }
1045
+ .cron-form .field { margin: 12px 0; }
1046
+ .cron-form .field:last-child { margin-bottom: 0; }
1047
+ /* `.field` est en colonne : sans redresser la direction ici, les trois champs
1048
+ s'empilaient — et `align-items: center` les centrait au lieu de les aligner. */
1049
+ .cron-when { flex-direction: row; align-items: center; gap: 8px; flex-wrap: wrap; }
1050
+ .cron-when input[type=number] { width: 76px; }
983
1051
  .sec-panel .field { margin: 8px 0; }
984
1052
  .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
1053
  #profilesList { list-style: none; padding: 0; margin: 0 0 8px; }
@@ -1059,7 +1127,7 @@
1059
1127
  <div class="vm-hint" id="vmModeHint"></div>
1060
1128
  </div>
1061
1129
 
1062
- <div id="secretsOverlay" hidden>
1130
+ <div id="secretsOverlay" class="overlay" hidden>
1063
1131
  <div id="secretsPanel">
1064
1132
  <div class="sec-head">
1065
1133
  <strong>Secret vault</strong>
@@ -1075,36 +1143,106 @@
1075
1143
  </div>
1076
1144
  </div>
1077
1145
 
1078
- <div id="cronOverlay" hidden>
1079
- <div id="secretsPanel">
1146
+ <div id="cronOverlay" class="overlay" hidden>
1147
+ <!-- Le panneau portait id="secretsPanel" — un id en DOUBLON de la vraie popin
1148
+ des secrets, donc $("secretsPanel") pouvait tomber sur le mauvais. -->
1149
+ <div id="cronPanel" class="sec-panel modal">
1080
1150
  <div class="sec-head">
1081
- <strong>⏰ Schedule — <span id="cronChanName">this agent</span></strong>
1151
+ <strong>⏰ Schedule — <span class="obj" id="cronChanName">this agent</span></strong>
1082
1152
  <button id="cronClose" title="Close">✕</button>
1083
1153
  </div>
1084
- <p class="sec-note">Scheduled prompts sent to <b>this agent</b> on a timer (monitoring, reporting…). The server fires them even when nobody's watching; results land here and in Telegram. Times are server-local. <b>Tip:</b> just ask the agent (« surveille X chaque matin et alerte si Y ») — it writes the check and schedules it. Or set one manually:</p>
1085
- <ul id="cronList"></ul>
1086
- <form id="cronAdd">
1087
- <textarea id="cronPrompt" rows="2" placeholder="Prompt to run when there's news, e.g. « Rédige une alerte à partir de l'anomalie détectée »" style="width:100%;box-sizing:border-box"></textarea>
1088
- <input id="cronCheck" placeholder="Check command (optional) — silent = nothing to report → 0 tokens" autocomplete="off" spellcheck="false" style="width:100%;box-sizing:border-box;margin-top:6px" />
1089
- <div class="field" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:6px">
1090
- <select id="cronKind">
1091
- <option value="interval">Every…</option>
1092
- <option value="daily">Daily at…</option>
1093
- </select>
1094
- <span id="cronIntervalFields">
1095
- <input id="cronEvery" type="number" min="1" value="60" style="width:70px">
1096
- <select id="cronUnit"><option value="1">minutes</option><option value="60">hours</option></select>
1097
- </span>
1098
- <span id="cronDailyFields" hidden>
1099
- <input id="cronTime" type="time" value="09:00">
1100
- </span>
1101
- <button type="submit" class="primary">Add schedule</button>
1102
- </div>
1103
- </form>
1154
+ <div class="modal-body">
1155
+ <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>
1156
+ <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>
1157
+ <ul id="cronList"></ul>
1158
+ <form id="cronAdd" class="cron-form">
1159
+ <div class="field">
1160
+ <span class="label">Prompt to run</span>
1161
+ <textarea id="cronPrompt" rows="2" placeholder="e.g. « Rédige une alerte à partir de l'anomalie détectée »"></textarea>
1162
+ </div>
1163
+ <div class="field">
1164
+ <span class="label">Check command <span class="check-hint">— optional; prints nothing = nothing to report, and the agent stays asleep (0 tokens)</span></span>
1165
+ <input id="cronCheck" placeholder="e.g. ~/scripts/check-budget.sh" autocomplete="off" spellcheck="false" />
1166
+ </div>
1167
+ <div class="field cron-when">
1168
+ <select id="cronKind" aria-label="How often">
1169
+ <option value="interval">Every…</option>
1170
+ <option value="daily">Daily at…</option>
1171
+ </select>
1172
+ <span id="cronIntervalFields">
1173
+ <input id="cronEvery" type="number" min="1" value="60" aria-label="Interval">
1174
+ <select id="cronUnit" aria-label="Unit"><option value="1">minutes</option><option value="60">hours</option></select>
1175
+ </span>
1176
+ <span id="cronDailyFields" hidden>
1177
+ <input id="cronTime" type="time" value="09:00" aria-label="Time of day">
1178
+ </span>
1179
+ </div>
1180
+ </form>
1181
+ </div>
1182
+ <div class="modal-foot">
1183
+ <span class="foot-note" id="cronTzNote"></span>
1184
+ <button type="submit" form="cronAdd" class="primary">Add schedule</button>
1185
+ </div>
1186
+ </div>
1187
+ </div>
1188
+
1189
+ <div id="setupOverlay" class="overlay" hidden>
1190
+ <div id="setupPanel" class="sec-panel modal">
1191
+ <div class="sec-head">
1192
+ <strong>New <span class="obj">agent</span></strong>
1193
+ <button id="setupClose" title="Close">✕</button>
1194
+ </div>
1195
+ <div class="modal-body">
1196
+ <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>
1197
+ <div class="field" id="profileField">
1198
+ <div class="field-head">
1199
+ <span class="label">Which agent?</span>
1200
+ <button type="button" class="linkish" id="editProfilesLink" title="Create or edit agent profiles">✎ edit profiles</button>
1201
+ </div>
1202
+ <div id="profileGrid" role="radiogroup" aria-label="Agent profile"></div>
1203
+ <span class="check-hint" id="profileNaNote" hidden>Applies to new sessions only.</span>
1204
+ </div>
1205
+ <div class="field">
1206
+ <span class="label">Working directory</span>
1207
+ <input type="text" id="cwdInput" placeholder="/path/to/project">
1208
+ </div>
1209
+ <label class="check" id="worktreeField">
1210
+ <input type="checkbox" id="worktreeInput" checked>
1211
+ <span>Isolate in a git worktree <span class="check-hint">— the agent edits a fresh branch; review &amp; merge from the Diff panel. Auto-removed on close if unused, kept if it has changes. (new sessions only)</span></span>
1212
+ </label>
1213
+ <details class="fold" id="advancedField">
1214
+ <summary>Advanced — resume an existing session</summary>
1215
+ <div class="field">
1216
+ <div class="radio-row">
1217
+ <label><input type="radio" name="mode" value="new" checked> new session</label>
1218
+ <label><input type="radio" name="mode" value="continue"> latest in directory</label>
1219
+ <label><input type="radio" name="mode" value="resume"> by id</label>
1220
+ </div>
1221
+ </div>
1222
+ <div class="field" id="resumeField" hidden>
1223
+ <span class="label">Session id</span>
1224
+ <input type="text" id="resumeInput" placeholder="5fe046dd-…">
1225
+ <span class="label">Sessions in this directory</span>
1226
+ <div class="session-list" id="sessionList"></div>
1227
+ </div>
1228
+ </details>
1229
+ <details class="fold" id="recoverField" hidden>
1230
+ <summary>Reopen a past session <span class="fold-count" id="recoverCount"></span></summary>
1231
+ <div class="recover-list" id="recoverList"></div>
1232
+ </details>
1233
+ <details class="fold" id="liveField" hidden>
1234
+ <summary>Agents running now <span class="fold-count" id="liveCount"></span></summary>
1235
+ <div class="recover-list" id="liveList"></div>
1236
+ </details>
1237
+ </div>
1238
+ <div class="modal-foot">
1239
+ <button id="setupCancel">Cancel</button>
1240
+ <button class="primary" id="startBtn">Start agent</button>
1241
+ </div>
1104
1242
  </div>
1105
1243
  </div>
1106
1244
 
1107
- <div id="profilesOverlay" hidden>
1245
+ <div id="profilesOverlay" class="overlay" hidden>
1108
1246
  <div id="profilesPanel" class="sec-panel">
1109
1247
  <div class="sec-head">
1110
1248
  <strong>Agent profiles</strong>
@@ -1127,7 +1265,7 @@
1127
1265
  </div>
1128
1266
  </div>
1129
1267
 
1130
- <div id="telegramOverlay" hidden>
1268
+ <div id="telegramOverlay" class="overlay" hidden>
1131
1269
  <div id="telegramPanel" class="sec-panel">
1132
1270
  <div class="sec-head">
1133
1271
  <strong>Telegram bot</strong>
@@ -1179,52 +1317,13 @@
1179
1317
  </span>
1180
1318
  </div>
1181
1319
 
1182
- <div id="setup" hidden>
1183
- <h1>New agent</h1>
1184
- <p class="hint">Starts a real Claude Code session, driven inside a
1185
- pseudo-terminal. Each agent is an independent session — open as many
1186
- agents as you want Claudes working in parallel.</p>
1187
- <div class="field" id="profileField">
1188
- <div class="field-head">
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 &amp; 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>
1320
+ <!-- Aucun agent : une invitation, pas un formulaire pré-rempli. La création
1321
+ se fait dans une popin (#setupOverlay) et ne crée le canal qu'à la
1322
+ validation — ouvrir puis renoncer ne laisse plus d'onglet mort-né. -->
1323
+ <div id="emptyState">
1324
+ <span class="es-title">No agent open</span>
1325
+ <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>
1326
+ <button class="primary" id="emptyNewBtn">+ new agent</button>
1228
1327
  </div>
1229
1328
 
1230
1329
  <div id="transcripts"></div>
@@ -1299,7 +1398,7 @@
1299
1398
  const $ = (id) => document.getElementById(id);
1300
1399
  const tabbar = $("tabbar"), newTabBtn = $("newTab"), newGroupBtn = $("newGroup");
1301
1400
  const ungroupedEl = $("ungrouped"), groupsEl = $("groups");
1302
- const setup = $("setup"), transcripts = $("transcripts"), thinking = $("thinking");
1401
+ const transcripts = $("transcripts"), thinking = $("thinking");
1303
1402
  const composer = $("composer"), promptInput = $("promptInput"), sendBtn = $("sendBtn");
1304
1403
  const machine = $("machine"), screenEl = $("screen");
1305
1404
 
@@ -1315,7 +1414,7 @@
1315
1414
  .then((r) => r.json())
1316
1415
  .then((d) => {
1317
1416
  serverCwd = d.cwd || "";
1318
- if (active && active.status === "setup" && !$("cwdInput").value) refreshChrome();
1417
+ if (!$("setupOverlay").hidden && !$("cwdInput").value) $("cwdInput").value = serverCwd || "";
1319
1418
  })
1320
1419
  .catch(() => {});
1321
1420
 
@@ -1915,7 +2014,8 @@
1915
2014
  refreshTitle();
1916
2015
  if (active === tab) {
1917
2016
  if (tabs.length) activate(tabs[tabs.length - 1]);
1918
- else createTab();
2017
+ // Plus aucun agent : l'invitation, pas un onglet « setup » fantôme.
2018
+ else { active = null; refreshChrome(); }
1919
2019
  }
1920
2020
  }
1921
2021
 
@@ -2126,9 +2226,38 @@
2126
2226
  refreshBadge();
2127
2227
  }
2128
2228
 
2129
- /** Keeps header, setup, composer and engine room in sync with the active channel. */
2229
+ /**
2230
+ * Keeps header, composer, channel bar and engine room in sync with the active
2231
+ * channel — ou avec l'ABSENCE de canal.
2232
+ *
2233
+ * `active` peut désormais être nul : la création vit dans une popin et ne crée
2234
+ * plus d'onglet « setup » d'avance, donc fermer le dernier agent ne laisse plus
2235
+ * un formulaire à la place. Tout ce qui décrit un canal retombe alors sur « — »
2236
+ * et le panneau central montre l'invitation.
2237
+ */
2130
2238
  function refreshChrome() {
2131
2239
  const t = active;
2240
+ if (!t) {
2241
+ $("led").className = "led ";
2242
+ $("state").textContent = "no agent";
2243
+ $("cwdView").textContent = "—";
2244
+ $("sessionView").textContent = "—";
2245
+ $("sessionView").title = "";
2246
+ $("branchGauge").hidden = true;
2247
+ composer.hidden = true;
2248
+ $("chanBar").hidden = true;
2249
+ thinking.classList.remove("visible");
2250
+ screenEl.textContent = "";
2251
+ // Programmer ou arrêter n'a pas de cible : mieux vaut des boutons éteints
2252
+ // qu'une popin qui parle d'un agent inexistant.
2253
+ $("cronBtn").disabled = true;
2254
+ $("stopBtn").disabled = true;
2255
+ $("emptyState").classList.add("visible");
2256
+ return;
2257
+ }
2258
+ $("emptyState").classList.remove("visible");
2259
+ $("cronBtn").disabled = !t.sessionId;
2260
+ $("stopBtn").disabled = false;
2132
2261
  $("led").className = "led " +
2133
2262
  ({ setup: "", connecting: "busy", ready: "on", busy: "busy", dead: "off" }[t.status]);
2134
2263
  $("state").textContent = t.stateText;
@@ -2139,33 +2268,14 @@
2139
2268
  $("sessionView").title = t.sessionId || "";
2140
2269
  $("branchGauge").hidden = !t.branch;
2141
2270
  $("branchView").textContent = t.branch || "—";
2142
- setup.hidden = t.status !== "setup";
2143
- composer.hidden = t.status === "setup";
2144
- // La barre du canal accompagne les deux vues, mais pas le formulaire « New
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";
2271
+ composer.hidden = false;
2272
+ // La barre du canal accompagne les deux vues du canal.
2273
+ $("chanBar").hidden = false;
2149
2274
  $("chanBarName").textContent = t.nameEl ? t.nameEl.textContent : "";
2150
2275
  $("chanBarName").title = t.cwd || "";
2151
2276
  sendBtn.disabled = t.status !== "ready" || !!t.pendingChoices;
2152
2277
  thinking.classList.toggle("visible", t.status === "busy");
2153
2278
  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
2279
  }
2170
2280
 
2171
2281
  /** Paints a channel's context-window usage bar + tiny number in its tab. */
@@ -2778,9 +2888,33 @@
2778
2888
  }
2779
2889
  }
2780
2890
 
2781
- function startActiveTab() {
2782
- const t = active;
2783
- if (t.status !== "setup") return;
2891
+ /**
2892
+ * Ouvre la popin de création. Elle ne crée RIEN : l'onglet naît au « Start
2893
+ * agent » (cf. startNewAgent). Ouvrir puis renoncer ne laissait auparavant pas
2894
+ * seulement un onglet mort-né — il devenait le canal actif, avec son formulaire
2895
+ * à la place de la conversation qu'on regardait.
2896
+ */
2897
+ function openSetup() {
2898
+ // Ne jamais écraser ce qui est déjà tapé (la popin peut se rouvrir).
2899
+ if (!$("cwdInput").value) {
2900
+ $("cwdInput").value = (active && active.cwd) || localStorage.getItem("cp.cwd") || serverCwd || "";
2901
+ }
2902
+ const last = localStorage.getItem("cp.session");
2903
+ if (last && !$("resumeInput").value) $("resumeInput").value = last;
2904
+ $("startBtn").disabled = false;
2905
+ // Le profil retenu n'est rappelé qu'à l'OUVERTURE : rappelé en continu, il
2906
+ // écraserait le choix en cours de saisie.
2907
+ profileTouched = false;
2908
+ applyRememberedProfile();
2909
+ refreshRecoverList();
2910
+ refreshLiveList();
2911
+ $("setupOverlay").hidden = false;
2912
+ $("cwdInput").focus();
2913
+ }
2914
+ function closeSetup() { $("setupOverlay").hidden = true; }
2915
+
2916
+ /** Valide le formulaire → C'EST ICI que le canal est créé, et pas avant. */
2917
+ function startNewAgent() {
2784
2918
  const cwd = $("cwdInput").value.trim();
2785
2919
  const mode = document.querySelector('input[name="mode"]:checked').value;
2786
2920
  const msg = { cwd };
@@ -2797,7 +2931,9 @@
2797
2931
  if (cwd) localStorage.setItem("cp.profile:" + cwd, selectedProfile);
2798
2932
  }
2799
2933
  $("startBtn").disabled = true;
2800
- launchTab(t, msg);
2934
+ closeSetup();
2935
+ // createTab active l'onglet créé ; launchTab ouvre le lien.
2936
+ launchTab(createTab(), msg);
2801
2937
  }
2802
2938
 
2803
2939
  // ── Version display + auto-reload ─────────────────────────────────────────
@@ -3189,13 +3325,17 @@
3189
3325
  }
3190
3326
 
3191
3327
  // ── Controls ────────────────────────────────────────────
3192
- $("startBtn").addEventListener("click", startActiveTab);
3328
+ $("startBtn").addEventListener("click", startNewAgent);
3329
+ $("setupClose").addEventListener("click", closeSetup);
3330
+ $("setupCancel").addEventListener("click", closeSetup);
3331
+ $("setupOverlay").addEventListener("click", (e) => { if (e.target === $("setupOverlay")) closeSetup(); });
3332
+ $("emptyNewBtn").addEventListener("click", openSetup);
3193
3333
  document.querySelectorAll('input[name="mode"]').forEach((r) =>
3194
3334
  r.addEventListener("change", () => {
3195
3335
  const mode = document.querySelector('input[name="mode"]:checked').value;
3196
3336
  const isNew = mode === "new";
3197
3337
  $("resumeField").hidden = mode !== "resume";
3198
- // Profil et worktree ne valent que pour une session neuve : startActiveTab
3338
+ // Profil et worktree ne valent que pour une session neuve : startNewAgent
3199
3339
  // les ignore déjà en resume/continue, l'UI cesse simplement de le cacher.
3200
3340
  $("profileField").classList.toggle("na", !isNew);
3201
3341
  $("profileNaNote").hidden = isNew;
@@ -3213,7 +3353,7 @@
3213
3353
  // Agents can appear while the setup screen sits open (pilotctl spawns one,
3214
3354
  // another tab starts one) — keep the list current without a reload.
3215
3355
  setInterval(() => {
3216
- if (active && active.status === "setup") refreshLiveList();
3356
+ if (!$("setupOverlay").hidden) refreshLiveList();
3217
3357
  }, 5000);
3218
3358
 
3219
3359
  function submitPrompt(t, text, force) {
@@ -3323,6 +3463,7 @@
3323
3463
  }
3324
3464
  promptInput.addEventListener("input", () => {
3325
3465
  autosizePrompt();
3466
+ if (!active) return; // aucun agent : le composer est masqué, mais pas de crash gratuit
3326
3467
  active.draft = promptInput.value;
3327
3468
  persistDraft(active);
3328
3469
  });
@@ -3452,7 +3593,7 @@
3452
3593
  $("refreshDiff").addEventListener("click", loadDiff);
3453
3594
  document.querySelectorAll(".keys button[data-key]").forEach((b) =>
3454
3595
  b.addEventListener("click", () => {
3455
- if (active.ws) active.ws.send(JSON.stringify({ type: "key", key: b.dataset.key }));
3596
+ if (active && active.ws) active.ws.send(JSON.stringify({ type: "key", key: b.dataset.key }));
3456
3597
  })
3457
3598
  );
3458
3599
  // La touche Échap envoie esc à la session active (comme le bouton Esc de
@@ -3469,6 +3610,9 @@
3469
3610
  // 1. Overlays / panneaux ouverts → fermer, pas d'esc.
3470
3611
  if (!$("secretsOverlay").hidden) { $("secretsOverlay").hidden = true; e.preventDefault(); return; }
3471
3612
  if (!$("profilesOverlay").hidden) { closeProfilesPanel(); e.preventDefault(); return; }
3613
+ if (!$("setupOverlay").hidden) { closeSetup(); e.preventDefault(); return; }
3614
+ if (!$("cronOverlay").hidden) { $("cronOverlay").hidden = true; e.preventDefault(); return; }
3615
+ if (!$("telegramOverlay").hidden) { $("telegramOverlay").hidden = true; e.preventDefault(); return; }
3472
3616
  if ($("diffpanel").classList.contains("open")) { $("diffpanel").classList.remove("open"); e.preventDefault(); return; }
3473
3617
  // 2. Dialogue clickable en attente de réponse → ne rien faire.
3474
3618
  if (active && active.dialogBubble) return;
@@ -3488,11 +3632,11 @@
3488
3632
  }
3489
3633
  });
3490
3634
  $("settleBtn").addEventListener("click", () => {
3491
- if (active.ws && active.status === "ready")
3635
+ if (active && active.ws && active.status === "ready")
3492
3636
  active.ws.send(JSON.stringify({ type: "settle" }));
3493
3637
  });
3494
3638
  $("stopBtn").addEventListener("click", () => {
3495
- if (active.ws) active.ws.send(JSON.stringify({ type: "stop" }));
3639
+ if (active && active.ws) active.ws.send(JSON.stringify({ type: "stop" }));
3496
3640
  });
3497
3641
 
3498
3642
  // ── Secret vault panel (central, global; profiles pick which to inject) ──
@@ -3577,8 +3721,23 @@
3577
3721
  if (s.kind === "interval") return s.everyMin % 60 === 0 ? "every " + (s.everyMin / 60) + "h" : "every " + s.everyMin + "m";
3578
3722
  return "daily at " + String(s.hour).padStart(2, "0") + ":" + String(s.minute).padStart(2, "0");
3579
3723
  }
3724
+ /**
3725
+ * Fuseau réellement appliqué aux horaires quotidiens, dans le pied de la popin.
3726
+ * Le panneau annonçait « Times are server-local », ce qui ne dit pas 9h OÙ —
3727
+ * or le serveur peut tourner dans un autre fuseau que le navigateur.
3728
+ */
3729
+ async function showCronTz() {
3730
+ const el = $("cronTzNote");
3731
+ el.textContent = "";
3732
+ try {
3733
+ const tz = await (await fetch("/timezone")).json();
3734
+ const zone = tz.timezone || tz.system;
3735
+ if (zone) el.textContent = "Daily times run in " + zone;
3736
+ } catch { /* la popin marche très bien sans cette note */ }
3737
+ }
3580
3738
  async function renderCrons() {
3581
3739
  const ul = $("cronList"); ul.innerHTML = "";
3740
+ showCronTz();
3582
3741
  $("cronChanName").textContent = (active && (active.name || (active.sessionId || "").slice(0, 8))) || "no agent";
3583
3742
  if (!active || !active.sessionId) { ul.innerHTML = '<li><span style="opacity:.6">Open an agent to schedule prompts.</span></li>'; return; }
3584
3743
  let all = [];
@@ -3937,7 +4096,7 @@
3937
4096
  el.value = "";
3938
4097
  });
3939
4098
 
3940
- newTabBtn.addEventListener("click", () => createTab());
4099
+ newTabBtn.addEventListener("click", openSetup);
3941
4100
  newGroupBtn.addEventListener("click", () => {
3942
4101
  createGroup();
3943
4102
  persistGroups();
@@ -4086,7 +4245,9 @@
4086
4245
  if (i !== targetIdx) launchTab(pending[i].t, pending[i].msg);
4087
4246
  }
4088
4247
  } else {
4089
- createTab();
4248
+ // Rien à restaurer : l'invitation. Le formulaire ne s'ouvre pas de force —
4249
+ // on peut aussi arriver là pour regarder, ou lancer depuis Telegram.
4250
+ refreshChrome();
4090
4251
  // Nothing to restore: enable persistence for user actions, but do NOT
4091
4252
  // push a blank list (a failed /channels fetch must never wipe the store).
4092
4253
  restoring = false;