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.
- package/README.md +2 -3
- package/README.zh-CN.md +2 -3
- package/lib/ai-tools/registry.js +0 -9
- package/lib/claude/config.js +82 -0
- package/lib/claude/structured-session.js +516 -0
- package/lib/commands/web.js +104 -12
- package/lib/session/session-manager.js +443 -3
- package/lib/web/index.html +1114 -11
- package/package.json +2 -1
package/lib/web/index.html
CHANGED
|
@@ -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
|
-
<
|
|
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') {
|