glad-web 1.0.18 → 1.0.20

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.
@@ -26,6 +26,11 @@
26
26
  .session-info { flex: 1; min-width: 0; }
27
27
  .session-info h3 { margin: 0 0 4px 0; font-size: 17px; display: flex; align-items: center; gap: 8px; }
28
28
  .session-info p { margin: 0; font-size: 13px; color: var(--text-dim); }
29
+ .timer-count-badge { display: inline-flex; align-items: center; gap: 3px; color: #fff; background: rgba(0,122,255,0.22); border: 1px solid rgba(0,122,255,0.34); border-radius: 999px; padding: 2px 7px; font-size: 11px; font-weight: 800; flex-shrink: 0; }
30
+ .session-dir-row { display: flex; align-items: center; gap: 6px; margin-top: 2px; }
31
+ .session-dir-row p { flex: 1; min-width: 0; font-family: monospace; overflow-wrap: anywhere; }
32
+ .copy-dir-btn { color: var(--text-dim); background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; width: 28px; height: 28px; padding: 0; display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; }
33
+ .copy-dir-btn:active { color: #fff; background: rgba(255,255,255,0.12); }
29
34
  .session-actions { display: flex; gap: 12px; align-items: center; margin-left: 10px; }
30
35
  .btn-join { background: rgba(255,255,255,0.1); border: none; color: var(--primary); padding: 8px 14px; border-radius: 18px; font-weight: 600; font-size: 14px; cursor: pointer; }
31
36
  .icon-btn { color: var(--text-dim); background: none; border: none; padding: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
@@ -47,7 +52,19 @@
47
52
  #cmd-input { flex: 1; min-height: 38px; max-height: 150px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.1); border-radius: 19px; color: #fff; padding: 9px 16px; font-size: 16px; outline: none; resize: none; overflow-y: auto; line-height: 20px; box-sizing: border-box; transition: background 0.18s ease, border-color 0.18s ease; }
48
53
  #cmd-input::placeholder { color: rgba(255,255,255,0.45); }
49
54
  #cmd-input:focus { background: rgba(255,255,255,0.1); border-color: rgba(0,122,255,0.42); color: #fff; }
55
+ #timer-btn { width: 38px; height: 38px; margin-left: 8px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.1); border-radius: 19px; color: #d1d5db; display: flex; align-items: center; justify-content: center; flex-shrink: 0; cursor: pointer; }
56
+ #timer-btn.active { color: #fff; border-color: rgba(0,122,255,0.45); background: rgba(0,122,255,0.22); }
50
57
  #send-btn { width: 44px; height: 38px; margin-left: 10px; background: #007aff; border: none; border-radius: 19px; color: #fff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; cursor: pointer; }
58
+ #timed-send-panel { display: none; margin: 0 14px 10px 14px; padding: 12px; border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; background: rgba(28,28,30,0.98); box-sizing: border-box; }
59
+ #timed-send-panel.active { display: block; }
60
+ .timed-row { display: grid; grid-template-columns: 1fr 1fr auto; gap: 8px; align-items: center; }
61
+ .timed-row select { min-width: 0; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.1); color: #fff; border-radius: 8px; padding: 8px 9px; font-size: 13px; outline: none; box-sizing: border-box; }
62
+ .timed-preview { margin-top: 8px; color: #d1d5db; font-size: 12px; line-height: 1.4; }
63
+ .timed-editor-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; }
64
+ #timed-tag-rail { position: absolute; left: 4px; top: 58px; display: flex; flex-direction: column; gap: 6px; z-index: 2400; pointer-events: none; }
65
+ .timed-tag { width: 54px; min-height: 34px; border: 1px solid rgba(255,255,255,0.14); border-left: 0; border-radius: 0 11px 11px 0; background: rgba(0,122,255,0.24); color: #fff; backdrop-filter: blur(12px); font-size: 12px; font-weight: 800; letter-spacing: 0; cursor: pointer; display: flex; align-items: center; justify-content: center; text-align: center; line-height: 1.08; white-space: pre-line; box-shadow: 0 8px 20px rgba(0,0,0,0.25); pointer-events: auto; }
66
+ .timed-tag.active { background: rgba(0,122,255,0.42); border-color: rgba(255,255,255,0.28); }
67
+ .timed-tag:active { transform: scale(0.96); }
51
68
  #shortcut-rail { display: flex; gap: 28px; overflow-x: auto; overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch; scrollbar-width: none; scroll-snap-type: x mandatory; padding: 0 14px 12px 14px; box-sizing: border-box; scroll-padding-left: 14px; scroll-padding-right: 14px; }
52
69
  #shortcut-rail::-webkit-scrollbar { display: none; }
53
70
  .shortcut-group { flex: 0 0 calc(100vw - 28px); display: grid; gap: 8px; scroll-snap-align: start; }
@@ -60,6 +77,90 @@
60
77
  .key-btn.active { background: rgba(0, 122, 255, 0.25); border-color: #007aff; color: #00a2ff; }
61
78
  #terminal-container { flex: 1; min-height: 0; width: 100%; background: #000; position: relative; overflow: hidden; overflow-anchor: none; }
62
79
  #terminal { height: 100%; width: 100%; }
80
+ #claude-chat-container { display: none; flex: 1; min-height: 0; overflow-y: auto; background: #050505; padding: 12px 12px 24px 12px; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
81
+ .claude-message { max-width: 92%; margin: 0 0 10px 0; padding: 10px 12px; border-radius: 8px; overflow-wrap: anywhere; line-height: 1.45; font-size: 14px; }
82
+ .claude-message.user { margin-left: auto; background: rgba(0,122,255,0.24); border: 1px solid rgba(0,122,255,0.32); color: #fff; border-radius: 12px; padding-top: 7px; padding-bottom: 7px; }
83
+ .claude-message.assistant { max-width: 100%; background: transparent; border: 0; color: #f5f5f7; padding: 0 4px; margin: 0 0 12px 0; }
84
+ .claude-message.event { max-width: 100%; background: transparent; border: 0; color: var(--text-dim); font-size: 12px; padding: 3px 2px; text-align: center; }
85
+ .claude-message.error { color: #ff6b61; }
86
+ .claude-md { white-space: normal; }
87
+ .claude-md > :first-child { margin-top: 0; }
88
+ .claude-md > :last-child { margin-bottom: 0; }
89
+ .claude-md p { margin: 0 0 8px 0; }
90
+ .claude-md h1, .claude-md h2, .claude-md h3, .claude-md h4 { margin: 12px 0 7px 0; line-height: 1.22; font-weight: 800; letter-spacing: 0; }
91
+ .claude-md h1 { font-size: 20px; }
92
+ .claude-md h2 { font-size: 18px; }
93
+ .claude-md h3 { font-size: 16px; }
94
+ .claude-md h4 { font-size: 15px; }
95
+ .claude-md ul, .claude-md ol { margin: 0 0 9px 0; padding-left: 20px; }
96
+ .claude-md li { margin: 4px 0; }
97
+ .claude-md code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.92em; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; padding: 1px 4px; }
98
+ .claude-md pre { margin: 8px 0 10px 0; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.34); border-radius: 8px; overflow: auto; }
99
+ .claude-md pre code { display: block; padding: 11px 12px; background: transparent; border: 0; border-radius: 0; white-space: pre; line-height: 1.45; }
100
+ .claude-md blockquote { margin: 8px 0; padding: 2px 0 2px 10px; border-left: 3px solid rgba(255,255,255,0.22); color: #c7c7cc; }
101
+ .claude-md table { width: 100%; border-collapse: collapse; margin: 8px 0 10px 0; font-size: 13px; display: block; overflow-x: auto; }
102
+ .claude-md th, .claude-md td { border: 1px solid rgba(255,255,255,0.12); padding: 6px 8px; text-align: left; vertical-align: top; }
103
+ .claude-md th { background: rgba(255,255,255,0.07); color: #fff; }
104
+ .claude-md a { color: #64b5ff; text-decoration: none; }
105
+ .claude-md img { max-width: 100%; border-radius: 8px; margin: 6px 0; }
106
+ .claude-md hr { border: 0; border-top: 1px solid rgba(255,255,255,0.12); margin: 12px 0; }
107
+ .claude-command-chip { display: inline-flex; align-items: center; max-width: 100%; border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.08); color: #f5f5f7; border-radius: 999px; padding: 4px 9px; font-size: 12px; font-weight: 800; }
108
+ .claude-tool { max-width: 100%; margin: 0 0 10px 0; border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.045); border-radius: 8px; color: #d1d5db; font-size: 13px; overflow: hidden; overflow-wrap: anywhere; }
109
+ .claude-tool strong { color: #fff; }
110
+ .claude-tool-card { display: block; }
111
+ .claude-tool-card summary { list-style: none; cursor: pointer; }
112
+ .claude-tool-card summary::-webkit-details-marker { display: none; }
113
+ .claude-tool-header { display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 7px 9px; color: #f5f5f7; }
114
+ .claude-tool-icon { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; color: var(--text-dim); flex: 0 0 auto; }
115
+ .claude-tool-icon::before { content: attr(data-icon); font-size: 11px; font-weight: 800; line-height: 1; }
116
+ .claude-tool-title { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; }
117
+ .claude-tool-status { color: var(--text-dim); font-size: 12px; white-space: nowrap; }
118
+ .claude-tool-command { flex: 1; min-width: 0; color: var(--text-dim); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
119
+ .claude-tool-body { padding: 0 10px 10px 10px; }
120
+ .claude-tool-section { margin-top: 8px; }
121
+ .claude-tool-section-title { color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 5px; }
122
+ .claude-tool-code { margin: 0; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.32); border-radius: 7px; padding: 9px 10px; color: #d1d5db; white-space: pre-wrap; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.42; overflow-x: auto; }
123
+ .claude-tool.error { border-color: rgba(255,107,97,0.38); background: rgba(255,59,48,0.07); }
124
+ .claude-work-group { max-width: 100%; margin: 0 0 10px 0; border: 1px solid rgba(255,255,255,0.09); background: rgba(255,255,255,0.035); border-radius: 8px; overflow: hidden; }
125
+ .claude-work-group summary { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 7px 10px; cursor: pointer; color: #d1d5db; list-style: none; }
126
+ .claude-work-group summary::-webkit-details-marker { display: none; }
127
+ .claude-work-group-title { flex: 1; min-width: 0; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
128
+ .claude-work-group-body { padding: 2px 8px 8px 8px; }
129
+ .claude-permission { border-color: rgba(255,204,0,0.4); background: rgba(255,204,0,0.08); }
130
+ .claude-permission-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; }
131
+ .claude-status { color: var(--text-dim); font-size: 12px; text-align: center; padding: 6px 0 0 0; }
132
+ #claude-control-panel { display: none; padding: 0 14px 10px 14px; box-sizing: border-box; }
133
+ .claude-control-row { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 7px; }
134
+ .claude-select, .claude-ctrl-btn { min-width: 0; height: 32px; border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; background: rgba(255,255,255,0.08); color: #f5f5f7; font-size: 11px; font-weight: 800; letter-spacing: 0; padding: 0 8px; outline: none; box-sizing: border-box; }
135
+ .claude-select { appearance: none; -webkit-appearance: none; text-align: center; text-align-last: center; }
136
+ .claude-picker-wrap { min-width: 0; position: relative; }
137
+ .claude-picker-wrap select { display: none; }
138
+ .claude-picker-btn { width: 100%; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
139
+ .claude-picker-btn.active { background: rgba(0,122,255,0.22); border-color: rgba(0,122,255,0.5); }
140
+ #claude-picker-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.99); border-radius: 12px; max-height: min(280px, 42dvh); overflow-y: auto; box-shadow: 0 16px 36px rgba(0,0,0,0.34); }
141
+ #claude-picker-panel.active { display: block; }
142
+ .claude-picker-title { padding: 9px 11px 7px 11px; color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid rgba(255,255,255,0.06); }
143
+ .claude-picker-option { width: 100%; border: 0; border-bottom: 1px solid rgba(255,255,255,0.06); background: transparent; color: #f5f5f7; padding: 10px 11px; text-align: left; cursor: pointer; box-sizing: border-box; }
144
+ .claude-picker-option:last-child { border-bottom: 0; }
145
+ .claude-picker-option:active, .claude-picker-option.selected { background: rgba(0,122,255,0.18); }
146
+ .claude-picker-option-main { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; font-weight: 800; }
147
+ .claude-picker-option-value { min-width: 0; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: #d1d5db; font-size: 12px; margin-top: 3px; line-height: 1.35; }
148
+ .claude-ctrl-btn { cursor: pointer; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; }
149
+ .claude-ctrl-btn:active, .claude-select:focus { background: rgba(255,255,255,0.14); border-color: rgba(0,122,255,0.45); }
150
+ .claude-ctrl-btn.danger { color: #ff6b61; background: rgba(255,59,48,0.12); }
151
+ .claude-ctrl-btn.primary { color: #fff; background: rgba(0,122,255,0.32); border-color: rgba(0,122,255,0.45); }
152
+ .claude-ctrl-btn:disabled { opacity: 0.45; cursor: default; }
153
+ #claude-state-bar { display: flex; align-items: center; gap: 7px; min-height: 24px; margin-top: 8px; color: var(--text-dim); font-size: 11px; overflow-x: auto; scrollbar-width: none; white-space: nowrap; }
154
+ #claude-state-bar::-webkit-scrollbar { display: none; }
155
+ .claude-state-pill { display: inline-flex; align-items: center; gap: 4px; border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.045); border-radius: 999px; padding: 3px 8px; flex: 0 0 auto; }
156
+ .claude-state-pill.warn { color: #ffcc00; border-color: rgba(255,204,0,0.24); background: rgba(255,204,0,0.08); }
157
+ #claude-resume-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.98); border-radius: 12px; max-height: 180px; overflow-y: auto; }
158
+ #claude-resume-panel.active { display: block; }
159
+ .claude-resume-item { width: 100%; text-align: left; border: 0; border-bottom: 1px solid rgba(255,255,255,0.06); background: transparent; color: #fff; padding: 10px 11px; cursor: pointer; box-sizing: border-box; }
160
+ .claude-resume-item:last-child { border-bottom: 0; }
161
+ .claude-resume-item:active { background: rgba(255,255,255,0.08); }
162
+ .claude-resume-title { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; font-weight: 800; }
163
+ .claude-resume-meta { color: var(--text-dim); font-size: 11px; margin-top: 3px; overflow-wrap: anywhere; }
63
164
  #terminal .xterm, #terminal .xterm-viewport, #terminal .xterm-screen, #terminal .xterm-rows, #terminal .xterm-rows span { user-select: text !important; -webkit-user-select: text !important; -webkit-touch-callout: default; touch-action: pan-y; }
64
165
  #terminal .xterm-viewport { overflow-anchor: none; }
65
166
  #terminal .xterm-helper-textarea { user-select: none !important; -webkit-user-select: none !important; -webkit-touch-callout: none; }
@@ -109,6 +210,7 @@
109
210
  .schedule-row { flex-direction: column; }
110
211
  .schedule-actions { justify-content: flex-start; }
111
212
  .step-grid { grid-template-columns: 1fr; }
213
+ .timed-row { grid-template-columns: 1fr; }
112
214
  }
113
215
  </style>
114
216
  </head>
@@ -159,12 +261,62 @@
159
261
  </div>
160
262
  </div>
161
263
  <div id="terminal-controls">
264
+ <div id="timed-tag-rail"></div>
162
265
  <div id="input-row">
163
266
  <textarea id="cmd-input" rows="1" placeholder="Type a message..."></textarea>
267
+ <button id="timer-btn" title="Schedule send">
268
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></svg>
269
+ </button>
164
270
  <button id="send-btn">
165
271
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
166
272
  </button>
167
273
  </div>
274
+ <div id="claude-control-panel">
275
+ <div class="claude-control-row">
276
+ <div class="claude-picker-wrap">
277
+ <select id="claude-permission-select" class="claude-select" title="Permission mode" onchange="updateClaudeSettingsFromControls()">
278
+ <option value="default">P:Def</option>
279
+ <option value="acceptEdits">P:Edit</option>
280
+ <option value="plan">P:Plan</option>
281
+ <option value="bypassPermissions">P:Byp</option>
282
+ </select>
283
+ <button id="claude-permission-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('permission')" title="Permission mode">P</button>
284
+ </div>
285
+ <div class="claude-picker-wrap">
286
+ <select id="claude-model-select" class="claude-select" title="Model" onchange="updateClaudeSettingsFromControls()">
287
+ <option value="default">M:Def</option>
288
+ </select>
289
+ <button id="claude-model-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('model')" title="Model">M</button>
290
+ </div>
291
+ <div class="claude-picker-wrap">
292
+ <select id="claude-effort-select" class="claude-select" title="Effort" onchange="updateClaudeSettingsFromControls()">
293
+ <option value="low">E:Low</option>
294
+ <option value="medium">E:Med</option>
295
+ <option value="high">E:High</option>
296
+ <option value="xhigh">E:XHi</option>
297
+ <option value="max">E:Max</option>
298
+ </select>
299
+ <button id="claude-effort-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('effort')" title="Effort">E</button>
300
+ </div>
301
+ <button id="claude-abort-btn" class="claude-ctrl-btn danger" onclick="abortClaudeSession()" title="Abort current Claude turn">Abort</button>
302
+ <button id="claude-resume-btn" class="claude-ctrl-btn primary" onclick="toggleClaudeResumePanel()" title="Choose a Claude session to resume">Resume</button>
303
+ </div>
304
+ <div id="claude-state-bar"></div>
305
+ <div id="claude-picker-panel"></div>
306
+ <div id="claude-resume-panel"></div>
307
+ </div>
308
+ <div id="timed-send-panel">
309
+ <div class="timed-row">
310
+ <select id="timed-hours" onchange="updateTimedSendPreview()" title="Hours"></select>
311
+ <select id="timed-minutes" onchange="updateTimedSendPreview()" title="Minutes"></select>
312
+ <button id="timed-save-btn" class="small-btn primary" onclick="saveTimedSend()">Add Timer</button>
313
+ </div>
314
+ <div id="timed-preview" class="timed-preview"></div>
315
+ <div class="timed-editor-actions">
316
+ <button id="timed-cancel-edit-btn" class="small-btn" onclick="resetTimedEditor()" style="display:none;">Cancel Edit</button>
317
+ <button id="timed-delete-btn" class="small-btn danger" onclick="deleteEditingTimedInput()" style="display:none;">Delete</button>
318
+ </div>
319
+ </div>
168
320
  <div id="shortcut-rail">
169
321
  <div class="shortcut-group simple-shortcuts">
170
322
  <div class="key-btn special-key" data-key="up">↑</div>
@@ -191,6 +343,7 @@
191
343
  </div>
192
344
  <div id="terminal-controls-spacer"></div>
193
345
  <div id="terminal-container"><div id="terminal"></div></div>
346
+ <div id="claude-chat-container"></div>
194
347
  </div>
195
348
 
196
349
  <!-- Plain Text History View -->
@@ -317,6 +470,26 @@
317
470
  let editingScheduleId = null;
318
471
  let editingSteps = [];
319
472
  let selectedWeekdays = [1, 2, 3, 4, 5];
473
+ let timedSendRefreshTimer = null;
474
+ let timedTagTimer = null;
475
+ let editingTimedInputId = null;
476
+ let claudeMessages = [];
477
+ let claudePendingPermissions = [];
478
+ let claudeStatus = 'idle';
479
+ let claudeRuntimeConfig = null;
480
+ let claudePickerOpen = null;
481
+ let claudeState = {
482
+ permissionMode: 'default',
483
+ model: 'default',
484
+ effort: 'medium',
485
+ contextRemaining: null,
486
+ claudeSessionId: null,
487
+ resumeSessionId: null,
488
+ canAbort: false,
489
+ pendingPermissionCount: 0
490
+ };
491
+ let claudeResumePanelOpen = false;
492
+ let claudeResumeItemsLoaded = false;
320
493
  const modifiers = { ctrl: false };
321
494
 
322
495
  function log(msg) {
@@ -360,6 +533,39 @@
360
533
  return decodeURIComponent(path);
361
534
  }
362
535
 
536
+ async function copyTextToClipboard(text) {
537
+ if (navigator.clipboard && window.isSecureContext) {
538
+ await navigator.clipboard.writeText(text);
539
+ return;
540
+ }
541
+ const el = document.createElement('textarea');
542
+ el.value = text;
543
+ el.setAttribute('readonly', '');
544
+ el.style.position = 'fixed';
545
+ el.style.opacity = '0';
546
+ document.body.appendChild(el);
547
+ el.select();
548
+ document.execCommand('copy');
549
+ document.body.removeChild(el);
550
+ }
551
+
552
+ async function copySessionDirectory(directory, event) {
553
+ event.stopPropagation();
554
+ const btn = event.currentTarget;
555
+ try {
556
+ await copyTextToClipboard(directory);
557
+ const oldTitle = btn.title;
558
+ btn.title = 'Copied';
559
+ btn.style.color = '#fff';
560
+ setTimeout(() => {
561
+ btn.title = oldTitle || 'Copy directory';
562
+ btn.style.color = '';
563
+ }, 1200);
564
+ } catch (e) {
565
+ alert('Copy failed');
566
+ }
567
+ }
568
+
363
569
  function formatWeekdays(days = []) {
364
570
  const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
365
571
  return days.map(day => labels[day]).filter(Boolean).join(', ') || 'No days';
@@ -443,12 +649,25 @@
443
649
  sessions.forEach(s => {
444
650
  const workingDirectory = s.workingDirectory || 'Unknown directory';
445
651
  const encodedName = encodePathValue(s.name);
652
+ const encodedDir = encodePathValue(workingDirectory);
653
+ const timedInputCount = Number(s.timedInputCount) || 0;
654
+ const timerBadge = timedInputCount > 0
655
+ ? `<span class="timer-count-badge" title="${timedInputCount} scheduled timer${timedInputCount > 1 ? 's' : ''}">
656
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></svg>
657
+ ${timedInputCount}
658
+ </span>`
659
+ : '';
446
660
  html += `<div class="session-card">
447
661
  <div class="session-info">
448
- <h3>${escapeHtml(s.name)}${s.hasUnreadCompletion ? '<span class="completion-dot" title="Completed"></span>' : ''} <button class="icon-btn" onclick="renameSession('${s.id}', decodePathValue('${encodedName}'), event)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button></h3>
662
+ <h3>${escapeHtml(s.name)}${s.hasUnreadCompletion ? '<span class="completion-dot" title="Completed"></span>' : ''}${timerBadge} <button class="icon-btn" onclick="renameSession('${s.id}', decodePathValue('${encodedName}'), event)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button></h3>
449
663
  <p>${escapeHtml(s.tool)}</p>
450
664
  <p>${new Date(s.startTime).toLocaleTimeString()}</p>
451
- <p title="${escapeHtml(workingDirectory)}" style="font-family: monospace; overflow-wrap: anywhere;">${escapeHtml(workingDirectory)}</p>
665
+ <div class="session-dir-row">
666
+ <button class="copy-dir-btn" title="Copy directory" onclick="copySessionDirectory(decodePathValue('${encodedDir}'), event)">
667
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
668
+ </button>
669
+ <p title="${escapeHtml(workingDirectory)}">${escapeHtml(workingDirectory)}</p>
670
+ </div>
452
671
  </div>
453
672
  <div class="session-actions">
454
673
  <button class="btn-join" onclick="joinSession('${s.id}', decodePathValue('${encodedName}'), '${escapeHtml(s.toolKey || '')}')">Connect</button>
@@ -558,13 +777,657 @@
558
777
  }
559
778
  }
560
779
 
780
+ function isClaudeSession() {
781
+ return activeToolKey === 'claude-code';
782
+ }
783
+
784
+ function setClaudeModeEnabled(enabled) {
785
+ document.getElementById('terminal-container').style.display = enabled ? 'none' : '';
786
+ document.getElementById('claude-chat-container').style.display = enabled ? 'block' : 'none';
787
+ document.getElementById('claude-control-panel').style.display = enabled ? 'block' : 'none';
788
+ document.getElementById('timer-btn').style.display = enabled ? 'none' : '';
789
+ if (enabled) closeTimedSendPanel();
790
+ document.getElementById('shortcut-rail').style.display = enabled ? 'none' : '';
791
+ document.getElementById('scroll-controls').style.display = enabled ? 'none' : '';
792
+ document.getElementById('cmd-input').placeholder = enabled ? 'Message Claude...' : 'Type a message...';
793
+ updateTerminalControlsHeight();
794
+ }
795
+
796
+ function updateTerminalControlsHeight() {
797
+ if (!isClaudeSession()) {
798
+ document.documentElement.style.setProperty('--terminal-controls-rest-height', '108px');
799
+ return;
800
+ }
801
+ const restHeight = claudeResumePanelOpen ? '308px' : (claudePickerOpen ? '322px' : '154px');
802
+ document.documentElement.style.setProperty('--terminal-controls-rest-height', restHeight);
803
+ }
804
+
805
+ function shortModelLabel(value, resolved) {
806
+ const source = String(value || 'default');
807
+ const target = String(resolved || source);
808
+ const compact = target
809
+ .replace(/^claude-/, '')
810
+ .replace(/-20\d{6,}.*$/, '')
811
+ .slice(0, 9);
812
+ if (source === 'default') return 'M:Def';
813
+ if (source === 'env') return `M:${compact || 'Env'}`;
814
+ if (source === 'sonnet') return `M:${compact || 'Son'}`;
815
+ if (source === 'opus') return `M:${compact || 'Opus'}`;
816
+ if (source === 'haiku') return `M:${compact || 'Hai'}`;
817
+ return `M:${compact || source.slice(0, 9)}`;
818
+ }
819
+
820
+ function pickerLabelFromOption(option) {
821
+ return option ? (option.textContent || option.value || '') : '';
822
+ }
823
+
824
+ function pickerFullValue(option) {
825
+ if (!option) return '';
826
+ return option.dataset.resolved || '';
827
+ }
828
+
829
+ function syncClaudePickerButtons() {
830
+ const mappings = [
831
+ ['permission', 'claude-permission-select', 'claude-permission-picker-btn', 'P'],
832
+ ['model', 'claude-model-select', 'claude-model-picker-btn', 'M'],
833
+ ['effort', 'claude-effort-select', 'claude-effort-picker-btn', 'E']
834
+ ];
835
+ mappings.forEach(([type, selectId, buttonId, fixedLabel]) => {
836
+ const select = document.getElementById(selectId);
837
+ const button = document.getElementById(buttonId);
838
+ if (!select || !button) return;
839
+ const option = select.selectedOptions && select.selectedOptions[0];
840
+ const label = pickerLabelFromOption(option);
841
+ const fullValue = pickerFullValue(option);
842
+ button.textContent = fixedLabel;
843
+ button.title = fullValue || label || fixedLabel;
844
+ button.classList.toggle('active', claudePickerOpen === type);
845
+ });
846
+ }
847
+
848
+ function closeClaudePicker() {
849
+ claudePickerOpen = null;
850
+ const panel = document.getElementById('claude-picker-panel');
851
+ if (panel) {
852
+ panel.classList.remove('active');
853
+ panel.innerHTML = '';
854
+ }
855
+ syncClaudePickerButtons();
856
+ updateTerminalControlsHeight();
857
+ }
858
+
859
+ function pickerTitle(type) {
860
+ if (type === 'permission') return 'Permission mode';
861
+ if (type === 'model') return 'Model';
862
+ if (type === 'effort') return 'Effort';
863
+ return 'Options';
864
+ }
865
+
866
+ function selectIdForPicker(type) {
867
+ if (type === 'permission') return 'claude-permission-select';
868
+ if (type === 'model') return 'claude-model-select';
869
+ if (type === 'effort') return 'claude-effort-select';
870
+ return '';
871
+ }
872
+
873
+ function renderClaudePicker(type) {
874
+ const panel = document.getElementById('claude-picker-panel');
875
+ const select = document.getElementById(selectIdForPicker(type));
876
+ if (!panel || !select) return;
877
+ const options = Array.from(select.options);
878
+ panel.innerHTML = `<div class="claude-picker-title">${escapeHtml(pickerTitle(type))}</div>${options.map(option => {
879
+ const selected = option.value === select.value;
880
+ const full = pickerFullValue(option);
881
+ return `<button class="claude-picker-option${selected ? ' selected' : ''}" onclick="chooseClaudePickerOption('${type}', decodePathValue('${encodePathValue(option.value)}'))">
882
+ <div class="claude-picker-option-main">
883
+ <span>${escapeHtml(option.textContent || option.value)}</span>
884
+ ${selected ? '<span>Selected</span>' : ''}
885
+ </div>
886
+ ${full ? `<div class="claude-picker-option-value">${escapeHtml(full)}</div>` : ''}
887
+ </button>`;
888
+ }).join('')}`;
889
+ panel.classList.add('active');
890
+ syncClaudePickerButtons();
891
+ updateTerminalControlsHeight();
892
+ }
893
+
894
+ async function toggleClaudePicker(type) {
895
+ if (claudePickerOpen === type) {
896
+ closeClaudePicker();
897
+ return;
898
+ }
899
+ if (type === 'model') await refreshClaudeRuntimeConfig();
900
+ claudeResumePanelOpen = false;
901
+ const resumePanel = document.getElementById('claude-resume-panel');
902
+ if (resumePanel) resumePanel.classList.remove('active');
903
+ claudePickerOpen = type;
904
+ renderClaudePicker(type);
905
+ }
906
+
907
+ function chooseClaudePickerOption(type, value) {
908
+ const select = document.getElementById(selectIdForPicker(type));
909
+ if (!select) return;
910
+ select.value = value;
911
+ closeClaudePicker();
912
+ updateClaudeSettingsFromControls();
913
+ }
914
+
915
+ function applyClaudeRuntimeConfig(config) {
916
+ if (!config || !Array.isArray(config.models)) return;
917
+ claudeRuntimeConfig = config;
918
+ const modelEl = document.getElementById('claude-model-select');
919
+ if (modelEl) {
920
+ const current = modelEl.value || claudeState.model || config.defaultModel || 'default';
921
+ modelEl.innerHTML = '';
922
+ config.models.forEach(item => {
923
+ const option = new Option(shortModelLabel(item.value, item.resolved), item.value);
924
+ option.title = item.resolved ? `${item.label}: ${item.resolved}` : item.label;
925
+ option.dataset.resolved = item.resolved || '';
926
+ modelEl.add(option);
927
+ });
928
+ const next = Array.from(modelEl.options).some(option => option.value === current)
929
+ ? current
930
+ : (config.defaultModel || 'default');
931
+ modelEl.value = next;
932
+ claudeState.model = next;
933
+ }
934
+ if (config.defaultEffort && claudeState.effort === 'medium') {
935
+ claudeState.effort = config.defaultEffort;
936
+ }
937
+ syncClaudePickerButtons();
938
+ renderClaudeStateBar();
939
+ }
940
+
941
+ async function refreshClaudeRuntimeConfig() {
942
+ try {
943
+ const res = await fetchWithTimeout('/api/claude-config', {}, 10000);
944
+ const data = await res.json();
945
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load Claude config');
946
+ applyClaudeRuntimeConfig(data.config);
947
+ return data.config;
948
+ } catch (e) {
949
+ log('Failed to load Claude config: ' + e.message);
950
+ return null;
951
+ }
952
+ }
953
+
954
+ function ensureSelectOption(selectEl, value, labelPrefix) {
955
+ const normalized = String(value || '').trim();
956
+ if (!normalized) return;
957
+ const exists = Array.from(selectEl.options).some(option => option.value === normalized);
958
+ if (!exists) {
959
+ const label = `${labelPrefix}:${normalized.replace(/^claude-/, '').replace(/-20\d{6,}.*$/, '').slice(0, 10)}`;
960
+ selectEl.add(new Option(label, normalized));
961
+ }
962
+ }
963
+
964
+ function applyClaudeState(state = {}) {
965
+ claudeState = { ...claudeState, ...state };
966
+ claudeStatus = claudeState.status || claudeStatus;
967
+ const permissionEl = document.getElementById('claude-permission-select');
968
+ const modelEl = document.getElementById('claude-model-select');
969
+ const effortEl = document.getElementById('claude-effort-select');
970
+ if (permissionEl) permissionEl.value = claudeState.permissionMode || 'default';
971
+ if (modelEl) {
972
+ ensureSelectOption(modelEl, claudeState.model || 'default', 'M');
973
+ modelEl.value = claudeState.model || 'default';
974
+ }
975
+ if (effortEl) effortEl.value = claudeState.effort || 'medium';
976
+ syncClaudePickerButtons();
977
+ const abortBtn = document.getElementById('claude-abort-btn');
978
+ if (abortBtn) abortBtn.disabled = !(claudeState.canAbort || claudeStatus === 'thinking');
979
+ renderClaudeStateBar();
980
+ }
981
+
982
+ function shortValue(value, fallback = 'N/A') {
983
+ const text = String(value || fallback);
984
+ return text.length > 16 ? text.slice(0, 13) + '...' : text;
985
+ }
986
+
987
+ function renderClaudeStateBar() {
988
+ const el = document.getElementById('claude-state-bar');
989
+ if (!el) return;
990
+ const modelEl = document.getElementById('claude-model-select');
991
+ const modelOption = modelEl && modelEl.selectedOptions && modelEl.selectedOptions[0];
992
+ const modelLabel = modelOption && modelOption.dataset.resolved
993
+ ? modelOption.dataset.resolved
994
+ : (claudeState.model || 'default');
995
+ const context = typeof claudeState.contextRemaining === 'number'
996
+ ? `Ctx ${claudeState.contextRemaining}`
997
+ : 'Ctx N/A';
998
+ const pending = Number(claudeState.pendingPermissionCount || claudePendingPermissions.filter(item => item.status === 'pending').length) || 0;
999
+ const status = claudeStatus === 'thinking' ? 'Working' : shortValue(claudeStatus || 'Idle');
1000
+ el.innerHTML = [
1001
+ `<span class="claude-state-pill">${escapeHtml(shortValue(claudeState.permissionMode || 'default'))}</span>`,
1002
+ `<span class="claude-state-pill">${escapeHtml(shortValue(modelLabel))}</span>`,
1003
+ `<span class="claude-state-pill">Eff ${escapeHtml(shortValue(claudeState.effort || 'medium'))}</span>`,
1004
+ `<span class="claude-state-pill">${escapeHtml(context)}</span>`,
1005
+ `<span class="claude-state-pill${pending ? ' warn' : ''}">${pending ? `${pending} approval` : escapeHtml(status)}</span>`
1006
+ ].join('');
1007
+ }
1008
+
1009
+ async function updateClaudeSettingsFromControls() {
1010
+ await refreshClaudeRuntimeConfig();
1011
+ const settings = {
1012
+ permissionMode: document.getElementById('claude-permission-select').value,
1013
+ model: document.getElementById('claude-model-select').value,
1014
+ effort: document.getElementById('claude-effort-select').value
1015
+ };
1016
+ applyClaudeState(settings);
1017
+ if (currentSocket && currentSocket.readyState === 1) {
1018
+ currentSocket.send(JSON.stringify({ type: 'claude-settings', settings }));
1019
+ }
1020
+ }
1021
+
1022
+ function abortClaudeSession() {
1023
+ if (!currentSocket || currentSocket.readyState !== 1) return;
1024
+ currentSocket.send(JSON.stringify({ type: 'claude-abort' }));
1025
+ }
1026
+
1027
+ async function toggleClaudeResumePanel() {
1028
+ claudeResumePanelOpen = !claudeResumePanelOpen;
1029
+ if (claudeResumePanelOpen) closeClaudePicker();
1030
+ const panel = document.getElementById('claude-resume-panel');
1031
+ panel.classList.toggle('active', claudeResumePanelOpen);
1032
+ updateTerminalControlsHeight();
1033
+ if (claudeResumePanelOpen && !claudeResumeItemsLoaded) {
1034
+ await loadClaudeResumeSessions();
1035
+ }
1036
+ }
1037
+
1038
+ async function loadClaudeResumeSessions() {
1039
+ const panel = document.getElementById('claude-resume-panel');
1040
+ if (!activeSessionId) return;
1041
+ panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading sessions...</div>';
1042
+ try {
1043
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/claude-resume-sessions`, {}, 15000);
1044
+ const data = await res.json();
1045
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load resume sessions');
1046
+ const items = data.items || [];
1047
+ claudeResumeItemsLoaded = true;
1048
+ if (!items.length) {
1049
+ panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">No local Claude sessions found for this folder.</div>';
1050
+ return;
1051
+ }
1052
+ panel.innerHTML = items.map(item => {
1053
+ const encodedId = encodePathValue(item.id);
1054
+ const updated = item.updatedAt ? new Date(item.updatedAt).toLocaleString() : 'Unknown';
1055
+ const active = item.id === claudeState.resumeSessionId || item.id === claudeState.claudeSessionId;
1056
+ return `<button class="claude-resume-item" onclick="selectClaudeResumeSession(decodePathValue('${encodedId}'))">
1057
+ <div class="claude-resume-title"><span>${escapeHtml(item.id.slice(0, 8))}${active ? ' · current' : ''}</span><span>${escapeHtml(updated)}</span></div>
1058
+ <div class="claude-resume-meta">${escapeHtml(item.lastText || item.firstText || item.cwd || 'No preview')}</div>
1059
+ </button>`;
1060
+ }).join('');
1061
+ } catch (e) {
1062
+ panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px; color:#ff6b61;">${escapeHtml(e.message)}</div>`;
1063
+ }
1064
+ }
1065
+
1066
+ function selectClaudeResumeSession(id) {
1067
+ if (!id || !currentSocket || currentSocket.readyState !== 1) return;
1068
+ currentSocket.send(JSON.stringify({ type: 'claude-resume', resumeSessionId: id }));
1069
+ applyClaudeState({ resumeSessionId: id, claudeSessionId: id });
1070
+ claudeResumePanelOpen = false;
1071
+ document.getElementById('claude-resume-panel').classList.remove('active');
1072
+ updateTerminalControlsHeight();
1073
+ }
1074
+
1075
+ function textFromClaudeMessage(message) {
1076
+ if (!message) return '';
1077
+ if (message.text) return String(message.text);
1078
+ if (message.summary) return String(message.summary);
1079
+ return '';
1080
+ }
1081
+
1082
+ function escapeRegExp(text) {
1083
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1084
+ }
1085
+
1086
+ function inlineMarkdown(text) {
1087
+ let html = escapeHtml(text || '');
1088
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
1089
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
1090
+ html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
1091
+ html = html.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
1092
+ html = html.replace(/_([^_\n]+)_/g, '<em>$1</em>');
1093
+ html = html.replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g, '<img src="$2" alt="$1">');
1094
+ html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
1095
+ return html;
1096
+ }
1097
+
1098
+ function splitMarkdownBlocks(markdown) {
1099
+ const lines = String(markdown || '').replace(/\r\n/g, '\n').split('\n');
1100
+ const blocks = [];
1101
+ let i = 0;
1102
+ while (i < lines.length) {
1103
+ if (!lines[i].trim()) {
1104
+ i++;
1105
+ continue;
1106
+ }
1107
+ const fence = lines[i].match(/^```(\w+)?\s*$/);
1108
+ if (fence) {
1109
+ const language = fence[1] || '';
1110
+ const content = [];
1111
+ i++;
1112
+ while (i < lines.length && !/^```\s*$/.test(lines[i])) {
1113
+ content.push(lines[i]);
1114
+ i++;
1115
+ }
1116
+ if (i < lines.length) i++;
1117
+ blocks.push({ type: 'code', language, content: content.join('\n') });
1118
+ continue;
1119
+ }
1120
+ if (/^\s*[-*_]{3,}\s*$/.test(lines[i])) {
1121
+ blocks.push({ type: 'hr' });
1122
+ i++;
1123
+ continue;
1124
+ }
1125
+ if (/^\s{0,3}#{1,4}\s+/.test(lines[i])) {
1126
+ const match = lines[i].match(/^(\s{0,3})(#{1,4})\s+(.+)$/);
1127
+ blocks.push({ type: 'header', level: match[2].length, text: match[3] });
1128
+ i++;
1129
+ continue;
1130
+ }
1131
+ if (lines[i].includes('|') && i + 1 < lines.length && /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[i + 1])) {
1132
+ const rows = [lines[i]];
1133
+ i += 2;
1134
+ while (i < lines.length && lines[i].includes('|') && lines[i].trim()) {
1135
+ rows.push(lines[i]);
1136
+ i++;
1137
+ }
1138
+ blocks.push({ type: 'table', rows });
1139
+ continue;
1140
+ }
1141
+ if (/^\s*([-*+])\s+/.test(lines[i]) || /^\s*\d+\.\s+/.test(lines[i])) {
1142
+ const ordered = /^\s*\d+\.\s+/.test(lines[i]);
1143
+ const items = [];
1144
+ while (i < lines.length && (ordered ? /^\s*\d+\.\s+/.test(lines[i]) : /^\s*[-*+]\s+/.test(lines[i]))) {
1145
+ items.push(lines[i].replace(ordered ? /^\s*\d+\.\s+/ : /^\s*[-*+]\s+/, ''));
1146
+ i++;
1147
+ }
1148
+ blocks.push({ type: ordered ? 'ol' : 'ul', items });
1149
+ continue;
1150
+ }
1151
+ if (/^\s*>\s?/.test(lines[i])) {
1152
+ const quote = [];
1153
+ while (i < lines.length && /^\s*>\s?/.test(lines[i])) {
1154
+ quote.push(lines[i].replace(/^\s*>\s?/, ''));
1155
+ i++;
1156
+ }
1157
+ blocks.push({ type: 'quote', text: quote.join('\n') });
1158
+ continue;
1159
+ }
1160
+ const paragraph = [];
1161
+ while (i < lines.length && lines[i].trim()) {
1162
+ if (/^```/.test(lines[i]) || /^\s{0,3}#{1,4}\s+/.test(lines[i]) || /^\s*([-*+])\s+/.test(lines[i]) || /^\s*\d+\.\s+/.test(lines[i])) break;
1163
+ paragraph.push(lines[i]);
1164
+ i++;
1165
+ }
1166
+ blocks.push({ type: 'paragraph', text: paragraph.join('\n') });
1167
+ }
1168
+ return blocks;
1169
+ }
1170
+
1171
+ function renderMarkdown(markdown) {
1172
+ const blocks = splitMarkdownBlocks(markdown);
1173
+ return `<div class="claude-md">${blocks.map(block => {
1174
+ if (block.type === 'code') {
1175
+ const language = block.language ? `<div class="claude-tool-section-title">${escapeHtml(block.language)}</div>` : '';
1176
+ return `<pre>${language}<code>${escapeHtml(block.content)}</code></pre>`;
1177
+ }
1178
+ if (block.type === 'hr') return '<hr>';
1179
+ if (block.type === 'header') return `<h${block.level}>${inlineMarkdown(block.text)}</h${block.level}>`;
1180
+ if (block.type === 'ul') return `<ul>${block.items.map(item => `<li>${inlineMarkdown(item)}</li>`).join('')}</ul>`;
1181
+ if (block.type === 'ol') return `<ol>${block.items.map(item => `<li>${inlineMarkdown(item)}</li>`).join('')}</ol>`;
1182
+ if (block.type === 'quote') return `<blockquote>${renderMarkdown(block.text)}</blockquote>`;
1183
+ if (block.type === 'table') return renderMarkdownTable(block.rows);
1184
+ return `<p>${inlineMarkdown(block.text).replace(/\n/g, '<br>')}</p>`;
1185
+ }).join('')}</div>`;
1186
+ }
1187
+
1188
+ function renderMarkdownTable(rows) {
1189
+ const parsed = rows.map(row => row.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(cell => cell.trim()));
1190
+ if (!parsed.length) return '';
1191
+ const [head, ...body] = parsed;
1192
+ return `<table><thead><tr>${head.map(cell => `<th>${inlineMarkdown(cell)}</th>`).join('')}</tr></thead><tbody>${body.map(row => `<tr>${row.map(cell => `<td>${inlineMarkdown(cell)}</td>`).join('')}</tr>`).join('')}</tbody></table>`;
1193
+ }
1194
+
1195
+ function parseLocalCommandMessage(text) {
1196
+ const value = String(text || '');
1197
+ if (/^\s*<local-command-caveat>[\s\S]*?<\/local-command-caveat>\s*$/.test(value)) return { kind: 'hidden' };
1198
+ const stdoutMatch = value.match(/^\s*<local-command-stdout>\s*([\s\S]*?)\s*<\/local-command-stdout>\s*$/);
1199
+ if (stdoutMatch) {
1200
+ const stdout = stdoutMatch[1].trim();
1201
+ if (/^Goal set:/i.test(stdout)) return { kind: 'hidden' };
1202
+ return { kind: 'text', text: stdout };
1203
+ }
1204
+ const raw = value.trim().match(/^\/([a-zA-Z][\w:-]*)(?:\s+([\s\S]*?))?$/);
1205
+ if (raw) {
1206
+ return { kind: 'command', commandName: raw[1], args: raw[2] && raw[2].trim() };
1207
+ }
1208
+ const nameMatch = value.match(/<command-name>\s*\/?([^<]+?)\s*<\/command-name>/);
1209
+ if (nameMatch) {
1210
+ const argsMatch = value.match(/<command-args>\s*([\s\S]*?)\s*<\/command-args>/);
1211
+ const stripped = value
1212
+ .replace(/<command-message>[\s\S]*?<\/command-message>/g, '')
1213
+ .replace(/<command-name>[\s\S]*?<\/command-name>/g, '')
1214
+ .replace(/<command-args>[\s\S]*?<\/command-args>/g, '')
1215
+ .trim();
1216
+ if (!stripped) return { kind: 'command', commandName: nameMatch[1].trim(), args: argsMatch && argsMatch[1].trim() };
1217
+ return { kind: 'text', text: stripped };
1218
+ }
1219
+ return { kind: 'text', text: value };
1220
+ }
1221
+
1222
+ function toolCategory(name) {
1223
+ if (['Bash', 'CodexBash', 'execute'].includes(name)) return 'terminal';
1224
+ if (['Edit', 'MultiEdit', 'Write', 'NotebookEdit'].includes(name)) return 'edit';
1225
+ if (['Read', 'LS'].includes(name)) return 'read';
1226
+ if (['Grep', 'Glob'].includes(name)) return 'search';
1227
+ if (['WebFetch', 'WebSearch'].includes(name)) return 'web';
1228
+ if (['Task', 'Agent'].includes(name)) return 'task';
1229
+ return 'other';
1230
+ }
1231
+
1232
+ function toolIcon(category) {
1233
+ const labels = { terminal: '$', edit: '+/-', read: 'R', search: '?', web: 'W', task: 'A', other: '*' };
1234
+ return labels[category] || '*';
1235
+ }
1236
+
1237
+ function toolCommand(tool) {
1238
+ const input = tool && tool.input;
1239
+ if (!input || typeof input !== 'object') return '';
1240
+ if (typeof input.command === 'string') return input.command;
1241
+ if (Array.isArray(input.command)) return input.command.join(' ');
1242
+ if (typeof input.file_path === 'string') return input.file_path;
1243
+ if (typeof input.path === 'string') return input.path;
1244
+ if (typeof input.pattern === 'string') return input.pattern;
1245
+ if (typeof input.prompt === 'string') return input.prompt;
1246
+ return tool.summary || '';
1247
+ }
1248
+
1249
+ function toolTitle(tool) {
1250
+ const name = tool.name || 'Tool';
1251
+ const input = tool.input || {};
1252
+ if (name === 'Bash') return (tool.description || 'Terminal');
1253
+ if (name === 'Read' && input.file_path) return input.file_path;
1254
+ if (name === 'Edit' || name === 'MultiEdit' || name === 'Write') return input.file_path || name;
1255
+ if (name === 'Grep') return input.pattern ? `grep(pattern: ${input.pattern})` : 'Search Content';
1256
+ if (name === 'Glob') return input.pattern || 'Search Files';
1257
+ if ((name === 'Task' || name === 'Agent') && input.description) return input.description;
1258
+ if (name.startsWith('mcp__')) return name.replace(/^mcp__/, '').replace(/__/g, ': ');
1259
+ return name;
1260
+ }
1261
+
1262
+ function renderToolSection(title, value) {
1263
+ if (value === undefined || value === null || value === '') return '';
1264
+ const code = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
1265
+ return `<div class="claude-tool-section"><div class="claude-tool-section-title">${escapeHtml(title)}</div><pre class="claude-tool-code">${escapeHtml(code)}</pre></div>`;
1266
+ }
1267
+
1268
+ function renderClaudeTool(tool) {
1269
+ const category = toolCategory(tool.name || '');
1270
+ const command = toolCommand(tool);
1271
+ const hasResult = tool.resultText || tool.isError;
1272
+ const running = claudeStatus === 'thinking' && !hasResult;
1273
+ const status = tool.isError ? 'error' : (hasResult ? 'completed' : (running ? 'running' : 'running'));
1274
+ const open = tool.isError || (!hasResult && !running) ? ' open' : '';
1275
+ const compact = category === 'terminal' || category === 'search' || category === 'read';
1276
+ return `<details class="claude-tool claude-tool-card${tool.isError ? ' error' : ''}"${open}>
1277
+ <summary class="claude-tool-header">
1278
+ <span class="claude-tool-icon" data-icon="${escapeHtml(toolIcon(category))}"></span>
1279
+ <span class="claude-tool-title">${escapeHtml(toolTitle(tool))}</span>
1280
+ <span class="claude-tool-status">${escapeHtml(status)}</span>
1281
+ <span class="claude-tool-command">${escapeHtml(command)}</span>
1282
+ </summary>
1283
+ ${compact && !tool.isError ? '' : `<div class="claude-tool-body">
1284
+ ${renderToolSection('Input', tool.input)}
1285
+ ${renderToolSection(tool.isError ? 'Error' : 'Output', tool.resultText)}
1286
+ </div>`}
1287
+ </details>`;
1288
+ }
1289
+
1290
+ function renderWorkGroup(items) {
1291
+ const running = items.some(item => item.kind === 'tool' && !item.resultText);
1292
+ const label = `${items.length} tool call${items.length > 1 ? 's' : ''}`;
1293
+ return `<details class="claude-work-group"${running ? ' open' : ''}>
1294
+ <summary><span class="claude-tool-icon" data-icon="*"></span><span class="claude-work-group-title">Worked on ${escapeHtml(label)}</span>${running ? '<span class="claude-tool-status">running</span>' : ''}</summary>
1295
+ <div class="claude-work-group-body">${items.map(renderDisplayItem).join('')}</div>
1296
+ </details>`;
1297
+ }
1298
+
1299
+ function buildClaudeDisplayItems(messages) {
1300
+ const items = [];
1301
+ const byToolUseId = new Map();
1302
+ for (const message of messages) {
1303
+ if (!message) continue;
1304
+ if (message.kind === 'tool') {
1305
+ const item = { ...message, kind: 'tool', resultText: '', isError: false };
1306
+ items.push(item);
1307
+ if (item.toolUseId) byToolUseId.set(item.toolUseId, item);
1308
+ continue;
1309
+ }
1310
+ if (message.kind === 'tool-result') {
1311
+ const parent = message.toolUseId && byToolUseId.get(message.toolUseId);
1312
+ if (parent) {
1313
+ parent.resultText = message.text || '';
1314
+ parent.isError = Boolean(message.isError);
1315
+ } else {
1316
+ items.push(message);
1317
+ }
1318
+ continue;
1319
+ }
1320
+ items.push(message);
1321
+ }
1322
+
1323
+ const grouped = [];
1324
+ let run = [];
1325
+ const flush = () => {
1326
+ if (run.length > 1) grouped.push({ kind: 'work-group', items: run });
1327
+ else if (run.length === 1) grouped.push(run[0]);
1328
+ run = [];
1329
+ };
1330
+ for (const item of items) {
1331
+ if (item.kind === 'tool') {
1332
+ run.push(item);
1333
+ continue;
1334
+ }
1335
+ flush();
1336
+ grouped.push(item);
1337
+ }
1338
+ flush();
1339
+ return grouped;
1340
+ }
1341
+
1342
+ function renderUserMessage(message) {
1343
+ const parsed = parseLocalCommandMessage(textFromClaudeMessage(message));
1344
+ if (parsed.kind === 'hidden') return '';
1345
+ if (parsed.kind === 'command') {
1346
+ const args = parsed.args ? `<div class="claude-message user">${renderMarkdown(parsed.args)}</div>` : '';
1347
+ return `${args}<div class="claude-message user"><span class="claude-command-chip">/${escapeHtml(parsed.commandName)}</span></div>`;
1348
+ }
1349
+ return `<div class="claude-message user">${renderMarkdown(parsed.text)}</div>`;
1350
+ }
1351
+
1352
+ function renderDisplayItem(message) {
1353
+ if (!message) return '';
1354
+ if (message.kind === 'user') return renderUserMessage(message);
1355
+ if (message.kind === 'assistant') return `<div class="claude-message assistant">${renderMarkdown(textFromClaudeMessage(message))}</div>`;
1356
+ if (message.kind === 'tool') return renderClaudeTool(message);
1357
+ if (message.kind === 'tool-result') return `<div class="claude-tool${message.isError ? ' error' : ''}">${renderToolSection(message.isError ? 'Error' : 'Output', message.text || '')}</div>`;
1358
+ if (message.kind === 'work-group') return renderWorkGroup(message.items);
1359
+ if (message.kind === 'event') return `<div class="claude-message event${message.level === 'error' ? ' error' : ''}">${escapeHtml(message.text || '')}</div>`;
1360
+ return '';
1361
+ }
1362
+
1363
+ function renderClaudeChat() {
1364
+ const container = document.getElementById('claude-chat-container');
1365
+ const parts = buildClaudeDisplayItems(claudeMessages).map(renderDisplayItem);
1366
+ for (const req of claudePendingPermissions.filter(item => item.status === 'pending')) {
1367
+ parts.push(`<div class="claude-tool claude-permission">
1368
+ <div class="claude-tool-header">
1369
+ <span class="claude-tool-icon" data-icon="!"></span>
1370
+ <strong>${escapeHtml(req.title || req.toolName || 'Permission required')}</strong>
1371
+ </div>
1372
+ <div class="claude-tool-body">
1373
+ ${req.reason ? `<div>${escapeHtml(req.reason)}</div>` : ''}
1374
+ ${renderToolSection('Input', req.input || {})}
1375
+ <div class="claude-permission-actions">
1376
+ <button class="small-btn danger" onclick="respondClaudePermission('${req.id}', false)">Deny</button>
1377
+ <button class="small-btn primary" onclick="respondClaudePermission('${req.id}', true)">Allow</button>
1378
+ </div>
1379
+ </div>
1380
+ </div>`);
1381
+ }
1382
+ if (claudeStatus === 'thinking') {
1383
+ parts.push('<div class="claude-status">Claude is working...</div>');
1384
+ }
1385
+ container.innerHTML = parts.join('') || '<div class="claude-message event">Send a message to start Claude.</div>';
1386
+ renderClaudeStateBar();
1387
+ requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
1388
+ }
1389
+
1390
+ function applyClaudeEvent(event) {
1391
+ if (!event) return;
1392
+ if (event.type === 'message' && event.message) {
1393
+ claudeMessages.push(event.message);
1394
+ } else if (event.type === 'status') {
1395
+ claudeStatus = event.status || 'idle';
1396
+ } else if (event.type === 'permission-request' && event.request) {
1397
+ claudePendingPermissions = claudePendingPermissions.filter(item => item.id !== event.request.id);
1398
+ claudePendingPermissions.push(event.request);
1399
+ } else if (event.type === 'permission-updated' && event.request) {
1400
+ claudePendingPermissions = claudePendingPermissions.map(item => item.id === event.request.id ? event.request : item);
1401
+ } else if (event.type === 'history-reset' && Array.isArray(event.messages)) {
1402
+ claudeMessages = event.messages;
1403
+ } else if (event.type === 'state' && event.state) {
1404
+ applyClaudeState(event.state);
1405
+ }
1406
+ if (event.type !== 'state') applyClaudeState({
1407
+ status: claudeStatus,
1408
+ pendingPermissionCount: claudePendingPermissions.filter(item => item.status === 'pending').length,
1409
+ canAbort: claudeStatus === 'thinking'
1410
+ });
1411
+ renderClaudeChat();
1412
+ }
1413
+
1414
+ function respondClaudePermission(id, approved) {
1415
+ if (!currentSocket || currentSocket.readyState !== 1) return;
1416
+ currentSocket.send(JSON.stringify({ type: 'claude-permission', id, approved }));
1417
+ }
1418
+
561
1419
  async function createSession(toolKey, sessionName) {
562
1420
  try {
563
1421
  const workingDirectory = document.getElementById('cwd-field').value;
1422
+ const runtimeConfig = toolKey === 'claude-code' ? await refreshClaudeRuntimeConfig() : null;
1423
+ const claudeOptions = runtimeConfig ? {
1424
+ model: runtimeConfig.defaultModel || 'default',
1425
+ effort: runtimeConfig.defaultEffort || 'medium'
1426
+ } : undefined;
564
1427
  const res = await fetchWithTimeout('/api/sessions', {
565
1428
  method: 'POST',
566
1429
  headers: { 'Content-Type': 'application/json' },
567
- body: JSON.stringify({ toolKey, workingDirectory })
1430
+ body: JSON.stringify({ toolKey, workingDirectory, claudeOptions })
568
1431
  });
569
1432
  const data = await res.json();
570
1433
  if (!res.ok || !data.id) throw new Error(data.error || 'Failed to create session');
@@ -826,6 +1689,8 @@
826
1689
 
827
1690
  function showLobby() {
828
1691
  if (currentSocket) { currentSocket.close(); currentSocket = null; }
1692
+ closeTimedSendPanel();
1693
+ stopTimedInputTimers();
829
1694
  document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
830
1695
  document.getElementById('lobby-view').classList.add('active');
831
1696
  refreshSessionsNow();
@@ -849,13 +1714,6 @@
849
1714
  { command: '/model' },
850
1715
  { command: '/resume' },
851
1716
  { command: '/stat' }
852
- ],
853
- gemini: [
854
- { key: 'ctrl-c', label: 'Ctrl+C' },
855
- { key: 'ctrl-y', label: 'Ctrl+Y', title: 'Send Ctrl+Y' },
856
- { command: '/model' },
857
- { command: '/resume' },
858
- { command: '/stat' }
859
1717
  ]
860
1718
  };
861
1719
  const shortcuts = toolShortcuts[toolKey] || defaultShortcuts;
@@ -882,20 +1740,28 @@
882
1740
  }
883
1741
 
884
1742
  function joinSession(id, sessionName, toolKey = null) {
1743
+ stopTimedInputTimers();
885
1744
  activeSessionId = id;
886
1745
  window.activeSessionId = id;
887
1746
  activeToolKey = toolKey;
888
1747
  clearTimeout(sessionPollTimer);
889
1748
  sessionPollTimer = null;
890
1749
  markCompletionRead(id);
1750
+ loadTimedInputs();
891
1751
  document.getElementById('session-title').innerText = sessionName;
892
1752
  updateToolShortcuts(activeToolKey);
893
1753
  document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
894
1754
  document.getElementById('terminal-view').classList.add('active');
1755
+ setClaudeModeEnabled(isClaudeSession());
1756
+ if (currentSocket) { currentSocket.close(); currentSocket = null; }
895
1757
  initTerminal(id);
896
1758
  }
897
1759
 
898
1760
  function initTerminal(sessionId) {
1761
+ if (isClaudeSession()) {
1762
+ initClaudeSession(sessionId);
1763
+ return;
1764
+ }
899
1765
  if (typeof Terminal === 'undefined') {
900
1766
  alert('Terminal library not loaded yet.');
901
1767
  showLobby();
@@ -933,6 +1799,52 @@
933
1799
  };
934
1800
  }
935
1801
 
1802
+ async function initClaudeSession(sessionId) {
1803
+ claudeMessages = [];
1804
+ claudePendingPermissions = [];
1805
+ claudeStatus = 'idle';
1806
+ const runtimeConfig = await refreshClaudeRuntimeConfig();
1807
+ claudeState = {
1808
+ permissionMode: 'default',
1809
+ model: (runtimeConfig && runtimeConfig.defaultModel) || 'default',
1810
+ effort: (runtimeConfig && runtimeConfig.defaultEffort) || 'medium',
1811
+ contextRemaining: null,
1812
+ claudeSessionId: null,
1813
+ resumeSessionId: null,
1814
+ canAbort: false,
1815
+ pendingPermissionCount: 0
1816
+ };
1817
+ claudeResumePanelOpen = false;
1818
+ claudePickerOpen = null;
1819
+ claudeResumeItemsLoaded = false;
1820
+ document.getElementById('claude-resume-panel').classList.remove('active');
1821
+ document.getElementById('claude-resume-panel').innerHTML = '';
1822
+ closeClaudePicker();
1823
+ updateTerminalControlsHeight();
1824
+ applyClaudeRuntimeConfig(runtimeConfig);
1825
+ applyClaudeState(claudeState);
1826
+ renderClaudeChat();
1827
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1828
+ currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
1829
+ currentSocket.onopen = () => logViewSnapshot('claude-ws-open');
1830
+ currentSocket.onmessage = (e) => {
1831
+ const msg = JSON.parse(e.data);
1832
+ if (msg.type === 'claude-snapshot' && msg.snapshot) {
1833
+ claudeMessages = msg.snapshot.messages || [];
1834
+ claudePendingPermissions = msg.snapshot.pendingPermissions || [];
1835
+ claudeStatus = msg.snapshot.status || 'idle';
1836
+ applyClaudeState(msg.snapshot.state || {
1837
+ status: claudeStatus,
1838
+ pendingPermissionCount: claudePendingPermissions.filter(item => item.status === 'pending').length,
1839
+ canAbort: claudeStatus === 'thinking'
1840
+ });
1841
+ renderClaudeChat();
1842
+ }
1843
+ if (msg.type === 'claude-event') applyClaudeEvent(msg.event);
1844
+ if (msg.type === 'exit') showLobby();
1845
+ };
1846
+ }
1847
+
936
1848
  function sendWS(data) {
937
1849
  if (currentSocket && currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'input', data }));
938
1850
  }
@@ -1143,7 +2055,15 @@
1143
2055
 
1144
2056
  function performSend() {
1145
2057
  const val = inputEl.value;
1146
- if (val) {
2058
+ if (val && isClaudeSession()) {
2059
+ if (currentSocket && currentSocket.readyState === 1) {
2060
+ currentSocket.send(JSON.stringify({ type: 'claude-input', text: val }));
2061
+ }
2062
+ inputEl.value = '';
2063
+ inputEl.style.height = '38px';
2064
+ return;
2065
+ }
2066
+ if (val) {
1147
2067
  const formattedVal = val.replace(/\n/g, '\r');
1148
2068
  sendWS(formattedVal);
1149
2069
  inputEl.value = '';
@@ -1152,8 +2072,191 @@
1152
2072
  setTimeout(() => { sendWS('\r'); }, 1000);
1153
2073
  }
1154
2074
  }
2075
+
2076
+ let timedInputs = [];
2077
+
2078
+ function initTimedDelaySelectors() {
2079
+ const hours = document.getElementById('timed-hours');
2080
+ const minutes = document.getElementById('timed-minutes');
2081
+ if (hours.options.length) return;
2082
+ for (let i = 0; i <= 23; i++) {
2083
+ hours.add(new Option(`${i} hr`, String(i)));
2084
+ }
2085
+ for (let i = 0; i <= 59; i++) {
2086
+ minutes.add(new Option(`${i} min`, String(i)));
2087
+ }
2088
+ hours.value = '0';
2089
+ minutes.value = '5';
2090
+ }
2091
+
2092
+ function getTimedDelayMs() {
2093
+ const hours = Number(document.getElementById('timed-hours').value);
2094
+ const minutes = Number(document.getElementById('timed-minutes').value);
2095
+ const delayMs = ((hours * 60) + minutes) * 60 * 1000;
2096
+ return Number.isFinite(delayMs) && delayMs > 0 ? delayMs : 0;
2097
+ }
2098
+
2099
+ function parseSendAt() {
2100
+ const delayMs = getTimedDelayMs();
2101
+ return delayMs > 0 ? Date.now() + delayMs : null;
2102
+ }
2103
+
2104
+ function setTimedDelayFromMs(ms) {
2105
+ const totalMinutes = Math.max(1, Math.ceil(ms / 60000));
2106
+ const hours = Math.min(23, Math.floor(totalMinutes / 60));
2107
+ const minutes = Math.min(59, totalMinutes - hours * 60);
2108
+ document.getElementById('timed-hours').value = String(hours);
2109
+ document.getElementById('timed-minutes').value = String(minutes);
2110
+ }
2111
+
2112
+ function updateTimedSendPreview() {
2113
+ const preview = document.getElementById('timed-preview');
2114
+ const sendAt = parseSendAt();
2115
+ preview.textContent = sendAt
2116
+ ? `Will run at ${new Date(sendAt).toLocaleString()}`
2117
+ : 'Choose at least 1 minute.';
2118
+ }
2119
+
2120
+ function formatCountdown(sendAt) {
2121
+ const remaining = Math.max(0, sendAt - Date.now());
2122
+ const totalSeconds = Math.ceil(remaining / 1000);
2123
+ const hours = Math.floor(totalSeconds / 3600);
2124
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
2125
+ const seconds = totalSeconds % 60;
2126
+ if (hours > 0) return `${hours}h\n${String(minutes).padStart(2, '0')}m`;
2127
+ return `${minutes}:${String(seconds).padStart(2, '0')}`;
2128
+ }
2129
+
2130
+ function renderTimedTags() {
2131
+ const rail = document.getElementById('timed-tag-rail');
2132
+ const now = Date.now();
2133
+ const activeItems = timedInputs.filter(item => item.sendAt > now);
2134
+ rail.innerHTML = activeItems.map(item => {
2135
+ const encodedId = encodePathValue(item.id);
2136
+ return `<button class="timed-tag${item.id === editingTimedInputId ? ' active' : ''}" title="${escapeHtml(item.text)}" onclick="editTimedInput(decodePathValue('${encodedId}'))">${escapeHtml(formatCountdown(item.sendAt))}</button>`;
2137
+ }).join('');
2138
+ }
2139
+
2140
+ function closeTimedSendPanel() {
2141
+ document.getElementById('timed-send-panel').classList.remove('active');
2142
+ document.getElementById('timer-btn').classList.remove('active');
2143
+ editingTimedInputId = null;
2144
+ renderTimedTags();
2145
+ }
2146
+
2147
+ function stopTimedInputTimers() {
2148
+ clearTimeout(timedSendRefreshTimer);
2149
+ timedSendRefreshTimer = null;
2150
+ if (timedTagTimer) clearInterval(timedTagTimer);
2151
+ timedTagTimer = null;
2152
+ timedInputs = [];
2153
+ renderTimedTags();
2154
+ }
2155
+
2156
+ function resetTimedEditor(options = {}) {
2157
+ editingTimedInputId = null;
2158
+ document.getElementById('timed-save-btn').textContent = 'Add Timer';
2159
+ document.getElementById('timed-cancel-edit-btn').style.display = 'none';
2160
+ document.getElementById('timed-delete-btn').style.display = 'none';
2161
+ if (!options.keepInput) {
2162
+ inputEl.value = '';
2163
+ inputEl.style.height = '38px';
2164
+ }
2165
+ setTimedDelayFromMs(5 * 60 * 1000);
2166
+ updateTimedSendPreview();
2167
+ renderTimedTags();
2168
+ }
2169
+
2170
+ async function loadTimedInputs() {
2171
+ clearTimeout(timedSendRefreshTimer);
2172
+ timedSendRefreshTimer = null;
2173
+ if (!activeSessionId) return;
2174
+
2175
+ try {
2176
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/timed-inputs`);
2177
+ if (!res.ok) throw new Error('HTTP ' + res.status);
2178
+ const data = await res.json();
2179
+ timedInputs = data.items || [];
2180
+ renderTimedTags();
2181
+ if (!timedTagTimer) timedTagTimer = setInterval(renderTimedTags, 1000);
2182
+ timedSendRefreshTimer = setTimeout(loadTimedInputs, 15000);
2183
+ } catch (e) {
2184
+ timedInputs = [];
2185
+ renderTimedTags();
2186
+ }
2187
+ }
2188
+
2189
+ function editTimedInput(id) {
2190
+ const item = timedInputs.find(value => value.id === id);
2191
+ if (!item) return;
2192
+ initTimedDelaySelectors();
2193
+ editingTimedInputId = id;
2194
+ inputEl.value = item.text || '';
2195
+ inputEl.style.height = 'auto';
2196
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + 'px';
2197
+ setTimedDelayFromMs(item.sendAt - Date.now());
2198
+ document.getElementById('timed-save-btn').textContent = 'Update Timer';
2199
+ document.getElementById('timed-cancel-edit-btn').style.display = '';
2200
+ document.getElementById('timed-delete-btn').style.display = '';
2201
+ document.getElementById('timed-send-panel').classList.add('active');
2202
+ document.getElementById('timer-btn').classList.add('active');
2203
+ updateTimedSendPreview();
2204
+ renderTimedTags();
2205
+ }
2206
+
2207
+ async function saveTimedSend() {
2208
+ if (!activeSessionId) return alert('No active session');
2209
+ const text = inputEl.value;
2210
+ if (!text.trim()) return alert('Type a message first');
2211
+ const sendAt = parseSendAt();
2212
+ if (!sendAt) return alert('Choose at least 1 minute');
2213
+
2214
+ try {
2215
+ const url = editingTimedInputId
2216
+ ? `/api/sessions/${activeSessionId}/timed-inputs/${editingTimedInputId}`
2217
+ : `/api/sessions/${activeSessionId}/timed-inputs`;
2218
+ const res = await fetchWithTimeout(url, {
2219
+ method: editingTimedInputId ? 'PATCH' : 'POST',
2220
+ headers: { 'Content-Type': 'application/json' },
2221
+ body: JSON.stringify({ text, sendAt })
2222
+ });
2223
+ const data = await res.json();
2224
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to save timer');
2225
+ resetTimedEditor();
2226
+ await loadTimedInputs();
2227
+ } catch (e) {
2228
+ alert('Save failed: ' + e.message);
2229
+ }
2230
+ }
2231
+
2232
+ async function cancelTimedInput(id) {
2233
+ if (!activeSessionId) return;
2234
+ try {
2235
+ await fetchWithTimeout(`/api/sessions/${activeSessionId}/timed-inputs/${id}`, { method: 'DELETE' });
2236
+ if (editingTimedInputId === id) resetTimedEditor();
2237
+ loadTimedInputs();
2238
+ } catch (e) {
2239
+ alert('Delete failed');
2240
+ }
2241
+ }
2242
+
2243
+ function deleteEditingTimedInput() {
2244
+ if (editingTimedInputId) cancelTimedInput(editingTimedInputId);
2245
+ }
1155
2246
 
1156
2247
  document.getElementById('send-btn').addEventListener('click', performSend);
2248
+ document.getElementById('timer-btn').addEventListener('click', () => {
2249
+ const panel = document.getElementById('timed-send-panel');
2250
+ const isOpen = panel.classList.toggle('active');
2251
+ document.getElementById('timer-btn').classList.toggle('active', isOpen);
2252
+ if (isOpen) {
2253
+ initTimedDelaySelectors();
2254
+ resetTimedEditor({ keepInput: true });
2255
+ updateTimedSendPreview();
2256
+ loadTimedInputs();
2257
+ }
2258
+ else closeTimedSendPanel();
2259
+ });
1157
2260
  inputEl.addEventListener('keydown', (e) => {
1158
2261
  if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); performSend(); }
1159
2262
  else if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete') {